CyberMatris port for Miyoo Mini Plus
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

802 lines
32 KiB

#include "Synth.hpp"
#include <cmath>
#include <cstdlib>
#include <algorithm>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Midi note number to frequency conversion helper
static inline float midiToFreq(int note) {
if (note <= 0) return 0.0f;
return 440.0f * std::pow(2.0f, (note - 69.0f) / 12.0f);
}
Synth::Synth() {
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) {
mVisBuffer[i] = 0.0f;
}
initSequencer();
}
Synth::~Synth() {
if (mAudioDevice > 0) {
SDL_CloseAudioDevice(mAudioDevice);
}
}
bool Synth::init() {
SDL_AudioSpec wanted, obtained;
SDL_zero(wanted);
wanted.freq = 44100;
wanted.format = AUDIO_S16SYS; // Signed 16-bit, native endian
wanted.channels = 2; // Stereo
#ifdef MIYOO_BUILD
wanted.samples = 1024; // Larger buffer size to satisfy Miyoo MI_AO requirements
#else
wanted.samples = 256; // Ultra low latency
#endif
wanted.callback = &Synth::audioCallbackWrapper;
wanted.userdata = this;
mAudioDevice = SDL_OpenAudioDevice(nullptr, 0, &wanted, &obtained, 0);
if (mAudioDevice == 0) {
return false;
}
// Start playing silence initially
SDL_PauseAudioDevice(mAudioDevice, 0);
return true;
}
void Synth::playBGM(bool play) {
std::lock_guard<std::mutex> lock(mMutex);
mBGMEnabled = play;
if (!play) {
// Stop music voices (0, 1, 2, 3)
for (int i = 0; i < 4; ++i) {
mVoices[i].active = false;
}
}
}
void Synth::stopAllSFX() {
std::lock_guard<std::mutex> lock(mMutex);
for (int i = 4; i < NUM_VOICES; ++i) {
mVoices[i].active = false;
}
}
void Synth::shufflePlaylist() {
mPlaylist[0] = 0; // Always anchor with original theme
int variants[] = {1, 2, 3, 4, 5};
std::random_shuffle(std::begin(variants), std::end(variants));
mPlaylist[1] = variants[0];
mPlaylist[2] = variants[1];
mPlaylist[3] = variants[2];
}
void Synth::initSequencer() {
mSecsPerTick = 60.0f / (mBPM * 2.0f);
mTickTimer = 0.0f;
mCurrentTick = 0;
for (int p = 0; p < NUM_PATTERNS; ++p) {
for (int i = 0; i < 64; ++i) {
mPatternLead[p][i] = 0;
mPatternBass[p][i] = 0;
}
for (int b = 0; b < 8; ++b) {
mPatternChords[p][b] = ChordType::AM;
}
}
// PATTERN 0: Original Korobeiniki
mPatternLead[0][0] = 76; mPatternLead[0][1] = 76; mPatternLead[0][2] = 71; mPatternLead[0][3] = 72;
mPatternLead[0][4] = 74; mPatternLead[0][5] = 74; mPatternLead[0][6] = 72; mPatternLead[0][7] = 71;
mPatternLead[0][8] = 69; mPatternLead[0][9] = 69; mPatternLead[0][10] = 69; mPatternLead[0][11] = 72;
mPatternLead[0][12] = 76; mPatternLead[0][13] = 76; mPatternLead[0][14] = 74; mPatternLead[0][15] = 72;
mPatternLead[0][16] = 71; mPatternLead[0][17] = 71; mPatternLead[0][18] = 71; mPatternLead[0][19] = 72;
mPatternLead[0][20] = 74; mPatternLead[0][21] = 74; mPatternLead[0][22] = 76; mPatternLead[0][23] = 76;
mPatternLead[0][24] = 72; mPatternLead[0][25] = 72; mPatternLead[0][26] = 69; mPatternLead[0][27] = 69;
mPatternLead[0][28] = 69;
mPatternLead[0][32] = 74; mPatternLead[0][33] = 74; mPatternLead[0][34] = 74; mPatternLead[0][35] = 77;
mPatternLead[0][36] = 81; mPatternLead[0][37] = 81; mPatternLead[0][38] = 79; mPatternLead[0][39] = 77;
mPatternLead[0][40] = 76; mPatternLead[0][41] = 76; mPatternLead[0][42] = 72; mPatternLead[0][43] = 72;
mPatternLead[0][44] = 76; mPatternLead[0][45] = 76; mPatternLead[0][46] = 74; mPatternLead[0][47] = 72;
mPatternLead[0][48] = 71; mPatternLead[0][49] = 71; mPatternLead[0][50] = 71; mPatternLead[0][51] = 72;
mPatternLead[0][52] = 74; mPatternLead[0][53] = 74; mPatternLead[0][54] = 76; mPatternLead[0][55] = 76;
mPatternLead[0][56] = 72; mPatternLead[0][57] = 72; mPatternLead[0][58] = 69; mPatternLead[0][59] = 69;
mPatternLead[0][60] = 69;
mPatternBass[0][0] = 45; mPatternBass[0][2] = 57; mPatternBass[0][4] = 45; mPatternBass[0][6] = 57;
mPatternBass[0][8] = 45; mPatternBass[0][10] = 57; mPatternBass[0][12] = 45; mPatternBass[0][14] = 57;
mPatternBass[0][16] = 40; mPatternBass[0][18] = 52; mPatternBass[0][20] = 40; mPatternBass[0][22] = 52;
mPatternBass[0][24] = 45; mPatternBass[0][26] = 57; mPatternBass[0][28] = 45; mPatternBass[0][30] = 45;
mPatternBass[0][32] = 50; mPatternBass[0][34] = 62; mPatternBass[0][36] = 50; mPatternBass[0][38] = 62;
mPatternBass[0][40] = 45; mPatternBass[0][42] = 57; mPatternBass[0][44] = 45; mPatternBass[0][46] = 57;
mPatternBass[0][48] = 40; mPatternBass[0][50] = 52; mPatternBass[0][52] = 40; mPatternBass[0][54] = 52;
mPatternBass[0][56] = 45; mPatternBass[0][58] = 57; mPatternBass[0][60] = 45; mPatternBass[0][62] = 45;
mPatternChords[0][0] = ChordType::AM; mPatternChords[0][1] = ChordType::AM;
mPatternChords[0][2] = ChordType::E7; mPatternChords[0][3] = ChordType::AM;
mPatternChords[0][4] = ChordType::DM; mPatternChords[0][5] = ChordType::AM;
mPatternChords[0][6] = ChordType::E7; mPatternChords[0][7] = ChordType::AM;
// PATTERN 1: Fast octave higher walking bass
for(int i=0; i<64; ++i) {
if(mPatternLead[0][i] > 0) mPatternLead[1][i] = mPatternLead[0][i] + 12;
}
mPatternLead[1][32] = 86; mPatternLead[1][33] = 0; mPatternLead[1][34] = 86; mPatternLead[1][35] = 89;
mPatternLead[1][36] = 93; mPatternLead[1][37] = 0; mPatternLead[1][38] = 91; mPatternLead[1][39] = 89;
mPatternLead[1][40] = 88; mPatternLead[1][41] = 0; mPatternLead[1][42] = 84; mPatternLead[1][43] = 84;
mPatternLead[1][44] = 88; mPatternLead[1][45] = 0; mPatternLead[1][46] = 86; mPatternLead[1][47] = 84;
mPatternBass[1][0] = 45; mPatternBass[1][2] = 57; mPatternBass[1][4] = 60; mPatternBass[1][6] = 57;
mPatternBass[1][8] = 45; mPatternBass[1][10] = 57; mPatternBass[1][12] = 60; mPatternBass[1][14] = 57;
mPatternBass[1][16] = 40; mPatternBass[1][18] = 52; mPatternBass[1][20] = 56; mPatternBass[1][22] = 52;
mPatternBass[1][24] = 45; mPatternBass[1][26] = 57; mPatternBass[1][28] = 45; mPatternBass[1][30] = 45;
mPatternBass[1][32] = 50; mPatternBass[1][34] = 62; mPatternBass[1][36] = 65; mPatternBass[1][38] = 62;
mPatternBass[1][40] = 45; mPatternBass[1][42] = 57; mPatternBass[1][44] = 60; mPatternBass[1][46] = 57;
mPatternBass[1][48] = 40; mPatternBass[1][50] = 52; mPatternBass[1][52] = 56; mPatternBass[1][54] = 52;
mPatternBass[1][56] = 45; mPatternBass[1][58] = 57; mPatternBass[1][60] = 45; mPatternBass[1][62] = 45;
for(int i=0; i<8; ++i) mPatternChords[1][i] = mPatternChords[0][i];
// PATTERN 2: Bridge Motif
mPatternLead[2][0] = 81; mPatternLead[2][2] = 84; mPatternLead[2][4] = 89; mPatternLead[2][6] = 84;
mPatternBass[2][0] = 41; mPatternBass[2][2] = 53; mPatternBass[2][4] = 41; mPatternBass[2][6] = 53;
mPatternLead[2][8] = 79; mPatternLead[2][10] = 83; mPatternLead[2][12] = 86; mPatternLead[2][14] = 83;
mPatternBass[2][8] = 43; mPatternBass[2][10] = 55; mPatternBass[2][12] = 43; mPatternBass[2][14] = 55;
mPatternLead[2][16] = 84; mPatternLead[2][18] = 88; mPatternLead[2][20] = 91; mPatternLead[2][22] = 88;
mPatternBass[2][16] = 48; mPatternBass[2][18] = 60; mPatternBass[2][20] = 48; mPatternBass[2][22] = 60;
mPatternLead[2][24] = 81; mPatternLead[2][26] = 84; mPatternLead[2][28] = 88; mPatternLead[2][30] = 84;
mPatternBass[2][24] = 45; mPatternBass[2][26] = 57; mPatternBass[2][28] = 45; mPatternBass[2][30] = 57;
mPatternLead[2][32] = 89; mPatternLead[2][34] = 86; mPatternLead[2][36] = 81; mPatternLead[2][38] = 86;
mPatternBass[2][32] = 50; mPatternBass[2][34] = 62; mPatternBass[2][36] = 50; mPatternBass[2][38] = 62;
mPatternLead[2][40] = 88; mPatternLead[2][42] = 84; mPatternLead[2][44] = 81; mPatternLead[2][46] = 84;
mPatternBass[2][40] = 45; mPatternBass[2][42] = 57; mPatternBass[2][44] = 45; mPatternBass[2][46] = 57;
mPatternLead[2][48] = 88; mPatternLead[2][50] = 86; mPatternLead[2][52] = 83; mPatternLead[2][54] = 80;
mPatternBass[2][48] = 40; mPatternBass[2][50] = 52; mPatternBass[2][52] = 40; mPatternBass[2][54] = 52;
mPatternLead[2][56] = 81; mPatternLead[2][57] = 81; mPatternLead[2][58] = 81;
mPatternBass[2][56] = 45; mPatternBass[2][58] = 45; mPatternBass[2][60] = 45;
mPatternChords[2][0] = ChordType::F; mPatternChords[2][1] = ChordType::G;
mPatternChords[2][2] = ChordType::C; mPatternChords[2][3] = ChordType::AM;
mPatternChords[2][4] = ChordType::DM; mPatternChords[2][5] = ChordType::AM;
mPatternChords[2][6] = ChordType::E7; mPatternChords[2][7] = ChordType::AM;
// PATTERN 3: Driving Resolution
mPatternLead[3][0] = 81; mPatternLead[3][1] = 79; mPatternLead[3][2] = 77; mPatternLead[3][3] = 81;
mPatternLead[3][4] = 84; mPatternLead[3][5] = 81; mPatternLead[3][6] = 77; mPatternLead[3][7] = 81;
mPatternBass[3][0] = 41; mPatternBass[3][2] = 53; mPatternBass[3][4] = 57; mPatternBass[3][6] = 53;
mPatternLead[3][8] = 83; mPatternLead[3][9] = 81; mPatternLead[3][10] = 79; mPatternLead[3][11] = 83;
mPatternLead[3][12] = 86; mPatternLead[3][13] = 83; mPatternLead[3][14] = 79; mPatternLead[3][15] = 83;
mPatternBass[3][8] = 43; mPatternBass[3][10] = 55; mPatternBass[3][12] = 59; mPatternBass[3][14] = 55;
mPatternLead[3][16] = 84; mPatternLead[3][17] = 83; mPatternLead[3][18] = 84; mPatternLead[3][19] = 86;
mPatternLead[3][20] = 88; mPatternLead[3][21] = 86; mPatternLead[3][22] = 84; mPatternLead[3][23] = 88;
mPatternBass[3][16] = 48; mPatternBass[3][18] = 60; mPatternBass[3][20] = 64; mPatternBass[3][22] = 60;
mPatternLead[3][24] = 81; mPatternLead[3][25] = 80; mPatternLead[3][26] = 81; mPatternLead[3][27] = 83;
mPatternLead[3][28] = 84; mPatternLead[3][29] = 83; mPatternLead[3][30] = 81; mPatternLead[3][31] = 84;
mPatternBass[3][24] = 45; mPatternBass[3][26] = 57; mPatternBass[3][28] = 60; mPatternBass[3][30] = 57;
mPatternLead[3][32] = 86; mPatternLead[3][33] = 84; mPatternLead[3][34] = 86; mPatternLead[3][35] = 88;
mPatternLead[3][36] = 89; mPatternLead[3][37] = 88; mPatternLead[3][38] = 86; mPatternLead[3][39] = 89;
mPatternBass[3][32] = 50; mPatternBass[3][34] = 62; mPatternBass[3][36] = 65; mPatternBass[3][38] = 62;
mPatternLead[3][40] = 88; mPatternLead[3][41] = 86; mPatternLead[3][42] = 84; mPatternLead[3][43] = 83;
mPatternLead[3][44] = 81; mPatternLead[3][45] = 84; mPatternLead[3][46] = 88; mPatternLead[3][47] = 84;
mPatternBass[3][40] = 45; mPatternBass[3][42] = 57; mPatternBass[3][44] = 60; mPatternBass[3][46] = 57;
mPatternLead[3][48] = 83; mPatternLead[3][49] = 81; mPatternLead[3][50] = 80; mPatternLead[3][51] = 81;
mPatternLead[3][52] = 83; mPatternLead[3][53] = 84; mPatternLead[3][54] = 86; mPatternLead[3][55] = 83;
mPatternBass[3][48] = 40; mPatternBass[3][50] = 52; mPatternBass[3][52] = 56; mPatternBass[3][54] = 52;
mPatternLead[3][56] = 81; mPatternLead[3][58] = 81; mPatternLead[3][60] = 69;
mPatternBass[3][56] = 45; mPatternBass[3][58] = 45; mPatternBass[3][60] = 45;
for(int i=0; i<8; ++i) mPatternChords[3][i] = mPatternChords[2][i];
// PATTERN 4: Slow menacing / half-time feel of original
for(int i=0; i<8; ++i) mPatternChords[4][i] = mPatternChords[0][i];
mPatternLead[4][0] = 64; mPatternLead[4][4] = 59; mPatternLead[4][8] = 60; mPatternLead[4][12] = 62;
mPatternLead[4][16] = 59; mPatternLead[4][20] = 60; mPatternLead[4][24] = 57; mPatternLead[4][28] = 57;
mPatternLead[4][32] = 62; mPatternLead[4][36] = 65; mPatternLead[4][40] = 64; mPatternLead[4][44] = 60;
mPatternLead[4][48] = 59; mPatternLead[4][52] = 60; mPatternLead[4][56] = 57; mPatternLead[4][60] = 57;
mPatternBass[4][0] = 33; mPatternBass[4][8] = 33;
mPatternBass[4][16] = 28; mPatternBass[4][24] = 33;
mPatternBass[4][32] = 38; mPatternBass[4][40] = 33;
mPatternBass[4][48] = 28; mPatternBass[4][56] = 33;
// PATTERN 5: Frantic Arpeggiator (uses Bridge chords)
for(int i=0; i<8; ++i) mPatternChords[5][i] = mPatternChords[2][i];
for(int b=0; b<8; ++b) {
int root = 0;
switch(mPatternChords[5][b]) {
case ChordType::F: root = 77; break;
case ChordType::G: root = 79; break;
case ChordType::C: root = 84; break;
case ChordType::AM: root = 81; break;
case ChordType::DM: root = 86; break;
case ChordType::E7: root = 80; break;
}
for(int t=0; t<8; ++t) {
if(t%4 == 0) mPatternLead[5][b*8+t] = root;
else if(t%4 == 1) mPatternLead[5][b*8+t] = root+4;
else if(t%4 == 2) mPatternLead[5][b*8+t] = root+7;
else if(t%4 == 3) mPatternLead[5][b*8+t] = root+12;
}
if(mPatternChords[5][b] == ChordType::AM || mPatternChords[5][b] == ChordType::DM) {
mPatternLead[5][b*8+1] -= 1;
mPatternLead[5][b*8+5] -= 1;
}
mPatternBass[5][b*8] = root - 36;
mPatternBass[5][b*8+2] = root - 24;
mPatternBass[5][b*8+4] = root - 36;
mPatternBass[5][b*8+6] = root - 24;
}
shufflePlaylist();
}
void Synth::triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan, float volume) {
if (midiNote == 0) {
if (mVoices[voiceIdx].active && !mVoices[voiceIdx].releasing) {
mVoices[voiceIdx].releasing = true;
mVoices[voiceIdx].releaseAge = 0.0f;
}
return;
}
Voice& v = mVoices[voiceIdx];
v.active = true;
v.wave = wave;
v.frequency = midiToFreq(midiNote);
v.phase = 0.0f;
v.volume = volume;
v.pan = pan;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.sweepDuration = 0.0f;
// Default music envelopes
if (voiceIdx == 0) { // Lead
v.adsr.attackTime = 0.005f;
v.adsr.decayTime = 0.03f;
v.adsr.sustainLevel = 0.6f;
v.adsr.releaseTime = duration * 0.4f;
// Add a soft vibrato for the lead melody
v.vibratoFreq = 6.0f; // 6 Hz LFO
v.vibratoDepth = 0.008f; // ~0.8% frequency wobble
} else if (voiceIdx == 1) { // Harmony
v.adsr.attackTime = 0.01f;
v.adsr.decayTime = 0.05f;
v.adsr.sustainLevel = 0.5f;
v.adsr.releaseTime = duration * 0.3f;
v.vibratoDepth = 0.0f;
} else if (voiceIdx == 2) { // Bass
v.adsr.attackTime = 0.005f;
v.adsr.decayTime = 0.04f;
v.adsr.sustainLevel = 0.7f;
v.adsr.releaseTime = duration * 0.2f;
v.vibratoDepth = 0.0f;
}
}
void Synth::updateSequencer(float dt) {
if (!mBGMEnabled) return;
mTickTimer += dt;
if (mTickTimer >= mSecsPerTick) {
mTickTimer -= mSecsPerTick;
// Advance sequencer tick
mCurrentTick = (mCurrentTick + 1) % 256;
if (mCurrentTick == 0) {
shufflePlaylist();
}
int patternIdx = mPlaylist[mCurrentTick / 64];
int stepInPattern = mCurrentTick % 64;
// 1. Trigger Lead Note (Voice 0)
uint8_t leadNote = mPatternLead[patternIdx][stepInPattern];
if (leadNote > 0) {
triggerMusicNote(0, leadNote, WaveType::SQUARE, mSecsPerTick, 0.35f, 0.12f);
} else {
if (mVoices[0].active && !mVoices[0].releasing) {
mVoices[0].releasing = true;
mVoices[0].releaseAge = 0.0f;
}
}
// 2. Trigger Bass Note (Voice 2)
uint8_t bassNote = mPatternBass[patternIdx][stepInPattern];
if (bassNote > 0) {
triggerMusicNote(2, bassNote, WaveType::TRIANGLE, mSecsPerTick, 0.5f, 0.22f);
} else {
if (mVoices[2].active && !mVoices[2].releasing) {
mVoices[2].releasing = true;
mVoices[2].releaseAge = 0.0f;
}
}
// 3. Trigger Harmony/Chord Arpeggio (Voice 1)
int subTick = mCurrentTick % 4; // arpeggiate at 8th note speed
uint8_t harmonyNote = 0;
ChordType currentChord = mPatternChords[patternIdx][stepInPattern / 8];
if (currentChord == ChordType::F) {
uint8_t arpeggio[] = {53, 57, 60, 57}; // F3, A3, C4, A3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::G) {
uint8_t arpeggio[] = {55, 59, 62, 59}; // G3, B3, D4, B3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::C) {
uint8_t arpeggio[] = {48, 55, 60, 55}; // C3, G3, C4, G3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::DM) {
uint8_t arpeggio[] = {57, 62, 65, 62}; // A3, D4, F4, D4
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::E7) {
uint8_t arpeggio[] = {56, 59, 64, 59}; // G#3, B3, E4, B3
harmonyNote = arpeggio[subTick];
} else { // AM
uint8_t arpeggio[] = {57, 60, 64, 60}; // A3, C4, E4, C4
harmonyNote = arpeggio[subTick];
}
triggerMusicNote(1, harmonyNote, WaveType::TRIANGLE, mSecsPerTick * 0.9f, 0.65f, 0.08f);
// 4. Trigger Procedural Retro Drums (Voice 3) - Kick, Hat, Snare synthesis!
int drumTick = mCurrentTick % 8;
if (drumTick == 0) { // Kick drum: fast triangle frequency sweep (A1 55Hz to A0 27Hz)
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::TRIANGLE;
v.frequency = 120.0f;
v.startFreq = 120.0f;
v.targetFreq = 30.0f;
v.sweepDuration = 0.09f;
v.sweepAge = 0.0f;
v.phase = 0.0f;
v.volume = 0.45f;
v.pan = 0.5f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.adsr.attackTime = 0.002f;
v.adsr.decayTime = 0.08f;
v.adsr.sustainLevel = 0.1f;
v.adsr.releaseTime = 0.05f;
} else if (drumTick == 4) { // Snare drum: White noise burst + Triangle pop
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::NOISE;
v.frequency = 100.0f; // dummy frequency
v.phase = 0.0f;
v.volume = 0.26f;
v.pan = 0.5f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.sweepDuration = 0.0f;
v.adsr.attackTime = 0.001f;
v.adsr.decayTime = 0.12f;
v.adsr.sustainLevel = 0.05f;
v.adsr.releaseTime = 0.04f;
} else if (drumTick == 2 || drumTick == 6 || drumTick == 7) { // Hi-Hat: very fast noise pop
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::NOISE;
v.frequency = 100.0f; // dummy
v.phase = 0.0f;
v.volume = 0.09f;
v.pan = 0.55f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.sweepDuration = 0.0f;
v.adsr.attackTime = 0.001f;
v.adsr.decayTime = 0.025f;
v.adsr.sustainLevel = 0.0f;
v.adsr.releaseTime = 0.015f;
}
}
}
void Synth::triggerSFX(SFXType type) {
std::lock_guard<std::mutex> lock(mMutex);
// Choose one of the dedicated SFX voices (Voices 4, 5, 6, 7) based on sound type
// to prevent overlap truncation.
int voiceIdx = 4;
Voice* v = &mVoices[voiceIdx];
switch (type) {
case SFXType::MOVE: // Voice 4: quick, quiet triangle pluck
v->active = true;
v->wave = WaveType::TRIANGLE;
v->frequency = 180.0f;
v->phase = 0.0f;
v->volume = 0.22f;
v->pan = 0.45f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->sweepDuration = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.001f;
v->adsr.decayTime = 0.04f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.02f;
break;
case SFXType::ROTATE: // Voice 5: quick laser frequency sweep
voiceIdx = 5;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = 330.0f;
v->startFreq = 330.0f;
v->targetFreq = 660.0f;
v->sweepDuration = 0.08f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.07f;
v->pan = 0.55f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.002f;
v->adsr.decayTime = 0.06f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.03f;
break;
case SFXType::LAND: // Voice 4: sub-bass thud (sine decay)
v->active = true;
v->wave = WaveType::SINE;
v->frequency = 90.0f;
v->startFreq = 90.0f;
v->targetFreq = 45.0f;
v->sweepDuration = 0.08f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.35f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.003f;
v->adsr.decayTime = 0.10f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.04f;
break;
case SFXType::LINE_CLEAR: // Voice 6: rapid, bright ascending arpeggio (C5-E5-G5-C6)
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(72); // C5
v->startFreq = midiToFreq(72);
v->targetFreq = midiToFreq(84); // sweeps to C6
v->sweepDuration = 0.22f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.11f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.18f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.10f;
break;
case SFXType::TETRIS_CLEAR: // Voice 6: dramatic multi-freq sweep + noise chord
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(72); // C5
v->startFreq = midiToFreq(72);
v->targetFreq = midiToFreq(96); // sweeps high! C7
v->sweepDuration = 0.40f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.13f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.015f; // extreme vibrato!
v->vibratoFreq = 12.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.35f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.20f;
// Trigger extra white noise burst on SFX voice 7 for explosive impact
v = &mVoices[7];
v->active = true;
v->wave = WaveType::NOISE;
v->phase = 0.0f;
v->volume = 0.22f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->sweepDuration = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.30f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
break;
case SFXType::LEVEL_UP: // Voice 6: triumphant two-note chord fanfares
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(76); // E5
v->startFreq = midiToFreq(76);
v->targetFreq = midiToFreq(88); // Sweeps to E6
v->sweepDuration = 0.30f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.11f;
v->pan = 0.40f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.20f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
// Trigger third note harmony on voice 7
v = &mVoices[7];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(80); // G#5
v->startFreq = midiToFreq(80);
v->targetFreq = midiToFreq(92); // Sweeps to G#6
v->sweepDuration = 0.30f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.10f;
v->pan = 0.60f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.20f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
break;
case SFXType::GAME_OVER: // Voice 6: sad, sliding down detuned chord
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::TRIANGLE;
v->frequency = 220.0f;
v->startFreq = 220.0f;
v->targetFreq = 80.0f; // sweep down
v->sweepDuration = 0.80f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.35f;
v->pan = 0.40f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.02f; // highly detuned detuning wobble
v->vibratoFreq = 8.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.60f;
v->adsr.sustainLevel = 0.2f; // Game Over chord detunes and sustains nicely!
v->adsr.releaseTime = 0.40f;
// Voice 7 plays detuned counter note
v = &mVoices[7];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = 233.0f; // slightly detuned semitone above
v->startFreq = 233.0f;
v->targetFreq = 83.0f; // sweep down
v->sweepDuration = 0.80f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.08f;
v->pan = 0.60f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.60f;
v->adsr.sustainLevel = 0.2f;
v->adsr.releaseTime = 0.40f;
break;
}
}
void Synth::updateVoice(Voice& voice, float dt) {
if (!voice.active) return;
if (voice.releasing) {
voice.releaseAge += dt;
if (voice.releaseAge >= voice.adsr.releaseTime) {
voice.active = false;
return;
}
} else {
voice.age += dt;
if (voice.adsr.sustainLevel == 0.0f && voice.age >= voice.adsr.attackTime + voice.adsr.decayTime) {
voice.active = false;
return;
}
}
// Apply frequency sweeps if active
if (voice.sweepDuration > 0.0f) {
voice.sweepAge += dt;
if (voice.sweepAge >= voice.sweepDuration) {
voice.frequency = voice.targetFreq;
voice.sweepDuration = 0.0f;
} else {
float t = voice.sweepAge / voice.sweepDuration;
voice.frequency = voice.startFreq + (voice.targetFreq - voice.startFreq) * t;
}
}
}
float Synth::generateSample(Voice& voice) {
if (!voice.active) return 0.0f;
// Calculate ADSR envelope volume multiplier
float envVolume = 0.0f;
if (voice.releasing) {
if (voice.releaseAge < voice.adsr.releaseTime) {
float progress = voice.releaseAge / voice.adsr.releaseTime;
envVolume = voice.adsr.sustainLevel * (1.0f - progress);
}
} else {
float age = voice.age;
if (age < voice.adsr.attackTime) {
envVolume = age / voice.adsr.attackTime;
} else if (age < voice.adsr.attackTime + voice.adsr.decayTime) {
float progress = (age - voice.adsr.attackTime) / voice.adsr.decayTime;
envVolume = 1.0f - (1.0f - voice.adsr.sustainLevel) * progress;
} else {
envVolume = voice.adsr.sustainLevel;
}
}
if (envVolume <= 0.0f) {
return 0.0f;
}
// Apply vibrato modulation
float modulatedFreq = voice.frequency;
if (voice.vibratoDepth > 0.0f) {
float lfo = std::sin(voice.age * 2.0f * M_PI * voice.vibratoFreq);
modulatedFreq += lfo * voice.vibratoDepth * voice.frequency;
}
// Phase increment
float phaseInc = 2.0f * M_PI * modulatedFreq / 44100.0f;
voice.phase += phaseInc;
if (voice.phase >= 2.0f * M_PI) {
voice.phase -= 2.0f * M_PI;
}
// Oscillator waveform generation
float oscValue = 0.0f;
switch (voice.wave) {
case WaveType::SINE:
oscValue = std::sin(voice.phase);
break;
case WaveType::SQUARE:
// Custom pulse width modulation (50% standard duty cycle, slightly narrow)
oscValue = (std::sin(voice.phase) >= 0.1f ? 1.0f : -1.0f);
break;
case WaveType::TRIANGLE: {
float normalizedPhase = voice.phase / (2.0f * M_PI);
oscValue = 4.0f * std::abs(normalizedPhase - std::floor(normalizedPhase + 0.5f)) - 1.0f;
break;
}
case WaveType::NOISE:
oscValue = (((float)std::rand() / RAND_MAX) * 2.0f - 1.0f);
break;
}
return oscValue * voice.volume * envVolume;
}
void Synth::audioCallback(Uint8* stream, int len) {
int frames = len / 4; // 16-bit signed stereo = 4 bytes per frame
int16_t* out = (int16_t*)stream;
float dt = 1.0f / 44100.0f;
std::lock_guard<std::mutex> lock(mMutex);
for (int f = 0; f < frames; ++f) {
// Advance chiptune sequencer clock
updateSequencer(dt);
float leftMixed = 0.0f;
float rightMixed = 0.0f;
// Render each voice and mix with panning
for (int i = 0; i < NUM_VOICES; ++i) {
if (!mVoices[i].active) continue;
// Render and update phase/envelope
float voiceSample = generateSample(mVoices[i]);
updateVoice(mVoices[i], dt);
// Stereo panning
leftMixed += voiceSample * (1.0f - mVoices[i].pan);
rightMixed += voiceSample * mVoices[i].pan;
}
// Clamp mixed signals to prevent audio clipping
leftMixed = std::clamp(leftMixed, -1.0f, 1.0f);
rightMixed = std::clamp(rightMixed, -1.0f, 1.0f);
// Convert mixed floats back to 16-bit PCM samples
out[f * 2] = (int16_t)(leftMixed * 32767.0f);
out[f * 2 + 1] = (int16_t)(rightMixed * 32767.0f);
// Log sample into the visualizer ring buffer
float avgSample = (leftMixed + rightMixed) * 0.5f;
mVisBuffer[mVisWritePos] = avgSample;
mVisWritePos = (mVisWritePos + 1) % VIS_BUFFER_SIZE;
}
}
void Synth::audioCallbackWrapper(void* userdata, Uint8* stream, int len) {
((Synth*)userdata)->audioCallback(stream, len);
}
std::vector<float> Synth::getVisualizerBuffer() {
std::lock_guard<std::mutex> lock(mMutex);
std::vector<float> res(VIS_BUFFER_SIZE);
// Copy the ring buffer chronologically starting from the current write position
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) {
int idx = (mVisWritePos + i) % VIS_BUFFER_SIZE;
res[i] = mVisBuffer[idx];
}
return res;
}