Skip to content

Instantly share code, notes, and snippets.

@pachuco
Last active July 16, 2024 18:40
Show Gist options
  • Save pachuco/1e2a5df46ef37f716654691b024666e0 to your computer and use it in GitHub Desktop.
Save pachuco/1e2a5df46ef37f716654691b024666e0 to your computer and use it in GitHub Desktop.
Stupid FM synth
{
"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]
]
}
]
}
#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