Browse Source

Building on Windows and Linux.

David Piuva 3 years ago
parent
commit
c2f4ab7c59

+ 53 - 56
Source/DFPSR/DFPSR.DsrHead

@@ -1,56 +1,53 @@
-# A project header for using the DFPSR library.
-#   Backends:
-#     * Give the Graphics flag if the application should be able to create a window.
-#     * Give the Sound flag if the application should be able to generate sounds.
-#   Systems:
-#     * Give the Linux flag when compiling on Linux or similar Posix systems having the same dependencies installed.
-#     * Give the Windows flag when compiling on Microsoft Windows.
-#   Strings use a subset of the C standard for mangling, so \\ is used to write \.
-#     You can also use / and let the file abstraction layer convert it into \ automatically when running on Windows.
-
-if Linux
-	message "Building for Linux\n"
-end if
-if Windows
-	message "Building for Windows\n"
-end if
-
-# Change this if the compiler uses a different prefix for linking a library.
-linkerPrefix = "-l"
-
-# Paths are relative to the current script, even if imported somewhere else
-#   so we use .. to leave the Source/DFPSR folder and then go into the windowManagers folder.
-WindowManager = "../windowManagers/NoWindow.cpp"
-if Graphics
-	message "Building with graphics enabled"
-	if Linux
-		message "  Using X11\n"
-		LinkerFlag linkerPrefix & "X11"
-		WindowManager = "../windowManagers/X11Window.cpp"
-	end if
-	if Windows
-		message "  Using Win32\n"
-		LinkerFlag linkerPrefix & "gdi32"
-		LinkerFlag linkerPrefix & "user32"
-		LinkerFlag linkerPrefix & "kernel32"
-		LinkerFlag linkerPrefix & "comctl32"
-		WindowManager = "../windowManagers/Win32Window.cpp"
-	end if
-end if
-Crawl WindowManager
-
-SoundManager = "../soundManagers/NoSound.cpp"
-if Sound
-	message "Building with sound enabled"
-	if Linux
-		message "  Using Alsa\n"
-		LinkerFlag linkerPrefix & "asound"
-		SoundManager = "../soundManagers/AlsaSound.cpp"
-	end if
-	if Windows
-		message "  Using WinMM\n"
-		LinkerFlag linkerPrefix & "winmm"
-		SoundManager = "../soundManagers/WinMMSound.cpp"
-	end if
-end if
-Crawl SoundManager
+# A project header for using the DFPSR library.
+#   Backends:
+#     * Give the Graphics flag if the application should be able to create a window.
+#     * Give the Sound flag if the application should be able to generate sounds.
+#   Systems:
+#     * Give the Linux flag when compiling on Linux or similar Posix systems having the same dependencies installed.
+#     * Give the Windows flag when compiling on Microsoft Windows.
+#   Strings use a subset of the C standard for mangling, so \\ is used to write \.
+#     You can also use / and let the file abstraction layer convert it into \ automatically when running on Windows.
+
+if Linux
+	Message "Building for Linux\n"
+end if
+if Windows
+	Message "Building for Windows\n"
+end if
+
+# Paths are relative to the current script, even if imported somewhere else
+#   so we use .. to leave the Source/DFPSR folder and then go into the windowManagers folder.
+WindowManager = "../windowManagers/NoWindow.cpp"
+if Graphics
+	Message "Building with graphics enabled"
+	if Linux
+		Message "  Using X11\n"
+		Link "X11"
+		WindowManager = "../windowManagers/X11Window.cpp"
+	end if
+	if Windows
+		Message "  Using Win32\n"
+		Link "gdi32"
+		Link "user32"
+		Link "kernel32"
+		Link "comctl32"
+		WindowManager = "../windowManagers/Win32Window.cpp"
+	end if
+end if
+Crawl WindowManager
+
+SoundManager = "../soundManagers/NoSound.cpp"
+if Sound
+	Message "Building with sound enabled"
+	if Linux
+		Message "  Using Alsa\n"
+		Link "asound"
+		SoundManager = "../soundManagers/AlsaSound.cpp"
+	end if
+	if Windows
+		Message "  Using WinMM\n"
+		Link "winmm"
+		SoundManager = "../soundManagers/WinMMSound.cpp"
+	end if
+end if
+Crawl SoundManager

+ 0 - 5
Source/DFPSR/api/fileAPI.cpp

@@ -431,21 +431,16 @@ String file_combinePaths(const ReadableString &a, const ReadableString &b, PathS
 // Used for converting drive relative paths into true absolute paths on MS-Windows.
 static String applyDriveLetter(const ReadableString &path, const ReadableString &currentPath) {
 	// Convert implicit drive into a named drive.
-	printText("applyDriveLetter(", path, ", ", currentPath, ")\n");
 	if (path[0] == U'\\') {
-		printText("  Implicit drive.\n");
 		int64_t colonIndex = string_findFirst(currentPath, U':', -1);
 		if (colonIndex == -1) {
-			printText("  No colon found!\n");
 			return U"?";
 		} else {
 			// Get the drive letter from the current path.
 			String drive = string_until(currentPath, colonIndex);
-			printText("  drive = ", drive, "\n");
 			return string_combine(drive, path);
 		}
 	} else {
-		printText("  Already absolute.\n");
 		// Already absolute.
 		return path;
 	}

+ 9 - 4
Source/tools/builder/Machine.cpp

@@ -148,13 +148,13 @@ static String evaluateExpression(Machine &target, List<String> &tokens, int64_t
 static void analyzeSource(const dsr::ReadableString &absolutePath) {
 	EntryType pathType = file_getEntryType(absolutePath);
 	if (pathType == EntryType::File) {
-		printText(U"  Using source from ", absolutePath, U".\n");
+		printText(U"Using source from ", absolutePath, U".\n");
 		analyzeFromFile(absolutePath);
 	} else if (pathType == EntryType::Folder) {
 		// TODO: Being analyzing from each source file in the folder recursively.
 		//       Each file that is already included will quickly be ignored.
 		//       The difficult part is that exploring a folder returns files in non-deterministic order and GNU's compiler is order dependent.
-		printText(U"  Searching for source code from the folder ", absolutePath, U" is not yet supported due to order dependent linking!\n");
+		printText(U"Searching for source code from the folder ", absolutePath, U" is not yet supported due to order dependent linking!\n");
 	} else if (pathType == EntryType::SymbolicLink) {
 		// Symbolic links can point to both files and folder, so we need to follow it and find out what it really is.
 		analyzeSource(file_followSymbolicLink(absolutePath));
@@ -196,8 +196,13 @@ static void interpretLine(Machine &target, List<String> &tokens, const dsr::Read
 			} else if (string_caseInsensitiveMatch(first, U"crawl")) {
 				// The right hand expression is evaluated into a path relative to the build script and used as the root for searching for source code.
 				analyzeSource(PATH_EXPR(1, tokens.length() - 1));
-			} else if (string_caseInsensitiveMatch(first, U"linkerflag")) {
-				target.linkerFlags.push(STRING_EXPR(1, tokens.length() - 1));
+			} else if (string_caseInsensitiveMatch(first, U"link")) {
+				// Only the path name itself is needed, so any redundant -l prefixes will be stripped away.
+				String libraryName = STRING_EXPR(1, tokens.length() - 1);
+				if (libraryName[0] == U'-' && (libraryName[1] == U'l' || libraryName[1] == U'L')) {
+					libraryName = string_after(libraryName, 2);
+				}
+				target.linkerFlags.push(libraryName);
 			} else if (string_caseInsensitiveMatch(first, U"compilerflag")) {
 				target.compilerFlags.push(STRING_EXPR(1, tokens.length() - 1));
 			} else if (string_caseInsensitiveMatch(first, U"message")) {

+ 52 - 0
Source/tools/builder/buildProject.bat

@@ -0,0 +1,52 @@
+@echo off
+
+rem Using buildProject.sh
+rem   %1 must be the *.DsrProj path, which is relative to the caller location.
+rem   %2... are variable assignments sent as input to the given project file.
+
rem   CPP_COMPILER_PATH should be modified if it does not already refer to an installed C++ compiler.
+
+echo Running buildProject.bat %*
+
+rem Get the build system's folder, where the build system is located.
+set BUILDER_FOLDER=%~dp0%
+echo BUILDER_FOLDER = %BUILDER_FOLDER%
+set BUILDER_EXECUTABLE=%BUILDER_FOLDER%builder.exe
+echo BUILDER_EXECUTABLE = %BUILDER_EXECUTABLE%
+set DFPSR_LIBRARY=%BUILDER_FOLDER%..\..\DFPSR
+echo DFPSR_LIBRARY = %DFPSR_LIBRARY%
+set BUILDER_SOURCE=%BUILDER_FOLDER%\main.cpp %BUILDER_FOLDER%\Machine.cpp %BUILDER_FOLDER%\generator.cpp %DFPSR_LIBRARY%\collection\collections.cpp %DFPSR_LIBRARY%\api\fileAPI.cpp %DFPSR_LIBRARY%\api\bufferAPI.cpp %DFPSR_LIBRARY%\api\stringAPI.cpp %DFPSR_LIBRARY%\base\SafePointer.cpp
+echo BUILDER_SOURCE = %BUILDER_SOURCE%
+
+set CPP_COMPILER_FOLDER=C:\Program\CodeBlocks\MinGW\bin
+set CPP_COMPILER_PATH=%CPP_COMPILER_FOLDER%\x86_64-w64-mingw32-g++.exe
+echo Change CPP_COMPILER_FOLDER and CPP_COMPILER_PATH in %BUILDER_FOLDER%\buildProject.sh if you are not using %CPP_COMPILER_PATH% as your compiler.
+
+rem Check if the build system is compiled
+if exist %BUILDER_EXECUTABLE% (
+	echo Found the build system's binary.
+) else (
+	echo Building the Builder build system for first time use.
+	pushd %CPP_COMPILER_FOLDER%
+		%CPP_COMPILER_PATH% -o %BUILDER_EXECUTABLE% %BUILDER_SOURCE% -static -static-libgcc -static-libstdc++ -std=c++14
+	popd
+	if errorlevel 0 (
+		echo Completed building the Builder build system.
+	) else (
+		echo Failed building the Builder build system, which is needed to build your project!
+		pause
+		exit /b 1
+	)
+)
+
+rem Call the build system with a filename for the output script, which is later called.
+set SCRIPT_PATH=%TEMP%\dfpsr_compile.bat
+echo Generating %SCRIPT_PATH% from %1%
+if exist %SCRIPT_PATH% (
+	del %SCRIPT_PATH%
+)
+%BUILDER_EXECUTABLE% %* ScriptPath=%SCRIPT_PATH% Compiler=%CPP_COMPILER_PATH% CompileFrom=%CPP_COMPILER_FOLDER%
+if exist %SCRIPT_PATH% (
+	echo Running %SCRIPT_PATH%
+	%SCRIPT_PATH%
+)
+pause

+ 12 - 12
Source/tools/builder/buildProject.sh

@@ -22,25 +22,25 @@ else
 	LIBRARY_PATH="$(realpath ${BUILDER_FOLDER}/../../DFPSR)"
 	SOURCE_CODE="${BUILDER_FOLDER}/main.cpp ${BUILDER_FOLDER}/Machine.cpp ${BUILDER_FOLDER}/generator.cpp ${LIBRARY_PATH}/collection/collections.cpp ${LIBRARY_PATH}/api/fileAPI.cpp ${LIBRARY_PATH}/api/bufferAPI.cpp ${LIBRARY_PATH}/api/stringAPI.cpp ${LIBRARY_PATH}/base/SafePointer.cpp"
 	"${BUILDER_CPP_COMPILER_PATH}" -o "${BUILDER_EXECUTABLE}" ${SOURCE_CODE} -std=c++14
-	if [ $? -ne 0 ]
-	then
+	if [ $? -eq 0 ]; then
+		echo "Completed building the Builder build system."
+	else
 		echo "Failed building the Builder build system, which is needed to build your project!"
 		exit 1
 	fi
-	chmod +x "${BUILDER_EXECUTABLE}"
 fi
+chmod +x "${BUILDER_EXECUTABLE}"
 
 # Call the build system with a filename for the output script, which is later given execution permission and called.
 SCRIPT_PATH="/tmp/dfpsr_compile.sh"
 echo "Generating ${SCRIPT_PATH} from $1"
-rm "${SCRIPT_PATH}"
+if [ -e "${SCRIPT_PATH}" ]; then
+	rm "${SCRIPT_PATH}"
+fi
 "${BUILDER_EXECUTABLE}" $@ "ScriptPath=${SCRIPT_PATH}";
-if [ $? -ne 0 ]
-then
-	echo "Failed building the Builder build system, which is needed to build your project!"
-	exit 1
+if [ -e "${SCRIPT_PATH}" ]; then
+	echo "Giving execution permission to ${SCRIPT_PATH}"
+	chmod +x "${SCRIPT_PATH}"
+	echo "Running ${SCRIPT_PATH}"
+	"${SCRIPT_PATH}"
 fi
-echo "Giving execution permission to ${SCRIPT_PATH}"
-chmod +x "${SCRIPT_PATH}"
-echo "Running ${SCRIPT_PATH}"
-"${SCRIPT_PATH}"

+ 66 - 8
Source/tools/builder/generator.cpp

@@ -232,20 +232,56 @@ static void script_executeLocalBinary(String &output, ScriptLanguage language, c
 	if (language == ScriptLanguage::Batch) {
 		string_append(output, code, ".exe\n");
 	} else if (language == ScriptLanguage::Bash) {
-		string_append(output, file_combinePaths(U".", code), U"\n");
+		string_append(output, file_combinePaths(U".", code), U";\n");
 	}
 }
 
+// TODO: Make a checksum for binary buffers too, so that changes can be detected in a dependency graph for lazy compilation.
+static uint64_t checksum(const ReadableString& text) {
+	uint64_t a = 0x8C2A03D4;
+	uint64_t b = 0xF42B1583;
+	uint64_t c = 0xA6815E74;
+	uint64_t d = 0;
+	for (int i = 0; i < string_length(text); i++) {
+		a = (b * c + ((i * 3756 + 2654) & 58043)) & 0xFFFFFFFF;
+		b = (231 + text[i] * (a & 154) + c * 867 + 28294061) & 0xFFFFFFFF;
+		c = (a ^ b ^ (text[i] * 1543217521)) & 0xFFFFFFFF;
+		d = d ^ (a << 32) ^ b ^ (c << 16);
+	}
+	return d;
+}
+
+struct SourceObject {
+	// TODO: Assert that there are no name collisions between identity checksums.
+	uint64_t identityChecksum = 0; // Identification number for the object's name.
+	// TODO: Content checksum, dependency checksum.
+	String sourcePath, objectPath;
+	SourceObject(const ReadableString& sourcePath, const ReadableString& tempFolder, const ReadableString& identity)
+	: identityChecksum(checksum(identity)), sourcePath(sourcePath) {
+		// TODO: Include compiler flags in the checksum.
+		this->objectPath = file_combinePaths(tempFolder, string_combine(U"dfpsr_builder_", identityChecksum, U".o"));
+	}
+};
+
 void generateCompilationScript(const Machine &settings, const ReadableString& projectPath) {
 	ReadableString scriptPath = getFlag(settings, U"ScriptPath", U"");
+	ReadableString tempFolder = file_getAbsoluteParentFolder(scriptPath);
 	if (string_length(scriptPath) == 0) {
-		printText(U"No script path was given, skipping script generation");
+		printText(U"No script path was given, skipping script generation\n");
 		return;
 	}
 	ScriptLanguage language = identifyLanguage(scriptPath);
 	scriptPath = file_getTheoreticalAbsolutePath(scriptPath, projectPath);
 	// The compiler is often a global alias, so the user must supply either an alias or an absolute path.
 	ReadableString compilerName = getFlag(settings, U"Compiler", U"g++"); // Assume g++ as the compiler if not specified.
+	ReadableString compileFrom = getFlag(settings, U"CompileFrom", U"");
+	// Check if the build system was asked to run the compiler from a specific folder.
+	bool changePath = (string_length(compileFrom) > 0);
+	if (changePath) {
+		printText(U"Using ", compilerName, " as the compiler executed from ", compileFrom, ".\n");
+	} else {
+		printText(U"Using ", compilerName, " as the compiler from the current directory.\n");
+	}
 
 	// Convert lists of linker and compiler flags into strings.
 	// TODO: Give a warning if two contradictory flags are used, such as optimization levels and language versions.
@@ -256,11 +292,11 @@ void generateCompilationScript(const Machine &settings, const ReadableString& pr
 	}
 	String linkerFlags;
 	for (int i = 0; i < settings.linkerFlags.length(); i++) {
-		string_append(linkerFlags, " ", settings.linkerFlags[i]);
+		string_append(linkerFlags, " -l", settings.linkerFlags[i]);
 	}
 
 	// Interpret ProgramPath relative to the project path.
-	ReadableString binaryPath = getFlag(settings, U"ProgramPath", language == ScriptLanguage::Batch ? U"program.exe" : U"program"); 
+	ReadableString binaryPath = getFlag(settings, U"ProgramPath", language == ScriptLanguage::Batch ? U"program.exe" : U"program");
 	binaryPath = file_getTheoreticalAbsolutePath(binaryPath, projectPath);
 
 	String output;
@@ -272,7 +308,7 @@ void generateCompilationScript(const Machine &settings, const ReadableString& pr
 		printText(U"The type of script could not be identified for ", scriptPath, U"!\nUse *.bat for Batch or *.sh for Bash.\n");
 		return;
 	}
-	String compiledFiles;
+	List<SourceObject> sourceObjects;
 	bool hasSourceCode = false;
 	bool needCppCompiler = false;
 	for (int d = 0; d < dependencies.length(); d++) {
@@ -283,7 +319,7 @@ void generateCompilationScript(const Machine &settings, const ReadableString& pr
 		if (extension == Extension::C || extension == Extension::Cpp) {
 			// Dependency paths are already absolute from the recursive search.
 			String sourcePath = dependencies[d].path;
-			string_append(compiledFiles, U" ", sourcePath);
+			sourceObjects.pushConstruct(sourcePath, tempFolder, string_combine(sourcePath, compilerFlags, projectPath));
 			if (file_getEntryType(sourcePath) != EntryType::File) {
 				throwError(U"The source file ", sourcePath, U" could not be found!\n");
 			} else {
@@ -293,8 +329,30 @@ void generateCompilationScript(const Machine &settings, const ReadableString& pr
 	}
 	if (hasSourceCode) {
 		// TODO: Give a warning if a known C compiler incapable of handling C++ is given C++ source code when needCppCompiler is true.
-		script_printMessage(output, language, string_combine(U"Compiling with", compilerFlags, linkerFlags));
-		string_append(output, compilerName, U" -o ", binaryPath, compilerFlags, linkerFlags, " ", compiledFiles, U"\n");
+		if (changePath) {
+			// Go into the requested folder.
+			if (language == ScriptLanguage::Batch) {
+				string_append(output,  "pushd ", compileFrom, "\n");
+			} else if (language == ScriptLanguage::Bash) {
+				string_append(output, U"(cd ", compileFrom, ";\n");
+			}
+		}
+		String allObjects;
+		for (int i = 0; i < sourceObjects.length(); i++) {
+			script_printMessage(output, language, string_combine(U"Compiling ", sourceObjects[i].sourcePath, U" ID:", sourceObjects[i].identityChecksum, U" with ", compilerFlags, U"."));
+			string_append(output, compilerName, compilerFlags, U" -c ", sourceObjects[i].sourcePath, U" -o ", sourceObjects[i].objectPath, U"\n");
+			string_append(allObjects, U" ", sourceObjects[i].objectPath);
+		}
+		script_printMessage(output, language, string_combine(U"Linking with ", linkerFlags, U"."));
+		string_append(output, compilerName, allObjects, linkerFlags, U" -o ", binaryPath, U"\n");
+		if (changePath) {
+			// Get back to the previous folder.
+			if (language == ScriptLanguage::Batch) {
+				string_append(output,  "popd\n");
+			} else if (language == ScriptLanguage::Bash) {
+				string_append(output, U")\n");
+			}
+		}
 		script_printMessage(output, language, U"Done compiling.");
 		script_printMessage(output, language, string_combine(U"Starting ", binaryPath));
 		script_executeLocalBinary(output, language, binaryPath);

+ 2 - 0
Source/tools/builder/main.cpp

@@ -45,10 +45,12 @@ void dsrMain(List<String> args) {
 			int64_t assignmentIndex = string_findFirst(argument, U'=');
 			if (assignmentIndex == -1) {
 				assignValue(settings, argument, U"1");
+				printText(U"Assigning ", argument, U" to 1 from input argument.\n");
 			} else {
 				String key = string_removeOuterWhiteSpace(string_before(argument, assignmentIndex));
 				String value = string_removeOuterWhiteSpace(string_after(argument, assignmentIndex));
 				assignValue(settings, key, value);
+				printText(U"Assigning ", key, U" to ", value, U" from input argument.\n");
 			}
 		}
 		// Evaluate compiler settings while searching for source code mentioned in the project and imported headers.

BIN
Source/tools/wizard/Boom.wav


+ 2 - 2
Source/tools/wizard/buildLinux.sh → Source/tools/wizard/build_linux.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
 # Launch the build system with main.DsrProj and Linux selected as the platform.
-echo "Running buildLinux.sh $@"
+echo "Running build_linux.sh $@"
 chmod +x ../builder/buildProject.sh;
-../builder/buildProject.sh ./main.DsrProj Linux;
+../builder/buildProject.sh main.DsrProj Linux $@;

+ 7 - 0
Source/tools/wizard/build_windows.bat

@@ -0,0 +1,7 @@
+@echo off
+
+rem Launch the build system with main.DsrProj and Windows selected as the platform.
+
+echo "Running build_windows.bat %@%
+
+../builder/buildProject.bat main.DsrProj Windows %@%

+ 26 - 21
Source/tools/wizard/main.DsrProj

@@ -1,21 +1,26 @@
-# This project will use both sound and graphics, so we default initialize them to 1 before evaluating the library's header.
-# The caller should provide the operating system's name (Windows, Linux)
-message "Starting main.DsrProj\n"
-Graphics
-Sound
-Import "../../DFPSR/DFPSR.DsrHead"
-
-# The library uses C++14 by default, but you can override it with a newer version as well.
-CompilerFlag "-std=c++14"
-
-# Find source code included by main recursively across header and implementation files with the same names.
-Crawl "main.cpp"
-
-# Enable if you want to see which includes and connected names are used to find source code.
-ListDependencies
-
-# Extensionless path to the generated binary for this project.
-# Will automatically be named on each operating system.
-ProgramPath = "wizard"
-
-message "Ending main.DsrProj\n"
+# This project will use both sound and graphics, so we default initialize them to 1 before evaluating the library's header.
+# The caller should provide the operating system's name (Windows, Linux)
+Message "Starting main.DsrProj\n"
+Graphics
+Sound
+Import "../../DFPSR/DFPSR.DsrHead"
+
+# The DFPSR library uses C++14 as a minimum requirement, but you can use newer versions as well.
+CompilerFlag "-std=c++14"
+
+# Using static linking to avoid having to install standard libraries.
+CompilerFlag "-static"
+CompilerFlag "-static-libgcc"
+CompilerFlag "-static-libstdc++"
+
+# Find source code included by main recursively across header and implementation files with the same names.
+Crawl "main.cpp"
+
+# Enable if you want to see which includes and connected names are used to find source code.
+#ListDependencies
+
+# Extensionless path to the generated binary for this project.
+# Will automatically be named on each operating system.
+ProgramPath = "wizard"
+
+Message "Ending main.DsrProj\n"

+ 100 - 18
Source/tools/wizard/main.cpp

@@ -7,48 +7,120 @@
 //     * Offer one-click build and execution of SDK examples on multiple platforms, while explaining how the building works.
 
 #include "../../DFPSR/includeFramework.h"
+#include "sound.h"
 
 using namespace dsr;
 
-// Embedding your interface's layout is the simplest way to get started
-// It works even if the application is called from another folder
+// Global
+bool running = true;
+Window window;
+Component mainPanel;
+Component toolPanel;
+
 String interfaceContent =
 UR"QUOTE(
 Begin : Panel
 	Name = "mainPanel"
-	Color = 150,160,170
-	Solid = 1
+	Solid = 0
+	Begin : Panel
+		Name = "toolPanel"
+		Color = 180,180,180
+		Solid = 1
+		bottom = 50
+	End
 End
 )QUOTE";
 
-// Global
-bool running = true;
+static const double pi = 3.1415926535897932384626433832795;
+static const double cyclesToRadians = pi * 2.0;
+static const int toneCount = 9;
+int basicTone, boomSound;
+int playing[toneCount];
+void createTestProject() {
+	for (int t = 0; t < toneCount; t++) {
+		playing[t] = -1;
+	}
+	// Pure tone
+	basicTone = generateMonoSoundBuffer(U"sine", 441, 44100, soundFormat_F32, [](double time) -> double {
+		return sin(time * (cyclesToRadians * 100));
+	});
+	// Loaded from file
+	boomSound = loadSoundFromFile(file_combinePaths(file_getApplicationFolder(), U"Boom.wav"));
+}
 
-// GUI handles
-Window window;
+static EnvelopeSettings envelope = EnvelopeSettings(0.1, 0.2, 0.8, 0.4, 0.1, -0.02, 0.04, 0.5);
+static double previewPressTime = 1.0;
+static double previewViewTime = 4.0;
 
 DSR_MAIN_CALLER(dsrMain)
 void dsrMain(List<String> args) {
-	// Create a window
-	window = window_create(U"Project wizard", 1000, 700);
+	printText(U"Input arguments:\n");
+	for (int a = 0; a < args.length(); a++) {
+		printText(U"  args[", a, "] = ", args[a], U"\n");
+	}
 
-	// Register your custom components here
-	//REGISTER_PERSISTENT_CLASS(className);
+	// Start sound thread
+	printText(U"Initializing sound\n");
+	sound_initialize();
+
+	// Create something to test
+	printText(U"Creating test project\n");
+	createTestProject();
+
+	// 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;
+		if (event.keyboardEventType == KeyboardEventType::KeyDown) {
+			if (key == DsrKey_Escape) {
+				running = false;
+			} else if (key >= DsrKey_1 && key <= DsrKey_9) {
+				int toneIndex = key - DsrKey_1;
+				printText(U"Start tone ", toneIndex, U"\n");
+				playing[toneIndex] = playSound(basicTone, true, 0.25, 0.25, 3.0 + toneIndex * 0.25, envelope);
+			} else if (key == DsrKey_0) {
+				playSound(boomSound, false, 0.25, 1.0, 1.0);
+			}
+		} else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
+			if (key >= DsrKey_1 && key <= DsrKey_9) {
+				int toneIndex = key - DsrKey_1;
+				printText(U"End tone ", toneIndex, U"\n");
+				releaseSound(playing[toneIndex]); // Soft stop with following release
+			} else if (key == DsrKey_Space) {
+				stopAllSounds();
+			}
+		} else if (event.keyboardEventType == KeyboardEventType::KeyType) {
+			String message;
+			string_append(message, U"Typed ");
+			string_appendChar(message, event.character);
+			string_append(message, " of code ", event.character, "\n");
+			printText(message);
+		}
+	});
+	/*
+	component_setMouseDownEvent(mainPanel, [](const MouseEvent& event) {
+		
+	});
+	component_setMouseMoveEvent(mainPanel, [](const MouseEvent& event) {
+		
+	});
+	component_setMouseUpEvent(mainPanel, [](const MouseEvent& event) {
+		
+	});
+	*/
 	window_setCloseEvent(window, []() {
 		running = false;
 	});
 
-	// Get your component handles here
-	//myComponent = window_findComponentByName(window, U"myComponent");
-
-	// Bind your components to events here
-	//component_setPressedEvent(myButton, []() {});
-
 	// Execute
 	while(running) {
 		// Wait for actions so that we don't render until an action has been recieved
@@ -56,9 +128,19 @@ void dsrMain(List<String> args) {
 		while (!window_executeEvents(window)) {
 			time_sleepSeconds(0.01);
 		}
+		// Fill the background
+		AlignedImageRgbaU8 canvas = window_getCanvas(window);
+		image_fill(canvas, ColorRgbaI32(64, 64, 64, 255));
+		// Draw things
+		drawEnvelope(canvas, IRect(0, 50, 550, 100), envelope, previewPressTime, previewViewTime);
+		drawSound(canvas, IRect(0, 150, 550, 100), boomSound);
+		drawSound(canvas, IRect(0, 250, 550, 100), basicTone);
 		// Draw interface
 		window_drawComponents(window);
 		// Show the final image
 		window_showCanvas(window);
 	}
+	// Close sound thread
+	printText(U"Terminating sound\n");
+	sound_terminate();
 }

+ 520 - 0
Source/tools/wizard/sound.cpp

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

+ 43 - 0
Source/tools/wizard/sound.h

@@ -0,0 +1,43 @@
+
+#ifndef MODULE_SOUND
+#define MODULE_SOUND
+
+#include "../../DFPSR/includeFramework.h"
+
+static const int soundFormat_I16 = 0; // Hi-fi bit-depth
+static const int soundFormat_F32 = 1; // Studio bit-depth
+
+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, int soundFormat, std::function<double(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);
+
+#endif