diff --git a/.gitignore b/.gitignore index 827658e..49fb7de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .vs/ x64/ -*.wav +output.wav *.vcxproj.user \ No newline at end of file diff --git a/snare.wav b/snare.wav new file mode 100644 index 0000000..6e04a05 Binary files /dev/null and b/snare.wav differ diff --git a/synthetizer.cpp b/synthetizer.cpp index 1f23864..7dc6134 100644 --- a/synthetizer.cpp +++ b/synthetizer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -7,13 +8,17 @@ const int SAMPLE_RATE = 44100; const double M_TAU = 6.283185307179586476925286766559; +uint16_t lfsr = 0xACE1u; +unsigned bit; // Enum for waveform types enum Waveform { SINE, TRIANGLE, SAWTOOTH, - SQUARE + SQUARE, + NOISE, + DPCM }; // Structure to hold the waveform parameters @@ -22,9 +27,30 @@ struct Wave { int start_ms; int duration_ms; int frequency; - double volume; // New field for volume control (0.0 to 1.0) + double volume; + double dutyCycle; // For SQUARE + double sweepDepth; // Vibrato or slide + double sweepRate; + double attack; + double decay; + std::string samplePath; + std::vector sampleData; }; +bool loadWavFile(const std::string& path, std::vector& outSamples) { + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) return false; + + file.seekg(44); // skip header + outSamples.clear(); + + int16_t sample; + while (file.read(reinterpret_cast(&sample), sizeof(sample))) { + outSamples.push_back(sample); + } + return true; +} + // Read the parameters from the text file bool loadWaveParams(const std::string& filename, std::vector& waves, int& totalDuration) { std::ifstream file(filename); @@ -34,41 +60,97 @@ bool loadWaveParams(const std::string& filename, std::vector& waves, int& } std::string line; + bool inCommentBlock = false; while (std::getline(file, line)) { + // Remove C-style comments + if (inCommentBlock) { + size_t endComment = line.find("*/"); + if (endComment != std::string::npos) { + inCommentBlock = false; + line = line.substr(endComment + 2); // Remaining part of the line after the comment + } + else { + continue; // Skip entire line if still in a comment block + } + } + + size_t startComment = line.find("/*"); + while (startComment != std::string::npos) { + size_t endComment = line.find("*/", startComment + 2); + if (endComment != std::string::npos) { + line.erase(startComment, endComment - startComment + 2); // Remove the comment + startComment = line.find("/*"); // Look for another comment + } + else { + inCommentBlock = true; + line = line.substr(0, startComment); // Remove everything after /* till end of line + break; + } + } + + // Strip whitespace + line.erase(0, line.find_first_not_of(" \t\r\n")); + if (line.empty() || line[0] == '#') continue; + + // Check if the line defines the total duration if (line.find("TOTAL_DURATION") != std::string::npos) { size_t startPos = line.find("(") + 1; size_t endPos = line.find(")"); totalDuration = std::stoi(line.substr(startPos, endPos - startPos)); + continue; } - else if (line.find("SINE") != std::string::npos || line.find("SAWTOOTH") != std::string::npos || - line.find("SQUARE") != std::string::npos || line.find("TRIANGLE") != std::string::npos) { - std::istringstream ss(line); - std::string waveType; - int start_ms, duration_ms, frequency; - double volume; - ss >> waveType >> start_ms >> duration_ms >> frequency >> volume; - Wave wave; - wave.start_ms = start_ms; - wave.duration_ms = duration_ms; - wave.frequency = frequency; - wave.volume = volume; + // Initialize variables for waveform properties + std::istringstream ss(line); + std::string waveType; + int start_ms, duration_ms, frequency; + double volume = 1.0; + double duty = 0.5; + double sweepDepth = 0.0, sweepRate = 0.0; + double attack = 0.01, decay = 0.2; - if (waveType == "SINE") { - wave.type = SINE; - } - else if (waveType == "SAWTOOTH") { - wave.type = SAWTOOTH; - } - else if (waveType == "SQUARE") { - wave.type = SQUARE; - } - else if (waveType == "TRIANGLE") { - wave.type = TRIANGLE; - } + // Read the basic wave parameters + ss >> waveType >> start_ms >> duration_ms >> frequency >> volume; - waves.push_back(wave); + // Set default values if optional parameters are missing + if (!(ss >> duty)) duty = 0.5; + if (!(ss >> sweepDepth)) sweepDepth = 0.0; + if (!(ss >> sweepRate)) sweepRate = 0.0; + if (!(ss >> attack)) attack = 0.01; + if (!(ss >> decay)) decay = 0.2; + + // Create a new Wave object and set its properties + Wave wave; + wave.start_ms = start_ms; + wave.duration_ms = duration_ms; + wave.frequency = frequency; + wave.volume = volume; + wave.dutyCycle = duty; + wave.sweepDepth = sweepDepth; + wave.sweepRate = sweepRate; + wave.attack = attack; + wave.decay = decay; + + // Determine wave type and set corresponding enum + if (waveType == "SINE") wave.type = SINE; + else if (waveType == "SAWTOOTH") wave.type = SAWTOOTH; + else if (waveType == "SQUARE") wave.type = SQUARE; + else if (waveType == "TRIANGLE") wave.type = TRIANGLE; + else if (waveType == "NOISE") wave.type = NOISE; + else if (waveType == "DPCM") { + wave.type = DPCM; + ss >> wave.samplePath; // Read sample file path for DPCM waves + if (!loadWavFile(wave.samplePath, wave.sampleData)) { + std::cout << "Failed to load sample: " << wave.samplePath << std::endl; + } } + else { + std::cout << "Unknown waveform type: " << waveType << std::endl; + continue; // Skip unknown waveform types + } + + // Add the wave to the vector + waves.push_back(wave); } return true; @@ -96,43 +178,105 @@ double sawtoothWave(double frequency, double time) { return 2 * phase - 1; // Ramp from -1 to 1 } -double squareWave(double frequency, double time) { +double squareWave(double frequency, double time, double duty = 0.5) { double period = 1.0 / frequency; - double phase = fmod(time, period) / period; // Normalized phase (0 to 1) - return phase < 0.5 ? 1.0 : -1.0; // 1 for the first half, -1 for the second half + double phase = fmod(time, period) / period; // 0 to 1 + return phase < duty ? 1.0 : -1.0; } // Mix the waveforms with volume control -double generateWave(Waveform waveform, double frequency, double time) { +double generateWave(Waveform waveform, double frequency, double time, double duty = 0.5) { switch (waveform) { - case SINE: - return sineWave(frequency, time); - case TRIANGLE: - return triangleWave(frequency, time); - case SAWTOOTH: - return sawtoothWave(frequency, time); - case SQUARE: - return squareWave(frequency, time); - default: - return sineWave(frequency, time); // Default to sine wave + case SINE: return sineWave(frequency, time); + case TRIANGLE: return triangleWave(frequency, time); + case SAWTOOTH: return sawtoothWave(frequency, time); + case SQUARE: return squareWave(frequency, time, duty); + default: return sineWave(frequency, time); } } +double applyVibrato(double baseFreq, double time, double depth = 5.0, double rate = 5.0) { + return baseFreq + depth * std::sin(M_TAU * rate * time); +} + +double applyEnvelope(double time, double startTime, double duration, double volume, double attack, double decay) { + double elapsed = time - startTime; + if (elapsed < 0.0) return 0.0; + + if (elapsed < attack) { + // Attack phase: ramp up from 0 to volume + return volume * (elapsed / attack); + } + + if (elapsed < (attack + decay)) { + // Decay phase: ramp down to 0 + return volume * (1.0 - ((elapsed - attack) / decay)); + } + + // After envelope ends + return 0.0; +} + +double noiseWave() { + // 16-bit Galois LFSR + bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1; + lfsr = (lfsr >> 1) | (bit << 15); + return ((lfsr & 1) ? 1.0 : -1.0); // White-ish noise +} + std::vector generateAudio(const std::vector& waves, int totalDuration) { std::vector samples(SAMPLE_RATE * totalDuration / 1000, 0); // Initialize with zeros - // Process each wave for (const auto& wave : waves) { int startSample = wave.start_ms * SAMPLE_RATE / 1000; int endSample = (wave.start_ms + wave.duration_ms) * SAMPLE_RATE / 1000; - // Generate the waveform for the specified duration + if (wave.type == DPCM) { + // DPCM Sample Playback (raw PCM sample, assumed 16-bit mono) + for (int i = 0; i < wave.sampleData.size(); ++i) { + int t = startSample + i; + if (t >= samples.size()) break; + + double envelopeVol = applyEnvelope( + t / (double)SAMPLE_RATE, + wave.start_ms / 1000.0, + wave.duration_ms / 1000.0, + 1.0, wave.attack, wave.decay + ); + + samples[t] += static_cast(wave.sampleData[i] * envelopeVol); + } + continue; // Skip standard waveform generation for DPCM + } + for (int t = startSample; t < endSample; ++t) { - if (t >= samples.size()) break; // Prevent out-of-bounds access + if (t >= samples.size()) break; + double time = static_cast(t) / SAMPLE_RATE; - double sample = generateWave(wave.type, wave.frequency, time); - sample *= wave.volume; // Apply volume - samples[t] += static_cast(sample * 32767); // Linear mixing + double startTime = wave.start_ms / 1000.0; + double relTime = time - startTime; + if (relTime < 0) continue; + + // Frequency sweep / vibrato + double modFreq = wave.frequency + wave.sweepDepth * std::sin(M_TAU * wave.sweepRate * time); + + // Generate waveform + double rawSample = 0.0; + if (wave.type == NOISE) { + rawSample = noiseWave(); // LFSR noise + } + else { + rawSample = generateWave(wave.type, modFreq, time, wave.dutyCycle); + } + + // Envelope application + double envelopeVolume = applyEnvelope(time, startTime, wave.duration_ms / 1000.0, wave.volume, wave.attack, wave.decay); + + // Final sample + double finalSample = rawSample * envelopeVolume; + + // Mix into audio buffer + samples[t] += static_cast(std::clamp(finalSample * 32767.0, -32768.0, 32767.0)); } } diff --git a/synthetizer.vcxproj b/synthetizer.vcxproj index 6c7d13f..b80f998 100644 --- a/synthetizer.vcxproj +++ b/synthetizer.vcxproj @@ -76,6 +76,8 @@ true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true + stdcpp20 + stdc17 Console @@ -90,6 +92,8 @@ true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true + stdcpp20 + stdc17 Console @@ -104,6 +108,8 @@ true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true + stdcpp20 + stdc17 Console @@ -118,6 +124,8 @@ true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true + stdcpp20 + stdc17 Console diff --git a/waves.txt b/waves.txt index a8ce288..1b7288b 100644 --- a/waves.txt +++ b/waves.txt @@ -1,61 +1,6 @@ -TOTAL_DURATION(10000) +TOTAL_DURATION(3000) // 3 seconds total -SQUARE 0 400 392 0.7 -SAWTOOTH 0 400 392 0.4 -SINE 0 400 392 0.3 -TRIANGLE 0 400 392 0.2 - -SQUARE 450 400 392 0.7 -SAWTOOTH 450 400 392 0.4 -SINE 450 400 392 0.3 -TRIANGLE 450 400 392 0.2 - -SQUARE 900 800 440 0.7 -SAWTOOTH 900 800 440 0.4 -SINE 900 800 440 0.3 -TRIANGLE 900 800 440 0.2 - -SQUARE 1800 800 392 0.7 -SAWTOOTH 1800 800 392 0.4 -SINE 1800 800 392 0.3 -TRIANGLE 1800 800 392 0.2 - -SQUARE 2700 800 523 0.7 -SAWTOOTH 2700 800 523 0.4 -SINE 2700 800 523 0.3 -TRIANGLE 2700 800 523 0.2 - -SQUARE 3600 1600 494 0.7 -SAWTOOTH 3600 1600 494 0.4 -SINE 3600 1600 494 0.3 -TRIANGLE 3600 1600 494 0.2 - -SQUARE 5400 400 392 0.7 -SAWTOOTH 5400 400 392 0.4 -SINE 5400 400 392 0.3 -TRIANGLE 5400 400 392 0.2 - -SQUARE 5850 400 392 0.7 -SAWTOOTH 5850 400 392 0.4 -SINE 5850 400 392 0.3 -TRIANGLE 5850 400 392 0.2 - -SQUARE 6300 800 440 0.7 -SAWTOOTH 6300 800 440 0.4 -SINE 6300 800 440 0.3 -TRIANGLE 6300 800 440 0.2 - -SQUARE 7200 800 392 0.7 -SAWTOOTH 7200 800 392 0.4 -SINE 7200 800 392 0.3 -TRIANGLE 7200 800 392 0.2 - -SQUARE 8100 800 587 0.7 -SAWTOOTH 8100 800 587 0.4 -SINE 8100 800 587 0.3 -TRIANGLE 8100 800 587 0.2 - -SQUARE 9000 1600 523 0.7 -SAWTOOTH 9000 1600 523 0.4 -SINE 9000 1600 523 0.3 -TRIANGLE 9000 1600 523 0.2 +SINE 0 1000 440 1.0 0 3 5 0.05 0.3 // sine with slow vibrato and long decay +SQUARE 500 1000 330 0.7 0.25 6 4 0.01 0.2 // square with duty cycle and sweep +NOISE 1500 400 0 0.8 0 0 0 0.02 0.1 // noise burst +DPCM 2000 800 /* boilerplate */ 0 0 0 0 0 0 1.0 snare.wav \ No newline at end of file