Pārlūkot izejas kodu

Optimized sound engine.

David Piuva 9 mēneši atpakaļ
vecāks
revīzija
9e5663ba0b

+ 2 - 0
Source/DFPSR/History.txt

@@ -77,3 +77,5 @@ Changes from version 0.2.0 to version 0.3.0 (Performance, safety and template im
 		Replace 'tools/buildAndRun.sh' with 'tools/buildScripts/buildAndRun.sh'.
 		Replace 'tools/buildLibrary.sh' with 'tools/buildScripts/buildLibrary.sh'.
 		Replace 'tools/clean.sh' with 'tools/buildScripts/clean.sh'.
+	* sound_streamToSpeakers uses int32_t instead of int.
+		Replace 'int' with 'int32_t' in your sound engine's callback.

+ 60 - 3
Source/DFPSR/api/soundAPI.cpp

@@ -9,6 +9,48 @@ namespace dsr {
 
 // See the Source/soundManagers folder for implementations of sound_streamToSpeakers for different operating systems.
 
+bool sound_streamToSpeakers_fixed(int32_t channels, int32_t sampleRate, int32_t periodSamplesPerChannel, std::function<bool(SafePointer<float> fixedTarget)> soundOutput) {
+	int32_t bufferSamplesPerChannel = periodSamplesPerChannel * 2;
+	int32_t blockBytes = channels * sizeof(float);
+	Buffer fixedBuffer = buffer_create(bufferSamplesPerChannel * blockBytes);
+	SafePointer<float> bufferPointer = buffer_getSafeData<float>(fixedBuffer, "Fixed size output sound buffer");
+	int32_t writeLocation = 0;
+	int32_t readLocation = 0;
+	return sound_streamToSpeakers(channels, sampleRate, [&](SafePointer<float> dynamicTarget, int32_t requestedSamplesPerChannel) -> bool {
+		// TODO: Allow having a fixed period smaller than what the hardware requests, by delaying buffer allocation.
+		if (requestedSamplesPerChannel > periodSamplesPerChannel) {
+			throwError(U"The fixed period length was smaller than the requested period!\n");
+		}
+		int32_t availableSamplesPerChannel = writeLocation - readLocation;
+		if (availableSamplesPerChannel < 0) availableSamplesPerChannel += bufferSamplesPerChannel;
+		while (availableSamplesPerChannel < requestedSamplesPerChannel) {
+			safeMemorySet(bufferPointer + (writeLocation * channels), 0, periodSamplesPerChannel * blockBytes);
+			if (!soundOutput(bufferPointer + (writeLocation * channels))) {
+				return false;
+			}
+			availableSamplesPerChannel += periodSamplesPerChannel;
+			writeLocation += periodSamplesPerChannel;
+			while (writeLocation >= bufferSamplesPerChannel) writeLocation -= bufferSamplesPerChannel;
+		}
+		int32_t readEndLocation = readLocation + requestedSamplesPerChannel;
+		if (readEndLocation <= bufferSamplesPerChannel) {
+			// Continuous memory.
+			safeMemoryCopy(dynamicTarget, bufferPointer + (readLocation * channels), requestedSamplesPerChannel * blockBytes);
+		} else {
+			// Wraps around the fixed buffer's end.
+			int32_t firstLength = bufferSamplesPerChannel - readLocation;
+			int32_t secondLength = requestedSamplesPerChannel - firstLength;
+			int32_t firstSize = firstLength * blockBytes;
+			int32_t secondSize = secondLength * blockBytes;
+			safeMemoryCopy(dynamicTarget, bufferPointer + (readLocation * channels), firstSize);
+			safeMemoryCopy(dynamicTarget + (firstLength * channels), bufferPointer, secondSize);
+		}
+		readLocation = readEndLocation;
+		while (readLocation >= bufferSamplesPerChannel) readLocation -= bufferSamplesPerChannel;
+		return true;
+	});
+}
+
 SoundBuffer::SoundBuffer(uint32_t samplesPerChannel, uint32_t channelCount, uint32_t sampleRate) {
 	this->impl_samplesPerChannel = samplesPerChannel;
 	if (this->impl_samplesPerChannel < 1) this->impl_samplesPerChannel = 1;
@@ -237,7 +279,6 @@ SoundBuffer sound_decode_RiffWave(const Buffer &fileBuffer) {
 	bool hasData = false;
 	SafePointer<uint8_t> bufferStart = buffer_getSafeData<uint8_t>(fileBuffer, "File buffer");
 	getRiffChunks(fileBuffer, [&bufferStart, &fmtChunk, &hasFmt, &dataChunk, &hasData](const ReadableString &name, const Chunk &chunk) {
-		intptr_t byteOffset = intptr_t(chunk.chunkStart.getUnchecked()) - intptr_t(bufferStart.getUnchecked());
 		if (string_match(name, U"fmt ")) {
 			fmtChunk = chunk;
 			hasFmt = true;
@@ -262,7 +303,7 @@ SoundBuffer sound_decode_RiffWave(const Buffer &fileBuffer) {
 	uintptr_t audioFormat      = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_audioFormat);
 	uintptr_t channelCount     = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_channelCount);
 	uintptr_t sampleRate       = format_readU32_LE(fmtChunk.chunkStart + fmtOffset_sampleRate);
-	uintptr_t bytesPerSecond = format_readU32_LE(fmtChunk.chunkStart + fmtOffset_bytesPerSecond);
+	//uintptr_t bytesPerSecond = format_readU32_LE(fmtChunk.chunkStart + fmtOffset_bytesPerSecond);
 	uintptr_t blockAlign       = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_blockAlign);
 	uintptr_t bitsPerSample    = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_bitsPerSample);
 	uintptr_t bytesPerSample   = bitsPerSample / 8;
@@ -340,7 +381,7 @@ enum class SoundFileFormat {
 
 static SoundFileFormat detectSoundFileExtension(const ReadableString& filename) {
 	SoundFileFormat result = SoundFileFormat::Unknown;
-	int lastDotIndex = string_findLast(filename, U'.');
+	intptr_t lastDotIndex = string_findLast(filename, U'.');
 	if (lastDotIndex != -1) {
 		String extension = string_upperCase(file_getExtension(filename));
 		if (string_match(extension, U"WAV")) {
@@ -392,4 +433,20 @@ bool sound_save_RiffWave(const ReadableString& filename, const SoundBuffer &soun
 	}
 }
 
+SoundBuffer sound_generate_function(uint32_t samplesPerChannel, uint32_t channelCount, uint32_t sampleRate, std::function<float(double time, uint32_t channelIndex)> generator) {
+	SoundBuffer result = sound_create(samplesPerChannel, channelCount, sampleRate);
+	SafePointer<float> target = sound_getSafePointer(result);
+	double time = 0.0;
+	double step = 1.0 / sampleRate;
+	for (uintptr_t b = 0u; b < samplesPerChannel; b++) {
+		for (uintptr_t c = 0u; c < channelCount; c++) {
+			*target = generator(time, c);
+			target += 1;
+		}
+		time += step;
+	}
+	return result;
+
+}
+
 }

+ 50 - 5
Source/DFPSR/api/soundAPI.h

@@ -8,22 +8,28 @@
 namespace dsr {
 	// Call this function from a separate thread in a sound engine to initialize the sound system, call back with sound output requests and terminate when the callback returns false.
 	// The sound_streamToSpeakers function returns false if the backend could not be created, and true iff the backend completed all work and terminated safely.
-	// Channels: The number of virtual speakers to send data to.
+	// Channels: The number of virtual speakers to send samples to.
 	//   How this is mapped to physical speakers depends on the system, because surround speakers may choose to play mono and stereo sound using only the front speakers.
 	// sampleRate: The number of ticks per second for each channel.
-	// soundOutput: A callback that requests length number of ticks generated by the sound engine and written in a packed format into the data array.
+	// soundOutput: A callback that requests length number of ticks generated by the sound engine and written in a packed format into the target array.
 	//   The soundOutput function returns true iff the audio backend should keep fetching sound samples, and false iff the engine is done and ready for the call to sound_streamToSpeakers to return.
-	//   data: The data array should be filled with sound samples in the -1.0f to 1.0f range, in indices from 0 to (length * channels) - 1.
+	//   target: The target array should be filled with sound samples in the -1.0f to 1.0f range, in indices from 0 to (length * channels) - 1.
 	//     The audio backend is responsible for converting the 32-bit float samples into a bit-depth chosen by the backend.
 	//     The backend is supposed to padd the SafePointer's range to at least be divisible by DSR_FLOAT_ALIGNMENT, which allow using both X vectors and F vectors.
 	//       The F vector can be larger than the X vector if building for a SIMD extension that only supports the widest vector length for floating-point operations.
 	//     Padding elements will not add to the time passed in the sound engine, for only played elements increment time.
-	//   length: The number of ticks per channel. The total number of elements to write into data is channels * length.
+	//   length: The number of samples per channel. The total number of elements to write into target is channels * length.
 	// How to use:
 	//   Call sound_streamToSpeakers with desired channels and sampleRate from a separate thread.
 	//   Handle callbacks to soundOutput by feeding the next packed sound samples and letting it return false when done.
 	//   Close the thread and let the sound engine clean up resources.
-	bool sound_streamToSpeakers(int channels, int sampleRate, std::function<bool(dsr::SafePointer<float> data, int length)> soundOutput);
+	bool sound_streamToSpeakers(int32_t channels, int32_t sampleRate, std::function<bool(dsr::SafePointer<float> target, int32_t length)> soundOutput);
+
+	// Wrapper for sound_streamToSpeakers to allow working with a fixed size period for better determinism across different hardware.
+	//   The target elements should be filled for indices 0 to (periodSamplesPerChannel * channels) - 1
+	// This allow using SIMD vectorization with a perfectly aligned period size without wasting any padding, even if the hardware's period size is an odd number.
+	// A fixed period can also be used for perfect timing when playing music.
+	bool sound_streamToSpeakers_fixed(int32_t channels, int32_t sampleRate, int32_t periodSamplesPerChannel, std::function<bool(dsr::SafePointer<float> target)> soundOutput);
 
 	// A sound buffer with packed channels of 32-bit floats.
 	// The duration in seconds equals samplesPerChannel / sampleRate
@@ -48,6 +54,8 @@ namespace dsr {
 
 	inline SafePointer<float> sound_getSafePointer(const SoundBuffer &sound) { return buffer_getSafeData<float>(sound.impl_samples, "Sound buffer"); }
 
+	SoundBuffer sound_generate_function(uint32_t samplesPerChannel, uint32_t channelCount, uint32_t sampleRate, std::function<float(double time, uint32_t channelIndex)> generator);
+
 	enum class RiffWaveFormat {
 		RawU8,
 		RawI16,
@@ -79,6 +87,43 @@ namespace dsr {
 	bool sound_save(const ReadableString& filename, const SoundBuffer &sound, bool mustWork = true);
 	// When you want to select a spefific version of the RIFF wave format.
 	bool sound_save_RiffWave(const ReadableString& filename, const SoundBuffer &sound, RiffWaveFormat format, RoundingMethod roundingMethod = RoundingMethod::Nearest, bool mustWork = true);
+
+	// TODO:
+	// * Resample sound buffers in a different speed, so that tones are generated in advance.
+	//   Limit sound buffers to 2^32 total samples (17 GB with 27 hours in a single sound) and try to use gather functions.
+	/*
+		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<float> 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);
+		}
+	*/
+	// * Repeat sound buffers along time and channels for fast playback with a memory copy.
+	//   Balance shifts and random variations can be applied after the duplication to make use of all the memory.
+	// * Convert between power ratios and decibels.
+	// * Compress sounds using a bruteforce search for patterns.
+	//   Start with a low-pass filter separating high and low frequencies.
+	//   Analyze local minima and maxima to initialize an initial guess.
+	//   Compute error gradients to adjust frequencies, offsets and amplitudes.
+	//   Repeat for the higher frequencies until all tones have been matched and only some noise remains to compress or discard.
 }
 
 #endif

+ 3 - 3
Source/DFPSR/api/stringAPI.h

@@ -327,7 +327,7 @@ inline String& string_toStreamIndented(String& target, const T &value, const Rea
 	} else if (DSR_SAME_TYPE(T, long)) {
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<long>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, long long)) {
-		static_assert(sizeof(long long) == 8, U"You need to implement integer printing for integers larger than 64 bits, or printing long long will be truncated!");
+		static_assert(sizeof(long long) == 8, "You need to implement integer printing for integers larger than 64 bits, or printing long long will be truncated!");
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<long long>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned short)) {
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned short>(value), indentation);
@@ -336,8 +336,8 @@ inline String& string_toStreamIndented(String& target, const T &value, const Rea
 	} else if (DSR_SAME_TYPE(T, unsigned long)) {
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned long>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned long long)) {
-		static_assert(sizeof(unsigned long long) == 8, U"You need to implement integer printing for integers larger than 64 bits, or printing unsigned long long will be truncated!");
-		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned long long>(value), indentation);
+		static_assert(sizeof(unsigned long long) == 8, "You need to implement integer printing for integers larger than 64 bits, or printing unsigned long long will be truncated!");
+		impl_toStreamIndented_int64(target, (uint64_t)unsafeCast<unsigned long long>(value), indentation);
 	}
 	return target;
 }

+ 10 - 8
Source/DFPSR/base/heap.cpp

@@ -820,15 +820,17 @@ namespace dsr {
 		defaultHeap.cleanUp(false);
 	}
 
-	static void printCharacter(char32_t character) {
-		if (character < 32) {
-			putchar(' ');
-		} else if (character > 127) {
-			putchar('?');
-		} else {
-			putchar((char)character);
+	#ifdef SAFE_POINTER_CHECKS
+		static void printCharacter(char32_t character) {
+			if (character < 32) {
+				putchar(' ');
+			} else if (character > 127) {
+				putchar('?');
+			} else {
+				putchar((char)character);
+			}
 		}
-	}
+	#endif
 
 	// TODO: Can whole pointers be displayed using printf?
 	void heap_debugPrintAllocation(void const * const allocation, uintptr_t maxLength) {

+ 4 - 4
Source/DFPSR/base/virtualStack.h

@@ -40,11 +40,11 @@ namespace dsr {
 	// Pre-condition:
 	//   sizeof(T) % alignof(T) == 0
 	template <typename T>
-	SafePointer<T> virtualStack_push(uint64_t elementCount, const char *name = "Nameless virtual stack allocation") {
+	SafePointer<T> virtualStack_push(uint64_t elementCount, const char *name = "Nameless virtual stack allocation", uintptr_t alignmentAndMask = ~uintptr_t(0u)) {
 		// Calculate element size and multiply by element count to get the total size.
 		uint64_t paddedSize = sizeof(T) * elementCount;
 		// Allocate the data with the amount of alignment requested by the element type T.
-		UnsafeAllocation result = virtualStack_push(paddedSize, memory_createAlignmentAndMask((uintptr_t)alignof(T)), name);
+		UnsafeAllocation result = virtualStack_push(paddedSize, alignmentAndMask & memory_createAlignmentAndMask((uintptr_t)alignof(T)), name);
 		// Return a safe pointer to the allocated data.
 		#ifdef SAFE_POINTER_CHECKS
 			return SafePointer<T>(result.header, result.header->allocationIdentity, name, (T*)(result.data), (intptr_t)paddedSize);
@@ -62,8 +62,8 @@ namespace dsr {
 	template <typename T>
 	class VirtualStackAllocation : public SafePointer<T> {
 	public:
-		VirtualStackAllocation(uint64_t elementCount, const char *name = "Nameless virtual stack allocation")
-		: SafePointer<T>(virtualStack_push<T>(elementCount, name)) {}
+		VirtualStackAllocation(uint64_t elementCount, const char *name = "Nameless virtual stack allocation", uintptr_t alignmentAndMask = ~uintptr_t(0u))
+		: SafePointer<T>(virtualStack_push<T>(elementCount, name, alignmentAndMask)) {}
 		~VirtualStackAllocation() {
 			virtualStack_pop();
 		}

+ 127 - 0
Source/SDK/SoundEngine/Envelope.cpp

@@ -0,0 +1,127 @@
+
+#include "Envelope.h"
+#include "../../DFPSR/api/drawAPI.h"
+
+namespace dsr {
+
+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), used(false) {}
+
+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), used(true) {}
+
+Envelope::Envelope(const EnvelopeSettings &envelopeSettings)
+: envelopeSettings(envelopeSettings) {
+	// Avoiding division by zero using very short fades
+	double shortestTime = 0.001;
+	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; }
+}
+
+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;
+}
+
+// TODO: Pre-calculate divisions to save time.
+double Envelope::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 Envelope::done() {
+	return this->currentVolume <= 0.0000000001 && !this->lastSustained;
+}
+
+void soundEngine_drawEnvelope(ImageRgbaU8 target, const IRect &region, const EnvelopeSettings &envelopeSettings, double releaseTime, double viewTime) {
+	int32_t top = region.top();
+	int32_t 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));
+	int32_t oldHardY = bottom;
+	for (int32_t s = 0; s < region.width(); s++) {
+		int32_t 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));
+			int32_t hardY = (top * hardLevel) + (bottom * (1.0 - hardLevel));
+			draw_line(target, x, oldHardY, x, hardY, ColorRgbaI32(255, 255, 255, 255));
+			oldHardY = hardY;
+		}
+	}
+}
+
+}

+ 37 - 0
Source/SDK/SoundEngine/Envelope.h

@@ -0,0 +1,37 @@
+
+#ifndef MODULE_SOUND_ENVELOPE
+#define MODULE_SOUND_ENVELOPE
+
+#include "../../DFPSR/api/soundAPI.h"
+#include "../../DFPSR/api/imageAPI.h"
+
+namespace dsr {
+
+struct EnvelopeSettings {
+	// Basic ADSR
+	double attack, decay, sustain, release;
+	// Extended
+	double hold, rise, sustainedSmooth, releasedSmooth;
+	bool used;
+	EnvelopeSettings();
+	EnvelopeSettings(double attack, double decay, double sustain, double release, double hold = 0.0, double rise = 0.0, double sustainedSmooth = 0.0, double releasedSmooth = 0.0);
+};
+
+struct Envelope {
+	// Settings
+	EnvelopeSettings envelopeSettings;
+	// Dynamic
+	int state = 0;
+	double currentVolume = 0.0, currentGoal = 0.0, releaseVolume = 0.0, timeSinceChange = 0.0;
+	bool lastSustained = true;
+	Envelope(const EnvelopeSettings &envelopeSettings);
+	double getVolume(bool sustained, double seconds);
+	bool done();
+};
+
+// Visualization
+void soundEngine_drawEnvelope(ImageRgbaU8 target, const IRect &region, const EnvelopeSettings &envelopeSettings, double releaseTime, double viewTime);
+
+}
+
+#endif

+ 78 - 0
Source/SDK/SoundEngine/SoundPlayer.cpp

@@ -0,0 +1,78 @@
+
+#include "SoundPlayer.h"
+
+namespace dsr {
+
+SoundPlayer::SoundPlayer(const SoundBuffer &soundBuffer, int32_t soundIndex, int64_t playerID, bool repeat, uint32_t startLocation, float leftVolume, float rightVolume, const EnvelopeSettings &envelopeSettings)
+: soundBuffer(soundBuffer),
+  soundIndex(soundIndex),
+  playerID(playerID),
+  repeat(repeat),
+  location(startLocation % sound_getSamplesPerChannel(soundBuffer)),
+  fadeLeft(leftVolume < 0.9999f || leftVolume > 1.0001f),
+  fadeRight(rightVolume < 0.9999f || rightVolume > 1.0001f),
+  leftVolume(leftVolume),
+  rightVolume(rightVolume),
+  envelope(envelopeSettings) {}
+
+void player_getNextSamples(SoundPlayer &player, SafePointer<float> target, int32_t playedSamplesPerChannel, double secondsPerSample) {
+	uint32_t totalSamplesPerChannel = sound_getSamplesPerChannel(player.soundBuffer);
+	uint32_t channelCount = sound_getChannelCount(player.soundBuffer);
+	SafePointer<const float> source = sound_getSafePointer(player.soundBuffer);
+	uint32_t startIndex = player.location;
+	uint32_t blockBytes = channelCount * sizeof(float);
+	if (startIndex + playedSamplesPerChannel > totalSamplesPerChannel) {
+		// TODO: Properly test this part of the code using regression tests.
+		SafePointer<float> writer = target;
+		// Calculate how many samples that are inside of the buffer.
+		uint32_t insideSamples = totalSamplesPerChannel - startIndex;
+		if (insideSamples > 0) {
+			uintptr_t insideBytes = insideSamples * blockBytes;
+			safeMemoryCopy(writer, source + (player.location * channelCount), insideBytes);
+			writer.increaseBytes(insideBytes);
+		}
+		// Calculate how many samples that are outside of the buffer.
+		uint32_t outsideSamples = playedSamplesPerChannel - insideSamples;
+		if (player.repeat) {
+			// Copy whole laps of the sound buffer.
+			uintptr_t wholeLapBytes = totalSamplesPerChannel * blockBytes;
+			while (outsideSamples >= totalSamplesPerChannel) {
+				safeMemoryCopy(writer, source, wholeLapBytes);
+				writer.increaseBytes(wholeLapBytes);
+				outsideSamples -= totalSamplesPerChannel;
+			}
+			// Copy a partial lap at the end if there are samples remaining.
+			if (outsideSamples > 0) {
+				uintptr_t lastLapBytes = outsideSamples * blockBytes;
+				safeMemoryCopy(writer, source, lastLapBytes);
+			}
+		} else {
+			// Padd the outside with zeroes.
+			uint32_t outsideBytes = outsideSamples * blockBytes;
+			safeMemorySet(target, 0, outsideBytes);
+			player.sustained = false;
+		}
+	} else {
+		// Only a single copy is needed to fill the output buffer with samples.
+		safeMemoryCopy(target, source + (player.location * channelCount), playedSamplesPerChannel * blockBytes);
+	}
+	player.location += playedSamplesPerChannel;
+	if (player.repeat) {
+		while (player.location >= totalSamplesPerChannel) {
+			player.location -= totalSamplesPerChannel;
+		}
+	}
+	// Apply optional envelope
+	if (player.envelope.envelopeSettings.used) {
+		SafePointer<float> currentTarget = target;
+		for (int32_t s = 0; s < playedSamplesPerChannel; s++) {
+			float volume = float(player.envelope.getVolume(player.sustained, secondsPerSample));
+			for (uint32_t c = 0; c < channelCount; c++) {
+				*currentTarget *= volume;
+				currentTarget += 1;
+			}
+		}
+	}
+}
+
+}

+ 31 - 0
Source/SDK/SoundEngine/SoundPlayer.h

@@ -0,0 +1,31 @@
+
+#ifndef MODULE_SOUND_PLAYER
+#define MODULE_SOUND_PLAYER
+
+#include "../../DFPSR/api/soundAPI.h"
+#include "Envelope.h"
+
+namespace dsr {
+
+struct SoundPlayer {
+	SoundBuffer soundBuffer;
+	int32_t soundIndex;
+	int64_t playerID;
+	bool repeat = false;
+	bool sustained = true;
+	// TODO: Use a union for the location to allow switching between fixed and interpolating implementations.
+	// Fixed
+	uint32_t location = 0;
+	// Optional volume to be applied externally, because the player does not duplicate channels.
+	bool fadeLeft, fadeRight; // True iff the corresponding volume is not 1.0.
+	float leftVolume, rightVolume;
+	// Optional Envelope
+	Envelope envelope;
+	SoundPlayer(const SoundBuffer &soundBuffer, int32_t soundIndex, int64_t playerID, bool repeat, uint32_t startLocation, float leftVolume, float rightVolume, const EnvelopeSettings &envelopeSettings);
+};
+
+void player_getNextSamples(SoundPlayer &source, SafePointer<float> target, int32_t playedSamplesPerChannel, double secondsPerSample);
+
+}
+
+#endif

+ 0 - 4
Source/SDK/SoundEngine/build_windows.bat

@@ -1,4 +0,0 @@
-@echo off
-
-echo "Running build_windows.bat %@%
-..\..\tools\builder\buildProject.bat SoundEngine.DsrProj Windows %@%

+ 0 - 413
Source/SDK/SoundEngine/sound.cpp

@@ -1,413 +0,0 @@
-
-#include "sound.h"
-#include "../../DFPSR/api/soundAPI.h"
-
-#include <future>
-#include <atomic>
-
-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<void> soundFuture;
-static std::atomic<bool> 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<float> 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<float> source = sound_getSafePointer(this->buffer);
-		for (int s = startSample; s <= endSample; s++) {
-			minMax(minimum, maximum, source[bufferIndex]);
-			bufferIndex += sound_getChannelCount(this->buffer);
-		}
-	}
-};
-List<Sound> 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<float(double time)> generator) {
-	int result = createEmptySoundBuffer(name, false, samplesPerChannel, sampleRate, 1);
-	double time = 0.0;
-	double soundStep = 1.0 / (double)sampleRate;
-	SafePointer<float> 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<Player> 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<void()> task = []() {
-		sound_streamToSpeakers(outputChannels, outputSampleRate, [](SafePointer<float> 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 &region, 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 &region, 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);
-}
-
-}

+ 0 - 45
Source/SDK/SoundEngine/sound.h

@@ -1,45 +0,0 @@
-
-#ifndef MODULE_SOUND
-#define MODULE_SOUND
-
-#include "../../DFPSR/includeFramework.h"
-#include "../../DFPSR/api/soundAPI.h"
-
-namespace dsr {
-
-void sound_initialize();
-void sound_terminate();
-
-// Creates a single-channel sound using the generator function
-// generator takes the time in seconds as input and returns a value from -1.0 to 1.0
-int generateMonoSoundBuffer(const dsr::ReadableString &name, int sampleCount, int sampleRate, std::function<float(double time)> generator);
-int getSoundBufferCount();
-
-int loadWaveSoundFromBuffer(const dsr::ReadableString &name, dsr::Buffer bufferconst);
-int loadSoundFromFile(const dsr::ReadableString &filename, bool mustExist = true);
-
-struct EnvelopeSettings {
-	// Basic ADSR
-	double attack, decay, sustain, release;
-	// Extended
-	double hold, rise, sustainedSmooth, releasedSmooth;
-	EnvelopeSettings();
-	EnvelopeSettings(double attack, double decay, double sustain, double release, double hold = 0.0, double rise = 0.0, double sustainedSmooth = 0.0, double releasedSmooth = 0.0);
-};
-
-int playSound(int soundIndex, bool repeat, double volumeLeft, double volumeRight, double speed);
-int playSound(int soundIndex, bool repeat, double volumeLeft, double volumeRight, double speed, const EnvelopeSettings &envelopeSettings);
-// Begin to fade out the sound and let it delete itself once done
-void releaseSound(int64_t playerID);
-// Stop the sound at once
-void stopSound(int64_t playerID);
-// Stop all sounds at once
-void stopAllSounds();
-
-// Visualization
-void drawEnvelope(dsr::ImageRgbaU8 target, const dsr::IRect &region, const EnvelopeSettings &envelopeSettings, double releaseTime, double viewTime);
-void drawSound(dsr::ImageRgbaU8 target, const dsr::IRect &region, int soundIndex, bool selected);
-
-}
-
-#endif

+ 317 - 0
Source/SDK/SoundEngine/soundEngine.cpp

@@ -0,0 +1,317 @@
+
+#include "soundEngine.h"
+#include "../../DFPSR/api/soundAPI.h"
+#include "../../DFPSR/api/drawAPI.h"
+#include "../../DFPSR/api/fileAPI.h"
+#include "../../DFPSR/api/fontAPI.h"
+#include "../../DFPSR/base/virtualStack.h"
+#include "../../DFPSR/base/simd.h"
+#include "SoundPlayer.h"
+
+#include <future>
+#include <atomic>
+
+namespace dsr {
+
+static const int maxChannels = 2;
+static const int outputChannels = 2;
+static const int outputSampleRate = 44100;
+double outputSoundStep = 1.0 / (double)outputSampleRate;
+
+std::future<void> soundFuture;
+static std::atomic<bool> 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<float> 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<float> source = sound_getSafePointer(this->buffer);
+		for (int s = startSample; s <= endSample; s++) {
+			minMax(minimum, maximum, source[bufferIndex]);
+			bufferIndex += sound_getChannelCount(this->buffer);
+		}
+	}
+};
+List<Sound> sounds;
+
+int soundEngine_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 soundEngine_insertSoundBuffer(sound_load(filename, mustExist), filename, true);
+}
+
+int soundEngine_getSoundBufferCount() {
+	return sounds.length();
+}
+
+List<SoundPlayer> fixedPlayers;
+int64_t nextPlayerID = 0;
+int soundEngine_playSound(int soundIndex, bool repeat, float leftVolume, float rightVolume, const EnvelopeSettings &envelopeSettings) {
+	int result;
+	if (soundIndex < 0 || soundIndex >= sounds.length()) {
+		sendWarning(U"playSound_simple: Sound index ", soundIndex, U" does not exist!\n");
+		return -1;
+	}
+	Sound *sound = &(sounds[soundIndex]);
+	if (!sound_exists(sound->buffer)) {
+		// Nothing to play.
+		return -1;
+	}
+	int soundSampleRate = sound_getSampleRate(sound->buffer);
+	if (soundSampleRate != outputSampleRate) {
+		throwError(U"playSound_simple: The sound ", sound->name, U" has ", soundSampleRate, U" samples per second in each channel, but the sound engine samples output at ", outputSampleRate, U" samples per second!\n");
+	}
+	int soundChannel = sound_getChannelCount(sound->buffer);
+	if (soundChannel > maxChannels) {
+		throwError(U"playSound_simple: The sound ", sound->name, U" has ", soundChannel, U" channels, but the sound engine can not play more than ", maxChannels, U"channels!\n");
+	}
+	soundMutex.lock();
+		result = nextPlayerID;
+		fixedPlayers.pushConstruct(sounds[soundIndex].buffer, soundIndex, result, repeat, 0u, leftVolume, rightVolume, envelopeSettings);
+		nextPlayerID++;
+	soundMutex.unlock();
+	return result;
+}
+
+static int findFixedPlayer(int64_t playerID) {
+	for (int p = 0; p < fixedPlayers.length(); p++) {
+		if (fixedPlayers[p].playerID == playerID) {
+			return p;
+		}
+	}
+	return -1;
+}
+void soundEngine_releaseSound(int64_t playerID) {
+	if (playerID != -1) {
+		soundMutex.lock();
+			int index = findFixedPlayer(playerID);
+			if (index > -1) {
+				fixedPlayers[index].sustained = false;
+			}
+		soundMutex.unlock();
+	}
+}
+void soundEngine_stopSound(int64_t playerID) {
+	if (playerID != -1) {
+		soundMutex.lock();
+			int index = findFixedPlayer(playerID);
+			if (index > -1) {
+				fixedPlayers.remove(index);
+			}
+		soundMutex.unlock();
+	}
+}
+void soundEngine_stopAllSounds() {
+	soundMutex.lock();
+		fixedPlayers.clear();
+	soundMutex.unlock();
+}
+
+// By using a fixed period size independently of the hardware's period size with sound_streamToSpeakers_fixed, we can reduce waste from SIMD padding and context switches.
+static const int32_t periodSize = 1024;
+
+void soundEngine_initialize() {
+	// Start a worker thread mixing sounds in realtime
+	std::function<void()> task = []() {
+		//sound_streamToSpeakers(outputChannels, outputSampleRate, [](SafePointer<float> target, int32_t requestedSamplesPerChannel) -> bool {
+		sound_streamToSpeakers_fixed(outputChannels, outputSampleRate, periodSize, [](SafePointer<float> target) -> bool {
+			// TODO: Create a thread-safe swap chain or input queue, so that the main thread and sound thread can work at the same time.
+			// Anyone wanting to change the played sounds from another thread will have to wait until this section has finished processing
+			soundMutex.lock();
+				VirtualStackAllocation<float> playerBuffer(periodSize * maxChannels, "Sound target buffer", memory_createAlignmentAndMask(sizeof(DSR_FLOAT_VECTOR_SIZE)));
+				for (int32_t p = fixedPlayers.length() - 1; p >= 0; p--) {
+					SoundPlayer *player = &(fixedPlayers[p]);
+					SoundBuffer *sound = &(player->soundBuffer);
+					// Get samples from the player.
+					player_getNextSamples(*player, playerBuffer, periodSize, 1.0 / (double)outputSampleRate);
+					// TODO: Use a swap chain to update volumes for sound players without stalling.
+					// TODO: Fade volume transitions by assigning soft targets to slowly move towards.
+					// Apply any volume scaling.
+					//if (player->leftVolume < 0.999f || player->leftVolume > 1.001f || player->rightVolume < 0.999f || player->rightVolume > 1.001f) {
+					//	
+					//}
+					//
+					if (sound_getChannelCount(*sound) == 1) { // Mono source to stereo target
+						// TODO: Use a zip/shuffle operation for duplicating channels when available in hardware.
+						SafePointer<float> sourceBlock = playerBuffer;
+						SafePointer<float> targetBlock = target;
+						const bool multiplyLeft = player->fadeLeft;
+						const bool multiplyRight = player->fadeRight;
+						for (int32_t t = 0; t < periodSize; t++) {
+							float value = *sourceBlock;
+							if (multiplyLeft) {
+								targetBlock[0] += value * player->leftVolume;
+							} else {
+								targetBlock[0] += value;
+							}
+							if (multiplyRight) {
+								targetBlock[1] += value * player->rightVolume;
+							} else {
+								targetBlock[1] += value;
+							}
+							sourceBlock += 1;
+							targetBlock += 2;
+						}
+					} else if (sound_getChannelCount(*sound) == 2) { // Stereo source to stereo target
+						// Accumulating sound samples with the same number of channels in and out.
+						SafePointer<const float> sourceBlock = playerBuffer;
+						SafePointer<float> targetBlock = target;
+						if (player->fadeLeft || player->fadeRight) {
+							for (int32_t t = 0; t < periodSize; t++) {
+								targetBlock[0] += sourceBlock[0] * player->leftVolume;
+								targetBlock[1] += sourceBlock[1] * player->rightVolume;
+								sourceBlock += 2;
+								targetBlock += 2;
+							}
+						} else {
+							for (int32_t t = 0; t < periodSize * outputChannels; t += laneCountF) {
+								F32xF packedSamples = F32xF::readAligned(sourceBlock, "Reading stereo sound samples");
+								F32xF oldTarget = F32xF::readAligned(targetBlock, "Reading stereo sound samples");
+								F32xF result = oldTarget + packedSamples;
+								result.writeAligned(targetBlock, "Incrementing stereo samples");
+								// Move pointers to the next block of input and output data.
+								sourceBlock.increaseBytes(DSR_FLOAT_VECTOR_SIZE);
+								targetBlock.increaseBytes(DSR_FLOAT_VECTOR_SIZE);
+							}
+						}
+					} else {
+						// TODO: How should more input than output channels be handled?
+					}
+					// Remove players that are done.
+					if (player->envelope.envelopeSettings.used) {
+						// Remove after fading out when an envelope is used.
+						if (player->envelope.done()) {
+							fixedPlayers.remove(p);
+						}
+					} else {
+						// Remove instantly on release if there is no envelope.
+						if (!(player->sustained)) {
+							fixedPlayers.remove(p);
+						}
+					}
+				}
+			soundMutex.unlock();
+			return soundRunning;
+		});
+	};
+	soundFuture = std::async(std::launch::async, task);
+}
+
+void soundEngine_terminate() {
+	if (soundRunning) {
+		soundRunning = false;
+		if (soundFuture.valid()) {
+			soundFuture.wait();
+		}
+	}
+}
+
+void soundEngine_drawSound(dsr::ImageRgbaU8 target, const dsr::IRect &region, int32_t soundIndex, bool selected) {
+	uint32_t playerCount = 0u;
+	for (int32_t p = 0; p < fixedPlayers.length(); p++) {
+		if (fixedPlayers[p].soundIndex == soundIndex) {
+			playerCount++;
+		}
+	}
+	draw_rectangle(target, region, selected ? ColorRgbaI32(128, 255, 128, 255) : ColorRgbaI32(40, 40, 40, 255));
+	Sound *sound = &(sounds[soundIndex]);
+	int32_t innerHeight = region.height() / sound_getChannelCount(sound->buffer);
+	ColorRgbaI32 foreColor = selected ? ColorRgbaI32(200, 255, 200, 255) : ColorRgbaI32(200, 200, 200, 255);
+	for (int32_t 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, playerCount ? ColorRgbaI32(40, 40, 0, 255) : 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 (int32_t 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, (int32_t)startSample, (int32_t)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 (int32_t 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);
+			}
+		}
+	}
+	// Draw a location for each player using the sound.
+	double pixelsPerSample = (double)(region.width()) / (double)sound_getSamplesPerChannel(sound->buffer);
+	for (int32_t p = 0; p < fixedPlayers.length(); p++) {
+		SoundPlayer *player = &(fixedPlayers[p]);
+		if (player->soundIndex == soundIndex) {
+			// TODO: Display a line along the sound for each player.
+			int32_t pixelX = region.left() + int32_t(double(player->location) * pixelsPerSample);
+			draw_line(target, pixelX, region.top(), pixelX, region.bottom(), foreColor);
+			playerCount++;
+		}
+	}
+	font_printLine(target, font_getDefault(), sound->name, IVector2D(region.left() + 5, region.top() + 5), foreColor);
+}
+
+int32_t soundEngine_insertSoundBuffer(const SoundBuffer &buffer, const ReadableString &name, bool fromFile) {
+	return sounds.pushConstructGetIndex(buffer, name, fromFile);
+}
+
+SoundBuffer soundEngine_getSound(int32_t soundIndex) {
+	if (soundIndex < 0 || soundIndex >= sounds.length()) {
+		return SoundBuffer();
+	} else {
+		return sounds[soundIndex].buffer;
+	}
+}
+
+}

+ 45 - 0
Source/SDK/SoundEngine/soundEngine.h

@@ -0,0 +1,45 @@
+
+#ifndef MODULE_SOUND
+#define MODULE_SOUND
+
+#include "../../DFPSR/api/soundAPI.h"
+#include "../../DFPSR/api/imageAPI.h"
+#include "Envelope.h"
+
+namespace dsr {
+
+void soundEngine_initialize();
+void soundEngine_terminate();
+
+// Creates a single-channel sound using the generator function
+// generator takes the time in seconds as input and returns a value from -1.0 to 1.0
+int32_t soundEngine_getSoundBufferCount();
+
+int32_t soundEngine_insertSoundBuffer(const SoundBuffer &buffer, const ReadableString &name, bool fromFile);
+
+int32_t soundEngine_loadSoundFromFile(const ReadableString &filename, bool mustExist = true);
+
+SoundBuffer soundEngine_getSound(int32_t soundIndex);
+
+// TODO: Create getters for the ouput buffer settings for converting sounds with the wrong sample rate, merging too many channels, or repeating looped sounds that are shorter than the output buffer.
+// Pre-conditions:
+//   The sound at soundIndex must have the same sample rate as the engine's output buffer
+//     and may not contain more channels than the engine's output buffer.
+int32_t soundEngine_playSound(int32_t soundIndex, bool repeat, float leftVolume, float rightVolume, const EnvelopeSettings &envelopeSettings);
+inline int32_t soundEngine_playSound(int32_t soundIndex, bool repeat = false, float leftVolume = 1.0f, float rightVolume = 1.0f) { return soundEngine_playSound(soundIndex, repeat, leftVolume, rightVolume, EnvelopeSettings()); }
+
+//int32_t playSound(int32_t soundIndex, bool repeat, double volumeLeft, double volumeRight, double speed);
+//int32_t playSound(int32_t soundIndex, bool repeat, double volumeLeft, double volumeRight, double speed, const EnvelopeSettings &envelopeSettings);
+// Begin to fade out the sound and let it delete itself once done
+void soundEngine_releaseSound(int64_t playerID);
+// Stop the sound at once
+void soundEngine_stopSound(int64_t playerID);
+// Stop all sounds at once
+void soundEngine_stopAllSounds();
+
+// Visualization
+void soundEngine_drawSound(ImageRgbaU8 target, const IRect &region, int32_t soundIndex, bool selected);
+
+}
+
+#endif

+ 1 - 0
Source/SDK/music/Description.txt

@@ -0,0 +1 @@
+SDK example of how to play sounds with loops and envelopes. Uses Source/SDK/SoundEngine as a sound engine on top of soundAPI in the DFPSR library.

+ 2 - 1
Source/SDK/SoundEngine/SoundEngine.DsrProj → Source/SDK/music/Music.DsrProj

@@ -14,4 +14,5 @@ Import "../../DFPSR/DFPSR.DsrHead"
 StaticRuntime
 
 # Uncomment to enable debug mode, which is slower
-#Debug
+Debug
+CompilerFlag "-g"

+ 0 - 0
Source/SDK/SoundEngine/Water.wav → Source/SDK/music/Water.wav


+ 1 - 1
Source/SDK/SoundEngine/build_linux.sh → Source/SDK/music/build_linux.sh

@@ -2,4 +2,4 @@
 
 echo "Running build_linux.sh $@"
 chmod +x ../../tools/builder/buildProject.sh;
-../../tools/builder/buildProject.sh SoundEngine.DsrProj Linux $@;
+../../tools/builder/buildProject.sh Music.DsrProj Linux $@;

+ 4 - 0
Source/SDK/music/build_windows.bat

@@ -0,0 +1,4 @@
+@echo off
+
+echo "Running build_windows.bat %@%
+..\..\tools\builder\buildProject.bat Music.DsrProj Windows %@%

+ 55 - 53
Source/SDK/SoundEngine/main.cpp → Source/SDK/music/main.cpp

@@ -8,48 +8,48 @@ TODO:
 		Each voice refers to a sound buffer by index (using names in files) and an envelope for how to play the sound.
 		Each voice will be played as its own instrument but from the same input for a richer sound without having to duplicate notes.
 		The sounds can be either embedded into the project (editable for tiny instrument patterns) or refer to external files (for whole music tracks).
+	* Store, modify, import and export MIDI tracks.
 */
 
 #include "../../DFPSR/includeFramework.h"
-#include "sound.h"
+#include "../SoundEngine/soundEngine.h"
 
 using namespace dsr;
 
 // Global
 bool running = true;
 Window window;
-Component mainPanel;
-Component toolPanel;
-
-String interfaceContent =
-UR"QUOTE(
-Begin : Panel
-	Name = "mainPanel"
-	Solid = 0
-	Begin : Panel
-		Name = "toolPanel"
-		Color = 180,180,180
-		Solid = 1
-		bottom = 50
-	End
-End
-)QUOTE";
 
 static const double pi = 3.1415926535897932384626433832795;
 static const double cyclesToRadians = pi * 2.0;
-static const int toneCount = 9;
-int basicTone, testSound;
-int playing[toneCount];
+static const int toneCount = 10;
+Array<int> basicTone = Array<int>(toneCount, -1);
+int testSound;
+Array<int> playing = Array<int>(toneCount, -1);
+
+int createSine(int frequency, const ReadableString &name) {
+	return soundEngine_insertSoundBuffer(sound_generate_function(44100 / frequency, 1, 44100, [frequency](double time, uint32_t channelIndex) {
+		return sin(time * (cyclesToRadians * double(frequency))) * 0.25f;
+	}), name, false);
+}
+
 void createTestProject() {
+	// Loaded from file
+	testSound = soundEngine_loadSoundFromFile(U"Water.wav");
+	// Pure tones
 	for (int t = 0; t < toneCount; t++) {
 		playing[t] = -1;
 	}
-	// Pure tone
-	basicTone = generateMonoSoundBuffer(U"sine", 441, 44100, [](double time) -> float {
-		return sin(time * (cyclesToRadians * 100));
-	});
-	// Loaded from file
-	testSound = loadSoundFromFile(U"Water.wav");
+	basicTone[0] = createSine(261, U"C 4"); // C 4
+	basicTone[1] = createSine(293, U"D 4"); // D 4
+	basicTone[2] = createSine(329, U"E 4"); // E 4
+	basicTone[3] = createSine(349, U"F 4"); // F 4
+	basicTone[4] = createSine(392, U"G 4"); // G 4
+	basicTone[5] = createSine(440, U"A 4"); // A 4
+	basicTone[6] = createSine(493, U"B 4"); // B 4
+	basicTone[7] = createSine(523, U"C 5"); // C 5
+	basicTone[8] = createSine(587, U"D 5"); // D 5
+	basicTone[9] = createSine(659, U"E 5"); // E 5
 }
 
 static EnvelopeSettings envelope = EnvelopeSettings(0.1, 0.2, 0.8, 0.4, 0.1, -0.02, 0.04, 0.5);
@@ -58,7 +58,7 @@ static double previewViewTime = 4.0;
 
 static int selectedBuffer = 0;
 static void limitSelection() {
-	int maxIndex = getSoundBufferCount() - 1;
+	int maxIndex = soundEngine_getSoundBufferCount() - 1;
 	if (selectedBuffer < 0) selectedBuffer = 0;
 	if (selectedBuffer > maxIndex) selectedBuffer = maxIndex;
 }
@@ -67,7 +67,7 @@ DSR_MAIN_CALLER(dsrMain)
 void dsrMain(List<String> args) {
 	// Start sound thread
 	printText("Initializing sound\n");
-	sound_initialize();
+	soundEngine_initialize();
 
 	// Create something to test
 	printText("Creating test project\n");
@@ -76,13 +76,6 @@ void dsrMain(List<String> args) {
 	// Create a window
 	window = window_create(U"Sound generator", 800, 600);
 
-	// Load an interface to the window
-	window_loadInterfaceFromString(window, interfaceContent);
-
-	// Find components
-	mainPanel = window_findComponentByName(window, U"mainPanel");
-	toolPanel = window_findComponentByName(window, U"toolPanel");
-
 	// Bind methods to events
 	window_setKeyboardEvent(window, [](const KeyboardEvent& event) {
 		DsrKey key = event.dsrKey;
@@ -91,13 +84,22 @@ void dsrMain(List<String> args) {
 				running = false;
 			} else if (key >= DsrKey_1 && key <= DsrKey_9) {
 				int toneIndex = key - DsrKey_1;
-				playing[toneIndex] = playSound(basicTone, true, 0.25, 0.25, 3.0 + toneIndex * 0.25, envelope);
+				// TODO: Stop or reactivate sounds that are still fading out with the same tone to reduce the number of sound players running at the same time.
+				playing[toneIndex] = soundEngine_playSound(basicTone[toneIndex], true, 1.0f, 1.0f, envelope);
+			} else if (key == DsrKey_0) {
+				playing[9] = soundEngine_playSound(basicTone[9], true, 1.0f, 1.0f, envelope);
+			} else if (key == DsrKey_Return) {
+				// TODO: Loop while holding return and then turn off looping on release.
+				soundEngine_playSound(selectedBuffer, false);
 			} else if (key == DsrKey_A) {
-				playSound(testSound, false, 1.0, 0.0, 1.0);
+				// Play from left side.
+				soundEngine_playSound(testSound, false, 1.0f, 0.0f);
 			} else if (key == DsrKey_S) {
-				playSound(testSound, false, 1.0, 1.0, 1.0);
+				// Play with half effect.
+				soundEngine_playSound(testSound, false, 0.5f, 0.5f);
 			} else if (key == DsrKey_D) {
-				playSound(testSound, false, 0.0, 1.0, 1.0);
+				// Play from right side.
+				soundEngine_playSound(testSound, false, 0.0f, 1.0f);
 			} else if (key == DsrKey_UpArrow) {
 				selectedBuffer--;
 				limitSelection();
@@ -108,9 +110,11 @@ void dsrMain(List<String> args) {
 		} else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
 			if (key >= DsrKey_1 && key <= DsrKey_9) {
 				int toneIndex = key - DsrKey_1;
-				releaseSound(playing[toneIndex]); // Soft stop with following release
+				soundEngine_releaseSound(playing[toneIndex]);
+			} else if (key == DsrKey_0) {
+				soundEngine_releaseSound(playing[9]);
 			} else if (key == DsrKey_Space) {
-				stopAllSounds();
+				soundEngine_stopAllSounds();
 			}
 		}
 	});
@@ -131,22 +135,20 @@ void dsrMain(List<String> args) {
 
 	// Execute
 	while(running) {
-		// Wait for actions so that we don't render until an action has been recieved
-		// This will save battery on laptops for applications that don't require animation
-		while (!window_executeEvents(window)) {
-			time_sleepSeconds(0.01);
-		}
+		// Run the application in a delayed loop.
+		time_sleepSeconds(0.01);
+		window_executeEvents(window);
 		// Fill the background
 		AlignedImageRgbaU8 canvas = window_getCanvas(window);
 		image_fill(canvas, ColorRgbaI32(64, 64, 64, 255));
 		int width = image_getWidth(canvas);
 		// Draw things
-		drawEnvelope(canvas, IRect(0, 50, width, 100), envelope, previewPressTime, previewViewTime);
-		// TODO: Group into a visual component for viewing sound buffers.
-		int top = 150;
-		for (int s = 0; s < getSoundBufferCount(); s++) {
-			int height = 100;
-			drawSound(canvas, IRect(0, top, width, height), s, s == selectedBuffer);
+		int height = 50;
+		int top = 0;
+		soundEngine_drawEnvelope(canvas, IRect(0, 0, width, height), envelope, previewPressTime, previewViewTime);
+		top += height;
+		for (int s = 0; s < soundEngine_getSoundBufferCount(); s++) {
+			soundEngine_drawSound(canvas, IRect(0, top, width, height), s, s == selectedBuffer);
 			top += height;
 		}
 		// Draw interface
@@ -156,5 +158,5 @@ void dsrMain(List<String> args) {
 	}
 	// Close sound thread
 	printText("Terminating sound\n");
-	sound_terminate();
+	soundEngine_terminate();
 }

+ 1 - 1
Source/soundManagers/AlsaSound.cpp

@@ -32,7 +32,7 @@ static void terminateSound() {
 	}
 }
 
-bool sound_streamToSpeakers(int channels, int sampleRate, std::function<bool(SafePointer<float> data, int length)> soundOutput) {
+bool sound_streamToSpeakers(int32_t channels, int32_t sampleRate, std::function<bool(SafePointer<float> data, int32_t length)> soundOutput) {
 	int errorCode;
 	if ((errorCode = snd_pcm_open(&pcm, "default", SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
 		terminateSound();

+ 1 - 1
Source/soundManagers/NoSound.cpp

@@ -3,7 +3,7 @@
 
 namespace dsr {
 
-bool sound_streamToSpeakers(int channels, int sampleRate, std::function<bool(float*, int)> soundOutput) {
+bool sound_streamToSpeakers(int32_t channels, int32_t sampleRate, std::function<bool(SafePointer<float>, int32_t)> soundOutput) {
 	return false;
 }
 

+ 1 - 1
Source/soundManagers/WinMMSound.cpp

@@ -53,7 +53,7 @@ static void terminateSound() {
 	}
 }
 
-bool sound_streamToSpeakers(int channels, int sampleRate, std::function<bool(SafePointer<float> data, int length)> soundOutput) {
+bool sound_streamToSpeakers(int32_t channels, int32_t sampleRate, std::function<bool(SafePointer<float> data, int32_t length)> soundOutput) {
 	bufferEndEvent = CreateEvent(0, FALSE, FALSE, 0);
 	if (bufferEndEvent == 0) {
 		terminateSound();

+ 12 - 0
Source/test/test.sh

@@ -21,6 +21,18 @@ TEMP_SUB=$(echo $TEMP_SUB | tr "+" "p")
 TEMP_SUB=$(echo $TEMP_SUB | tr -d " =-")
 TEMP_DIR=${TEMP_ROOT}/${TEMP_SUB}
 
+# Build empty backends to prevent getting linker errors
+g++ ${CPP_VERSION} ${MODE} ${DEBUGGER} ${SIMD} -c ${ROOT_PATH}/windowManagers/NoWindow.cpp -o ${TEMP_DIR}/NoWindow.o;
+if [ $? -ne 0 ]
+then
+	exit 1
+fi
+g++ ${CPP_VERSION} ${MODE} ${DEBUGGER} ${SIMD} -c ${ROOT_PATH}/soundManagers/NoSound.cpp -o ${TEMP_DIR}/NoSound.o;
+if [ $? -ne 0 ]
+then
+	exit 1
+fi
+
 for file in ./tests/*.cpp; do
 	[ -e $file ] || continue
 	# Get name without path

+ 24 - 6
Source/tools/buildScripts/build.sh

@@ -5,7 +5,7 @@ PROJECT_FOLDERS=$1 # Where your code is as a space separated list of folders in
 TARGET_FILE=$2 # Your executable to build
 ROOT_PATH=$3 # The parent folder of DFPSR, SDK and tools
 TEMP_ROOT=$4 # Where your temporary objects should be
-WINDOW_MANAGER=$5 # Which library to use for creating a window
+MANAGERS=$5 # Which libraries to use for creating windows and playing sounds
 COMPILER_FLAGS=$6 # -DDEBUG/-DNDEBUG -std=c++14/-std=c++17 -O2/-O3
 LINKER_FLAGS=$7 # Additional linker flags for libraries and such
 
@@ -49,17 +49,35 @@ fi
 BASELIBS="-lm -pthread"
 
 # Select window manager to compile and libraries to link
-if [ ${WINDOW_MANAGER} = "NONE" ]
+if [ ${MANAGERS} = "NONE" ]
 then
 	# Embedded/terminal mode
 	WINDOW_SOURCE=${ROOT_PATH}/windowManagers/NoWindow.cpp
+	SOUND_SOURCE=${ROOT_PATH}/soundManagers/NoSound.cpp
 	LIBS="${BASELIBS} ${LINKER_FLAGS}"
+elif [ ${MANAGERS} = "X11" ]
+then
+	# Desktop GUI mode without sound
+	WINDOW_SOURCE=${ROOT_PATH}/windowManagers/X11Window.cpp
+	SOUND_SOURCE=${ROOT_PATH}/soundManagers/NoSound.cpp
+	LIBS="${BASELIBS} ${LINKER_FLAGS} -lX11"
+elif [ ${MANAGERS} = "X11&ALSA" ]
+then
+	# Desktop GUI mode with sound
+	WINDOW_SOURCE=${ROOT_PATH}/windowManagers/X11Window.cpp
+	SOUND_SOURCE=${ROOT_PATH}/soundManagers/AlsaSound.cpp
+	LIBS="${BASELIBS} ${LINKER_FLAGS} -lX11 -lasound"
 else
-	# Desktop GUI mode
-	WINDOW_SOURCE=${ROOT_PATH}/windowManagers/${WINDOW_MANAGER}Window.cpp
-	LIBS="${BASELIBS} ${LINKER_FLAGS} -l${WINDOW_MANAGER}"
+	echo "Unrecognized argument ${MANAGERS} for sound and window managers!"
+	exit 1
 fi
 
+echo "Compiling sound manager (${SOUND_SOURCE})"
+g++ ${COMPILER_FLAGS} -Wall -c ${SOUND_SOURCE} -o ${TEMP_DIR}/NativeSound.o
+if [ $? -ne 0 ]
+then
+	exit 1
+fi
 echo "Compiling window manager (${WINDOW_SOURCE})"
 g++ ${COMPILER_FLAGS} -Wall -c ${WINDOW_SOURCE} -o ${TEMP_DIR}/NativeWindow.o
 if [ $? -ne 0 ]
@@ -68,7 +86,7 @@ then
 fi
 echo "Linking application with libraries (${LIBS})"
 # Main must exist in the first library when linking
-g++ ${TEMP_DIR}/application.a ${TEMP_DIR}/NativeWindow.o ${LIBS} ${TEMP_DIR}/dfpsr.a -o ${TARGET_FILE}
+g++ ${TEMP_DIR}/application.a ${TEMP_DIR}/NativeWindow.o ${TEMP_DIR}/NativeSound.o ${LIBS} ${TEMP_DIR}/dfpsr.a -o ${TARGET_FILE}
 if [ $? -ne 0 ]
 then
 	exit 1

+ 7 - 8
Source/tools/wizard/main.cpp

@@ -9,7 +9,7 @@
 //         Can let frames have a caption for when used within a container.
 
 #include "../../DFPSR/includeFramework.h"
-#include "../../SDK/SoundEngine/sound.h"
+#include "../../SDK/SoundEngine/soundEngine.h"
 
 using namespace dsr;
 
@@ -154,8 +154,8 @@ void dsrMain(List<String> args) {
 	String mediaFolder = file_combinePaths(applicationFolder, U"media");
 
 	// Start sound.
-	sound_initialize();
-	boomSound = loadSoundFromFile(file_combinePaths(mediaFolder, U"Boom.wav"));
+	soundEngine_initialize();
+	boomSound = soundEngine_loadSoundFromFile(file_combinePaths(mediaFolder, U"Boom.wav"));
 
 	// Create a window.
 	window = window_create(U"DFPSR wizard application", 800, 600);
@@ -192,8 +192,7 @@ void dsrMain(List<String> args) {
 		}
 	});
 	component_setPressedEvent(launchButton, []() {
-		// TODO: Implement building and running of the selected project.
-		playSound(boomSound, false, 1.0, 1.0, 0.7);
+		soundEngine_playSound(boomSound, false);
 		int projectIndex = component_getProperty_integer(projectList, U"SelectedIndex", true);
 		//Application name from project name?
 		if (projectIndex >= 0 && projectIndex < projects.length()) {
@@ -218,7 +217,7 @@ void dsrMain(List<String> args) {
 		}
 	});
 	component_setSelectEvent(projectList, [](int64_t index) {
-		playSound(boomSound, false, 0.5, 0.5, 0.5);
+		soundEngine_playSound(boomSound, false);
 		selectProject(index);
 	});
 	window_setCloseEvent(window, []() {
@@ -226,7 +225,7 @@ void dsrMain(List<String> args) {
 	});
 
 	// Execute.
-	playSound(boomSound, false, 1.0, 1.0, 0.25);
+	soundEngine_playSound(boomSound, false);
 	while(running) {
 		// Wait for actions so that we don't render until an action has been recieved.
 		// This will save battery on laptops for applications that don't require animation.
@@ -243,5 +242,5 @@ void dsrMain(List<String> args) {
 	}
 
 	// Close sound.
-	sound_terminate();
+	soundEngine_terminate();
 }

BIN
Source/tools/wizard/media/Boom.wav