diff --git a/Source/Instruments/Trojka/readme.md b/Source/Instruments/Trojka/readme.md new file mode 100644 index 0000000..82ff5e0 --- /dev/null +++ b/Source/Instruments/Trojka/readme.md @@ -0,0 +1,2 @@ +# Trojka +3-op minimalist FM synth to the mega! diff --git a/Source/Instruments/Trojka/trojka.c b/Source/Instruments/Trojka/trojka.c new file mode 100644 index 0000000..fda1bc7 --- /dev/null +++ b/Source/Instruments/Trojka/trojka.c @@ -0,0 +1,56 @@ +#if defined(STFU_MAIN) && !defined(STFU_TROJKA) +#define STFU_TROJKA + +#include "../../phaser.c" +#include "../../envelope.c" +#include "../../config.h" + +#define STFU_TROJKA_OP_COUNT 3 + +/* https://multimed.org/student/eim/en/04-FM.pdf */ + +// todo: A few more algorithm configurations. +// todo: Other oscillators. + +struct stfu_trojka { + struct stfu_phaser phasers[STFU_TROJKA_OP_COUNT]; + struct stfu_envelope envelopes[STFU_TROJKA_OP_COUNT]; + float gains[STFU_TROJKA_OP_COUNT]; + float freq_scales[STFU_TROJKA_OP_COUNT]; + float key_frequency; + float feedback_gain; // todo: Separate gain for channels? + float left_feedback, right_feedback; +}; + +static struct stfu_trojka stfu_sample_trojka(struct stfu_trojka synth, float buffer[static 2]) { + for (int i = 0; i < STFU_TROJKA_OP_COUNT; ++i) { + synth.phasers[i] = stfu_pump_phaser(synth.phasers[i]); + synth.envelopes[i] = stfu_pump_envelope(synth.envelopes[i]); + } + + float left_modulator_stack = synth.envelopes[1].v * sinf(synth.phasers[1].v + synth.envelopes[0].v * sinf(synth.phasers[0].v)) + synth.left_feedback * synth.feedback_gain; + float right_modulator_stack = synth.envelopes[1].v * sinf(synth.phasers[1].v + synth.envelopes[0].v * sinf(synth.phasers[0].v)) + synth.right_feedback * synth.feedback_gain; + + synth.left_feedback = left_modulator_stack; + synth.right_feedback = right_modulator_stack; + + float left = synth.envelopes[2].v * sinf(synth.phasers[2].v + left_modulator_stack); + float right = + synth.envelopes[2].v * sinf(synth.phasers[2].v + right_modulator_stack); + + buffer[0] = left; + buffer[1] = right; + + return synth; +} + +static struct stfu_trojka stfu_press_trojka(struct stfu_trojka synth, float key_frequency) { + for (int i = 0; i < STFU_TROJKA_OP_COUNT; ++i) { + synth.phasers[i] = stfu_set_phaser_frequency(synth.phasers[i], key_frequency * synth.freq_scales[i]); + synth.envelopes[i] = stfu_trigger_envelope(synth.envelopes[i]); + } + + return synth; +} + +#endif diff --git a/Source/Maker/Build/stfu-maker.exe b/Source/Maker/Build/stfu-maker.exe new file mode 100644 index 0000000..1ab8eb7 Binary files /dev/null and b/Source/Maker/Build/stfu-maker.exe differ diff --git a/Source/Maker/Build/windows-clang.bat b/Source/Maker/Build/windows-clang.bat new file mode 100644 index 0000000..9fb4103 --- /dev/null +++ b/Source/Maker/Build/windows-clang.bat @@ -0,0 +1 @@ +clang ../main.c -lkernel32 -luser32 -lshell32 -ld3d11 -ldxgi -lgdi32 -lole32 -DSOKOL_D3D11 -o stfu-maker.exe -O3 diff --git a/Source/Maker/main.c b/Source/Maker/main.c index e3dffb3..e196977 100644 --- a/Source/Maker/main.c +++ b/Source/Maker/main.c @@ -3,45 +3,27 @@ #include #define SOKOL_IMPL -#define SOKOL_GLCORE #include "sokol_app.h" #include "sokol_audio.h" #include "sokol_log.h" +#define STFU_MAIN #include "../config.h" #include "../envelope.c" -// #include "../interp.c" #include "../oscillators.c" #include "../phaser.c" -static struct stfu_sinewave sine0; -static struct stfu_phaser phaser0; -static struct stfu_phaser phaser1; -static struct stfu_phaser phaser2; -static struct stfu_envelope envelope; +#include "../Instruments/Trojka/trojka.c" -static float key_frequency = STFU_C4_FREQUENCY; +static struct stfu_trojka synth = {0}; +// todo: Separate to playback.c, generic for Player and Maker #define CROSSFADE_RING_RANGE \ (STFU_CROSSFADE_BUFFER_FRAMES * STFU_AUDIO_CHANNEL_COUNT) static float crossfade_ring[CROSSFADE_RING_RANGE] = {0}; static size_t crossfade_ring_needle = 0; static unsigned int crossfade_frames_left = 0; -static void sample(float buffer[static 2]) { - phaser0 = stfu_pump_phaser(phaser0); - phaser1 = stfu_pump_phaser(phaser1); - phaser2 = stfu_pump_phaser(phaser2); - envelope = stfu_pump_envelope(envelope); - - float left = envelope.v * sinf(phaser2.v + sinf(phaser0.v + sinf(phaser1.v))); - float right = - envelope.v * sinf(phaser2.v + sinf(phaser0.v + sinf(phaser1.v))); - - buffer[0] = left; - buffer[1] = right; -} - static void stream(float *buffer, int num_frames, int num_channels) { /* Fade away */ for (size_t i = 0; crossfade_frames_left > 0; @@ -58,7 +40,7 @@ static void stream(float *buffer, int num_frames, int num_channels) { buffer[0] = crossfade_ring[crossfade_ring_needle % CROSSFADE_RING_RANGE]; buffer[1] = crossfade_ring[(crossfade_ring_needle + 1) % CROSSFADE_RING_RANGE]; - sample(&crossfade_ring[crossfade_ring_needle % CROSSFADE_RING_RANGE]); + synth = stfu_sample_trojka(synth, &crossfade_ring[crossfade_ring_needle % CROSSFADE_RING_RANGE]); buffer = &buffer[2]; crossfade_ring_needle += 2; } @@ -71,15 +53,19 @@ static void init(void) { // .stream_cb = stream, }); - // sine0 = stfu_init_sinewave(STFU_A4_FREQUENCY / 2.0f, 0.0f, 0.8f); - phaser0 = stfu_init_phaser(key_frequency); - phaser1 = stfu_init_phaser(key_frequency * 0.25); - phaser2 = stfu_init_phaser(key_frequency * 2.0); - envelope = stfu_init_envelope( - (struct stfu_envelope_point[]){[0] = {.dur = 0.02, .vol = 1.0}, - [1] = {.dur = 0.5, .vol = 0.33}, - [2] = {.dur = 1.0, .vol = 0}}, - 3); + synth.phasers[0] = stfu_init_phaser(220); + synth.phasers[1] = stfu_init_phaser(220 * 0.25); + synth.phasers[2] = stfu_init_phaser(220 * 2.0); + synth.feedback_gain = 0.25; + for (int i = 0; i < STFU_TROJKA_OP_COUNT; ++i) { + synth.envelopes[i] = stfu_init_envelope( + (struct stfu_envelope_point[]){[0] = {.dur = 0.02, .vol = 1.0}, + [1] = {.dur = 0.5, .vol = 0.33}, + [2] = {.dur = 1.0, .vol = 0}}, + 3); + synth.gains[i] = 1.0; + synth.freq_scales[i] = 1.0; + } } static void cleanup(void) { saudio_shutdown(); } @@ -92,53 +78,38 @@ static void event(const sapp_event *e) { switch (e->key_code) { case SAPP_KEYCODE_Z: - key_frequency = STFU_C4_FREQUENCY; - break; + synth = stfu_press_trojka(synth, STFU_SCALE[0]); case SAPP_KEYCODE_X: - key_frequency = STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[1]); break; case SAPP_KEYCODE_C: - key_frequency = STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * - STFU_NOTE_UPSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[2]); break; case SAPP_KEYCODE_V: - key_frequency = STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * - STFU_NOTE_UPSCALE_FACTOR * STFU_NOTE_UPSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[3]); break; case SAPP_KEYCODE_B: - key_frequency = STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * - STFU_NOTE_UPSCALE_FACTOR * STFU_NOTE_UPSCALE_FACTOR * - STFU_NOTE_UPSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[4]); break; case SAPP_KEYCODE_N: - key_frequency = STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * - STFU_NOTE_DOWNSCALE_FACTOR * STFU_NOTE_DOWNSCALE_FACTOR * - STFU_NOTE_DOWNSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[5]); break; case SAPP_KEYCODE_M: - key_frequency = STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * - STFU_NOTE_DOWNSCALE_FACTOR * STFU_NOTE_DOWNSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[6]); break; case SAPP_KEYCODE_COMMA: - key_frequency = STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * - STFU_NOTE_DOWNSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[7]); break; case SAPP_KEYCODE_PERIOD: - key_frequency = STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR; + synth = stfu_press_trojka(synth, STFU_SCALE[8]); break; case SAPP_KEYCODE_SLASH: - key_frequency = STFU_A4_FREQUENCY; + synth = stfu_press_trojka(synth, STFU_SCALE[9]); break; default: break; } - phaser0 = stfu_set_phaser_frequency(phaser0, key_frequency); - phaser1 = stfu_set_phaser_frequency(phaser1, key_frequency * 0.25); - phaser2 = stfu_set_phaser_frequency(phaser2, key_frequency * 2.0); - envelope = stfu_trigger_envelope(envelope); - crossfade_frames_left = STFU_CROSSFADE_BUFFER_FRAMES; - break; } default: @@ -172,5 +143,6 @@ sapp_desc sokol_main(int argc, char *argv[]) { .gl_major_version = 2, .gl_minor_version = 0, // .swap_interval = 0, + .window_title = "stfu-maker", }; } diff --git a/Source/config.h b/Source/config.h index 84344e9..60af47c 100644 --- a/Source/config.h +++ b/Source/config.h @@ -1,3 +1,5 @@ +#if !defined(STFU_CONFIG) +#define STFU_CONFIG /* Determines output framerate, in times per second. */ #define STFU_AUDIO_FRAME_RATE 44100 @@ -10,6 +12,27 @@ /* Introduces delayed buffering for purposes of crossfading channels when notes * are retriggered, needed for filtering clicks */ -#define STFU_CROSSFADE_BUFFER_FRAMES 64 +#define STFU_CROSSFADE_BUFFER_FRAMES 32 -// todo: Array of precomputed note frequencies? +static const float STFU_SCALE[12] = { + STFU_C4_FREQUENCY, + STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR, + STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * + STFU_NOTE_UPSCALE_FACTOR, + STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * + STFU_NOTE_UPSCALE_FACTOR * STFU_NOTE_UPSCALE_FACTOR, + STFU_C4_FREQUENCY * STFU_NOTE_UPSCALE_FACTOR * + STFU_NOTE_UPSCALE_FACTOR * STFU_NOTE_UPSCALE_FACTOR * + STFU_NOTE_UPSCALE_FACTOR, + STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * + STFU_NOTE_DOWNSCALE_FACTOR * STFU_NOTE_DOWNSCALE_FACTOR * + STFU_NOTE_DOWNSCALE_FACTOR, + STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * + STFU_NOTE_DOWNSCALE_FACTOR * STFU_NOTE_DOWNSCALE_FACTOR, + STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR * + STFU_NOTE_DOWNSCALE_FACTOR, + STFU_A4_FREQUENCY * STFU_NOTE_DOWNSCALE_FACTOR, + STFU_A4_FREQUENCY, +}; + +#endif diff --git a/Source/envelope.c b/Source/envelope.c index 1c05c87..699d31e 100644 --- a/Source/envelope.c +++ b/Source/envelope.c @@ -1,3 +1,6 @@ +#if defined(STFU_MAIN) && !defined(STFU_ENVELOPE) +#define STFU_ENVELOPE + #include #include @@ -29,12 +32,13 @@ struct stfu_envelope { } struct stfu_envelope stfu_pump_envelope(struct stfu_envelope envelope) { - envelope.p += 1.0f / STFU_AUDIO_FRAME_RATE; float d = stfu_get_envelope_duration(envelope); if (envelope.p >= d) /* Needs retriggering after that point */ return envelope; + envelope.p += 1.0f / STFU_AUDIO_FRAME_RATE; + float p = 0.0f; for (uint8_t i = 0;; ++i) { float ends = p + envelope.pnts[i].dur; @@ -63,3 +67,5 @@ struct stfu_envelope stfu_trigger_envelope(struct stfu_envelope envelope) { envelope.v = 0.0f; return envelope; } + +#endif diff --git a/Source/interp.c b/Source/interp.c index dbe170a..2f81df7 100644 --- a/Source/interp.c +++ b/Source/interp.c @@ -1,4 +1,8 @@ +#if defined(STFU_MAIN) && !defined(STFU_INTERP) +#define STFU_INTERP float stfu_linear_interpolate(float x0, float y0, float x1, float y1, float n) { return (y0 * (x1 - n) + y1 * (n - x0)) / (x1 - x0); } + +#endif diff --git a/Source/oscillators.c b/Source/oscillators.c index 2fb2e4b..19f8726 100644 --- a/Source/oscillators.c +++ b/Source/oscillators.c @@ -1,10 +1,11 @@ +#if defined(STFU_MAIN) && !defined(STFU_OSCILLATORS) +#define STFU_OSCILLATORS + #include #include #include "config.h" -/* https://multimed.org/student/eim/en/04-FM.pdf */ - /* Intended to be executed offline with values then embedded in the binary. * By having usage of glibc sin and cos functions strictly offline it's easier * to have it freestanding @@ -76,3 +77,5 @@ struct stfu_sawtooth stfu_pump_sawtooth(struct stfu_sawtooth wave) { wave.v -= wave.a * 2.f; return wave; } + +#endif diff --git a/Source/phaser.c b/Source/phaser.c index 036c157..b4c0b7f 100644 --- a/Source/phaser.c +++ b/Source/phaser.c @@ -1,3 +1,6 @@ +#if defined(STFU_MAIN) && !defined(STFU_PHASER) +#define STFU_PHASER + #include #include "config.h" @@ -29,3 +32,5 @@ struct stfu_phaser stfu_set_phaser_frequency(struct stfu_phaser phaser, phaser.a = 2 * ((float)M_PI / (float)STFU_AUDIO_FRAME_RATE) * frequency; return phaser; } + +#endif diff --git a/Source/timer.c b/Source/timer.c index 78d609a..2111145 100644 --- a/Source/timer.c +++ b/Source/timer.c @@ -1,3 +1,6 @@ +#if defined(STFU_MAIN) && !defined(STFU_TIMER) +#define STFU_TIMER + #include #include "config.h" @@ -18,3 +21,5 @@ struct stfu_timer stfu_pump_timer(struct stfu_timer wave) { wave.v -= 1000; return wave; } + +#endif