#include "Synth.hpp" #include #include #include #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 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 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 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 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 Synth::getVisualizerBuffer() { std::lock_guard lock(mMutex); std::vector 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; }