Last active
July 16, 2024 18:40
-
-
Save pachuco/1e2a5df46ef37f716654691b024666e0 to your computer and use it in GitHub Desktop.
Stupid FM synth
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"id": "fmbank4stupidsynth", | |
"ver": "alpha01_4op_ARenv", | |
"instruments": [ | |
{ | |
"name": "2OP Tubular Bell", | |
"fbRatio": 0, | |
"opDetune": [0, 0, 0, 0], | |
"opFreqMul": [1.0, 3.5, 0, 0], | |
"opAmp": [1.0, 0.05, 0, 0], | |
"envIncr": [ | |
[20, 3000], | |
[0, 1750], | |
[0, 0], | |
[0, 0] | |
] | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <stdbool.h> | |
#include <stdint.h> | |
#include <string.h> | |
#include <ctype.h> | |
#include <assert.h> | |
#include <math.h> | |
#include <windows.h> | |
//#define JSMN_PARENT_LINKS | |
#define JSMN_STRICT | |
#include "jsmn.h" | |
#define MA_NO_DECODING | |
#define MA_NO_ENCODING | |
#define MA_NO_WAV | |
#define MA_NO_FLAC | |
#define MA_NO_MP3 | |
#define MA_NO_RESOURCE_MANAGER | |
#define MA_NO_NODE_GRAPH | |
#define MA_NO_ENGINE | |
#define MA_NO_GENERATION | |
#define MINIAUDIO_IMPLEMENTATION | |
#include "miniaudio.h" | |
#define AUDIOBUFSIZE 32 | |
#define AUDIOBUFNUM 256 | |
float audBuf[AUDIOBUFNUM * AUDIOBUFSIZE * 2] = {0}; | |
uint32_t audIdxRead = ((AUDIOBUFNUM - 4) * AUDIOBUFSIZE); | |
uint32_t audIdxWrite = 0; | |
bool isPlayerActive = false; | |
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { | |
float* pBuf = (float*)pOutput; | |
for (uint32_t i=0; i < frameCount; i++) { | |
if (!isPlayerActive) return; | |
pBuf[i*2 + 0] = audBuf[audIdxRead*2 + 0]; | |
pBuf[i*2 + 1] = audBuf[audIdxRead*2 + 1]; | |
audIdxRead = (audIdxRead + 1) % (AUDIOBUFNUM * AUDIOBUFSIZE); | |
} | |
} | |
#define OPL3CLOCK 49716 | |
#define BASE_A 440.0f | |
#define MIDI2DELTA(X) (BASE_A / (OPL3CLOCK * 32.0f)) * powf(2, (((float)(X) - 9) / 12.0f)) | |
#define TWO_PIE 6.28318530717958647692528676655901f | |
#define DO_OSC(X) cosf(TWO_PIE * (float)(X)) | |
#define DO_ENV(X) ((X)*(X)*(X)) | |
#define NUM_OPS 4 | |
#define NUM_VOICES 8 | |
typedef struct { | |
char name[48]; | |
float fbRatio; | |
float opDetune[NUM_OPS]; | |
float opFreqMul[NUM_OPS]; | |
float opAmp[NUM_OPS]; | |
float envIncr[NUM_OPS][2]; | |
} StupidInstrument; | |
#define MAX_INSTR 256 | |
typedef struct { | |
uint32_t num; | |
StupidInstrument data[MAX_INSTR]; | |
} StupidBank; | |
#define ENV_STAGE_IDLE 0 | |
enum { | |
ENV_AR_STAGE_IDLE, | |
ENV_AR_STAGE_R, | |
ENV_AR_STAGE_A, | |
ENV_AR_NUM_STAGES, | |
}; | |
typedef struct { | |
float oscPhase; | |
float envPos; | |
int envStage; | |
} StupidOperator; | |
typedef struct { | |
float baseDelta; | |
bool gate; | |
StupidOperator op[NUM_OPS]; | |
} StupidVoice; | |
typedef struct { | |
StupidInstrument* pIns; | |
StupidVoice v[NUM_VOICES]; | |
} StupidFm; | |
void SFM_init(StupidFm* pSynth) { | |
memset(pSynth, 0, sizeof(StupidFm)); | |
} | |
void SFM_generateFrame(StupidFm* pSynth, float* pOut) { | |
StupidInstrument* pIns = pSynth->pIns; | |
pOut[1] = pOut[0] = 0.0f; | |
for (int i=0; i < NUM_VOICES; i++) { | |
StupidVoice* pV = &pSynth->v[i]; | |
float busPrevOp = 0.0f; | |
float busOut = 0.0f; | |
float busAux = 0.0f; | |
for (int j=NUM_OPS-1; j >= 0; j--) { | |
StupidOperator* pOp = &pV->op[j]; | |
float ampCalc = 0.0f; | |
float oscCalc = 0.0f; | |
if (pOp->envStage > ENV_STAGE_IDLE) { | |
///////////// | |
// envelope | |
float a = 1.0f - pOp->envPos; | |
switch (pOp->envStage) { | |
case ENV_AR_STAGE_A: ampCalc = 1.0f - a*a; | |
break; | |
case ENV_AR_STAGE_R: ampCalc = a*a*a; | |
break; | |
} | |
pOp->envPos += pIns->envIncr[j][ENV_AR_NUM_STAGES - (pOp->envStage + 1)]; | |
if (pOp->envPos >= 1.0f) { | |
pOp->envPos = 0.0f; | |
pOp->envStage--; | |
} | |
///////////// | |
// oscilator | |
oscCalc = DO_OSC(pOp->oscPhase) * ampCalc * pIns->opAmp[j]; | |
pOp->oscPhase += ((pV->baseDelta * pIns->opFreqMul[j]) + pIns->opDetune[j] + busPrevOp); | |
} | |
busPrevOp = oscCalc; | |
} | |
pOut[0] += busPrevOp; | |
pOut[1] = pOut[0]; | |
} | |
pOut[0] *= 0.1f; | |
pOut[1] *= 0.1f; | |
} | |
void SFM_gate(StupidFm* pSynth, uint8_t note, uint8_t voiceNum, bool isNoteOn) { | |
StupidVoice* pV = &pSynth->v[voiceNum]; | |
assert(voiceNum < NUM_VOICES); | |
if (isNoteOn) { | |
pSynth->v[voiceNum].gate = true; | |
pSynth->v[voiceNum].baseDelta = MIDI2DELTA(note); | |
for (int i=0; i < NUM_OPS; i++) { | |
pSynth->v[voiceNum].op[i].oscPhase = 0.0f; | |
pSynth->v[voiceNum].op[i].envStage = ENV_AR_NUM_STAGES - 1; | |
pSynth->v[voiceNum].op[i].envPos = 0.0f; | |
} | |
} else { | |
pSynth->v[voiceNum].gate = false; | |
} | |
} | |
void SFM_setInstrument(StupidFm* pSynth, StupidInstrument* pInstr, uint8_t voiceNum) { | |
StupidVoice* pV = &pSynth->v[voiceNum]; | |
assert(voiceNum < NUM_VOICES); | |
pSynth->v[voiceNum].gate = false; | |
} | |
/* | |
// TODO | |
typedef struct { | |
float detune; | |
float freqMul; | |
float ampLastOp; // How much last OP contributes to modulation. Last Amp controls feedback. | |
float ampInput; // How much input bus contributes to modulation | |
float ampOutput; // How much current OP outputs to output bus | |
float ampEnv; | |
float envIncr[2]; | |
} FMInstrument_OP; | |
typedef struct { | |
char patchName[16]; | |
float baseDelta; | |
int feedbackThrow; // How many operators does FeedBack include. | |
float ampOutputOutput; // How much outpus bus outputs(channel volume). | |
FMInstrument_OP op[NUM_OPS]; | |
} FMInstrument; | |
// 1 1 1 10 | |
// 1<(2 3<4) | |
*/ | |
StupidBank instrumentos = {0}; | |
uint32_t curInstrument = 0; | |
bool loadInstrFromJsonFile(StupidBank* pSb, char* path) { | |
// WARN: GCC-ism, nested function | |
bool actuallyParseDamnedJsonAlready(StupidBank* pSb, jsmn_parser* pParser, jsmntok_t* pTokens, int numTokes) { | |
for (int i=0; i < numTokes; i++) { | |
printf("token type %d\n", pTokens[i].type); | |
// TODO: finish parser | |
} | |
abort(); | |
} | |
#define ASS(X) if (!(X)) {ret = false; goto l_cleanup;} | |
bool ret = true; | |
int numTokes; | |
jsmntok_t* pTokens = NULL; | |
jsmn_parser parser; | |
FILE* fin = fopen(path, "rb"); | |
uint32_t fileSize; | |
void* pJs = NULL; | |
ASS(fin); | |
fseek(fin, 0, SEEK_END); | |
fileSize = ftell(fin); | |
fseek(fin, 0, SEEK_SET); | |
pJs = malloc(fileSize); | |
ASS(pJs); | |
ASS(1 == fread(pJs, fileSize, 1, fin)); | |
jsmn_init(&parser); | |
numTokes = jsmn_parse(&parser, pJs, fileSize, NULL, 0); | |
ASS(numTokes > 0); | |
pTokens = malloc(numTokes * sizeof(jsmntok_t)); | |
ASS(pTokens); | |
jsmn_init(&parser); | |
ASS(0 <= jsmn_parse(&parser, pJs, fileSize, pTokens, numTokes)); | |
ret = actuallyParseDamnedJsonAlready(pSb, &parser, pTokens, numTokes); | |
l_cleanup: | |
if (fin) fclose(fin); | |
if (pJs) free(pJs); | |
if (pTokens) free(pTokens); | |
return ret; | |
#undef ASS | |
} | |
#define MS(X) (1000.0f/((float)(X)*(float)OPL3CLOCK)) | |
#define DET(X) ((float)(X)/(float)OPL3CLOCK) | |
StupidFm synth; | |
StupidInstrument instr = { | |
.fbRatio = 0.0, | |
.opDetune = { | |
0, | |
0, | |
}, | |
.opFreqMul = { | |
1.0, | |
3.5, | |
}, | |
.opAmp = { | |
1.0, | |
0.05, | |
}, | |
.envIncr = { | |
{MS(20), MS(3000)}, | |
{MS(0), MS(1750)}, | |
}, | |
}; | |
#define BM_GET(BMAP, IND) !!((BMAP)[(IND)>>3] & (1<<((IND)&7))) | |
#define BM_SET1(BMAP, IND) ((BMAP)[(IND)>>3] |= (1<<((IND)&7))) | |
#define BM_SET0(BMAP, IND) ((BMAP)[(IND)>>3] &= ~(1<<((IND)&7))) | |
char keyMap[] = "ZSXDCVGBHNJM" "Q2W3ER5T6Y7U" "I9O0P"; | |
#define AUDIORATE 44100 | |
int main(int argc, char *argv[]) { | |
ma_device_config config = {0}; | |
ma_device device = {0}; | |
config = ma_device_config_init(ma_device_type_playback); | |
config.sampleRate = AUDIORATE; | |
config.playback.format = ma_format_f32; | |
config.playback.channels = 2; | |
config.dataCallback = data_callback; | |
config.pUserData = NULL; | |
assert(MA_SUCCESS == ma_device_init(NULL, &config, &device)); | |
SFM_init(&synth); | |
synth.pIns = &instr; | |
float tapsL[4] = {0}; | |
float tapsR[4] = {0}; | |
float delta = (float)OPL3CLOCK / (float)AUDIORATE; | |
float posFrac = 0; | |
uint8_t curVoice = 0; | |
int8_t octave = 4; | |
HANDLE hInstr = CreateFileA("fminstr.json", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); | |
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); | |
assert(loadInstrFromJsonFile(&instrumentos, "fminstr.json")); | |
BYTE kbArr[256/8] = {0}; | |
DWORD lastInstrTimeCheck = 0; | |
FILETIME lastInstrFileStamp = {0}; | |
assert(hInstr != INVALID_HANDLE_VALUE); | |
assert(hStdin != INVALID_HANDLE_VALUE); | |
isPlayerActive = true; | |
ma_device_start(&device); | |
while (isPlayerActive) { | |
if (audIdxWrite/AUDIOBUFSIZE != audIdxRead/AUDIOBUFSIZE) { | |
while (audIdxWrite/AUDIOBUFSIZE != audIdxRead/AUDIOBUFSIZE) { | |
#define ITP_T04_SXX_F01_CUBIC(D, F) (D[1] + 0.5 * F*(D[2] - D[0] + F*(2.0 * D[0] - 5.0 * D[1] + 4.0 * D[2] - D[3] + F * (3.0 * (D[1] - D[2]) + D[3] - D[0])))) | |
#define INTERP_IN(BUF, X) BUF[0] = BUF[1];; BUF[1] = BUF[2];; BUF[2] = BUF[3];; BUF[3] = X | |
#define INTERP_OUT(BUF, F) ITP_T04_SXX_F01_CUBIC(BUF, F) | |
while (posFrac >= 1.0) { | |
float sampPair[2]; | |
SFM_generateFrame(&synth, &sampPair); | |
INTERP_IN(tapsL, (float)sampPair[0]); | |
INTERP_IN(tapsR, (float)sampPair[1]); | |
posFrac -= 1.0; | |
} | |
audBuf[audIdxWrite*2 + 0] = INTERP_OUT(tapsL, posFrac); | |
audBuf[audIdxWrite*2 + 1] = INTERP_OUT(tapsR, posFrac); | |
posFrac += delta; | |
audIdxWrite = (audIdxWrite + 1) % (AUDIOBUFNUM * AUDIOBUFSIZE); | |
} | |
} else { | |
SleepEx(10, 1); | |
} | |
// file monitor for instruments | |
DWORD curTick = GetTickCount(); | |
if (curTick - lastInstrTimeCheck >= 500) { //TODO: 49 day rollover check | |
FILETIME checkedTime = {0}; | |
lastInstrTimeCheck = curTick; | |
if (GetFileTime(hInstr, NULL, NULL, &checkedTime) && memcmp(&lastInstrFileStamp, &checkedTime, sizeof(FILETIME)) != 0) { | |
memcpy(&lastInstrFileStamp, &checkedTime, sizeof(FILETIME)); | |
//updateDynFile(); | |
} | |
} | |
// keypresses | |
DWORD numConEvents; | |
#define CONBUFSIZE 32 | |
while (GetNumberOfConsoleInputEvents(hStdin, &numConEvents) && numConEvents > 0) { | |
INPUT_RECORD conArr[CONBUFSIZE]; | |
if (numConEvents > CONBUFSIZE) numConEvents = CONBUFSIZE; | |
if (ReadConsoleInput(hStdin, &conArr, numConEvents, &numConEvents)) { | |
for (int i=0; i < numConEvents; i++) { | |
KEY_EVENT_RECORD* pKE = &conArr[i].Event.KeyEvent; | |
if (conArr[i].EventType != KEY_EVENT) continue; | |
if (pKE->wVirtualKeyCode > 0xFF) continue; | |
// filter keymatic repeat | |
if (BM_GET(kbArr, pKE->wVirtualKeyCode) == !!pKE->bKeyDown) continue; | |
pKE->bKeyDown ? BM_SET1(kbArr, pKE->wVirtualKeyCode) : BM_SET0(kbArr, pKE->wVirtualKeyCode); | |
kbArr[pKE->wVirtualKeyCode] = !!pKE->bKeyDown; | |
switch (pKE->wVirtualKeyCode) { | |
case VK_ESCAPE: | |
if (!pKE->bKeyDown) { | |
isPlayerActive = false; | |
} | |
case VK_UP: | |
if (!pKE->bKeyDown) { | |
if (octave+1 < 9) { | |
octave++; | |
printf("Octave: %d\n", octave); | |
} | |
} | |
break; | |
case VK_DOWN: | |
if (!pKE->bKeyDown) { | |
if (octave-1 >= 0) { | |
octave--; | |
printf("Octave: %d\n", octave); | |
} | |
} | |
break; | |
default: | |
for (int i=0; i < sizeof(keyMap)-1; i++) { | |
if (keyMap[i] == pKE->wVirtualKeyCode) { | |
if (pKE->bKeyDown) { | |
SFM_gate(&synth, octave*12 + i, curVoice, true); | |
curVoice = (curVoice+1) % NUM_VOICES; | |
} else { | |
//..... | |
} | |
break; | |
} | |
} | |
break; | |
} | |
} | |
} | |
} | |
} | |
ma_device_uninit(&device); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment