#include "sound.h" #include "../../soundManagers/soundManagers.h" using namespace dsr; #include #include inline float sound_convertI16ToF32(int64_t input) { return input * (1.0f / 32767.0f); } inline int sound_convertF32ToI16(float input) { int64_t result = input * 32767.0f; if (result > 32767) { result = 32767; } if (result < -32768) { result = -32768; } return result; } static const int outputChannels = 2; static const int outputSampleRate = 44100; double outputSoundStep = 1.0 / (double)outputSampleRate; double shortestTime = outputSoundStep * 0.01; std::future soundFuture; static std::atomic soundRunning{true}; static std::mutex soundMutex; static int soundFormatSize(int soundFormat) { if (soundFormat == soundFormat_I16) { return 2; } else if (soundFormat == soundFormat_F32) { return 4; } else { throwError("Cannot get size of unknown sound format!\n"); return 0; } } static void minMax(float &minimum, float &maximum, float value) { if (value < minimum) { minimum = value; } if (value > maximum) { maximum = value; } } struct Sound { String name; bool fromFile; int sampleCount; int sampleRate; Buffer samples; int channelCount; int soundFormat; Sound(const ReadableString &name, bool fromFile, int sampleCount, int sampleRate, int channelCount, int soundFormat) : name(name), fromFile(fromFile), sampleCount(sampleCount), sampleRate(sampleRate), samples(buffer_create(sampleCount * channelCount * soundFormatSize(soundFormat))), channelCount(channelCount), soundFormat(soundFormat) {} float sampleLinear(int64_t floor, int64_t ceiling, double ratio, int channel) { int bufferIndexF = floor * this->channelCount + channel; int bufferIndexC = ceiling * this->channelCount + channel; float a = 0.0, b = 0.0; if (this->soundFormat == soundFormat_I16) { SafePointer source = buffer_getSafeData(this->samples, "I16 source sound buffer in sampleLinear"); a = sound_convertI16ToF32(source[bufferIndexF]); b = sound_convertI16ToF32(source[bufferIndexC]); } else if (this->soundFormat == soundFormat_F32) { SafePointer source = buffer_getSafeData(this->samples, "F32 source sound buffer in sampleLinear"); a = source[bufferIndexF]; b = source[bufferIndexC]; } return b * ratio + a * (1.0 - ratio); } float sampleLinear_cyclic(double location, int channel) { int64_t truncated = (int64_t)location; int64_t floor = truncated % this->sampleCount; int64_t ceiling = floor + 1; if (ceiling == sampleCount) { ceiling = 0; } double ratio = location - truncated; return this->sampleLinear(floor, ceiling, ratio, channel); } float sampleLinear_clamped(double location, int channel) { int64_t truncated = (int64_t)location; int64_t floor = truncated; if (floor >= sampleCount) { floor = sampleCount - 1; } int64_t ceiling = floor + 1; if (ceiling >= sampleCount) { ceiling = sampleCount - 1; } double ratio = location - truncated; return this->sampleLinear(floor, ceiling, ratio, channel); } void sampleMinMax(float &minimum, float &maximum, int startSample, int endSample, int channel) { if (startSample < 0) { startSample = 0; } if (endSample >= this->sampleCount) { endSample = this->sampleCount - 1; } if (channel < 0) { channel = 0; } if (channel >= this->channelCount) { channel = this->channelCount - 1; } int bufferIndex = startSample * this->channelCount + channel; if (this->soundFormat == soundFormat_I16) { SafePointer source = buffer_getSafeData(this->samples, "I16 source sound buffer in sampleMinMax"); for (int s = startSample; s <= endSample; s++) { minMax(minimum, maximum, sound_convertI16ToF32(source[bufferIndex])); bufferIndex += this->channelCount; } } else if (this->soundFormat == soundFormat_F32) { SafePointer source = buffer_getSafeData(this->samples, "F32 source sound buffer in sampleMinMax"); for (int s = startSample; s <= endSample; s++) { minMax(minimum, maximum, source[bufferIndex]); bufferIndex += this->channelCount; } } } }; List sounds; static int createEmptySoundBuffer(const ReadableString &name, bool fromFile, int sampleCount, int sampleRate, int channelCount, int soundFormat) { if (sampleCount < 1) { throwError("Cannot create sound buffer without and length!\n");} if (channelCount < 1) { throwError("Cannot create sound buffer without any channels!\n");} if (sampleRate < 1) { throwError("Cannot create sound buffer without any sample rate!\n");} sounds.pushConstruct(name, fromFile, sampleCount, sampleRate, channelCount, soundFormat); return sounds.length() - 1; } int generateMonoSoundBuffer(const ReadableString &name, int sampleCount, int sampleRate, int soundFormat, std::function generator) { int result = createEmptySoundBuffer(name, false, sampleCount, sampleRate, 1, soundFormat); double time = 0.0; double soundStep = 1.0 / (double)sampleRate; if (soundFormat == soundFormat_I16) { SafePointer target = buffer_getSafeData(sounds.last().samples, "I16 target sound buffer"); for (int s = 0; s < sampleCount; s++) { target[s] = sound_convertF32ToI16(generator(time)); time += soundStep; } } else if (soundFormat == soundFormat_F32) { SafePointer target = buffer_getSafeData(sounds.last().samples, "F32 target sound buffer"); for (int s = 0; s < sampleCount; s++) { target[s] = generator(time); time += soundStep; } } return result; } uint16_t readU16LE(const SafePointer source, int firstByteIndex) { return ((uint16_t)source[firstByteIndex]) | ((uint16_t)source[firstByteIndex + 1] << 8); } uint32_t readU32LE(const SafePointer source, int firstByteIndex) { return ((uint32_t)source[firstByteIndex]) | ((uint32_t)source[firstByteIndex + 1] << 8) | ((uint32_t)source[firstByteIndex + 2] << 16) | ((uint32_t)source[firstByteIndex + 3] << 24); } /*struct WaveHeader { char chunkId[4]; // @0 RIFF uint32_t chunkSize; //@ 4 char format[4]; // @ 8 WAVE char subChunkId[4]; // @ 12 fmt uint32_t subChunkSize; // @ 16 uint16_t audioFormat; // @ 20 uint16_t numChannels; // @ 22 uint32_t sampleRate; // @ 24 uint32_t bytesPerSecond; // @ 28 uint16_t blockAlign; // @ 32 uint16_t bitsPerSample; // @ 34 char dataChunkId[4]; // @ 36 uint32_t dataSize; // @ 40 };*/ static const int waveFileHeaderOffset_chunkId = 0; static const int waveFileHeaderOffset_chunkSize = 4; static const int waveFileHeaderOffset_format = 8; static const int waveFileHeaderOffset_subChunkId = 12; static const int waveFileHeaderOffset_subChunkSize = 16; static const int waveFileHeaderOffset_audioFormat = 20; static const int waveFileHeaderOffset_numChannels = 22; static const int waveFileHeaderOffset_sampleRate = 24; static const int waveFileHeaderOffset_bytesPerSecond = 28; static const int waveFileHeaderOffset_blockAlign = 32; static const int waveFileHeaderOffset_bitsPerSample = 34; static const int waveFileHeaderOffset_dataChunkId = 36; static const int waveFileHeaderOffset_dataSize = 40; static const int waveFileDataOffset = 44; int loadWaveSoundFromBuffer(const ReadableString &name, Buffer buffer) { SafePointer fileContent = buffer_getSafeData(buffer, "Wave file buffer"); //uint32_t chunkSize = readU32LE(fileContent, waveFileHeaderOffset_chunkSize); uint32_t subChunkSize = readU32LE(fileContent, waveFileHeaderOffset_subChunkSize); uint16_t audioFormat = readU16LE(fileContent, waveFileHeaderOffset_audioFormat); uint16_t numChannels = readU16LE(fileContent, waveFileHeaderOffset_numChannels); uint32_t sampleRate = readU32LE(fileContent, waveFileHeaderOffset_sampleRate); //uint32_t bytesPerSecond = readU32LE(fileContent, waveFileHeaderOffset_bytesPerSecond); //uint16_t blockAlign = readU16LE(fileContent, waveFileHeaderOffset_blockAlign); //uint16_t bitsPerSample = readU16LE(fileContent, waveFileHeaderOffset_bitsPerSample); uint32_t dataSize = readU32LE(fileContent, waveFileHeaderOffset_dataSize); if (audioFormat != 1) { // Only PCM format supported throwError(U"Unhandled audio format ", audioFormat, " in wave file.\n"); return -1; } int result = -1; if (subChunkSize == 16) { if (dataSize > (buffer_getSize(buffer) - waveFileDataOffset)) { throwError(U"Data size out of bound in wave file.\n"); return -1; } int totalSamples = dataSize / 2; // Safer to calculate length from the file's size result = createEmptySoundBuffer(name, true, totalSamples, sampleRate, numChannels, soundFormat_I16); SafePointer target = buffer_getSafeData(sounds.last().samples, "I16 target sound buffer"); SafePointer waveContent = buffer_getSafeData(buffer, "Wave file buffer"); waveContent.increaseBytes(waveFileDataOffset); for (int s = 0; s < totalSamples; s ++) { target[s] = waveContent[s]; // This part has to assume little endian because the value is signed. :( } } else { throwError(U"Unsupported bit depth ", audioFormat, " in wave file.\n"); return -1; } return result; } int loadSoundFromFile(const ReadableString &filename, bool mustExist) { // Try to reuse any previously instance of the file before accessing the file system for (int s = 0; s < sounds.length(); s++) { if (sounds[s].fromFile && string_match(sounds[s].name, filename)) { return s; } } // Assuming the wave format until more are supported. return loadWaveSoundFromBuffer(filename, file_loadBuffer(filename, mustExist)); } int getSoundBufferCount() { return sounds.length(); } EnvelopeSettings::EnvelopeSettings() : attack(0.0), decay(0.0), sustain(1.0), release(0.0), hold(0.0), rise(0.0), sustainedSmooth(0.0), releasedSmooth(0.0) {} EnvelopeSettings::EnvelopeSettings(double attack, double decay, double sustain, double release, double hold, double rise, double sustainedSmooth, double releasedSmooth) : attack(attack), decay(decay), sustain(sustain), release(release), hold(hold), rise(rise), sustainedSmooth(sustainedSmooth), releasedSmooth(releasedSmooth) {} static double closerLinear(double &ref, double goal, double maxStep) { double difference; if (ref + maxStep < goal) { difference = maxStep; ref += maxStep; } else if (ref - maxStep > goal) { difference = -maxStep; ref -= maxStep; } else { difference = goal - ref; ref = goal; } return difference; } struct Envelope { // Settings EnvelopeSettings envelopeSettings; // TODO: Add different types of smoothing filters and interpolation methods // Dynamic int state = 0; double currentVolume = 0.0, currentGoal = 0.0, releaseVolume = 0.0, timeSinceChange = 0.0; bool lastSustained = true; Envelope(const EnvelopeSettings &envelopeSettings) : envelopeSettings(envelopeSettings) { // Avoiding division by zero using very short fades if (this->envelopeSettings.attack < shortestTime) { this->envelopeSettings.attack = shortestTime; } if (this->envelopeSettings.hold < shortestTime) { this->envelopeSettings.hold = shortestTime; } if (this->envelopeSettings.decay < shortestTime) { this->envelopeSettings.decay = shortestTime; } if (this->envelopeSettings.release < shortestTime) { this->envelopeSettings.release = shortestTime; } } double getVolume(bool sustained, double seconds) { if (sustained) { if (state == 0) { // Attack this->currentGoal += seconds / this->envelopeSettings.attack; if (this->currentGoal > 1.0) { this->currentGoal = 1.0; state = 1; this->timeSinceChange = 0.0; } } else if (state == 1) { // Hold if (this->timeSinceChange < this->envelopeSettings.hold) { this->currentGoal = 1.0; } else { state = 2; this->timeSinceChange = 0.0; } } else if (state == 2) { // Decay this->currentGoal += (this->envelopeSettings.sustain - 1.0) * seconds / this->envelopeSettings.decay; if (this->currentGoal < this->envelopeSettings.sustain) { this->currentGoal = this->envelopeSettings.sustain; state = 3; this->timeSinceChange = 0.0; } } else if (state == 3) { // Sustain / rise this->currentGoal += this->envelopeSettings.rise * seconds / this->envelopeSettings.decay; if (this->currentGoal < 0.0) { this->currentGoal = 0.0; } else if (this->currentGoal > 1.0) { this->currentGoal = 1.0; } } } else { // Release if (this->lastSustained) { this->releaseVolume = this->currentGoal; } // Linear release, using releaseVolume to calculate the slope needed for the current release time this->currentGoal -= this->releaseVolume * seconds / this->envelopeSettings.release; if (this->currentGoal < 0.0) { this->currentGoal = 0.0; } this->lastSustained = false; } double smooth = sustained ? this->envelopeSettings.sustainedSmooth : this->envelopeSettings.releasedSmooth; if (smooth > 0.0) { // Move faster to the goal the further away it is double change = seconds / smooth; if (change > 1.0) { change = 1.0; } double keep = 1.0 - change; this->currentVolume = this->currentVolume * keep + this->currentGoal * change; // Move slowly towards the goal with a fixed speed to finally reach zero and stop sampling the sound closerLinear(this->currentVolume, this->currentGoal, seconds * 0.01); } else { this->currentVolume = this->currentGoal; } this->timeSinceChange += seconds; return this->currentVolume; } bool done() { return this->currentVolume <= 0.0000000001 && !this->lastSustained; } }; // Currently playing sounds struct Player { // Unique identifier int64_t playerID; // Assigned from instrument int soundIndex; Envelope envelope; bool repeat; double leftVolume, rightVolume; double speed; // TODO: Use for playing with interpolation double location = 0; // Floating sample index bool sustained = true; // If the sound is still being generated Player(int64_t playerID, int soundIndex, bool repeat, double leftVolume, double rightVolume, double speed, const EnvelopeSettings &envelopeSettings) : playerID(playerID), soundIndex(soundIndex), envelope(envelopeSettings), repeat(repeat), leftVolume(leftVolume), rightVolume(rightVolume), speed(speed) {} }; List players; int64_t nextPlayerID = 0; int playSound(int soundIndex, bool repeat, double leftVolume, double rightVolume, double speed, const EnvelopeSettings &envelopeSettings) { int result; soundMutex.lock(); result = nextPlayerID; players.pushConstruct(nextPlayerID, soundIndex, repeat, leftVolume, rightVolume, speed, envelopeSettings); nextPlayerID++; soundMutex.unlock(); return result; } int playSound(int soundIndex, bool repeat, double leftVolume, double rightVolume, double speed) { return playSound(soundIndex, repeat, leftVolume, rightVolume, speed, EnvelopeSettings()); } static int findSound(int64_t playerID) { for (int p = 0; p < players.length(); p++) { if (players[p].playerID == playerID) { return p; } } return -1; } void releaseSound(int64_t playerID) { if (playerID != -1) { soundMutex.lock(); int index = findSound(playerID); if (index > -1) { players[index].sustained = false;; } soundMutex.unlock(); } } void stopSound(int64_t playerID) { if (playerID != -1) { soundMutex.lock(); int index = findSound(playerID); if (index > -1) { players.remove(index); } soundMutex.unlock(); } } void stopAllSounds() { soundMutex.lock(); players.clear(); soundMutex.unlock(); } #define PREPARE_SAMPLE \ double envelope = player->envelope.getVolume(player->sustained, outputSoundStep); #define NEXT_SAMPLE_CYCLIC \ player->location += sampleStep; \ if (player->location >= sourceSampleCount) { \ player->location -= sourceSampleCount; \ } \ if (player->envelope.done()) { \ players.remove(p); \ break; \ } #define NEXT_SAMPLE_ONCE \ player->location += sampleStep; \ if (player->location >= sourceSampleCount) { \ players.remove(p); \ break; \ } \ if (player->envelope.done()) { \ players.remove(p); \ break; \ } void sound_initialize() { // Start a worker thread mixing sounds in realtime std::function task = []() { sound_streamToSpeakers(outputChannels, outputSampleRate, [](SafePointer target, int requestedSamples) -> bool { // Anyone wanting to change the played sounds from another thread will have to wait until this section has finished processing soundMutex.lock(); // TODO: Create a graph of filters for different instruments // TODO: Let the output buffer be just another sound buffer, so that a reusable function can stream to sections of larger sound buffers for (int p = players.length() - 1; p >= 0; p--) { Player *player = &(players[p]); int soundIndex = player->soundIndex; Sound *sound = &(sounds[soundIndex]); int sourceSampleCount = sound->sampleCount; double sampleStep = player->speed * sound->sampleRate * outputSoundStep; if (player->repeat) { if (sound->channelCount == 1) { // Mono source for (int t = 0; t < requestedSamples; t++) { PREPARE_SAMPLE float monoSource = sound->sampleLinear_cyclic(player->location, 0) * envelope; target[t * outputChannels + 0] += monoSource * player->leftVolume; target[t * outputChannels + 1] += monoSource * player->rightVolume; NEXT_SAMPLE_CYCLIC } } else if (sound->channelCount == 2) { // Stereo source for (int t = 0; t < requestedSamples; t++) { PREPARE_SAMPLE target[t * outputChannels + 0] += sound->sampleLinear_cyclic(player->location, 0) * envelope * player->leftVolume; target[t * outputChannels + 1] += sound->sampleLinear_cyclic(player->location, 1) * envelope * player->rightVolume; NEXT_SAMPLE_CYCLIC } } } else { if (sound->channelCount == 1) { // Mono source for (int t = 0; t < requestedSamples; t++) { PREPARE_SAMPLE float monoSource = sound->sampleLinear_clamped(player->location, 0) * envelope; target[t * outputChannels + 0] += monoSource * player->leftVolume; target[t * outputChannels + 1] += monoSource * player->rightVolume; NEXT_SAMPLE_ONCE } } else if (sound->channelCount == 2) { // Stereo source for (int t = 0; t < requestedSamples; t++) { PREPARE_SAMPLE target[t * outputChannels + 0] += sound->sampleLinear_clamped(player->location, 0) * envelope * player->leftVolume; target[t * outputChannels + 1] += sound->sampleLinear_clamped(player->location, 1) * envelope * player->rightVolume; NEXT_SAMPLE_ONCE } } } } soundMutex.unlock(); return soundRunning; }); }; soundFuture = std::async(std::launch::async, task); } void sound_terminate() { if (soundRunning) { soundRunning = false; if (soundFuture.valid()) { soundFuture.wait(); } } }