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
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; |
|
}
|
|
|