#include "sound.h" #include "../../DFPSR/api/soundAPI.h" #include #include namespace dsr { 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 void minMax(float &minimum, float &maximum, float value) { if (value < minimum) { minimum = value; } if (value > maximum) { maximum = value; } } struct Sound { SoundBuffer buffer; String name; bool fromFile; Sound(const SoundBuffer &buffer, const ReadableString &name, bool fromFile) : buffer(buffer), name(name), fromFile(fromFile) {} Sound(const ReadableString &name, bool fromFile, int32_t samplesPerChannel, int32_t channelCount, int32_t sampleRate) : buffer(samplesPerChannel, channelCount, sampleRate), name(name), fromFile(fromFile) {} float sampleLinear(int32_t leftIndex, int32_t rightIndex, double ratio, int32_t channel) { int64_t leftOffset = leftIndex * sound_getChannelCount(this->buffer) + channel; int64_t rightOffset = rightIndex * sound_getChannelCount(this->buffer) + channel; float a = 0.0, b = 0.0; SafePointer source = sound_getSafePointer(this->buffer); a = source[leftOffset]; b = source[rightOffset]; return b * ratio + a * (1.0 - ratio); } float sampleLinear_cyclic(double location, int32_t channel) { int32_t truncated = (int64_t)location; int32_t floor = truncated % sound_getSamplesPerChannel(this->buffer); int32_t ceiling = floor + 1; if (ceiling == sound_getSamplesPerChannel(this->buffer)) { ceiling = 0; } double ratio = location - truncated; return this->sampleLinear(floor, ceiling, ratio, channel); } float sampleLinear_clamped(double location, int32_t channel) { int32_t truncated = (int64_t)location; int32_t floor = truncated; if (floor >= sound_getSamplesPerChannel(this->buffer)) { floor = sound_getSamplesPerChannel(this->buffer) - 1; } int32_t ceiling = floor + 1; if (ceiling >= sound_getSamplesPerChannel(this->buffer)) { ceiling = sound_getSamplesPerChannel(this->buffer) - 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 >= sound_getSamplesPerChannel(this->buffer)) { endSample = sound_getSamplesPerChannel(this->buffer) - 1; } if (channel < 0) { channel = 0; } if (channel >= sound_getChannelCount(this->buffer)) { channel = sound_getChannelCount(this->buffer) - 1; } int bufferIndex = startSample * sound_getChannelCount(this->buffer) + channel; SafePointer source = sound_getSafePointer(this->buffer); for (int s = startSample; s <= endSample; s++) { minMax(minimum, maximum, source[bufferIndex]); bufferIndex += sound_getChannelCount(this->buffer); } } }; List sounds; static int createEmptySoundBuffer(const ReadableString &name, bool fromFile, int samplesPerChannel, int sampleRate, int channelCount) { if (samplesPerChannel < 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");} return sounds.pushConstructGetIndex(name, fromFile, samplesPerChannel, channelCount, sampleRate); } int generateMonoSoundBuffer(const ReadableString &name, int samplesPerChannel, int sampleRate, std::function generator) { int result = createEmptySoundBuffer(name, false, samplesPerChannel, sampleRate, 1); double time = 0.0; double soundStep = 1.0 / (double)sampleRate; SafePointer target = sound_getSafePointer(sounds.last().buffer); for (int s = 0; s < samplesPerChannel; s++) { target[s] = generator(time); time += soundStep; } 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; } } return sounds.pushConstructGetIndex(Sound(sound_decode_RiffWave(file_loadBuffer(filename, mustExist)), filename, true)); } 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_getSamplesPerChannel(sound->buffer); double sampleStep = player->speed * sound_getSampleRate(sound->buffer) * outputSoundStep; if (player->repeat) { if (sound_getChannelCount(sound->buffer) == 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_getChannelCount(sound->buffer) == 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_getChannelCount(sound->buffer) == 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_getChannelCount(sound->buffer) == 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(); } } } void drawEnvelope(ImageRgbaU8 target, const IRect ®ion, const EnvelopeSettings &envelopeSettings, double releaseTime, double viewTime) { int top = region.top(); int bottom = region.bottom() - 1; Envelope envelope = Envelope(envelopeSettings); double secondsPerPixel = viewTime / region.width(); draw_rectangle(target, region, ColorRgbaI32(0, 0, 0, 255)); draw_rectangle(target, IRect(region.left(), region.top(), region.width() * (releaseTime / viewTime), region.height() / 8), ColorRgbaI32(0, 128, 128, 255)); int oldHardY = bottom; for (int s = 0; s < region.width(); s++) { int x = s + region.left(); double time = s * secondsPerPixel; double smoothLevel = envelope.getVolume(time < releaseTime, secondsPerPixel); double hardLevel = envelope.currentGoal; if (envelope.done()) { draw_line(target, x, top, x, (top * 7 + bottom) / 8, ColorRgbaI32(128, 0, 0, 255)); } else { draw_line(target, x, (top * smoothLevel) + (bottom * (1.0 - smoothLevel)), x, bottom, ColorRgbaI32(64, 64, 0, 255)); int hardY = (top * hardLevel) + (bottom * (1.0 - hardLevel)); draw_line(target, x, oldHardY, x, hardY, ColorRgbaI32(255, 255, 255, 255)); oldHardY = hardY; } } } void drawSound(dsr::ImageRgbaU8 target, const dsr::IRect ®ion, int soundIndex, bool selected) { draw_rectangle(target, region, selected ? ColorRgbaI32(128, 255, 128, 255) : ColorRgbaI32(40, 40, 40, 255)); Sound *sound = &(sounds[soundIndex]); int innerHeight = region.height() / sound_getChannelCount(sound->buffer); ColorRgbaI32 foreColor = selected ? ColorRgbaI32(200, 255, 200, 255) : ColorRgbaI32(200, 200, 200, 255); for (int c = 0; c < sound_getChannelCount(sound->buffer); c++) { IRect innerBound = IRect(region.left() + 1, region.top() + 1, region.width() - 2, innerHeight - 2); draw_rectangle(target, innerBound, selected ? ColorRgbaI32(0, 0, 0, 255) : ColorRgbaI32(20, 20, 20, 255)); double strideX = ((double)sound_getSamplesPerChannel(sound->buffer) - 1.0) / (double)innerBound.width(); double scale = innerBound.height() * 0.5; double center = innerBound.top() + scale; draw_line(target, innerBound.left(), center, innerBound.right() - 1, center, ColorRgbaI32(0, 0, 255, 255)); if (strideX > 1.0) { double startSample = 0.0; double endSample = strideX; for (int x = innerBound.left(); x < innerBound.right(); x++) { float minimum = 1.0, maximum = -1.0; // TODO: Switch between min-max sampling (denser) and linear interpolation (sparser) sound->sampleMinMax(minimum, maximum, (int)startSample, (int)endSample, c); draw_line(target, x, center - (minimum * scale), x, center - (maximum * scale), foreColor); startSample = endSample; endSample = endSample + strideX; } } else { double sampleX = 0.0; for (int x = innerBound.left(); x < innerBound.right(); x++) { float valueLeft = sound->sampleLinear_clamped(sampleX, c); sampleX += strideX; float valueRight = sound->sampleLinear_clamped(sampleX, c); draw_line(target, x, center - (valueLeft * scale), x, center - (valueRight * scale), foreColor); } } } font_printLine(target, font_getDefault(), sound->name, IVector2D(region.left() + 5, region.top() + 5), foreColor); } }