v0.9.5 beta — full feature build ready for distribution
Core additions since initial commit: - Platform-split MIDI (midi_linux.c / midi_windows.c) - Patch system: save/load/scan, subdirectory navigation, favourites - Config system: last MIDI device and CC mappings persisted to disk - Custom embedded pixel font and sprite sheet (no loose asset files at runtime) - Window icon embedded at runtime (SetWindowIcon) and in Windows .exe (windres) - chdirToExeDir() in platform.c so double-click launch finds patches/ correctly - About screen accessible from Master panel UI polish: - TITLE_BAR_H constant (20 px) — sprite buttons now fit inside title bars - All title bar text, button overlay text, and close/active labels vertically centred - Full-screen dim overlay computed from camera transform (no more partial coverage) - Patch browser: folder navigation, breadcrumb title, 256-slot limit lifted Build: - Makefile auto-discovers sources; embeds sprites/font/icon via xxd rules - Windows cross-compile: make PLATFORM=windows (mingw-w64 + windres) - windows.h isolated in platform.c to avoid Rectangle/CloseWindow conflicts with raylib.h - WIN_RES uses lazy = assignment so OBJ_DIR expands correctly for windres output path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
438
source/synth.c
438
source/synth.c
@@ -2,6 +2,7 @@
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
@@ -9,26 +10,231 @@
|
||||
|
||||
void synthInit(Synth *s, float sampleRate)
|
||||
{
|
||||
s->sampleRate = sampleRate;
|
||||
s->attackSec = 0.005f;
|
||||
s->releaseSec = 0.050f;
|
||||
s->dutyCycle = 0.5f;
|
||||
s->waveform = WAVE_SINE;
|
||||
s->lastStolenVoice = 0;
|
||||
s->sampleRate = sampleRate;
|
||||
s->pitchBend = 0.0f;
|
||||
s->pitchBendRange = 2.0f;
|
||||
s->lastStolenVoice = 0;
|
||||
s->volume = 0.8f;
|
||||
|
||||
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||
s->voices[i].phase = 0.0f;
|
||||
s->voices[i].freqHz = 440.0f;
|
||||
s->voices[i].amp = 0.0f;
|
||||
s->voices[i].targetAmp = 0.0f;
|
||||
s->voices[i].active = 0;
|
||||
s->voices[i].midiNote = -1;
|
||||
s->pitchBend = 0.0f;
|
||||
s->pitchBendRange = 2.0f;
|
||||
s->voices[i].freqHz = 440.0f;
|
||||
s->voices[i].active = 0;
|
||||
s->voices[i].midiNote = -1;
|
||||
|
||||
oscillatorInit(&s->voices[i].oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||
oscillatorInit(&s->voices[i].oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f);
|
||||
|
||||
envelopeInit(&s->voices[i].ampEnv,
|
||||
0.005f, // attack
|
||||
0.10f, // decay
|
||||
0.70f, // sustain
|
||||
0.50f); // release
|
||||
|
||||
envelopeInit(&s->voices[i].modEnv,
|
||||
0.005f, // attack
|
||||
0.50f, // decay
|
||||
0.0f, // sustain
|
||||
0.10f); // release
|
||||
|
||||
s->voices[i].filter.cutoff = 8000.0f;
|
||||
s->voices[i].filter.resonance = 0.0f;
|
||||
s->voices[i].filter.type = FILTER_LOWPASS;
|
||||
s->voices[i].filter.active = 0;
|
||||
s->voices[i].filter.low = 0.0f;
|
||||
s->voices[i].filter.band = 0.0f;
|
||||
s->voices[i].filter.modRouting = MOD_SOURCE_NONE;
|
||||
s->voices[i].filter.modDepth = 0.0f;
|
||||
s->voices[i].filter.resModRouting = MOD_SOURCE_NONE;
|
||||
s->voices[i].filter.resModDepth = 0.0f;
|
||||
}
|
||||
s->voices[0].oscillators[0].active = 1;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
s->lfos[l].rate = 1.0f;
|
||||
s->lfos[l].waveform = WAVE_SINE;
|
||||
s->lfos[l].active = 0;
|
||||
s->lfos[l].noiseHeld = 0.0f;
|
||||
s->lfos[l].noisePhase = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float waveformSample(Waveform w, float phase, float dutyCycle)
|
||||
void synthResetPatch(Synth *s)
|
||||
{
|
||||
Voice *v = &s->voices[0];
|
||||
|
||||
oscillatorInit(&v->oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||
v->oscillators[0].active = 1;
|
||||
for (int m = 0; m < 3; m++) {
|
||||
v->oscillators[0].modRouting[m] = MOD_SOURCE_NONE;
|
||||
v->oscillators[0].modDepth[m] = 0.0f;
|
||||
}
|
||||
|
||||
oscillatorInit(&v->oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f);
|
||||
v->oscillators[1].active = 0;
|
||||
for (int m = 0; m < 3; m++) {
|
||||
v->oscillators[1].modRouting[m] = MOD_SOURCE_NONE;
|
||||
v->oscillators[1].modDepth[m] = 0.0f;
|
||||
}
|
||||
|
||||
envelopeInit(&v->ampEnv, 0.005f, 0.10f, 0.70f, 0.50f);
|
||||
envelopeInit(&v->modEnv, 0.005f, 0.50f, 0.0f, 0.10f);
|
||||
|
||||
v->filter.cutoff = 8000.0f;
|
||||
v->filter.resonance = 0.0f;
|
||||
v->filter.type = FILTER_LOWPASS;
|
||||
v->filter.active = 0;
|
||||
v->filter.low = 0.0f;
|
||||
v->filter.band = 0.0f;
|
||||
v->filter.modRouting = MOD_SOURCE_NONE;
|
||||
v->filter.modDepth = 0.0f;
|
||||
v->filter.resModRouting = MOD_SOURCE_NONE;
|
||||
v->filter.resModDepth = 0.0f;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
s->lfos[l].rate = 1.0f;
|
||||
s->lfos[l].waveform = WAVE_SINE;
|
||||
s->lfos[l].active = 0;
|
||||
s->lfos[l].noiseHeld = 0.0f;
|
||||
s->lfos[l].noisePhase = 0.0f;
|
||||
}
|
||||
|
||||
s->volume = 0.8f;
|
||||
s->pitchBendRange = 2.0f;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain)
|
||||
{
|
||||
o->phase = 0.0f;
|
||||
o->waveform = waveform;
|
||||
o->dutyCycle = dutyCycle;
|
||||
o->detune = detune;
|
||||
o->noiseHeld = 0.0f;
|
||||
o->noisePhase = 0.0f;
|
||||
o->gain = gain;
|
||||
o->octave = 0;
|
||||
}
|
||||
|
||||
|
||||
static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, ModSource source)
|
||||
{
|
||||
switch (source) {
|
||||
case MOD_SOURCE_AMP_ENV: return ampEnv;
|
||||
case MOD_SOURCE_MOD_ENV: return modEnv;
|
||||
case MOD_SOURCE_LFO: return lfo0;
|
||||
case MOD_SOURCE_LFO2: return lfo1;
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float lfoTick(LFO *l, float sampleRate)
|
||||
{
|
||||
if (!l->active) return 0.0f;
|
||||
l->phase += l->rate / sampleRate;
|
||||
if (l->phase >= 1.0f) l->phase -= 1.0f;
|
||||
if (l->waveform == WAVE_NOISE) {
|
||||
l->noisePhase += l->rate / sampleRate;
|
||||
if (l->noisePhase >= 1.0f) {
|
||||
l->noisePhase -= 1.0f;
|
||||
l->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f;
|
||||
}
|
||||
}
|
||||
return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld);
|
||||
}
|
||||
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate)
|
||||
{
|
||||
if (!f->active) return input;
|
||||
|
||||
if (cutoff < 20.0f) cutoff = 20.0f;
|
||||
if (cutoff > sampleRate * 0.499f) cutoff = sampleRate * 0.499f;
|
||||
if (resonance < 0.0f) resonance = 0.0f;
|
||||
if (resonance > 0.99f) resonance = 0.99f;
|
||||
|
||||
// Andy Simper TPT SVF (bilinear integration — unconditionally stable)
|
||||
float g = tanf((float)M_PI * cutoff / sampleRate);
|
||||
float Q = 0.5f + resonance * 9.5f; // resonance 0..0.99 → Q 0.5..10.0
|
||||
float k = 1.0f / Q;
|
||||
float a1 = 1.0f / (1.0f + g * (g + k));
|
||||
float a2 = g * a1;
|
||||
float a3 = g * a2;
|
||||
|
||||
// f->band = s1, f->low = s2 (integrator states)
|
||||
float v3 = input - f->low;
|
||||
float v1 = a1 * f->band + a2 * v3;
|
||||
float v2 = f->low + a2 * f->band + a3 * v3;
|
||||
f->band = 2.0f * v1 - f->band;
|
||||
f->low = 2.0f * v2 - f->low;
|
||||
|
||||
switch (f->type) {
|
||||
case FILTER_LOWPASS: return v2;
|
||||
case FILTER_HIGHPASS: return input - k * v1 - v2;
|
||||
case FILTER_BANDPASS: return v1;
|
||||
default: return v2;
|
||||
}
|
||||
}
|
||||
|
||||
const char *filterTypeName(FilterType t)
|
||||
{
|
||||
switch (t) {
|
||||
case FILTER_LOWPASS: return "LP";
|
||||
case FILTER_HIGHPASS: return "HP";
|
||||
case FILTER_BANDPASS: return "BP";
|
||||
default: return "??";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||
// float dutyCycle, float detune, float gain)
|
||||
// {
|
||||
// float detuneMultiplier = powf(2.0f, o->detune / 1200.0f);
|
||||
// float freq = freqHz * detuneMultiplier * bendMultiplier;
|
||||
//
|
||||
// // Advance phase
|
||||
// o->phase += freq / sampleRate;
|
||||
// if (o->phase >= 1.0f) o->phase -= 1.0f;
|
||||
//
|
||||
// // Clocked noise — draw a new random value once per cycle
|
||||
// if (o->waveform == WAVE_NOISE) {
|
||||
// o->noisePhase += freq / sampleRate;
|
||||
// if (o->noisePhase >= 1.0f) {
|
||||
// o->noisePhase -= 1.0f;
|
||||
// o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return waveformSample(o->waveform, o->phase, o->dutyCycle, o->noiseHeld) * o->gain;
|
||||
// }
|
||||
|
||||
float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||
float dutyCycle, float detune, float gain)
|
||||
{
|
||||
float detuneMultiplier = powf(2.0f, detune / 1200.0f);
|
||||
float freq = freqHz * detuneMultiplier * bendMultiplier;
|
||||
|
||||
// Advance phase
|
||||
o->phase += freq / sampleRate;
|
||||
if (o->phase >= 1.0f) o->phase -= 1.0f;
|
||||
|
||||
// Clocked noise — draw a new random value once per cycle
|
||||
if (o->waveform == WAVE_NOISE) {
|
||||
o->noisePhase += freq / sampleRate;
|
||||
if (o->noisePhase >= 1.0f) {
|
||||
o->noisePhase -= 1.0f;
|
||||
o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return waveformSample(o->waveform, o->phase, dutyCycle, o->noiseHeld) * gain;
|
||||
}
|
||||
|
||||
|
||||
|
||||
float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld)
|
||||
{
|
||||
switch (w) {
|
||||
case WAVE_SINE:
|
||||
@@ -44,7 +250,7 @@ float waveformSample(Waveform w, float phase, float dutyCycle)
|
||||
case WAVE_PULSE:
|
||||
return (phase < dutyCycle) ? 1.0f : -1.0f;
|
||||
case WAVE_NOISE:
|
||||
return ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f;
|
||||
return noiseHeld;
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
@@ -69,67 +275,95 @@ void synthNoteOn(Synth *s, int midiNote)
|
||||
|
||||
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||
if (!s->voices[i].active) {
|
||||
s->voices[i].freqHz = hz;
|
||||
s->voices[i].midiNote = midiNote;
|
||||
s->voices[i].targetAmp = 1.0f;
|
||||
s->voices[i].active = 1;
|
||||
s->voices[i].freqHz = hz;
|
||||
s->voices[i].midiNote = midiNote;
|
||||
s->voices[i].active = 1;
|
||||
s->voices[i].filter.low = 0.0f;
|
||||
s->voices[i].filter.band = 0.0f;
|
||||
envelopeNoteOn(&s->voices[i].ampEnv);
|
||||
envelopeNoteOn(&s->voices[i].modEnv);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No free voice — steal round-robin
|
||||
// Steal round-robin
|
||||
int i = s->lastStolenVoice % VOICE_COUNT;
|
||||
s->lastStolenVoice++;
|
||||
s->voices[i].freqHz = hz;
|
||||
s->voices[i].midiNote = midiNote;
|
||||
s->voices[i].targetAmp = 1.0f;
|
||||
s->voices[i].phase = 0.0f;
|
||||
s->voices[i].active = 1;
|
||||
s->voices[i].freqHz = hz;
|
||||
s->voices[i].midiNote = midiNote;
|
||||
s->voices[i].active = 1;
|
||||
s->voices[i].filter.low = 0.0f;
|
||||
s->voices[i].filter.band = 0.0f;
|
||||
envelopeNoteOn(&s->voices[i].ampEnv);
|
||||
envelopeNoteOn(&s->voices[i].modEnv);
|
||||
}
|
||||
|
||||
void synthNoteOff(Synth *s, int midiNote)
|
||||
{
|
||||
|
||||
void synthNoteOff(Synth *s, int midiNote) {
|
||||
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||
if (s->voices[i].active && s->voices[i].midiNote == midiNote)
|
||||
s->voices[i].targetAmp = 0.0f;
|
||||
if (s->voices[i].active && s->voices[i].midiNote == midiNote) {
|
||||
envelopeNoteOff(&s->voices[i].ampEnv);
|
||||
envelopeNoteOff(&s->voices[i].modEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames)
|
||||
{
|
||||
const float sr = s->sampleRate;
|
||||
const float attackInc = (s->attackSec <= 0.0f) ? 1.0f : (1.0f / (s->attackSec * sr));
|
||||
const float releaseInc = (s->releaseSec <= 0.0f) ? 1.0f : (1.0f / (s->releaseSec * sr));
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||
const float sr = s->sampleRate;
|
||||
const float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f);
|
||||
|
||||
for (int i = 0; i < frames; i++) {
|
||||
float mix = 0.0f;
|
||||
|
||||
float lfo0 = lfoTick(&s->lfos[0], sr);
|
||||
float lfo1 = lfoTick(&s->lfos[1], sr);
|
||||
|
||||
for (int v = 0; v < VOICE_COUNT; v++) {
|
||||
Voice *vv = &s->voices[v];
|
||||
if (!vv->active) continue;
|
||||
|
||||
if (vv->targetAmp > vv->amp) {
|
||||
vv->amp += attackInc;
|
||||
if (vv->amp > vv->targetAmp) vv->amp = vv->targetAmp;
|
||||
} else if (vv->targetAmp < vv->amp) {
|
||||
vv->amp -= releaseInc;
|
||||
if (vv->amp < vv->targetAmp) vv->amp = vv->targetAmp;
|
||||
}
|
||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||
float mod = envelopeTick(&vv->modEnv, sr);
|
||||
|
||||
if (vv->amp <= 0.0f && vv->targetAmp == 0.0f) {
|
||||
if (vv->ampEnv.stage == ENV_IDLE) {
|
||||
vv->active = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
//vv->phase += vv->freqHz / sr;
|
||||
float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f);
|
||||
vv->phase += (vv->freqHz * bendMultiplier) / sr;
|
||||
if (vv->phase >= 1.0f) vv->phase -= 1.0f;
|
||||
float oscMix = 0.0f;
|
||||
int activeOscs = 0;
|
||||
for (int o = 0; o < OSC_COUNT; o++) {
|
||||
Oscillator *osc = &vv->oscillators[o];
|
||||
if (!osc->active) continue;
|
||||
|
||||
mix += waveformSample(s->waveform, vv->phase, s->dutyCycle) * vv->amp;
|
||||
float dutyCycle = osc->dutyCycle +
|
||||
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[0]) * osc->modDepth[0];
|
||||
float detune = osc->detune + (float)osc->octave * 1200.0f +
|
||||
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[1]) * osc->modDepth[1];
|
||||
float gain = osc->gain +
|
||||
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[2]) * osc->modDepth[2];
|
||||
|
||||
if (dutyCycle < 0.05f) dutyCycle = 0.05f;
|
||||
if (dutyCycle > 0.95f) dutyCycle = 0.95f;
|
||||
if (gain < 0.0f) gain = 0.0f;
|
||||
if (gain > OSC_MAX_GAIN) gain = OSC_MAX_GAIN;
|
||||
|
||||
oscMix += oscillatorTick(osc, vv->freqHz, bendMultiplier, sr, dutyCycle, detune, gain);
|
||||
activeOscs++;
|
||||
}
|
||||
|
||||
if (activeOscs > 0) oscMix /= activeOscs;
|
||||
|
||||
float cutoff = vv->filter.cutoff +
|
||||
getModValue(amp, mod, lfo0, lfo1, vv->filter.modRouting) * vv->filter.modDepth;
|
||||
float resonance = vv->filter.resonance +
|
||||
getModValue(amp, mod, lfo0, lfo1, vv->filter.resModRouting) * vv->filter.resModDepth;
|
||||
oscMix = filterTick(&vv->filter, oscMix, cutoff, resonance, sr);
|
||||
|
||||
mix += oscMix * amp;
|
||||
}
|
||||
|
||||
mix *= (0.2f / VOICE_COUNT) * 4.0f;
|
||||
mix *= (0.2f / VOICE_COUNT) * 4.0f * s->volume;
|
||||
|
||||
int32_t sample = (int32_t)lrintf(mix * 32767.0f);
|
||||
if (sample > 32767) sample = 32767;
|
||||
@@ -137,3 +371,109 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames)
|
||||
out[i] = (int16_t)sample;
|
||||
}
|
||||
}
|
||||
|
||||
void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec)
|
||||
{
|
||||
e->stage = ENV_IDLE;
|
||||
e->value = 0.0f;
|
||||
e->attackSec = attackSec;
|
||||
e->decaySec = decaySec;
|
||||
e->sustainLevel = sustainLevel;
|
||||
e->releaseSec = releaseSec;
|
||||
}
|
||||
|
||||
void envelopeNoteOn(Envelope *e)
|
||||
{
|
||||
e->value = 0.0f;
|
||||
e->stage = ENV_ATTACK;
|
||||
}
|
||||
|
||||
void envelopeNoteOff(Envelope *e)
|
||||
{
|
||||
// Only trigger release if we're actually playing
|
||||
if (e->stage != ENV_IDLE)
|
||||
e->stage = ENV_RELEASE;
|
||||
}
|
||||
|
||||
float envelopeTick(Envelope *e, float sampleRate)
|
||||
{
|
||||
switch (e->stage) {
|
||||
case ENV_ATTACK: {
|
||||
float inc = (e->attackSec <= 0.0f) ? 1.0f : (1.0f / (e->attackSec * sampleRate));
|
||||
e->value += inc;
|
||||
if (e->value >= 1.0f) {
|
||||
e->value = 1.0f;
|
||||
e->stage = ENV_DECAY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENV_DECAY: {
|
||||
float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate));
|
||||
e->value -= inc;
|
||||
if (e->value <= e->sustainLevel) {
|
||||
e->value = e->sustainLevel;
|
||||
e->stage = ENV_SUSTAIN;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENV_SUSTAIN:
|
||||
e->value = e->sustainLevel;
|
||||
break;
|
||||
case ENV_RELEASE: {
|
||||
float inc = (e->releaseSec <= 0.0f) ? 1.0f : (1.0f / (e->releaseSec * sampleRate));
|
||||
e->value -= inc;
|
||||
if (e->value <= 0.0f) {
|
||||
e->value = 0.0f;
|
||||
e->stage = ENV_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENV_IDLE:
|
||||
e->value = 0.0f;
|
||||
break;
|
||||
}
|
||||
return e->value;
|
||||
}
|
||||
|
||||
void synthSyncVoices(Synth *s)
|
||||
{
|
||||
for (int v = 1; v < VOICE_COUNT; v++) {
|
||||
// Sync oscillator settings
|
||||
for (int o = 0; o < OSC_COUNT; o++) {
|
||||
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
|
||||
s->voices[v].oscillators[o].dutyCycle = s->voices[0].oscillators[o].dutyCycle;
|
||||
s->voices[v].oscillators[o].detune = s->voices[0].oscillators[o].detune;
|
||||
s->voices[v].oscillators[o].gain = s->voices[0].oscillators[o].gain;
|
||||
s->voices[v].oscillators[o].active = s->voices[0].oscillators[o].active;
|
||||
s->voices[v].oscillators[o].octave = s->voices[0].oscillators[o].octave;
|
||||
s->voices[v].oscillators[o].modRouting[0] = s->voices[0].oscillators[o].modRouting[0];
|
||||
s->voices[v].oscillators[o].modRouting[1] = s->voices[0].oscillators[o].modRouting[1];
|
||||
s->voices[v].oscillators[o].modRouting[2] = s->voices[0].oscillators[o].modRouting[2];
|
||||
s->voices[v].oscillators[o].modDepth[0] = s->voices[0].oscillators[o].modDepth[0];
|
||||
s->voices[v].oscillators[o].modDepth[1] = s->voices[0].oscillators[o].modDepth[1];
|
||||
s->voices[v].oscillators[o].modDepth[2] = s->voices[0].oscillators[o].modDepth[2];
|
||||
}
|
||||
|
||||
// Sync filter params but not state (low/band are per-voice)
|
||||
s->voices[v].filter.cutoff = s->voices[0].filter.cutoff;
|
||||
s->voices[v].filter.resonance = s->voices[0].filter.resonance;
|
||||
s->voices[v].filter.type = s->voices[0].filter.type;
|
||||
s->voices[v].filter.active = s->voices[0].filter.active;
|
||||
s->voices[v].filter.modRouting = s->voices[0].filter.modRouting;
|
||||
s->voices[v].filter.modDepth = s->voices[0].filter.modDepth;
|
||||
s->voices[v].filter.resModRouting = s->voices[0].filter.resModRouting;
|
||||
s->voices[v].filter.resModDepth = s->voices[0].filter.resModDepth;
|
||||
|
||||
// Sync envelope settings but NOT runtime state
|
||||
// Each voice needs its own stage, value — just copy the parameters
|
||||
s->voices[v].ampEnv.attackSec = s->voices[0].ampEnv.attackSec;
|
||||
s->voices[v].ampEnv.decaySec = s->voices[0].ampEnv.decaySec;
|
||||
s->voices[v].ampEnv.sustainLevel = s->voices[0].ampEnv.sustainLevel;
|
||||
s->voices[v].ampEnv.releaseSec = s->voices[0].ampEnv.releaseSec;
|
||||
// Sync the mod envelope, too.
|
||||
s->voices[v].modEnv.attackSec = s->voices[0].modEnv.attackSec;
|
||||
s->voices[v].modEnv.decaySec = s->voices[0].modEnv.decaySec;
|
||||
s->voices[v].modEnv.sustainLevel = s->voices[0].modEnv.sustainLevel;
|
||||
s->voices[v].modEnv.releaseSec = s->voices[0].modEnv.releaseSec;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user