summaryrefslogtreecommitdiff
path: root/saw.c
diff options
context:
space:
mode:
Diffstat (limited to 'saw.c')
-rw-r--r--saw.c4679
1 files changed, 4679 insertions, 0 deletions
diff --git a/saw.c b/saw.c
new file mode 100644
index 0000000..0abda6c
--- /dev/null
+++ b/saw.c
@@ -0,0 +1,4679 @@
+#if 0 /*
+#/ ================================================================
+#/
+#/ saw.c
+#/
+#/ Music sequencer and audio editor.
+#/
+#/ (C) 2024 Mitya Selivanov <guattari.tech>, MIT License
+#/
+#/ ----------------------------------------------------------------
+#/
+#/ To-Do list
+#/
+#/ - Code
+#/ - Self-compiling single-source code
+#/ - Logging routines
+#/ - Tools
+#/ - Transactional undo and redo
+#/ - Rational bezier curves
+#/ - Biquad filter
+#/ - Fourier transform
+#/ - Parallel computation
+#/ - Module code compilation
+#/ - Sound
+#/ - Apply volume during sound generation
+#/ - Implement proper frequency shift
+#/ - Unify similar logic for Oscillators and Samplers
+#/ - Internal sample rate for time values
+#/ (28224000, divisible by 192000 and 44100)
+#/ - Dynamic buffer size
+#/ - BPM
+#/ - Volume
+#/ - Simple tonal synth
+#/ - Unisons
+#/ - Wavetables
+#/ - Kick
+#/ - Snare
+#/ - Cymbal
+#/ - EQ
+#/ - Delay
+#/ - Reverb
+#/ - Combs
+#/ - All-passes
+#/ - Compressor
+#/ - Limiter
+#/ - Sample rendering
+#/ - UI
+#/ - Panning and scaling
+#/ - Grid size changing
+#/ - Selection and copy-paste
+#/ - Help pop-up
+#/ - Spectrum view
+#/ - Effects stack
+#/ - Catalog
+#/ - Curve view
+#/ - Wave view
+#/ - Matrix view
+#/ - Graph view
+#/ - Module code view
+#/ - File browser
+#/ - Dynamic layout
+#/ - Color theme customization
+#/ - Custom font and localization
+#/ - Data
+#/ - Floating point number format without data loss
+#/ - WAV export
+#/ - Project load and store
+#/ - Automatic serialization
+#/ - Hot loading
+#/ - Plugin module
+#/ - VST3 wrapper
+#/ - LV2 wrapper
+#/ - MIDI export
+#/ - MIDI import
+#/ - Drag & drop sheet files
+#/ - Drag & drop project files
+#/
+#/ Bugs
+#/
+#/ - Sampler clicking
+#/ - Windows and macOS compilation issues
+#/
+#/ Done features
+#/
+#/ - Build
+#/ - Code setup for dependencies
+#/ - nanovg and miniaudio setup
+#/ - Faster recompilation
+#/ - WebAssembly
+#/ - Sound
+#/ - Track looping
+#/ - Buffering
+#/ - Drag & drop audio files
+#/ - Sampler
+#/ - Pythagorean tuning
+#/ - UI
+#/ - Piano roll
+#/ - Playback control
+#/ - Text rendering
+#/ - Piano roll panning
+#/ - Track composing
+#/ - Instrument settings
+#/ - Touchscreen support
+#/ - Data
+#/ - State load and store
+#/ - WAV import
+#/ - Sample loading
+#/ - Drag & drop in web
+#/
+#/ ================================================================
+#/
+#/ Self-compilation shell script
+#/
+#/ ================================================================
+SRC=${0##*./}
+gcc -DBUILDME=1 -o buildme $SRC && ./buildme $@ && rm buildme
+exit $? */
+#endif
+
+// ================================================================
+//
+// GLOBAL COMPILATION OPTIONS
+//
+// ================================================================
+
+//#define BUILDME 1
+//#define DEPENDENCIES 1
+//#define EXE 1
+//#define TESTS 1
+
+#define _GNU_SOURCE
+
+// ================================================================
+//
+// BUILD SYSTEM
+//
+// ================================================================
+
+#if BUILDME
+
+#include "kit/allocator.c"
+#include "kit/array_ref.c"
+#include "kit/dynamic_array.c"
+#include "kit/file.c"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#define PROJECT "saw"
+#define SOURCE "saw.c"
+
+#define REQUIRE_MATH 1
+#define REQUIRE_DL 0
+#define REQUIRE_THREADS 0
+#define REQUIRE_SOCKETS 0
+#define REQUIRE_GRAPHICS 1
+#define REQUIRE_OPENGL 1
+#define REQUIRE_VULKAN 0
+#if defined(_WIN32) && !defined(__CYGWIN__)
+#define STATIC_RUNTIME 1
+#else
+#define STATIC_RUNTIME 0
+#endif
+
+enum {
+ LINUX,
+ WINDOWS,
+ MACOS,
+};
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+#define DLM "\\"
+#define OS WINDOWS
+#elif defined(__APPLE__)
+#define DLM "/"
+#define OS MACOS
+#else // assume Linux
+#define DLM "/"
+#define OS LINUX
+#endif
+
+enum {
+ BUFFER_COUNT = 16,
+ BUFFER_SIZE = 512,
+ STRING_COUNT = 64,
+ STRING_SIZE = 512,
+};
+
+c8 _buffers[BUFFER_COUNT][BUFFER_SIZE];
+i32 _buffer_index = 0;
+
+c8 _strings[STRING_COUNT][STRING_SIZE];
+i32 _string_index;
+
+c8 *build_type = "debug";
+c8 *compiler_c = "";
+c8 *destination = "";
+c8 *extra_options = "";
+c8 *extra_link_options = "";
+c8 *postfix_obj = ".o";
+c8 *postfix_exe = "";
+c8 *flag_obj = "-c -o ";
+c8 *flag_exe = "-o ";
+c8 *flags = "";
+c8 *link_flags = "";
+
+b8 run_tests = 1;
+
+void print_help(void) {
+ printf(
+ "Build tool for C projects\n\n"
+ "Usage: ./saw.c [OPTIONS]\n\n"
+ " -h --help - Print this help\n"
+ " -t --type - Set build type: debug, release\n"
+ " -c --compiler - Set compiler to use: gcc, clang, cl, emcc\n"
+ " -d --destination - Set destination path\n"
+ " -o --options - Set additional compiler options\n"
+ " -l --link - Set additional linker options\n"
+ " -s --skiptests - Do not run tests\n\n"
+ );
+ fflush(stdout);
+}
+
+c8 lowercase_char(c8 c) {
+ if (c >= 'A' && c <= 'Z')
+ return c + ('a' - 'A');
+ return c;
+}
+
+c8 *lowercase(c8 *s) {
+ i32 i = 0;
+ for (; s[i] != '\0'; ++i) {
+ assert(i + 1 < BUFFER_SIZE);
+ _buffers[_buffer_index][i] = lowercase_char(s[i]);
+ }
+ _buffers[_buffer_index][i] = '\0';
+ c8 *result = _buffers[_buffer_index];
+ _buffer_index = (_buffer_index + 1) % BUFFER_COUNT;
+ return result;
+}
+
+void fmt_v(c8 *format, va_list args) {
+ vsprintf(_buffers[_buffer_index], format, args);
+}
+
+c8 *fmt(c8 *format, ...) {
+ va_list args;
+ va_start(args, format);
+ fmt_v(format, args);
+ c8 *result = _buffers[_buffer_index];
+ _buffer_index = (_buffer_index + 1) % BUFFER_COUNT;
+ va_end(args);
+ return result;
+}
+
+void fmt_dup_v(c8 *format, va_list args) {
+ vsprintf(_strings[_string_index], format, args);
+}
+
+c8 *fmt_dup(c8 *format, ...) {
+ va_list args;
+ va_start(args, format);
+ assert(_string_index < STRING_COUNT);
+ fmt_v(format, args);
+ c8 *result = _strings[_string_index++];
+ va_end(args);
+ return result;
+}
+
+b8 str_eq_lower(c8 *a, c8 *b) {
+ return strcmp(lowercase(a), lowercase(b)) == 0;
+}
+
+b8 str_eq(c8 *a, c8 *b) {
+ return strcmp(a, b) == 0;
+}
+
+b8 check_compiler(c8 *name) {
+ if (str_eq_lower(name, "cl") || str_eq_lower(name, "cl.exe")) {
+ i32 s = system(fmt("%s", name));
+ if (WEXITSTATUS(s) != 0)
+ return 0;
+ return 1;
+ }
+ i32 s = system(fmt("%s --version", name));
+ if (WEXITSTATUS(s) != 0)
+ return 0;
+ return 1;
+}
+
+enum {
+ MAX_LENGTH = 200,
+};
+
+i64 int_len(u32 x) {
+ i64 len = 0;
+
+ do {
+ x /= 10;
+ ++len;
+ } while (x > 0);
+
+ return len;
+}
+
+i64 print_bytes(FILE *out, FILE *in) {
+ i64 size = 0, line_len = MAX_LENGTH;
+
+ while (!feof(in)) {
+ u32 x = 0;
+
+ i64 n = fread(&x, 1, sizeof x, in);
+ if (n <= 0)
+ break;
+
+ i64 len = int_len(x);
+
+ line_len += len + 2;
+
+ if (line_len >= MAX_LENGTH) {
+ fprintf(out, "\n ");
+ line_len = 3 + len;
+ }
+
+ fprintf(out, " %u,", x);
+
+ size += n;
+ }
+
+ return size;
+}
+
+i32 main(i32 argc, c8 **argv) {
+ // Handle command line arguments
+ //
+
+ for (i32 i = 1; i < argc; ++i) {
+ if (str_eq_lower(argv[i], "?")) {
+ print_help();
+ return 0;
+ } else if (argv[i][0] == '-') {
+ if (argv[i][1] == '-') {
+ c8 *opt = argv[i] + 2;
+ if (str_eq_lower(opt, "help")) {
+ print_help();
+ return 0;
+ }
+ else if (str_eq_lower(opt, "type"))
+ build_type = argv[++i];
+ else if (str_eq_lower(opt, "compiler"))
+ compiler_c = argv[++i];
+ else if (str_eq_lower(opt, "destination"))
+ destination = argv[++i];
+ else if (str_eq_lower(opt, "options"))
+ extra_options = argv[++i];
+ else if (str_eq_lower(opt, "link"))
+ extra_link_options = argv[++i];
+ else if (str_eq_lower(opt, "skiptests"))
+ run_tests = 0;
+ else
+ printf("Unknown option ignored `%s`\n", argv[i]);
+ } else {
+ i32 consumed = 0;
+ for (i32 j = 1; argv[i][j] != '\0'; ++j) {
+ switch (argv[i][j]) {
+ case 'h':
+ case 'H':
+ print_help();
+ return 0;
+
+ case 't':
+ build_type = argv[i + (++consumed)];
+ break;
+
+ case 'c':
+ compiler_c = argv[i + (++consumed)];
+ break;
+
+ case 'd':
+ destination = argv[i + (++consumed)];
+ break;
+
+ case 'o':
+ extra_options = argv[i + (++consumed)];
+ break;
+
+ case 'l':
+ extra_link_options = argv[i + (++consumed)];
+ break;
+
+ case 's':
+ run_tests = 0;
+ break;
+
+ default:
+ printf("Unknown option ignored `-%c`\n", argv[i][j]);
+ }
+ }
+ i += consumed;
+ }
+ } else {
+ printf("Unknown option ignored `%s`\n", argv[i]);
+ }
+ }
+
+ fflush(stdout);
+
+ // Find C compiler
+ //
+
+ if (compiler_c[0] != '\0') {
+ if (check_compiler(compiler_c))
+ printf("C compiler found - %s\n", compiler_c);
+ else {
+ printf("C compiler not found\n");
+ return 1;
+ }
+ } else
+ switch (OS) {
+ case LINUX:
+ if (check_compiler("gcc")) {
+ compiler_c = "gcc";
+ printf("C compiler found - GCC\n");
+ } else if (check_compiler("clang")) {
+ compiler_c = "clang";
+ printf("C compiler found - Clang");
+ } else if (check_compiler("cc")) {
+ compiler_c = "cc";
+ printf("C compiler found - cc");
+ }
+ break;
+
+ case WINDOWS:
+ if (check_compiler("cl")) {
+ compiler_c = "cl";
+ printf("C compiler found - MSVC\n");
+ } else if (check_compiler("gcc")) {
+ compiler_c = "gcc";
+ printf("C compiler found - GCC\n");
+ } else if (check_compiler("clang")) {
+ compiler_c = "clang";
+ printf("C compiler found - Clang");
+ }
+ break;
+
+ case MACOS:
+ if (check_compiler("clang")) {
+ compiler_c = "clang";
+ printf("C compiler found - Clang");
+ } else if (check_compiler("gcc")) {
+ compiler_c = "gcc";
+ printf("C compiler found - GCC\n");
+ } else if (check_compiler("cc")) {
+ compiler_c = "cc";
+ printf("C compiler found - cc");
+ }
+ break;
+
+ default:;
+ }
+
+ if (compiler_c[0] == '\0') {
+ printf("C compiler not found\n");
+ return 1;
+ }
+
+ fflush(stdout);
+
+ // Prepare compilation options
+ //
+
+ if (OS == WINDOWS)
+ postfix_exe = ".exe";
+
+ if (str_eq(compiler_c, "cc")) {
+ destination[0] == '\0' && (destination = "build_cc");
+ } else if (str_eq(compiler_c, "gcc")) {
+ destination[0] == '\0' && (destination = "build_gcc");
+ } else if (str_eq(compiler_c, "clang")) {
+ destination[0] == '\0' && (destination = "build_clang");
+ } else if (str_eq_lower(compiler_c, "cl") || str_eq_lower(compiler_c, "cl.exe")) {
+ destination[0] == '\0' && (destination = "build_cl");
+ postfix_obj = ".obj";
+ flag_obj = "-c -Fo";
+ flag_exe = "-Fe";
+ } else if (str_eq(compiler_c, "emcc")) {
+ destination[0] == '\0' && (destination = "build_emcc");
+ postfix_exe = ".js";
+ run_tests = 0;
+ }
+
+ if (str_eq_lower(compiler_c, "cl") || str_eq_lower(compiler_c, "cl.exe")) {
+ if (str_eq_lower(build_type, "release"))
+ flags = "-O2 -DNDEBUG";
+ else
+ flags = "-Od";
+ } else if (str_eq(compiler_c, "clang")) {
+ if (str_eq_lower(build_type, "release"))
+ flags = "-O3 -DNDEBUG";
+ else
+ flags = "-O0";
+ } else {
+ if (str_eq_lower(build_type, "release"))
+ flags = "-O3 -DNDEBUG";
+ else if (OS != WINDOWS && str_eq(compiler_c, "gcc") && !STATIC_RUNTIME)
+ flags = "-Wall -Wextra -Wno-missing-braces -Wno-missing-field-initializers -Werror -pedantic -O0 -fsanitize=undefined,address,leak -mshstk";
+ else
+ flags = "-Wall -Wextra -Wno-missing-braces -Wno-missing-field-initializers -Werror -pedantic -O0";
+ }
+
+ if (OS == WINDOWS) {
+ if (str_eq_lower(compiler_c, "cl") || str_eq_lower(compiler_c, "cl.exe")) {
+ if (str_eq_lower(build_type, "debug"))
+ link_flags =
+#if REQUIRE_SOCKETS
+ "Ws2_32.lib "
+#endif
+ "Shlwapi.lib Advapi32.lib "
+#if STATIC_RUNTIME
+ "/MTd "
+#endif
+ "";
+ else
+ link_flags =
+#if REQUIRE_SOCKETS
+ "Ws2_32.lib "
+#endif
+ "Shlwapi.lib Advapi32.lib "
+#if STATIC_RUNTIME
+ "/MT "
+#endif
+ "";
+ } else
+ link_flags =
+#if REQUIRE_SOCKETS
+ "-lWs2_32 "
+#endif
+ "-lShlwapi -lAdvapi32"
+#if STATIC_RUNTIME
+ "-static "
+#endif
+ "";
+ }
+
+ if (OS == LINUX)
+ link_flags =
+#if REQUIRE_THREADS
+ "-pthread "
+#endif
+#if REQUIRE_MATH
+ "-lm "
+#endif
+#if REQUIRE_DL
+ "-ldl "
+#endif
+#if REQUIRE_GRAPHICS
+ "-lX11 -lXi -lXcursor "
+#endif
+#if REQUIRE_OPENGL
+ "-lGL "
+#endif
+#if REQUIRE_VULKAN
+ "-lvulkan "
+#endif
+#if STATIC_RUNTIME
+ "-static "
+#endif
+ "";
+
+ if (str_eq(compiler_c, "emcc"))
+ link_flags =
+#if REQUIRE_OPENGL
+ "-sFULL_ES3=1 "
+#endif
+ "";
+
+ if (extra_options[0] != '\0')
+ flags = fmt_dup("%s %s", extra_options, flags);
+ if (extra_link_options[0] != '\0')
+ link_flags = fmt_dup("%s %s", extra_link_options, link_flags);
+
+ // Prepare destination folder
+ //
+
+ destination[0] == '\0' && (destination = "build");
+
+ if (OS == WINDOWS)
+ system(fmt("if not exist %s mkdir %s", destination, destination));
+ else
+ system(fmt("mkdir %s -p", destination));
+
+ // Print info
+ //
+
+ printf("\nCompiler options: %s\n", flags);
+ printf( "Link options: %s\n\n", link_flags);
+ fflush(stdout);
+
+ // Code generation
+ //
+
+ if (path_type(SZ("build_fonts.inl.h")) == PATH_NONE) {
+ printf("Code generation\n");
+ fflush(stdout);
+
+ FILE *out = fopen("build_fonts.inl.h", "wb");
+ assert(out != NULL);
+
+ fprintf(out, "// "
+ "====================================================="
+ "===========\n");
+ fprintf(out, "//\n");
+ fprintf(out, "// Saw generated code\n");
+ fprintf(out, "//\n");
+ fprintf(out, "// "
+ "====================================================="
+ "===========\n\n");
+
+ fprintf(out, "#ifndef BUILD_FONTS_INL_H\n");
+ fprintf(out, "#define BUILD_FONTS_INL_H\n\n");
+ fprintf(out, "#include \"kit/types.h\"\n\n");
+
+ // Write Domitian Roman
+ //
+ {
+ FILE *in = fopen("fonts/domitian_roman.ttf", "rb");
+ assert(in != NULL);
+
+ fprintf(out, "u32 ttf_text[] = {");
+
+ i64 n = print_bytes(out, in);
+
+ fprintf(out, "\n};\n\n");
+ fprintf(out, "enum { TTF_TEXT_SIZE = %lld, };\n\n", n);
+
+ fclose(in);
+ }
+
+ // Write Font Awesome
+ //
+ {
+ FILE *in = fopen("fonts/font_awesome_6_free_solid_900.ttf", "rb");
+ assert(in != NULL);
+
+ fprintf(out, "u32 ttf_icons[] = {");
+
+ i64 n = print_bytes(out, in);
+
+ fprintf(out, "\n};\n\n");
+ fprintf(out, "enum { TTF_ICONS_SIZE = %lld, };\n\n", n);
+
+ fclose(in);
+ }
+
+ fprintf(out, "#endif\n");
+ fclose(out);
+ }
+
+ // Build the project
+ //
+
+ c8 *deps = fmt("%s" DLM "thirdparty%s", destination, postfix_obj);
+
+ if (path_type(str(strlen(deps), deps)) == PATH_NONE) {
+ printf("Rebuild dependencies\n");
+ fflush(stdout);
+
+ i32 s = system(fmt(
+ "%s %s -DDEPENDENCIES=1 "
+ "%s%s" DLM "thirdparty%s "
+ SOURCE
+ " %s",
+ compiler_c, flags,
+ flag_obj, destination, postfix_obj,
+ link_flags)
+ );
+
+ if (WEXITSTATUS(s) != 0)
+ return 1;
+ fflush(stdout);
+ }
+
+ printf("Build the test suite\n");
+ fflush(stdout);
+
+ i32 s = system(fmt(
+ "%s %s -DTESTS=1 "
+ "%s%s" DLM "test_suite%s "
+ SOURCE
+ " %s",
+ compiler_c, flags,
+ flag_exe, destination, postfix_exe,
+ link_flags)
+ );
+
+ if (WEXITSTATUS(s) != 0)
+ return 1;
+ fflush(stdout);
+
+ printf("Build " PROJECT " executable\n");
+ fflush(stdout);
+
+ s = system(fmt(
+ "%s %s -DEXE=1 "
+ "%s%s" DLM PROJECT "%s "
+ "%s" DLM "thirdparty%s "
+ SOURCE
+ " %s",
+ compiler_c, flags,
+ flag_exe, destination, postfix_exe,
+ destination, postfix_obj,
+ link_flags)
+ );
+
+ if (WEXITSTATUS(s) != 0)
+ return 1;
+ fflush(stdout);
+
+ if (!run_tests)
+ return 0;
+
+ // Run tests
+ //
+
+ i32 status = 0;
+
+ printf("Run tests\n\n");
+ fflush(stdout);
+
+ s = system(fmt("\"%s" DLM "test_suite\"", destination));
+
+ if (WEXITSTATUS(s) != 0)
+ status = 1;
+ fflush(stdout);
+
+ if (status == 0)
+ printf("\nAll done - OK.\n");
+ else
+ printf("\nAll done - FAILURE.\n");
+
+ return status;
+}
+
+// ================================================================
+//
+// DEPENDENCIES
+//
+// ================================================================
+
+#elif DEPENDENCIES
+
+#if defined(__EMSCRIPTEN__)
+# define SOKOL_GLES3
+# define NANOVG_GLES3 1
+#else
+# define SOKOL_GLCORE33
+# define NANOVG_GL3 1
+#endif
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wpedantic"
+#endif
+#define SOKOL_APP_IMPL
+// hotfix for weird GCC bug
+#ifdef __linux__
+# include <emmintrin.h>
+#endif
+#include "kit/_lib.c"
+#include "sokol/sokol_app.h"
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-result"
+#endif
+#define MINIAUDIO_IMPLEMENTATION
+#include "miniaudio/miniaudio.h"
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
+#include "nanovg/nanovg.c"
+#include "nanovg/nanovg_gl.c"
+
+// ================================================================
+//
+// EXECUTABLE
+//
+// ================================================================
+
+#elif EXE
+
+// ================================================================
+//
+// Headers
+//
+// ================================================================
+
+#include "kit/math.h"
+#include "kit/time.h"
+#include "kit/string_ref.h"
+#include "kit/mersenne_twister_64.h"
+#include "kit/secure_random.h"
+#include "kit/file.h"
+#include "kit/input_buffer.h"
+#include "kit/threads.h"
+#include "kit/status.h"
+
+#if defined(__EMSCRIPTEN__)
+# include <GLES3/gl3.h>
+# define SOKOL_GLES3
+# define NANOVG_GLES3 1
+#else
+# include <GL/gl.h>
+# define SOKOL_GLCORE33
+# define NANOVG_GL3 1
+#endif
+
+#include "sokol/sokol_app.h"
+#include "nanovg/nanovg.h"
+#include "nanovg/nanovg_gl.h"
+#include "miniaudio/miniaudio.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+// ttf_text, TTF_TEXT_SIZE
+// ttf_icons, TTF_ICONS_SIZE
+#include "build_fonts.inl.h"
+
+// ================================================================
+//
+// Definitions
+//
+// ================================================================
+
+// Program version
+//
+
+#define VERSION_MAJOR 0
+#define VERSION_MINOR 0
+#define VERSION_BABY 3
+#define VERSION_DEV 1
+
+// Constants
+//
+
+#define EPS 0.00001
+#define REFERENCE_PITCH 440.0 // A_4
+#define EQUAL_TEMPERAMENT_FACTOR 1.05946309436 // 2^(1/12)
+#define GLOBAL_VOLUME 2.0
+
+#define SPACES SZ(" \t\n\r")
+#define DELIM SZ(" \t\n\r}")
+#define NUMS SZ(" \t\n\r+-0123456789.")
+
+enum {
+ CHANNEL_COUNT = 2,
+ SAMPLE_RATE = 44100,
+ BUFFER_SIZE = 1024 * 16,
+
+ REFERENCE_PITCH_INDEX = 5 * 12, // index of A_4
+
+#ifdef __EMSCRIPTEN__
+ TRACK_COUNT = 8,
+ ROLL_COUNT = 16,
+ PITCH_COUNT = 80,
+ VOICE_COUNT = 16,
+ SHEET_SIZE = 200,
+#else
+ TRACK_COUNT = 16,
+ ROLL_COUNT = 64,
+ PITCH_COUNT = 100,
+ VOICE_COUNT = 64,
+ SHEET_SIZE = 200,
+#endif
+
+ ROLL_DEFAULT_RATE = 8,
+ ROLL_DEFAULT_UI_OFFSET_Y = 2000,
+
+ TUNING_EQUAL_TEMPERAMENT = 0,
+ TUNING_PYTHAGOREAN,
+
+ WAVE_SINE = 0,
+ WAVE_UP,
+ WAVE_DOWN,
+ WAVE_SQUARE_UP,
+ WAVE_SQUARE_DOWN,
+ WAVE_KICK,
+ WAVE_COUNT,
+
+ INSTRUMENT_NONE = 0,
+ INSTRUMENT_OSCILLATOR,
+ INSTRUMENT_SAMPLER,
+
+ SAMPLER_OUTLINE_SIZE = 64,
+
+ EDIT_MODE_HAND = 0,
+ EDIT_MODE_ERASE,
+ EDIT_MODE_PAN,
+ EDIT_MODE_CLONE,
+
+ TINT_WHITE = 0,
+ TINT_ORANGE,
+ TINT_PINK,
+
+ UI_MAIN = 0,
+ UI_DEV,
+
+ UI_TAB_COUNT,
+};
+
+// Data types
+//
+
+typedef struct {
+ b8 enabled;
+ i64 time;
+ f64 duration;
+ f64 frequency;
+ f64 amplitude;
+ f64 phase[2];
+ i64 track;
+} Voice;
+
+typedef struct {
+ b8 enabled;
+ i64 time;
+ i64 duration;
+ i64 pitch;
+} Roll_Note;
+
+typedef struct {
+ b8 enabled;
+ i64 track;
+ b8 pitch_turned_off[PITCH_COUNT];
+ f64 tuning[PITCH_COUNT];
+ i64 tuning_tag;
+ i64 mark_pitch;
+ i64 rate;
+ Roll_Note notes[SHEET_SIZE];
+ i64 time;
+ i64 duration;
+ i64 loop_duration;
+ f64 ui_offset_x;
+ f64 ui_offset_y;
+
+ // dynamic properties
+ //
+ i32 last_index;
+ b8 grid_input;
+ i32 grid_note;
+ i32 grid_time;
+ b8 ui_offset_x_input;
+ b8 ui_offset_y_input;
+ b8 loop_input;
+} Roll;
+
+typedef struct {
+ f64 sustain;
+ f64 attack;
+ f64 decay;
+ f64 release;
+} Envelope;
+
+typedef struct {
+ i32 wave;
+ f64 warp;
+ f64 phase;
+ f64 stereo_width;
+ f64 volume;
+ Envelope envelope;
+} Oscillator;
+
+typedef DA(u8) da_u8_t;
+typedef DA(f32) da_f32_t;
+
+typedef struct {
+ da_f32_t data;
+ da_f32_t outline;
+ f64 begin;
+ f64 end;
+ f64 crossfade;
+ f64 base_frequency;
+ f64 volume;
+ Envelope envelope;
+} Sampler;
+
+typedef struct {
+ i32 instrument;
+ union {
+ Oscillator oscillator;
+ Sampler sampler;
+ };
+} Track;
+
+typedef struct {
+ // dynamic properties
+ //
+ b8 grid_input;
+ i32 grid_roll;
+ i32 grid_cell;
+ b8 ui_offset_input;
+ f64 ui_offset_x;
+ f64 ui_offset_y;
+ b8 duplicate_input;
+} Compose;
+
+typedef struct {
+ f32 normal[4];
+ f32 active[4];
+ f32 hover[4];
+} UI_Color;
+
+typedef struct {
+ b8 enabled;
+ b8 selected;
+ i64 row;
+ f64 time;
+ f64 duration;
+} UI_Grid_Item;
+
+typedef struct {
+ f64 x0; // widget position x
+ f64 y0; // widget position y
+ f64 width; // widget width
+ f64 height; // widget height
+ f64 offset_y; // grid offset y
+ f64 scale_x; // grid scale x
+ f64 scale_y; // grid scale y
+
+ // timeline
+ i64 time_begin;
+ i64 time_end;
+ i64 time_cursor;
+ i64 time_offset;
+
+ // The time rate determines how to convert item time into position
+ // on the X axis.
+ //
+ // x = (time - time_offset) * scale_x / time_rate
+ //
+ i64 time_rate;
+
+ i64 meter_num; // numerator
+ i64 meter_den; // denominator
+
+ i64 items_size;
+ UI_Grid_Item *items;
+} UI_Grid;
+
+// ================================================================
+//
+// Global state
+//
+// NOTE
+// At some point we want to move all global data into a struct
+// context_t.
+//
+// ================================================================
+
+// Graphics
+//
+
+struct NVGcontext *nvg;
+i32 font_text = -1;
+i32 font_icons = -1;
+
+// Audio
+//
+
+ma_device audio_device;
+
+// Input events
+//
+
+f64 mouse_x = 0;
+f64 mouse_y = 0;
+f64 mouse_dx = 0;
+f64 mouse_dy = 0;
+b8 lbutton_click = 0;
+b8 lbutton_down = 0;
+b8 rbutton_click = 0;
+b8 rbutton_down = 0;
+b8 mbutton_click = 0;
+b8 mbutton_down = 0;
+b8 mouse_on = 0;
+b8 shift_on = 0;
+b8 ctrl_on = 0;
+
+// Playback
+//
+
+b8 playback_suspended = 1;
+b8 playback_on = 0;
+i64 playback_frame = 0;
+i64 playback_lookahead = 0;
+i64 playback_offset_read = 0;
+i64 playback_offset_write = 0;
+mtx_t playback_mutex;
+
+// Buffers
+//
+
+f32 playback_buffer[BUFFER_SIZE];
+f32 playback_temp[BUFFER_SIZE];
+b8 key_pressed[512];
+c8 drop_file_name[4096];
+da_u8_t drop_file_data;
+
+// Project state
+//
+
+i64 current_track = 0;
+i64 current_roll = 0;
+i64 edit_mode = EDIT_MODE_HAND;
+mt64_state_t rng_mt64;
+
+c8 project_file_buf[4096];
+str_t project_file;
+
+Voice voices[VOICE_COUNT] = { 0 };
+Roll rolls[ROLL_COUNT];
+Track tracks[TRACK_COUNT];
+
+Compose compose = {
+ .grid_input = 0,
+ .grid_roll = 0,
+ .grid_cell = 0,
+ .ui_offset_input = 0,
+ .ui_offset_x = 0.,
+ .ui_offset_y = 0.,
+ .duplicate_input = 0,
+};
+
+UI_Color ui_colors[] = {
+ {
+ // TINT_WHITE
+ .normal = { .7f, .7f, .7f, .7f },
+ .active = { .7f, .7f, .7f, 1.f },
+ .hover = { 1.f, 1.f, 1.f, 1.f },
+ }, {
+ // TINT_ORANGE
+ .normal = { .9f, .65f, .4f, .6f },
+ .active = { .9f, .65f, .4f, 1.f },
+ .hover = { 1.f, 1.f, 1.f, 1.f },
+ }, {
+ // TINT_PINK
+ .normal = { .7f, .3f, .6f, .7f },
+ .active = { .8f, .25f, .6f, 1.f },
+ .hover = { 1.f, 1.f, 1.f, 1.f },
+ }
+};
+
+i64 ui_input_index = 0;
+i64 ui_input_active = -1;
+f64 ui_input_buffer = 0.;
+
+i32 ui_tab = UI_MAIN;
+
+// ================================================================
+//
+// Sound
+//
+// NOTE
+// When music and signal processing procedures become stable enough
+// we will separate them into a library and add tests.
+//
+// ================================================================
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunknown-pragmas"
+# pragma GCC push_options
+# pragma GCC optimize("O3")
+#endif
+
+void tuning_equal_temperament(f64 *tuning) {
+ assert(tuning != NULL);
+ if (tuning == NULL)
+ return;
+
+ for (i64 i = 0; i < PITCH_COUNT; ++i)
+ tuning[i] = REFERENCE_PITCH *
+ pow(EQUAL_TEMPERAMENT_FACTOR,
+ (f64) (i - REFERENCE_PITCH_INDEX));
+}
+
+void tuning_pythagorean(f64 *tuning, i64 base_pitch) {
+ assert(tuning != NULL && base_pitch >= 0 &&
+ base_pitch < PITCH_COUNT);
+ if (tuning == NULL || base_pitch < 0 || base_pitch >= PITCH_COUNT)
+ return;
+
+ f64 ref_freq = tuning[base_pitch];
+
+ f64 v[12] = {
+ ref_freq, // unison
+ ref_freq * 256. / 243., // minor second
+ ref_freq * 9. / 8., // major second
+ ref_freq * 32. / 27., // minor third
+ ref_freq * 81. / 64., // major third
+ ref_freq * 4. / 3., // perfect fourth
+ ref_freq * 1024. / 729., // diminished fifth
+ ref_freq * 3. / 2., // perfect fifth
+ ref_freq * 128. / 81., // minor sixth
+ ref_freq * 27. / 16., // major sixth
+ ref_freq * 16. / 9., // minor seventh
+ ref_freq * 243. / 128., // major seventh
+ };
+
+ for (i64 i = 0; i < PITCH_COUNT && i < base_pitch; i++) {
+ i64 octave = (base_pitch - i + 11) / 12;
+ i64 k = i - (base_pitch - 12 * octave);
+ assert(k >= 0 && k < 12);
+ tuning[i] = v[k] * pow(.5, octave);
+ }
+
+ for (i64 i = base_pitch; i < PITCH_COUNT && i < base_pitch + 12;
+ i++)
+ tuning[i] = v[i - base_pitch];
+
+ for (i64 i = base_pitch + 12; i < PITCH_COUNT; i++) {
+ i64 octave = (i - base_pitch) / 12;
+ i64 k = (i - base_pitch) % 12;
+ assert(k >= 0 && k < 12);
+ tuning[i] = v[k] * pow(2., octave);
+ }
+}
+
+f64 random_f64(f64 min, f64 max) {
+ if (max - min < EPS)
+ return min;
+ u64 x = mt64_generate(&rng_mt64);
+ u64 range = (u64) ((max - min) * 10000 + 0.5);
+ return min + (max - min) * ((1.0 / range) * (x % (range + 1)));
+}
+
+f64 envelope(f64 t, f64 attack, f64 decay, f64 sustain,
+ f64 duration, f64 release) {
+ f64 a = 1.;
+ if (t < attack)
+ a *= t / attack;
+ else if (t < attack + decay)
+ a *= 1. - (1. - sustain) * (t - attack) / decay;
+ else
+ a *= sustain;
+ if (t >= duration && t < duration + release)
+ a *= (duration + release - t) / release;
+ else if (t >= duration + release)
+ a = 0.;
+ return a;
+}
+
+f64 pitch_amplitude(i64 pitch) {
+ return .2 / exp(0.02 * pitch);
+}
+
+void play_voice(Track *track, Roll *roll,
+ i64 pitch, i64 duration) {
+ if (pitch < 0 || pitch >= PITCH_COUNT)
+ return;
+
+ assert(!voices[VOICE_COUNT - 1].enabled);
+ if (voices[VOICE_COUNT - 1].enabled)
+ return;
+
+ for (i32 n = VOICE_COUNT - 1; n > 0; --n)
+ voices[n] = voices[n - 1];
+
+ f64 frequency = roll->tuning[pitch];
+
+ switch (track->instrument) {
+ case INSTRUMENT_OSCILLATOR: {
+ Oscillator *osc = &track->oscillator;
+
+ f64 s = osc->stereo_width / 8;
+
+ voices[0] = (Voice) {
+ .enabled = 1,
+ .time = 0,
+ .duration = (f64) duration / (f64) SAMPLE_RATE,
+ .frequency = frequency,
+ .amplitude = pitch_amplitude(pitch) * osc->volume,
+ .phase = {
+ random_f64(-s, s),
+ random_f64(-s, s),
+ },
+ .track = roll->track,
+ };
+ } break;
+
+ case INSTRUMENT_SAMPLER: {
+ Sampler *sam = &track->sampler;
+
+ voices[0] = (Voice) {
+ .enabled = 1,
+ .time = 0,
+ .duration = (f64) duration / (f64) SAMPLE_RATE,
+ .frequency = frequency,
+ .amplitude = pitch_amplitude(pitch) * sam->volume,
+ .phase = { 0., 0., },
+ .track = roll->track,
+ };
+ } break;
+
+ default:;
+ }
+}
+
+f64 oscillator(i32 type, f64 frequency, f64 phase,
+ f64 warp, f64 t) {
+ if (type == WAVE_KICK) {
+ frequency /= 8.;
+ f64 q = t * frequency + phase;
+ frequency /= 8.;
+ f64 f = frequency * (10. + q) / (1. + q);
+ return sin(q * f * (M_PI * 2));
+ }
+
+ t = t * frequency + phase;
+ t = t - floor(t);
+
+ switch (type) {
+ case WAVE_SINE:
+ return sin(t * (M_PI * 2));
+ case WAVE_UP:
+ return -1. + t * 2.;
+ case WAVE_DOWN:
+ return 1. - t * 2.;
+ case WAVE_SQUARE_UP:
+ return t < .5 + warp / 2. ? -1. : 1.;
+ case WAVE_SQUARE_DOWN:
+ return t < .5 + warp / 2. ? 1. : -1.;
+ default:;
+ }
+
+ return 0.;
+}
+
+f64 sampler(Sampler *sam, i32 channel, f64 frequency,
+ i64 t) {
+ i64 i = (i64) floor((t * frequency) / sam->base_frequency + .5) *
+ 2 +
+ channel;
+
+ if (i < 0 || i >= sam->data.size)
+ return 0.;
+
+ return sam->data.values[i];
+}
+
+void audio_render(void) {
+ i64 frame_count = (BUFFER_SIZE / CHANNEL_COUNT);
+
+ if (mtx_lock(&playback_mutex) != thrd_success) {
+ assert(0);
+ return;
+ }
+
+ frame_count -= playback_lookahead;
+ mtx_unlock(&playback_mutex);
+
+ if (frame_count > 0) {
+ memset(playback_temp, 0,
+ frame_count * CHANNEL_COUNT * sizeof *playback_temp);
+
+ for (i64 i = 0; i < frame_count; i++) {
+ if (playback_on) {
+ // Note triggers
+ //
+
+ for (i32 k = 0; k < ROLL_COUNT; k++) {
+ Roll *roll = rolls + k;
+
+ if (!roll->enabled || playback_frame < roll->time ||
+ playback_frame >= roll->time + roll->duration)
+ continue;
+
+ i64 roll_frame = playback_frame - roll->time;
+ if (roll->loop_duration != 0)
+ roll_frame = roll_frame % roll->loop_duration;
+
+ for (i32 i = 0; i < SHEET_SIZE; i++) {
+ Roll_Note *note = roll->notes + i;
+ if (!note->enabled || note->time != roll_frame)
+ continue;
+
+ switch (tracks[roll->track].instrument) {
+ case INSTRUMENT_OSCILLATOR:
+ case INSTRUMENT_SAMPLER:
+ play_voice(tracks + roll->track, roll,
+ note->pitch, note->duration);
+ break;
+
+ default:;
+ }
+ }
+ }
+
+ ++playback_frame;
+ }
+
+ // Sound generation
+ //
+ {
+ b8 regroup_voices = 0;
+
+ for (i32 n = 0; n < VOICE_COUNT; n++) {
+ if (!voices[n].enabled)
+ break;
+
+ Track *track = tracks + voices[n].track;
+
+ switch (track->instrument) {
+ case INSTRUMENT_OSCILLATOR: {
+ Oscillator *osc = &track->oscillator;
+
+ i32 wave_type = osc->wave;
+ f64 warp = osc->warp;
+ f64 frequency = voices[n].frequency;
+ f64 amplitude = voices[n].amplitude;
+ f64 phase_l = osc->phase + voices[n].phase[0];
+ f64 phase_r = osc->phase + voices[n].phase[1];
+
+ f64 attack = osc->envelope.attack;
+ f64 decay = osc->envelope.decay;
+ f64 sustain = osc->envelope.sustain;
+ f64 duration = voices[n].duration;
+ f64 release = osc->envelope.release;
+
+ f64 t = (f64) voices[n].time / (f64) SAMPLE_RATE;
+ f64 a = amplitude * envelope(t, attack, decay,
+ sustain, duration,
+ release);
+
+ playback_temp[i * CHANNEL_COUNT] +=
+ (f32) (oscillator(wave_type, frequency, phase_l,
+ warp, t) *
+ a * GLOBAL_VOLUME);
+ playback_temp[i * CHANNEL_COUNT + 1] +=
+ (f32) (oscillator(wave_type, frequency, phase_r,
+ warp, t) *
+ a * GLOBAL_VOLUME);
+
+ voices[n].time++;
+
+ if (t > duration + release) {
+ voices[n].enabled = 0;
+ regroup_voices = 1;
+ }
+ } break;
+
+ case INSTRUMENT_SAMPLER: {
+ Sampler *sam = &track->sampler;
+
+ f64 frequency = voices[n].frequency;
+ f64 amplitude = voices[n].amplitude;
+
+ f64 attack = sam->envelope.attack;
+ f64 decay = sam->envelope.decay;
+ f64 sustain = sam->envelope.sustain;
+ f64 duration = voices[n].duration;
+ f64 release = sam->envelope.release;
+
+ f64 t = (f64) voices[n].time / (f64) SAMPLE_RATE;
+ f64 a = amplitude * envelope(t, attack, decay,
+ sustain, duration,
+ release);
+
+ if (frequency < EPS)
+ break;
+
+ f64 crossfade = (sam->crossfade * sam->base_frequency) /
+ frequency;
+ f64 sample_begin = (sam->begin * sam->base_frequency) /
+ frequency;
+ f64 sample_end = (sam->end * sam->base_frequency) /
+ frequency;
+ f64 sample_duration = sample_end - sample_begin -
+ crossfade;
+ f64 body_end = duration + release;
+
+ if (t <= body_end) {
+ // Play the body
+ //
+
+ f64 q = t;
+ if (sample_duration > EPS)
+ q -= sample_duration * floor(q / sample_duration);
+ q += sample_begin;
+
+ f64 u0 = 1.;
+
+ if (sample_duration > EPS && crossfade > EPS &&
+ t >= sample_duration && q < crossfade) {
+ // Play the body crossfade
+ //
+
+ f64 u = u0 * (1. - q / crossfade);
+ f64 r = sample_begin + sample_duration + q;
+ i64 k = (i64) floor(r * SAMPLE_RATE + .5);
+
+ playback_temp[i * CHANNEL_COUNT] +=
+ (f32) (sampler(sam, 0, frequency, k) * a *
+ u * GLOBAL_VOLUME);
+ playback_temp[i * CHANNEL_COUNT + 1] +=
+ (f32) (sampler(sam, 1, frequency, k) * a *
+ u * GLOBAL_VOLUME);
+ }
+
+ {
+ // Play the body main part
+ //
+
+ f64 u = u0;
+ if (t >= sample_duration && q < crossfade &&
+ crossfade > EPS)
+ u *= q / crossfade;
+
+ f64 r = sample_begin + q;
+ i64 k = (i64) floor(r * SAMPLE_RATE + .5);
+
+ playback_temp[i * CHANNEL_COUNT] +=
+ (f32) (sampler(sam, 0, frequency, k) * a *
+ u * GLOBAL_VOLUME);
+ playback_temp[i * CHANNEL_COUNT + 1] +=
+ (f32) (sampler(sam, 1, frequency, k) * a *
+ u * GLOBAL_VOLUME);
+ }
+ }
+
+ voices[n].time++;
+
+ if (t >= duration + release) {
+ voices[n].enabled = 0;
+ regroup_voices = 1;
+ }
+ } break;
+
+ default:;
+ }
+ }
+
+ if (regroup_voices)
+ for (i64 i = 0; i < VOICE_COUNT; ++i) {
+ if (voices[i].enabled)
+ continue;
+
+ b8 slot_found = 0;
+
+ for (i64 j = VOICE_COUNT - 1; j > i; --j) {
+ if (!voices[j].enabled)
+ continue;
+
+ memcpy(voices + i, voices + j,
+ sizeof *voices);
+ voices[j].enabled = 0;
+
+ slot_found = 1;
+ break;
+ }
+
+ if (!slot_found)
+ break;
+ }
+ }
+ }
+
+ i64 n0 = frame_count < (BUFFER_SIZE - playback_offset_write) /
+ CHANNEL_COUNT
+ ? frame_count
+ : (BUFFER_SIZE - playback_offset_write) /
+ CHANNEL_COUNT;
+ i64 n1 = frame_count - n0;
+
+ if (mtx_lock(&playback_mutex) != thrd_success) {
+ assert(0);
+ return;
+ }
+
+ if (n0 > 0)
+ memcpy(playback_buffer + playback_offset_write,
+ playback_temp,
+ n0 * CHANNEL_COUNT * sizeof *playback_temp);
+
+ if (n1 > 0)
+ memcpy(playback_buffer,
+ playback_temp + (n0 * CHANNEL_COUNT),
+ n1 * CHANNEL_COUNT * sizeof *playback_temp);
+
+ playback_offset_write = (playback_offset_write +
+ frame_count * CHANNEL_COUNT) %
+ BUFFER_SIZE;
+
+ playback_lookahead += frame_count;
+
+ mtx_unlock(&playback_mutex);
+ }
+
+#ifndef __EMSCRIPTEN__
+ if (frame_count == 0 && !playback_on)
+ // Sleep for 1/5 of the buffer duration
+ thrd_sleep(
+ &(struct timespec) {
+ .tv_nsec = (200000000ll * BUFFER_SIZE / CHANNEL_COUNT) /
+ SAMPLE_RATE },
+ NULL);
+#endif
+}
+
+void audio_callback(
+ ma_device * device,
+ void * void_out_,
+ void const *void_in_,
+ ma_uint32 frame_count
+) {
+ (void) device;
+ (void) void_in_;
+
+ f32 *out = (f32 *) void_out_;
+
+ for (i64 i = 0; i < frame_count; i++) {
+ out[i * 2] = 0.f;
+ out[i * 2 + 1] = 0.f;
+ }
+
+ if (mtx_lock(&playback_mutex) != thrd_success) {
+ assert(0);
+ return;
+ }
+
+ i64 n = frame_count < playback_lookahead
+ ? frame_count
+ : playback_lookahead;
+
+ i64 n0 = n < (BUFFER_SIZE - playback_offset_read) /
+ CHANNEL_COUNT
+ ? n
+ : (BUFFER_SIZE - playback_offset_read) /
+ CHANNEL_COUNT;
+ i64 n1 = n - n0;
+
+ if (n0 > 0)
+ memcpy(out, playback_buffer + playback_offset_read,
+ n0 * CHANNEL_COUNT * sizeof *out);
+
+ if (n1 > 0)
+ memcpy(out + (n0 * CHANNEL_COUNT), playback_buffer,
+ n1 * CHANNEL_COUNT * sizeof *out);
+
+ playback_offset_read = (playback_offset_read +
+ n * CHANNEL_COUNT) %
+ BUFFER_SIZE;
+
+ playback_lookahead -= n;
+
+ mtx_unlock(&playback_mutex);
+}
+
+void sampler_cleanup(Sampler *sampler) {
+ DA_DESTROY(sampler->data);
+ DA_DESTROY(sampler->outline);
+
+ memset(sampler, 0, sizeof *sampler);
+}
+
+#ifdef __GNUC__
+# pragma GCC pop_options
+# pragma GCC diagnostic pop
+#endif
+
+// ================================================================
+//
+// Data
+//
+// ================================================================
+
+ib_token_t parse_sign(ib_token_t tok, i64 *sign) {
+ assert(sign != NULL);
+ if (sign == NULL)
+ return (ib_token_t) {
+ .status = KIT_ERROR_INVALID_ARGUMENT,
+ };
+
+ ib_token_t next = ib_any(tok, SPACES);
+ ib_token_t plus = ib_exact(next, SZ("+"));
+ ib_token_t minus = ib_exact(next, SZ("-"));
+
+ *sign = 1;
+
+ if (plus.status == KIT_OK)
+ return ib_any(plus, SPACES);
+
+ if (minus.status == KIT_OK) {
+ *sign = -1;
+ return ib_any(minus, SPACES);
+ }
+
+ return next;
+}
+
+ib_token_t parse_int(ib_token_t tok, i64 *x) {
+ assert(x != NULL);
+ if (x == NULL)
+ return (ib_token_t) {
+ .status = KIT_ERROR_INVALID_ARGUMENT,
+ };
+
+ i64 sign;
+
+ tok = parse_sign(tok, &sign);
+ tok = ib_any(tok, SZ("0123456789"));
+
+ if (tok.status != KIT_OK)
+ return tok;
+
+ if (tok.size == 0) {
+ tok.status = KIT_PARSING_FAILED;
+ return tok;
+ }
+
+ str_t s = ib_str(tok);
+
+ *x = 0;
+
+ for (i64 i = 0; i < s.size; ++i)
+ *x = *x * 10 + (i64) (s.values[i] - '0');
+
+ *x *= sign;
+ return tok;
+}
+
+ib_token_t parse_float(ib_token_t tok, f64 *x) {
+ assert(x != NULL);
+ if (x == NULL)
+ return (ib_token_t) {
+ .status = KIT_ERROR_INVALID_ARGUMENT,
+ };
+
+ i64 sign;
+
+ tok = parse_sign(tok, &sign);
+ tok = ib_any(tok, SZ("0123456789."));
+
+ if (tok.status != KIT_OK)
+ return tok;
+
+ if (tok.size == 0) {
+ tok.status = KIT_PARSING_FAILED;
+ return tok;
+ }
+
+ str_t s = ib_str(tok);
+
+ i64 n = 0;
+ i64 dot = 0;
+
+ for (i64 i = 0; i < s.size; ++i) {
+ if (s.values[i] == '.')
+ dot = i;
+ else
+ n = n * 10 + (i64) (s.values[i] - '0');
+ }
+
+ *x = (f64) n;
+
+ if (dot < s.size - 1)
+ *x /= pow(10.0, s.size - dot - 1);
+
+ *x *= sign;
+ return tok;
+}
+
+ib_token_t parse_roll(ib_token_t tok, Roll *roll) {
+ assert(roll != NULL);
+ if (roll == NULL)
+ return (ib_token_t) {
+ .status = KIT_ERROR_INVALID_ARGUMENT,
+ };
+
+ // Init defaults
+ //
+ {
+ memset(roll, 0, sizeof *roll);
+
+ tuning_equal_temperament(roll->tuning);
+ roll->tuning_tag = TUNING_EQUAL_TEMPERAMENT;
+ roll->mark_pitch = REFERENCE_PITCH_INDEX;
+ roll->rate = ROLL_DEFAULT_RATE;
+ }
+
+ tok = ib_any(tok, SPACES);
+ tok = ib_exact(tok, SZ("roll"));
+ tok = ib_any(tok, SPACES);
+ tok = ib_exact(tok, SZ("{"));
+
+ while (tok.status == KIT_OK) {
+ tok = ib_any(tok, SPACES);
+
+ // Finish parsing if we reached closing brace
+ //
+
+ ib_token_t close = ib_exact(tok, SZ("}"));
+
+ if (close.status == KIT_OK) {
+ tok = close;
+ break;
+ }
+
+ // Parse property name
+ //
+
+ tok = ib_any(tok, SPACES);
+ tok = ib_none(tok, DELIM);
+
+ if (tok.status != KIT_OK)
+ break;
+
+ if (tok.size == 0) {
+ tok.status = KIT_PARSING_FAILED;
+ break;
+ }
+
+ ib_token_t name = tok;
+
+ // Parse property values
+ //
+
+ if (AR_EQUAL(ib_str(name), SZ("pitch_turned_off")))
+ for (i64 pitch = 0; tok.status == KIT_OK; ++pitch) {
+ i64 x;
+ ib_token_t next = parse_int(tok, &x);
+
+ if (next.status != KIT_OK)
+ break;
+
+ if (pitch >= PITCH_COUNT) {
+ printf("Too many roll pitch_turned_off values\n");
+ tok = ib_any(next, NUMS);
+ break;
+ }
+
+ roll->pitch_turned_off[pitch] = x ? 1 : 0;
+ tok = next;
+ }
+ else if (AR_EQUAL(ib_str(name), SZ("tuning")))
+ for (i64 pitch = 0; tok.status == KIT_OK; ++pitch) {
+ f64 x;
+ ib_token_t next = parse_float(tok, &x);
+
+ if (next.status != KIT_OK)
+ break;
+
+ if (pitch >= PITCH_COUNT) {
+ printf("Too many roll tuning values\n");
+ tok = ib_any(next, NUMS);
+ break;
+ }
+
+ roll->tuning[pitch] = x;
+ tok = next;
+ }
+ else if (AR_EQUAL(ib_str(name), SZ("notes")))
+ for (i64 note = 0; tok.status == KIT_OK; ++note) {
+ i64 note_enabled, note_time, note_duration, note_pitch;
+
+ ib_token_t next = parse_int(tok, &note_enabled);
+ next = parse_int(next, &note_time);
+ next = parse_int(next, &note_duration);
+ next = parse_int(next, &note_pitch);
+
+ if (next.status != KIT_OK)
+ break;
+
+ if (note >= SHEET_SIZE) {
+ printf("Too many roll notes values\n");
+ tok = ib_any(next, NUMS);
+ break;
+ }
+
+ roll->notes[note] = (Roll_Note) {
+ .enabled = note_enabled ? 1 : 0,
+ .time = note_time,
+ .duration = note_duration,
+ .pitch = note_pitch,
+ };
+
+ tok = next;
+ }
+ else if (AR_EQUAL(ib_str(name), SZ("ui_offset"))) {
+ f64 x, y;
+ ib_token_t next = parse_float(tok, &x);
+ next = parse_float(next, &y);
+
+ if (next.status == KIT_OK) {
+ tok = next;
+ roll->ui_offset_x = x;
+ roll->ui_offset_y = y;
+ } else {
+ printf("Ignored unknown roll property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ } else {
+ i64 x;
+ ib_token_t next = parse_int(tok, &x);
+
+ if (next.status == KIT_OK) {
+ tok = next;
+
+ if (AR_EQUAL(ib_str(name), SZ("enabled")))
+ roll->enabled = x ? 1 : 0;
+ else if (AR_EQUAL(ib_str(name), SZ("track")))
+ roll->track = x;
+ else if (AR_EQUAL(ib_str(name), SZ("tuning_tag")))
+ roll->tuning_tag = x;
+ else if (AR_EQUAL(ib_str(name), SZ("mark_pitch")))
+ roll->mark_pitch = x;
+ else if (AR_EQUAL(ib_str(name), SZ("rate")))
+ roll->rate = x;
+ else if (AR_EQUAL(ib_str(name), SZ("time")))
+ roll->time = x;
+ else if (AR_EQUAL(ib_str(name), SZ("duration")))
+ roll->duration = x;
+ else if (AR_EQUAL(ib_str(name), SZ("loop_duration")))
+ roll->loop_duration = x;
+ else {
+ printf("Ignored unknown roll property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ } else {
+ printf("Ignored unknown roll property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ }
+ }
+
+ return tok;
+}
+
+ib_token_t parse_track(ib_token_t tok,
+ Track *track) {
+ assert(track != NULL);
+ if (track == NULL)
+ return (ib_token_t) {
+ .status = KIT_ERROR_INVALID_ARGUMENT,
+ };
+
+ // Init defaults
+ //
+ {
+ memset(track, 0, sizeof *track);
+ track->instrument = INSTRUMENT_OSCILLATOR;
+ }
+
+ tok = ib_any(tok, SPACES);
+ tok = ib_exact(tok, SZ("track"));
+ tok = ib_any(tok, SPACES);
+ tok = ib_exact(tok, SZ("{"));
+
+ while (tok.status == KIT_OK) {
+ tok = ib_any(tok, SPACES);
+
+ // Finish parsing if we reached closing brace
+ //
+
+ ib_token_t close = ib_exact(tok, SZ("}"));
+
+ if (close.status == KIT_OK) {
+ tok = close;
+ break;
+ }
+
+ // Parse property name
+ //
+
+ tok = ib_any(tok, SPACES);
+ tok = ib_none(tok, DELIM);
+
+ if (tok.status != KIT_OK)
+ break;
+
+ if (tok.size == 0) {
+ tok.status = KIT_PARSING_FAILED;
+ break;
+ }
+
+ ib_token_t name = tok;
+
+ // Parse property values
+ //
+
+ if (AR_EQUAL(ib_str(name), SZ("data")))
+ // Array values
+ //
+
+ for (i64 k = 0; tok.status == KIT_OK; ++k) {
+ assert(track->instrument == INSTRUMENT_SAMPLER);
+ if (track->instrument != INSTRUMENT_SAMPLER) {
+ printf("Ignore unexpected track data property\n");
+ tok = ib_any(tok, NUMS);
+ break;
+ }
+
+ f64 x;
+ ib_token_t next = parse_float(tok, &x);
+
+ if (next.status != KIT_OK)
+ break;
+
+ DA_RESIZE(track->sampler.data, k + 1);
+
+ assert(track->sampler.data.size == k + 1);
+ if (track->sampler.data.size != k + 1) {
+ printf("Bad alloc\n");
+ fflush(stdout);
+ tok = ib_any(tok, NUMS);
+ break;
+ }
+
+ track->sampler.data.values[k] = x;
+ tok = next;
+ }
+ if (AR_EQUAL(ib_str(name), SZ("instrument")) ||
+ AR_EQUAL(ib_str(name), SZ("wave"))) {
+ // Integer values
+ //
+
+ i64 x;
+ ib_token_t next = parse_int(tok, &x);
+
+ if (next.status == KIT_OK) {
+ tok = next;
+
+ if (AR_EQUAL(ib_str(name), SZ("instrument"))) {
+ if (track->instrument == INSTRUMENT_SAMPLER)
+ DA_DESTROY(track->sampler.data);
+ if (x == INSTRUMENT_SAMPLER)
+ DA_INIT(track->sampler.data, 0, NULL);
+ track->instrument = (i32) x;
+ } else if (AR_EQUAL(ib_str(name), SZ("wave")) &&
+ track->instrument == INSTRUMENT_OSCILLATOR)
+ track->oscillator.wave = (i32) x;
+ else {
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ } else {
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ } else {
+ // Float values
+ //
+
+ f64 x;
+ ib_token_t next = parse_float(tok, &x);
+
+ if (next.status == KIT_OK) {
+ tok = next;
+
+ switch (track->instrument) {
+ case INSTRUMENT_OSCILLATOR:
+ if (AR_EQUAL(ib_str(name), SZ("warp")))
+ track->oscillator.warp = x;
+ else if (AR_EQUAL(ib_str(name), SZ("phase")))
+ track->oscillator.phase = x;
+ else if (AR_EQUAL(ib_str(name), SZ("stereo_width")))
+ track->oscillator.stereo_width = x;
+ else if (AR_EQUAL(ib_str(name), SZ("volume")))
+ track->oscillator.volume = x;
+ else if (AR_EQUAL(ib_str(name), SZ("sustain")))
+ track->oscillator.envelope.sustain = x;
+ else if (AR_EQUAL(ib_str(name), SZ("attack")))
+ track->oscillator.envelope.attack = x;
+ else if (AR_EQUAL(ib_str(name), SZ("decay")))
+ track->oscillator.envelope.decay = x;
+ else if (AR_EQUAL(ib_str(name), SZ("release")))
+ track->oscillator.envelope.release = x;
+ else {
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ break;
+
+ case INSTRUMENT_SAMPLER:
+ if (AR_EQUAL(ib_str(name), SZ("begin")))
+ track->sampler.begin = x;
+ else if (AR_EQUAL(ib_str(name), SZ("end")))
+ track->sampler.end = x;
+ else if (AR_EQUAL(ib_str(name), SZ("crossfade")))
+ track->sampler.crossfade = x;
+ else if (AR_EQUAL(ib_str(name), SZ("base_frequency")))
+ track->sampler.base_frequency = x;
+ else if (AR_EQUAL(ib_str(name), SZ("volume")))
+ track->sampler.volume = x;
+ else if (AR_EQUAL(ib_str(name), SZ("sustain")))
+ track->sampler.envelope.sustain = x;
+ else if (AR_EQUAL(ib_str(name), SZ("attack")))
+ track->sampler.envelope.attack = x;
+ else if (AR_EQUAL(ib_str(name), SZ("decay")))
+ track->sampler.envelope.decay = x;
+ else if (AR_EQUAL(ib_str(name), SZ("release")))
+ track->sampler.envelope.release = x;
+ else {
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ break;
+
+ default:
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ } else {
+ printf("Ignored unknown track property `%s`\n",
+ BS(ib_str(name)));
+ tok = ib_any(tok, NUMS);
+ }
+ }
+ }
+
+ return tok;
+}
+
+void project_parse_file(str_t file_name) {
+ FILE *f = fopen(BS(file_name), "rb");
+
+ if (f == NULL) {
+ printf("fopen failed.\n");
+ fflush(stdout);
+ return;
+ }
+
+ is_handle_t in = is_wrap_file(f, NULL);
+ input_buffer_t buf = ib_init(in, NULL);
+
+ ib_token_t last = ib_token(&buf);
+
+ memset(rolls, 0, sizeof rolls);
+ memset(tracks, 0, sizeof tracks);
+
+ current_roll = -1;
+ current_track = 0;
+
+ i64 roll_index = 0;
+ i64 track_index = 0;
+
+ for (;;) {
+ ib_token_t tok;
+
+ // Parse roll
+ //
+ {
+ Roll roll;
+ tok = parse_roll(last, &roll);
+
+ if (tok.status == KIT_OK) {
+ if (roll_index < ROLL_COUNT)
+ rolls[roll_index++] = roll;
+ else {
+ printf("Too many rolls.\n");
+ fflush(stdout);
+ break;
+ }
+
+ last = tok;
+ continue;
+ }
+ }
+
+ // Parse track
+ //
+ {
+ Track track;
+ tok = parse_track(last, &track);
+
+ if (tok.status == KIT_OK) {
+ if (track_index < TRACK_COUNT)
+ tracks[track_index++] = track;
+ else {
+ printf("Too many tracks.\n");
+ fflush(stdout);
+ break;
+ }
+
+ last = tok;
+ continue;
+ }
+ }
+
+ break;
+ }
+
+ ib_destroy(&buf);
+ is_destroy(in);
+
+ for (i64 i = 0; i < ROLL_COUNT; ++i)
+ if (rolls[i].enabled) {
+ current_roll = i;
+ break;
+ }
+}
+
+void project_print_to_file(str_t file_name) {
+ printf("Save project: %s\n", BS(file_name));
+ fflush(stdout);
+
+ FILE *f = fopen(BS(file_name), "wb");
+
+ if (f == NULL) {
+ printf("Failed to write file: %s\n", BS(file_name));
+ fflush(stdout);
+ return;
+ }
+
+ // Save rolls
+ //
+
+ i32 total_rolls = 0;
+
+ for (i64 i = 0; i < ROLL_COUNT; i++)
+ if (rolls[i].enabled)
+ total_rolls = i + 1;
+
+ for (i64 i = 0; i < total_rolls; i++) {
+ Roll *roll = rolls + i;
+
+ fprintf(f, "roll {\n");
+
+ fprintf(f, " enabled %d\n", (i32) roll->enabled);
+
+ if (roll->enabled) {
+ fprintf(f, " track %lld\n", roll->track);
+ fprintf(f, " pitch_turned_off ");
+ for (i64 pitch = 0; pitch < PITCH_COUNT; ++pitch)
+ fprintf(f, " %d", (i32) roll->pitch_turned_off[pitch]);
+ fprintf(f, "\n");
+ fprintf(f, " tuning ");
+ for (i64 pitch = 0; pitch < PITCH_COUNT; ++pitch)
+ fprintf(f, " %f", roll->tuning[pitch]);
+ fprintf(f, "\n");
+ fprintf(f, " tuning_tag %lld\n", roll->tuning_tag);
+ fprintf(f, " mark_pitch %lld\n", roll->mark_pitch);
+ fprintf(f, " rate %lld\n", roll->rate);
+
+ i32 total_notes = 0;
+ for (i32 n = 0; n < SHEET_SIZE; n++)
+ if (roll->notes[n].enabled)
+ total_notes = n + 1;
+
+ fprintf(f, " notes\n");
+ for (i32 n = 0; n < total_notes; n++)
+ fprintf(f, " %d %-6lld %-6lld %-6lld\n",
+ (i32) roll->notes[n].enabled, roll->notes[n].time,
+ roll->notes[n].duration, roll->notes[n].pitch);
+
+ fprintf(f, " time %lld\n", roll->time);
+ fprintf(f, " duration %lld\n", roll->duration);
+ fprintf(f, " loop_duration %lld\n", roll->loop_duration);
+ fprintf(f, " ui_offset %f %f\n\n", roll->ui_offset_x,
+ roll->ui_offset_y);
+ }
+
+ fprintf(f, "}\n");
+ }
+
+ // Save tracks
+ //
+
+ for (i64 i = 0; i < TRACK_COUNT; i++) {
+ Track *track = tracks + i;
+
+ fprintf(f, "track {\n");
+
+ fprintf(f, " instrument %d\n", track->instrument);
+
+ switch (track->instrument) {
+ case INSTRUMENT_OSCILLATOR: {
+ Oscillator *osc = &track->oscillator;
+
+ fprintf(f, " wave %d\n", osc->wave);
+ fprintf(f, " warp %f\n", osc->warp);
+ fprintf(f, " phase %f\n", osc->phase);
+ fprintf(f, " stereo_width %f\n", osc->stereo_width);
+ fprintf(f, " volume %f\n", osc->volume);
+ fprintf(f, " sustain %f\n", osc->envelope.sustain);
+ fprintf(f, " attack %f\n", osc->envelope.attack);
+ fprintf(f, " decay %f\n", osc->envelope.decay);
+ fprintf(f, " release %f\n\n", osc->envelope.release);
+ } break;
+
+ case INSTRUMENT_SAMPLER: {
+ Sampler *sam = &track->sampler;
+
+ fprintf(f, " data ");
+ for (i64 i = 0; i < sam->data.size; i++)
+ fprintf(f, " %f", sam->data.values[i]);
+ fprintf(f, "\n");
+
+ fprintf(f, " begin %f\n", sam->begin);
+ fprintf(f, " end %f\n", sam->end);
+ fprintf(f, " crossfade %f\n", sam->crossfade);
+ fprintf(f, " base_frequency %f\n", sam->base_frequency);
+ fprintf(f, " volume %f\n", sam->volume);
+ fprintf(f, " sustain %f\n", sam->envelope.sustain);
+ fprintf(f, " attack %f\n", sam->envelope.attack);
+ fprintf(f, " decay %f\n", sam->envelope.decay);
+ fprintf(f, " release %f\n\n", sam->envelope.release);
+ } break;
+
+ default:;
+ }
+
+ fprintf(f, "}\n");
+ }
+
+ fclose(f);
+}
+
+// ================================================================
+//
+// UI
+//
+// TODO
+// - UI library and tests.
+// - Grid widget.
+//
+// ================================================================
+
+void ui_begin(void) {
+ ui_input_index = 0;
+}
+
+void ui_end(void) {
+ if (ui_input_active != -1 && !lbutton_down) {
+ assert(0);
+
+ // Make sure to deactivate the input when the mouse button is not
+ // down.
+
+ ui_input_active = -1;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(0);
+#endif
+ }
+}
+
+b8 ui_button(f64 x0, f64 y0, f64 width, f64 height,
+ i64 color_index, str_t icon, str_t label,
+ b8 is_active) {
+ b8 has_cursor = mouse_x >= x0 && mouse_x < x0 + width &&
+ mouse_y >= y0 && mouse_y < y0 + height;
+ UI_Color c = ui_colors[color_index];
+
+ if (has_cursor)
+ nvgFillColor(nvg, nvgRGBAf(c.hover[0], c.hover[1], c.hover[2],
+ c.hover[3]));
+ else if (is_active)
+ nvgFillColor(nvg, nvgRGBAf(c.active[0], c.active[1],
+ c.active[2], c.active[3]));
+ else
+ nvgFillColor(nvg, nvgRGBAf(c.normal[0], c.normal[1],
+ c.normal[2], c.normal[3]));
+
+ if (icon.size > 0) {
+ nvgFontSize(nvg, height * .6);
+ nvgFontFaceId(nvg, font_icons);
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0 + height * .5, y0 + height * .5, icon.values,
+ icon.values + icon.size);
+ x0 += height;
+ width -= height * 2.;
+ }
+
+ if (label.size > 0) {
+ nvgFontSize(nvg, height * .8);
+ nvgFontFaceId(nvg, font_text);
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0 + width * .5, y0 + height * .5, label.values,
+ label.values + label.size);
+ }
+
+ return lbutton_click && has_cursor;
+}
+
+void ui_value_float(f64 x0, f64 y0, f64 width, f64 height,
+ i64 color_index, str_t label,
+ f64 scale, f64 min, f64 max,
+ f64 *data) {
+ assert(data != NULL && (scale < -EPS || scale > EPS));
+ if (data == NULL || !(scale < -EPS || scale > EPS))
+ return;
+
+ b8 has_cursor = mouse_x >= x0 && mouse_x < x0 + width &&
+ mouse_y >= y0 && mouse_y < y0 + height;
+ UI_Color c = ui_colors[color_index];
+
+ // Process input
+ //
+ {
+ if (ui_input_active == -1 && has_cursor &&
+ lbutton_click) {
+ ui_input_active = ui_input_index;
+ ui_input_buffer = *data * scale;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(1);
+#endif
+ }
+
+ if (ui_input_active == ui_input_index) {
+ if (lbutton_down) {
+ ui_input_buffer -= shift_on ? mouse_dy * 300.
+ : ctrl_on ? mouse_dy
+ : mouse_dy * 20.;
+ } else {
+ ui_input_active = -1;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(0);
+#endif
+ }
+
+ *data = ui_input_buffer / scale;
+ }
+
+ if (*data < min)
+ *data = min;
+ if (*data > max)
+ *data = max;
+ }
+
+ // Draw UI
+ //
+ {
+ if (ui_input_active == ui_input_index ||
+ (ui_input_active == -1 && has_cursor)) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x0 + width * .5, y0, width * .5, height);
+ nvgFillColor(nvg, nvgRGBAf(.9f, .95f, .9f, .5f));
+ nvgFill(nvg);
+ }
+
+ nvgFontSize(nvg, height * .8);
+ nvgFontFaceId(nvg, font_text);
+
+ if (label.size > 0) {
+ nvgFillColor(nvg, nvgRGBAf(c.normal[0], c.normal[1],
+ c.normal[2], c.normal[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0, y0 + height * 5. * .125, label.values,
+ label.values + label.size);
+ x0 += width / 2;
+ }
+
+ if (has_cursor)
+ nvgFillColor(nvg, nvgRGBAf(c.hover[0], c.hover[1],
+ c.hover[2], c.hover[3]));
+ else
+ nvgFillColor(nvg, nvgRGBAf(c.active[0], c.active[1],
+ c.active[2], c.active[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ c8 buf[256];
+ sprintf(buf, "%.3f", (f32) *data);
+ nvgText(nvg, x0 + width * .25, y0 + height * 5. * .125, buf,
+ NULL);
+ }
+
+ // Increment UI element input index
+ //
+
+ ++ui_input_index;
+}
+
+void ui_value_int(f64 x0, f64 y0, f64 width, f64 height,
+ i64 color_index, str_t label, f64 scale,
+ i64 min, i64 max, i64 *data) {
+ assert(data != NULL && (scale < -EPS || scale > EPS));
+ if (data == NULL || !(scale < -EPS || scale > EPS))
+ return;
+
+ b8 has_cursor = mouse_x >= x0 && mouse_x < x0 + width &&
+ mouse_y >= y0 && mouse_y < y0 + height;
+ UI_Color c = ui_colors[color_index];
+
+ // Process input
+ //
+ {
+ if (ui_input_active == -1 && has_cursor &&
+ lbutton_click) {
+ ui_input_active = ui_input_index;
+ ui_input_buffer = (f64) *data * scale;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(1);
+#endif
+ }
+
+ if (ui_input_active == ui_input_index) {
+ if (lbutton_down) {
+ ui_input_buffer -= shift_on ? mouse_dy * 300.
+ : ctrl_on ? mouse_dy
+ : mouse_dy * 20.;
+ } else {
+ ui_input_active = -1;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(0);
+#endif
+ }
+
+ *data = (i64) floor(ui_input_buffer / scale + .5);
+ }
+
+ if (*data < min)
+ *data = min;
+ if (*data > max)
+ *data = max;
+ }
+
+ // Draw UI
+ //
+ {
+ if (ui_input_active == ui_input_index ||
+ (ui_input_active == -1 && has_cursor)) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x0 + width * .5, y0, width * .5, height);
+ nvgFillColor(nvg, nvgRGBAf(.9f, .95f, .9f, .5f));
+ nvgFill(nvg);
+ }
+
+ nvgFontSize(nvg, (height * 4) / 5);
+ nvgFontFaceId(nvg, font_text);
+
+ if (label.size > 0) {
+ nvgFillColor(nvg, nvgRGBAf(c.normal[0], c.normal[1],
+ c.normal[2], c.normal[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0, y0 + (height * 5) / 8, label.values,
+ label.values + label.size);
+ x0 += width / 2;
+ }
+
+ if (has_cursor)
+ nvgFillColor(nvg, nvgRGBAf(c.hover[0], c.hover[1],
+ c.hover[2], c.hover[3]));
+ else
+ nvgFillColor(nvg, nvgRGBAf(c.active[0], c.active[1],
+ c.active[2], c.active[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ c8 buf[256];
+ sprintf(buf, "%lld", *data);
+ nvgText(nvg, x0 + width * .25, y0 + (height * 5.) * .125, buf,
+ NULL);
+ }
+
+ // Increment UI element input index
+ //
+
+ ++ui_input_index;
+}
+
+void ui_value_list(f64 x0, f64 y0, f64 width, f64 height,
+ i64 color_index, str_t label, f64 scale,
+ i64 size, str_t *names, i32 *data) {
+ assert(data != NULL && names != NULL &&
+ (scale < -EPS || scale > EPS));
+ if (data == NULL || names == NULL || !(scale < -EPS || scale > EPS))
+ return;
+
+ b8 has_cursor = mouse_x >= x0 && mouse_x < x0 + width &&
+ mouse_y >= y0 && mouse_y < y0 + height;
+ UI_Color c = ui_colors[color_index];
+
+ // Process input
+ //
+ {
+ if (ui_input_active == -1 && has_cursor &&
+ lbutton_click) {
+ ui_input_active = ui_input_index;
+ ui_input_buffer = (f64) *data * scale;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(1);
+#endif
+ }
+
+ if (ui_input_active == ui_input_index) {
+ if (lbutton_down) {
+ ui_input_buffer -= shift_on ? mouse_dy * 300.
+ : ctrl_on ? mouse_dy
+ : mouse_dy * 20.;
+ } else {
+ ui_input_active = -1;
+
+#ifndef __EMSCRIPTEN__
+ sapp_lock_mouse(0);
+#endif
+ }
+
+ *data = (i32) floor(ui_input_buffer / scale + .5);
+ }
+
+ if (*data < 0)
+ *data = 0;
+ if (*data >= size)
+ *data = size - 1;
+ }
+
+ // Draw UI
+ //
+ {
+ if (ui_input_active == ui_input_index ||
+ (ui_input_active == -1 && has_cursor)) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x0 + width * .5, y0, width * .5, height);
+ nvgFillColor(nvg, nvgRGBAf(.9f, .95f, .9f, .5f));
+ nvgFill(nvg);
+ }
+
+ nvgFontSize(nvg, height * .8);
+ nvgFontFaceId(nvg, font_text);
+
+ if (label.size > 0) {
+ nvgFillColor(nvg, nvgRGBAf(c.normal[0], c.normal[1],
+ c.normal[2], c.normal[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0, y0 + height * 5 * .125, label.values,
+ label.values + label.size);
+ x0 += width / 2;
+ }
+
+ if (has_cursor)
+ nvgFillColor(nvg, nvgRGBAf(c.hover[0], c.hover[1],
+ c.hover[2], c.hover[3]));
+ else
+ nvgFillColor(nvg, nvgRGBAf(c.active[0], c.active[1],
+ c.active[2], c.active[3]));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ str_t s = names[*data];
+ nvgText(nvg, x0 + width * .25, y0 + height * 5 * .125,
+ s.values, s.values + s.size);
+ }
+
+ // Increment UI element input index
+ //
+
+ ++ui_input_index;
+}
+
+void ui_grid(UI_Grid *grid) {
+ // TODO
+ //
+
+ (void) grid;
+}
+
+void ui_reset_offset(void) {
+ if (current_roll != -1) {
+ rolls[current_roll].ui_offset_x = 0.;
+ rolls[current_roll].ui_offset_y =
+ ROLL_DEFAULT_UI_OFFSET_Y;
+ }
+
+ compose.ui_offset_x = 0.;
+ compose.ui_offset_y = 0.;
+}
+
+void ui_header(f64 x0, f64 y0, f64 width, f64 height) {
+ if (height > 2. * width / 15.)
+ height = 2. * width / 15.;
+
+ f64 x = x0;
+ f64 s = height;
+
+ c8 backward_fast[] = "\uf049";
+ c8 play[] = "\uf04b";
+ c8 stop[] = "\uf04d";
+ c8 anchor[] = "\uf13d";
+ c8 hand_pointer[] = "\uf25a";
+ c8 eraser[] = "\uf12d";
+ c8 panning[] = "\uf047";
+ c8 clone[] = "\uf24d";
+
+ // Global actions
+ //
+
+ if (ui_button(x, y0, s, s, TINT_WHITE, SZ(backward_fast),
+ SZ(""), 1))
+ playback_frame = 0;
+ x += s;
+
+ if (ui_button(x, y0, s, s, TINT_WHITE,
+ playback_on ? SZ(stop) : SZ(play), SZ(""), 1))
+ playback_on = !playback_on;
+ x += s;
+
+ if (ui_button(x, y0, s, s, TINT_WHITE, SZ(anchor), SZ(""), 1))
+ ui_reset_offset();
+ x += s + s / 2;
+
+ // Editing mode
+ //
+
+ if (ui_button(x, y0, s, s, TINT_ORANGE, SZ(hand_pointer),
+ SZ(""), edit_mode == EDIT_MODE_HAND))
+ edit_mode = EDIT_MODE_HAND;
+ x += s;
+
+ if (ui_button(x, y0, s, s, TINT_ORANGE, SZ(eraser), SZ(""),
+ edit_mode == EDIT_MODE_ERASE))
+ edit_mode = EDIT_MODE_ERASE;
+ x += s;
+
+ if (ui_button(x, y0, s, s, TINT_ORANGE, SZ(panning), SZ(""),
+ edit_mode == EDIT_MODE_PAN))
+ edit_mode = EDIT_MODE_PAN;
+ x += s;
+
+ if (ui_button(x, y0, s, s, TINT_ORANGE, SZ(clone), SZ(""),
+ edit_mode == EDIT_MODE_CLONE))
+ edit_mode = EDIT_MODE_CLONE;
+}
+
+void ui_compose(f64 x0, f64 y0, f64 width, f64 height) {
+ f64 track_height = 60.;
+ f64 grid_scale = 50.;
+ f64 grid_rate = 3.;
+ f64 border = 2.;
+
+ assert(track_height > EPS);
+ assert(grid_rate > EPS);
+ assert(grid_scale > EPS);
+
+ // Time bar
+ //
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x0, y0 + border, width,
+ track_height * .2 - border * 2.);
+ nvgRect(nvg, x0, y0 + border + track_height * .8, width,
+ track_height * .2 - border * 2.);
+ nvgFillColor(nvg, nvgRGBAf(.7f, .6f, .5f, .65f));
+ nvgFill(nvg);
+
+ // Tracks
+ //
+
+ b8 hover_any = 0;
+
+ for (i64 i = 0; i < ROLL_COUNT; i++) {
+ Roll *roll = rolls + i;
+
+ if (!roll->enabled)
+ continue;
+
+ f64 top = y0 + track_height;
+ f64 bottom = y0 + height;
+ f64 dx = x0 + compose.ui_offset_x;
+ f64 l = dx + (roll->time * grid_scale) / SAMPLE_RATE;
+ f64 r = l + (roll->duration * grid_scale) / SAMPLE_RATE;
+ f64 u = top + compose.ui_offset_y +
+ roll->track * track_height;
+ f64 d = u + track_height;
+
+ f64 s = grid_scale / grid_rate;
+
+ assert(s > EPS);
+
+ if (l < x0)
+ l = x0;
+ if (r >= x0 + width)
+ r = x0 + width;
+ if (u < top)
+ u = top;
+ if (d > bottom)
+ d = bottom;
+
+ if (l >= r || u >= d)
+ continue;
+
+ f64 x = l;
+ f64 w = r - l;
+ f64 y = u;
+ f64 h = d - u;
+
+ b8 is_choosen = (current_roll == i);
+
+ b8 is_playing = playback_on &&
+ playback_frame >= roll->time &&
+ playback_frame < roll->time + roll->duration;
+
+ b8 has_cursor = (compose.grid_input &&
+ compose.grid_roll == i) ||
+ (mouse_x >= x && mouse_x < x + w &&
+ mouse_y >= y && mouse_y < y + h);
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x + border, y + border, w - border * 2.,
+ h - border * 2.);
+ nvgFillColor(nvg, is_choosen ? nvgRGBAf(.9f, .8f, .7f, 1.f)
+ : is_playing ? nvgRGBAf(.9f, .8f, .7f, .9f)
+ : has_cursor
+ ? nvgRGBAf(.8f, .8f, 1.f, 1.f)
+ : nvgRGBAf(.7f, .6f, .6f, .8f));
+ nvgFill(nvg);
+
+ if (has_cursor) {
+ if (rbutton_down ||
+ (edit_mode == EDIT_MODE_ERASE && lbutton_down)) {
+ if (current_roll == i)
+ current_roll = -1;
+ roll->enabled = 0;
+ } else {
+ if (edit_mode == EDIT_MODE_HAND && lbutton_click) {
+ if (current_roll == i) {
+ i64 cell = (i64) floor(
+ ((mouse_x - compose.ui_offset_x) *
+ grid_rate) /
+ grid_scale);
+ i64 c0 = (i64) floor((roll->time * grid_rate) /
+ SAMPLE_RATE);
+ i64 c1 = c0 + (i64) floor((roll->duration * grid_rate) /
+ SAMPLE_RATE);
+ compose.grid_input = 1;
+ compose.grid_roll = current_roll;
+ if (cell - c0 > c1 - cell) {
+ compose.grid_cell = c0;
+ roll->duration = (i64) floor(
+ ((cell - c0 + 1) * SAMPLE_RATE) / grid_rate);
+ } else {
+ compose.grid_cell = c1 - 1;
+ roll->duration = (i64) floor(
+ ((c1 - cell + 1) * SAMPLE_RATE) / grid_rate);
+ }
+ } else {
+ current_roll = i;
+ current_track = roll->track;
+ }
+ }
+ hover_any = 1;
+ }
+ }
+ }
+
+ // Placing new sheet
+ //
+
+ if (!hover_any && edit_mode == EDIT_MODE_HAND &&
+ lbutton_down &&
+ mouse_x >= x0 + compose.ui_offset_x &&
+ mouse_x < x0 + width) {
+ if (!compose.grid_input && mouse_y >= y0 &&
+ mouse_y < y0 + track_height)
+ playback_frame = ((mouse_x - compose.ui_offset_x) *
+ SAMPLE_RATE) /
+ grid_scale;
+ else if (edit_mode == EDIT_MODE_HAND && lbutton_click &&
+ mouse_y >= y0 + track_height &&
+ mouse_y < y0 + height) {
+ i64 track = (i64) floor(
+ (mouse_y - compose.ui_offset_y - y0) /
+ track_height) -
+ 1;
+ i64 cell = (i64) floor(
+ ((mouse_x - compose.ui_offset_x) * grid_rate) /
+ grid_scale);
+ i64 frame = (i64) floor((cell * SAMPLE_RATE) / grid_rate);
+
+ i64 n = -1;
+
+ for (i64 i = 0; i < ROLL_COUNT; i++)
+ if (!rolls[i].enabled) {
+ n = i;
+ break;
+ }
+
+ f64 x = x0 + compose.ui_offset_x +
+ (frame * grid_scale) / SAMPLE_RATE;
+ f64 y = y0 + track_height + compose.ui_offset_y +
+ track * track_height;
+
+ if (track < 0 || track >= TRACK_COUNT || x < x0 ||
+ x >= x0 + width || y < y0 ||
+ y + track_height >= y0 + height)
+ n = -1;
+
+ if (n != -1) {
+ compose.grid_input = 1;
+ compose.grid_roll = n;
+ compose.grid_cell = cell;
+
+ rolls[n] = (Roll) {
+ .enabled = 1,
+ .track = track,
+ .pitch_turned_off = { 0 },
+ .tuning = { 0 },
+ .tuning_tag = TUNING_EQUAL_TEMPERAMENT,
+ .mark_pitch = REFERENCE_PITCH_INDEX,
+ .rate = ROLL_DEFAULT_RATE,
+ .notes = { 0 },
+ .time = frame,
+ .duration = (i64) floor((ROLL_DEFAULT_RATE * SAMPLE_RATE) /
+ grid_rate),
+ .loop_duration = 0,
+ .ui_offset_x = 0,
+ .ui_offset_y = ROLL_DEFAULT_UI_OFFSET_Y,
+ };
+
+ tuning_equal_temperament(rolls[n].tuning);
+
+ current_roll = n;
+ current_track = track;
+ }
+ }
+ }
+
+ // Duplicate selected sheet
+ //
+
+ if (compose.duplicate_input) {
+ compose.duplicate_input = 0;
+
+ do {
+ if (current_roll == -1)
+ break;
+
+ i64 track = (i64) floor(
+ (mouse_y - compose.ui_offset_y - y0) /
+ track_height) -
+ 1;
+ i64 cell = (i64) floor(
+ ((mouse_x - compose.ui_offset_x) * grid_rate) /
+ grid_scale);
+
+ if (cell < 0 || track < 0 || track >= TRACK_COUNT)
+ break;
+
+ i64 frame = (i64) floor((cell * SAMPLE_RATE) / grid_rate);
+
+ i64 n = -1;
+
+ Roll *roll = rolls + current_roll;
+
+ b8 collision = 0;
+
+ for (i64 i = 0; i < ROLL_COUNT; i++) {
+ Roll *p = rolls + i;
+
+ if (p->enabled && p->track == track &&
+ ((p->time >= frame && p->time < frame + roll->duration) ||
+ (frame >= p->time && frame < p->time + p->duration))) {
+ collision = 1;
+ break;
+ }
+ }
+
+ if (collision)
+ break;
+
+ for (i64 i = 0; i < ROLL_COUNT; i++)
+ if (!rolls[i].enabled) {
+ n = i;
+ break;
+ }
+
+ if (n == -1)
+ break;
+
+ rolls[n] = *roll;
+ rolls[n].track = track;
+ rolls[n].time = frame;
+ } while (0);
+ }
+
+ // Panning input
+ //
+
+ if (mbutton_click ||
+ (edit_mode == EDIT_MODE_PAN && lbutton_click)) {
+ if (mouse_x >= x0 && mouse_y >= y0 + track_height &&
+ mouse_x < x0 + width && mouse_y < y0 + height)
+ compose.ui_offset_input = 1;
+ }
+
+ if (!(mbutton_down ||
+ (edit_mode == EDIT_MODE_PAN && lbutton_down)))
+ compose.ui_offset_input = 0;
+
+ // Track stretching input
+ //
+
+ if (compose.grid_input) {
+ if (edit_mode == EDIT_MODE_HAND && lbutton_down) {
+ i64 cell = (i64) floor(
+ ((mouse_x - compose.ui_offset_x) * grid_rate) /
+ grid_scale);
+ Roll *p = rolls + compose.grid_roll;
+
+ if (cell >= 0) {
+ if (compose.grid_cell <= cell) {
+ p->time = (compose.grid_cell * SAMPLE_RATE) / grid_rate;
+ p->duration = (i64) floor(
+ ((1 + cell - compose.grid_cell) * SAMPLE_RATE) /
+ grid_rate);
+ } else {
+ p->time = (cell * SAMPLE_RATE) / grid_rate;
+ p->duration = (i64) floor(
+ ((1 + compose.grid_cell - cell) * SAMPLE_RATE) /
+ grid_rate);
+ }
+ }
+
+ for (i64 i = 0; i < ROLL_COUNT; i++) {
+ if (i == compose.grid_roll)
+ continue;
+ Roll *q = rolls + i;
+ if (!q->enabled || p->track != q->track)
+ continue;
+ i64 q_cell = (i64) floor((q->time * grid_rate) / SAMPLE_RATE);
+ i64 q_size = (i64) floor((q->duration * grid_rate) /
+ SAMPLE_RATE);
+ if (compose.grid_cell < q_cell && cell >= q_cell) {
+ cell = q_cell - 1;
+ p->time = (i64) floor(
+ (compose.grid_cell * SAMPLE_RATE) / grid_rate);
+ p->duration = (i64) floor(
+ ((q_cell - compose.grid_cell) * SAMPLE_RATE) /
+ grid_rate);
+ }
+ if (compose.grid_cell > q_cell &&
+ cell < q_cell + q_size) {
+ cell = q_cell + q_size;
+ p->time = (i64) floor(((q_cell + q_size) * SAMPLE_RATE) /
+ grid_rate);
+ p->duration = (i64) floor(
+ ((1 + compose.grid_cell - q_cell - q_size) *
+ SAMPLE_RATE) /
+ grid_rate);
+ }
+ }
+
+ if (p->duration <= 0)
+ p->duration = (i64) floor(SAMPLE_RATE / grid_rate);
+ } else
+ compose.grid_input = 0;
+ }
+
+ // Playback indicator
+ //
+
+ f64 x = x0 + compose.ui_offset_x - border * 2. +
+ (playback_frame * grid_scale) / SAMPLE_RATE;
+ f64 w = border * 4.;
+
+ if (x >= x0 - border * 2 && x < x0 + width) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x, y0, w, height);
+ nvgFillColor(nvg, nvgRGBAf(.9f, .9f, .1f, .8f));
+ nvgFill(nvg);
+ }
+
+ // Draw cursor
+ //
+
+ if (mouse_on && !hover_any && !compose.grid_input &&
+ mouse_x >= x0 + compose.ui_offset_x) {
+ i64 track = (mouse_y - compose.ui_offset_y - y0) /
+ track_height -
+ 1;
+ i64 cell = (i64) floor(
+ ((mouse_x - compose.ui_offset_x) * grid_rate) /
+ grid_scale);
+ f64 x = x0 + compose.ui_offset_x +
+ (cell * grid_scale) / grid_rate;
+ f64 y = y0 + track_height + compose.ui_offset_y +
+ track * track_height;
+ f64 w = grid_scale / grid_rate;
+
+ if (track >= 0 && track < TRACK_COUNT && x >= x0 &&
+ x + w < x0 + width && y >= y0 + track_height &&
+ y + track_height < y0 + height) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x + border, y + border, w - border * 2,
+ track_height - border * 2);
+ nvgFillColor(nvg, nvgRGBAf(.8f, .7f, .6f, .7f));
+ nvgFill(nvg);
+ }
+ }
+
+ // Cursor indicator
+ //
+
+ if (mouse_on && mouse_x >= x0 + compose.ui_offset_x &&
+ mouse_x < x0 + width && mouse_y >= y0 &&
+ mouse_y < y0 + height) {
+ f64 dx = x0 + compose.ui_offset_x;
+ f64 s = grid_scale / grid_rate;
+ f64 x = dx + ((mouse_x - dx + s / 2) / s) * s;
+ f64 w = border * 4;
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x - w / 2, y0, w, height);
+ nvgFillColor(nvg, nvgRGBAf(.2f, .2f, .9f, .6f));
+ nvgFill(nvg);
+ }
+}
+
+void ui_choose_instrument(
+ Track *track,
+ f64 x0,
+ f64 y0,
+ f64 width,
+ f64 height
+) {
+ (void) height;
+
+ f64 text_height = 40.;
+
+ x0 += width * .125;
+ width *= .75;
+
+ if (ui_button(x0, y0, width, text_height, TINT_ORANGE,
+ SZ("\uf83e"), SZ("Oscillator"), 1)) {
+ track->instrument = INSTRUMENT_OSCILLATOR;
+
+ track->oscillator = (Oscillator) {
+ .wave = WAVE_SINE,
+ .warp = .0,
+ .phase = .0,
+ .stereo_width = .2,
+ .volume = 1.,
+ .envelope = {
+ .sustain = .15,
+ .attack = .007,
+ .decay = .3,
+ .release = .4,
+ },
+ };
+ }
+
+ if (ui_button(x0, y0 + text_height, width, text_height,
+ TINT_ORANGE, SZ("\uf1c7"), SZ("Sampler"), 1)) {
+ track->instrument = INSTRUMENT_SAMPLER;
+
+ memset(&track->sampler, 0, sizeof track->sampler);
+
+ DA_INIT(track->sampler.data, 0, NULL);
+ DA_INIT(track->sampler.outline, SAMPLER_OUTLINE_SIZE, NULL);
+
+ if (track->sampler.outline.size != SAMPLER_OUTLINE_SIZE) {
+ printf("Bad alloc\n");
+ fflush(stdout);
+ track->sampler.outline.size = 0;
+ }
+
+ track->sampler.begin = 0.;
+ track->sampler.end = 1.;
+ track->sampler.crossfade = .01;
+ track->sampler.base_frequency = 440.;
+ track->sampler.volume = .5;
+
+ track->sampler.envelope = (Envelope) {
+ .sustain = .15,
+ .attack = .007,
+ .decay = .3,
+ .release = .4,
+ };
+ }
+}
+
+void ui_oscillator(
+ Oscillator *osc,
+ f64 x0,
+ f64 y0,
+ f64 width,
+ f64 height
+) {
+ (void) height;
+
+ f64 text_height = 33.;
+ f64 x = x0 + width / 12.;
+ f64 w = width * 5. / 6.;
+
+ str_t wave_names[] = {
+ SZ("Sine"), SZ("Saw up"), SZ("Saw down"),
+ SZ("Sqr up"), SZ("Sqr down"), SZ("Kick"),
+ };
+
+ ui_value_list(x, y0, w, text_height, TINT_WHITE, SZ("Wave"),
+ 500., sizeof wave_names / sizeof *wave_names,
+ wave_names, &osc->wave);
+ ui_value_float(x, y0 + text_height * 1., w, text_height,
+ TINT_WHITE, SZ("Warp"), 10000, -1., 1.,
+ &osc->warp);
+ // FIXME
+ // Looping phase value.
+ ui_value_float(x, y0 + text_height * 2., w, text_height,
+ TINT_WHITE, SZ("Phase"), 10000, 0., 1.,
+ &osc->phase);
+ ui_value_float(x, y0 + text_height * 3., w, text_height,
+ TINT_WHITE, SZ("Stereo"), 10000, 0., 2.,
+ &osc->stereo_width);
+ ui_value_float(x, y0 + text_height * 4., w, text_height,
+ TINT_WHITE, SZ("Volume"), 10000, 0., 2.,
+ &osc->volume);
+
+ nvgFontSize(nvg, text_height);
+ nvgFontFaceId(nvg, font_text);
+ nvgFillColor(nvg, nvgRGBAf(1.f, 1.f, 1.f, 1.f));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
+ nvgText(nvg, x, y0 + text_height * 6.5, "Envelope", 0);
+
+ ui_value_float(x, y0 + text_height * 7., w, text_height,
+ TINT_WHITE, SZ("Sustain"), 10000, 0., 1.,
+ &osc->envelope.sustain);
+ ui_value_float(x, y0 + text_height * 8., w, text_height,
+ TINT_WHITE, SZ("Attack"), 100000, 0., 6.,
+ &osc->envelope.attack);
+ ui_value_float(x, y0 + text_height * 9., w, text_height,
+ TINT_WHITE, SZ("Decay"), 100000, 0., 6.,
+ &osc->envelope.decay);
+ ui_value_float(x, y0 + text_height * 10., w, text_height,
+ TINT_WHITE, SZ("Release"), 100000, 0., 6.,
+ &osc->envelope.release);
+}
+
+void ui_sampler(
+ Sampler *sampler,
+ f64 x0,
+ f64 y0,
+ f64 width,
+ f64 height
+) {
+ (void) height;
+
+ f64 text_height = 33.;
+ f64 x = x0 + width / 12.;
+ f64 w = width * 5. / 6.;
+ f64 sample_height = text_height * 4.;
+
+ if (drop_file_data.size != 0) {
+ // Load the audio sample from memory
+ //
+
+ ma_decoder_config decoder_config = ma_decoder_config_init(
+ ma_format_f32, CHANNEL_COUNT, SAMPLE_RATE);
+
+ ma_decoder decoder;
+
+ if (ma_decoder_init_memory(drop_file_data.values,
+ (size_t) drop_file_data.size,
+ &decoder_config,
+ &decoder) != MA_SUCCESS) {
+ printf("ma_decoder_init_memory failed.\n");
+ fflush(stdout);
+ } else {
+ do {
+ ma_uint64 length;
+
+ if (ma_decoder_get_length_in_pcm_frames(&decoder, &length) !=
+ MA_SUCCESS) {
+ printf("ma_decoder_get_length_in_pcm_frames failed.\n");
+ fflush(stdout);
+ break;
+ }
+
+ DA_RESIZE(sampler->data, length * CHANNEL_COUNT);
+
+ assert(sampler->data.size == (i64) (length * CHANNEL_COUNT));
+ if (sampler->data.size != (i64) (length * CHANNEL_COUNT)) {
+ printf("Bad alloc\n");
+ fflush(stdout);
+ sampler->data.size = 0;
+ break;
+ }
+
+ ma_uint64 frames_read;
+
+ if (ma_decoder_read_pcm_frames(
+ &decoder,
+ sampler->data.values,
+ length,
+ &frames_read
+ ) != MA_SUCCESS) {
+ printf("ma_decoder_read_pcm_frames failed.\n");
+ fflush(stdout);
+ sampler->data.size = 0;
+ break;
+ }
+
+ assert(sampler->data.size >= (i64) (frames_read * CHANNEL_COUNT));
+ sampler->data.size = frames_read * CHANNEL_COUNT;
+
+ if (sampler->outline.size == SAMPLER_OUTLINE_SIZE)
+ for (i64 i = 0; i < SAMPLER_OUTLINE_SIZE; ++i) {
+ f32 *v = sampler->data.values;
+ i64 k = (i * sampler->data.size) / SAMPLER_OUTLINE_SIZE;
+ assert(k >= 0 && k + 1 < sampler->data.size);
+ sampler->outline.values[i] = fabs(v[k] + v[k + 1]) * .5;
+ }
+ } while (0);
+
+ ma_decoder_uninit(&decoder);
+ }
+ }
+
+ if (sampler->data.size > 0) {
+ // Draw the sample outline
+ //
+
+ if (sampler->outline.size > 0) {
+ f64 dw = (f64) w / (f64) sampler->outline.size;
+ f64 h = sample_height * .5;
+ f64 y = y0 + h;
+
+ if (dw > .5) {
+ nvgBeginPath(nvg);
+
+ for (i64 i = 0; i < sampler->outline.size; i++)
+ nvgRect(nvg, x + dw * i,
+ y - h * sampler->outline.values[i], dw,
+ h * sampler->outline.values[i] * 2.);
+
+ nvgFillColor(nvg, nvgRGBAf(.8f, .4f, .0f, .7f));
+ nvgFill(nvg);
+ }
+ }
+ } else {
+ nvgFontSize(nvg, text_height * .5);
+ nvgFontFaceId(nvg, font_text);
+ nvgFillColor(nvg, nvgRGBAf(1.f, .7f, .2f, .5f));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x0 + width * .5, y0 + sample_height * .5,
+ "Drop a WAV file here", NULL);
+ }
+
+ ui_value_float(x, y0 + sample_height, w, text_height,
+ TINT_WHITE, SZ("Begin"), 100000, 0., 60.,
+ &sampler->begin);
+ ui_value_float(x, y0 + sample_height + text_height * 1., w,
+ text_height, TINT_WHITE, SZ("End"), 100000, 0.,
+ 60., &sampler->end);
+ ui_value_float(x, y0 + sample_height + text_height * 2., w,
+ text_height, TINT_WHITE, SZ("Crossfade"), 100000,
+ 0., 60., &sampler->crossfade);
+ ui_value_float(x, y0 + sample_height + text_height * 3., w,
+ text_height, TINT_WHITE, SZ("Base freq."), 500,
+ 1., 44100., &sampler->base_frequency);
+ ui_value_float(x, y0 + sample_height + text_height * 4., w,
+ text_height, TINT_WHITE, SZ("Volume."), 10000,
+ 0., 2., &sampler->volume);
+
+ nvgFontSize(nvg, text_height);
+ nvgFontFaceId(nvg, font_text);
+ nvgFillColor(nvg, nvgRGBAf(1.f, 1.f, 1.f, 1.f));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
+ nvgText(nvg, x, y0 + sample_height + text_height * 6.5,
+ "Envelope", NULL);
+
+ ui_value_float(x, y0 + sample_height + text_height * 7., w,
+ text_height, TINT_WHITE, SZ("Sustain"), 10000,
+ 0., 1., &sampler->envelope.sustain);
+ ui_value_float(x, y0 + sample_height + text_height * 8., w,
+ text_height, TINT_WHITE, SZ("Attack"), 100000,
+ 0., 6., &sampler->envelope.attack);
+ ui_value_float(x, y0 + sample_height + text_height * 9., w,
+ text_height, TINT_WHITE, SZ("Decay"), 100000, 0.,
+ 6., &sampler->envelope.decay);
+ ui_value_float(x, y0 + sample_height + text_height * 10., w,
+ text_height, TINT_WHITE, SZ("Release"), 100000,
+ 0., 6., &sampler->envelope.release);
+}
+
+void ui_track(Track *track, f64 x0, f64 y0,
+ f64 width, f64 height, str_t title) {
+ f64 text_height = 33.;
+ f64 header_offset = 60.;
+ f64 border = 2.;
+
+ nvgFontSize(nvg, text_height - border);
+ nvgFontFaceId(nvg, font_text);
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
+ nvgFillColor(nvg, nvgRGBAf(1.f, 1.f, 1.f, 1.f));
+
+ nvgText(nvg, x0 + border * 2., y0 + text_height - border * 2.,
+ title.values, title.values + title.size);
+
+ if (track->instrument != INSTRUMENT_NONE) {
+ f64 x = x0 + width - text_height * 1.5;
+ f64 s = text_height;
+
+ b8 has_cursor = mouse_x >= x && mouse_y >= y0 &&
+ mouse_x < x + s && mouse_y < y0 + s;
+
+ c8 xmark[] = "\uf00d";
+
+ nvgFontSize(nvg, text_height);
+ nvgFontFaceId(nvg, font_icons);
+
+ if (has_cursor)
+ nvgFillColor(nvg, nvgRGBAf(.9f, .8f, .3f, 1.f));
+ else
+ nvgFillColor(nvg, nvgRGBAf(.9f, .8f, .3f, .6f));
+
+ nvgText(nvg, x + border, y0 + s - border, xmark,
+ xmark + (sizeof xmark - 1));
+
+ if (has_cursor && lbutton_click) {
+ if (track->instrument == INSTRUMENT_SAMPLER)
+ sampler_cleanup(&track->sampler);
+
+ track->instrument = INSTRUMENT_NONE;
+ }
+ }
+
+ switch (track->instrument) {
+ case INSTRUMENT_OSCILLATOR:
+ ui_oscillator(&track->oscillator, x0, y0 + header_offset,
+ width, height - header_offset);
+ break;
+
+ case INSTRUMENT_SAMPLER:
+ ui_sampler(&track->sampler, x0, y0 + header_offset, width,
+ height - header_offset);
+ break;
+
+ default:
+ ui_choose_instrument(track, x0, y0 + header_offset, width,
+ height - header_offset);
+ }
+}
+
+void ui_roll(Roll *roll, f64 x0, f64 y0, f64 width,
+ f64 height, str_t title) {
+ f64 text_height = 35.;
+ f64 header_height = 35.;
+ f64 pianokey_height = 35.;
+ f64 pianokey_width = 100.;
+ f64 border = 2.;
+
+ f64 sheet_offset = 40.;
+ f64 sheet_scale = (40. * SAMPLE_RATE) / (10000. * roll->rate);
+
+ assert(sheet_scale > EPS);
+
+ // Title text
+ //
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x0, y0, width, text_height);
+ nvgFillColor(nvg, nvgRGBAf(.3f, .2f, .25f, .6f));
+ nvgFill(nvg);
+
+ nvgBeginPath(nvg);
+ nvgFontSize(nvg, text_height);
+ nvgFontFaceId(nvg, font_text);
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
+ nvgFillColor(nvg, nvgRGBAf(1.f, 1.f, 1.f, 1.f));
+ nvgText(nvg, x0 + border * 2., y0 + text_height - border * 2,
+ title.values, title.values + title.size);
+ nvgFill(nvg);
+
+ // Tuning control
+ //
+ {
+ f64 x = x0 + border;
+ f64 y = y0 + height - text_height - header_height;
+ f64 w = pianokey_width;
+ f64 h = header_height;
+
+ if (ui_button(x, y, w * .5, h, TINT_PINK, SZ("\uf52d"),
+ SZ(""),
+ roll->tuning_tag == TUNING_PYTHAGOREAN)) {
+ tuning_pythagorean(roll->tuning, roll->mark_pitch);
+ roll->tuning_tag = TUNING_PYTHAGOREAN;
+ }
+
+ if (ui_button(x + w * .5, y, w * .5, h, TINT_PINK,
+ SZ("\uf5ac"), SZ(""),
+ roll->tuning_tag == TUNING_EQUAL_TEMPERAMENT)) {
+ tuning_equal_temperament(roll->tuning);
+ roll->tuning_tag = TUNING_EQUAL_TEMPERAMENT;
+ }
+ }
+
+ // Loop control
+ //
+ {
+ f64 x = x0 + pianokey_width + sheet_offset;
+ f64 y = y0 + text_height;
+ f64 w = width - pianokey_width - sheet_offset;
+ f64 h = header_height * .2;
+ f64 border = 5.;
+
+ c8 repeat[] = "\uf363";
+
+ nvgFontSize(nvg, header_height - border * 2.);
+ nvgFontFaceId(nvg, font_icons);
+ if (roll->loop_duration == 0)
+ nvgFillColor(nvg, nvgRGBAf(.3f, .3f, .3f, .5f));
+ else
+ nvgFillColor(nvg, nvgRGBAf(.7f, .3f, .35f, .8f));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x - header_height / 2 - border,
+ y + header_height / 2, repeat,
+ repeat + (sizeof repeat - 1));
+
+ i64 rw = (roll->loop_duration * roll->rate * sheet_scale) /
+ SAMPLE_RATE;
+ i64 rx = roll->ui_offset_x;
+
+ if (rx < 0) {
+ rw += rx;
+ rx = 0;
+ }
+
+ if (rw > w - rx)
+ rw = w - rx;
+
+ if (rw > 0) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x + rx, y, rw, h);
+ nvgRect(nvg, x + rx, y + h * 4., rw, h);
+ nvgFillColor(nvg, nvgRGBAf(.7f, .4f, .3f, .6f));
+ nvgFill(nvg);
+ }
+
+ nvgBeginPath(nvg);
+ if (rx > 0) {
+ nvgRect(nvg, x, y, rx, h);
+ nvgRect(nvg, x, y + h * 4., rx, h);
+ }
+ nvgRect(nvg, x + rx + rw, y, w - rx - rw, h);
+ nvgRect(nvg, x + rx + rw, y + h * 4., w - rx - rw, h);
+ nvgFillColor(nvg, nvgRGBAf(.3f, .3f, .3f, .6f));
+ nvgFill(nvg);
+
+ if (mouse_x >= x && mouse_y >= y && mouse_x < x + w &&
+ mouse_y < y + header_height && !roll->loop_input &&
+ lbutton_click)
+ roll->loop_input = 1;
+
+ if (roll->loop_input && lbutton_down) {
+ f64 t = (mouse_x - x0 - pianokey_width - sheet_offset -
+ roll->ui_offset_x) /
+ sheet_scale;
+
+ if (t <= 0)
+ roll->loop_duration = 0;
+ else
+ roll->loop_duration = (i64) floor(
+ (t * SAMPLE_RATE) / roll->rate + .5);
+ }
+ }
+
+ if (!lbutton_down)
+ roll->loop_input = 0;
+
+ // Piano roll
+ //
+ {
+ f64 w = pianokey_width - border * 2.;
+ f64 h = pianokey_height - border * 2.;
+
+ b8 hover_any = 0;
+
+ for (i64 pitch = 0; pitch < PITCH_COUNT; pitch++) {
+ f64 x = x0 + border;
+ f64 y = y0 + height - (pitch + 1) * pianokey_height + border +
+ roll->ui_offset_y;
+ if (y > y0 + height - pianokey_height)
+ continue;
+ if (y < y0 + text_height + header_height)
+ break;
+
+ b8 has_cursor = mouse_x >= x && mouse_x < x + w &&
+ mouse_y >= y && mouse_y < y + h;
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x, y, w, h);
+ nvgFillColor(nvg, roll->pitch_turned_off[pitch]
+ ? nvgRGBAf(.8f, .8f, .8f, .6f)
+ : has_cursor
+ ? nvgRGBAf(.7f, .7f, 1.f, 1.f)
+ : nvgRGBAf(.8f, .8f, .8f, 1.f));
+ nvgFill(nvg);
+
+ nvgFontSize(nvg, h * .5);
+
+ if (pitch == roll->mark_pitch) {
+ nvgFontFaceId(nvg, font_icons);
+ nvgFillColor(nvg, nvgRGBAf(.1f, .1f, .1f, 1.f));
+ nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x + w / 3, y + h / 2, "\uf52d", NULL);
+ }
+
+ c8 buf[64];
+ c8 const *note_names[12] = {
+ "A", "Bb", "H", "C", "Db", "D",
+ "Eb", "E", "F", "Gb", "G", "Ab",
+ };
+
+ sprintf(buf, "%s",
+ note_names[pitch %
+ (sizeof note_names / sizeof *note_names)]);
+
+ nvgFontFaceId(nvg, font_text);
+ nvgFillColor(nvg, nvgRGBAf(.1f, .1f, .1f, 1.f));
+ nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x + border, y + h / 2, buf, NULL);
+
+ memset(buf, 0, sizeof buf);
+ sprintf(buf, "%.1f", (f32) roll->tuning[pitch]);
+
+ nvgFontSize(nvg, h * 2. / 3.);
+ nvgFillColor(nvg, nvgRGBAf(.1f, .1f, .1f, 1.f));
+ nvgTextAlign(nvg, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
+ nvgText(nvg, x + w, y + h * .5, buf, NULL);
+
+ // Piano roll input
+ //
+
+ if (has_cursor) {
+ hover_any = 1;
+
+ if (!roll->pitch_turned_off[pitch] &&
+ ((edit_mode == EDIT_MODE_HAND && lbutton_click) ||
+ (edit_mode == EDIT_MODE_HAND && lbutton_down &&
+ roll->last_index != pitch))) {
+ play_voice(tracks + roll->track, roll, pitch,
+ SAMPLE_RATE / roll->rate);
+ roll->mark_pitch = pitch;
+ }
+
+ if (rbutton_click ||
+ (edit_mode == EDIT_MODE_ERASE && lbutton_click))
+ roll->pitch_turned_off[pitch] =
+ !roll->pitch_turned_off[pitch];
+
+ roll->last_index = pitch;
+ }
+ }
+
+ if (!hover_any)
+ roll->last_index = -1;
+ }
+
+ // Panning input
+ //
+
+ if (mbutton_click || (edit_mode == EDIT_MODE_PAN &&
+ lbutton_click && !roll->loop_input)) {
+ if (mouse_x >= x0 + pianokey_width + sheet_offset &&
+ mouse_y >= y0 + text_height && mouse_x < x0 + width &&
+ mouse_y < y0 + height)
+ roll->ui_offset_x_input = 1;
+ if (mouse_x >= x0 && mouse_y >= y0 + text_height &&
+ mouse_x < x0 + width && mouse_y < y0 + height)
+ roll->ui_offset_y_input = 1;
+ }
+
+ if (!(mbutton_down ||
+ (edit_mode == EDIT_MODE_PAN && lbutton_down &&
+ !roll->loop_input))) {
+ roll->ui_offset_x_input = 0;
+ roll->ui_offset_y_input = 0;
+ }
+
+ // Draw music sheet
+ //
+
+ for (i64 pitch = 0; pitch < PITCH_COUNT; pitch++) {
+ f64 y = y0 + height - (pitch + 1) * pianokey_height +
+ roll->ui_offset_y;
+
+ if (y > y0 + height - pianokey_height)
+ continue;
+ if (y < y0 + text_height + header_height)
+ break;
+
+ f64 h = pianokey_height;
+
+ for (i64 t = 0; t < (roll->duration * roll->rate) / SAMPLE_RATE;
+ t++) {
+ f64 x = x0 + pianokey_width + sheet_offset + t * sheet_scale +
+ roll->ui_offset_x;
+
+ if (x >= x0 + width - sheet_scale)
+ break;
+
+ i64 note = -1;
+
+ for (i64 n = 0; n < SHEET_SIZE; n++) {
+ Roll_Note *p = roll->notes + n;
+ if (p->enabled && p->pitch == pitch &&
+ t >= (p->time * roll->rate + SAMPLE_RATE / 2) /
+ SAMPLE_RATE &&
+ t < ((p->time + p->duration) * roll->rate +
+ SAMPLE_RATE / 2) /
+ SAMPLE_RATE) {
+ note = n;
+ break;
+ }
+ }
+
+ if (note != -1)
+ continue;
+
+ // Draw empty cell
+ //
+
+ if (x < x0 + pianokey_width + sheet_offset)
+ continue;
+
+ f64 w = sheet_scale;
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x + border, y + border, w - border * 2.,
+ h - border * 2.);
+
+ b8 turned_off = roll->pitch_turned_off[pitch] ||
+ (roll->loop_duration > 0 &&
+ t >= (roll->loop_duration * roll->rate) /
+ SAMPLE_RATE);
+
+ b8 has_cursor = !roll->grid_input && mouse_x >= x &&
+ mouse_x < x + w && mouse_y >= y &&
+ mouse_y < y + h;
+
+ nvgFillColor(nvg, turned_off ? nvgRGBAf(.5f, .4f, .3f, .4f)
+ : has_cursor
+ ? nvgRGBAf(.7f, .7f, .8f, .6f)
+ : nvgRGBAf(.65f, .6f, .6f, .5f));
+ nvgFill(nvg);
+
+ // Empty cell input
+ //
+
+ if (!roll->grid_input && !turned_off && has_cursor &&
+ edit_mode == EDIT_MODE_HAND && lbutton_click)
+ for (i64 n = 0; n < SHEET_SIZE; n++)
+ if (!roll->notes[n].enabled) {
+ roll->notes[n] = (Roll_Note) {
+ .enabled = 1,
+ .time = (t * SAMPLE_RATE) / roll->rate,
+ .duration = SAMPLE_RATE / roll->rate,
+ .pitch = pitch
+ };
+ roll->grid_input = 1;
+ roll->grid_note = n;
+ roll->grid_time = t;
+ play_voice(tracks + roll->track, roll, pitch,
+ SAMPLE_RATE / roll->rate);
+ break;
+ }
+ }
+ }
+
+ // Draw notes
+ //
+
+ for (i64 n = 0; n < SHEET_SIZE; n++) {
+ Roll_Note *note = roll->notes + n;
+ if (!note->enabled)
+ continue;
+
+ f64 y = y0 + height - (note->pitch + 1) * pianokey_height +
+ roll->ui_offset_y;
+
+ if (y + pianokey_height > y0 + height)
+ continue;
+ if (y < y0 + text_height + header_height)
+ continue;
+
+ f64 x = x0 + pianokey_width + sheet_offset + roll->ui_offset_x +
+ (note->time * roll->rate * sheet_scale) / SAMPLE_RATE;
+ f64 w = (note->duration * roll->rate * sheet_scale) / SAMPLE_RATE;
+ f64 h = pianokey_height;
+
+ b8 has_cursor = (roll->grid_input && roll->grid_note == n) ||
+ (mouse_x >= x && mouse_x < x + w &&
+ mouse_y >= y && mouse_y < y + h);
+
+ if (x >= x0 + width)
+ continue;
+ if (x + w - sheet_scale < x0 + pianokey_width + sheet_offset)
+ continue;
+
+ // Draw note
+ //
+
+ i64 frame = playback_frame;
+
+ if (frame >= roll->time && frame < roll->time + roll->duration &&
+ roll->loop_duration > 0)
+ frame -= ((frame - roll->time) / roll->loop_duration) *
+ roll->loop_duration;
+
+ b8 is_playing = playback_on &&
+ frame >= roll->time + note->time &&
+ frame < roll->time + note->time + note->duration;
+
+ f64 underflow = ceil((x0 + pianokey_width + sheet_offset - x) /
+ sheet_scale);
+ f64 overflow = floor((x + w + sheet_scale - x0 - width) /
+ sheet_scale);
+
+ if (underflow > 0) {
+ x += underflow * sheet_scale;
+ w -= underflow * sheet_scale;
+ }
+
+ if (overflow > 0)
+ w -= overflow * sheet_scale;
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x + border, y + border, w - border * 2.,
+ h - border * 2.);
+
+ nvgFillColor(nvg, is_playing ? nvgRGBAf(1.f, .9f, .8f, 1.f)
+ : has_cursor
+ ? nvgRGBAf(.7f, .7f, .9f, 1.f)
+ : nvgRGBAf(.65f, .65f, .65f, 1.f));
+ nvgFill(nvg);
+
+ // Note input
+ //
+
+ if (has_cursor &&
+ (rbutton_down ||
+ (edit_mode == EDIT_MODE_ERASE && lbutton_down)))
+ note->enabled = 0;
+ }
+
+ // Note stretching input
+ //
+
+ if (roll->grid_input) {
+ if (edit_mode == EDIT_MODE_HAND && lbutton_down) {
+ i64 t = (i64) floor((mouse_x - x0 - pianokey_width -
+ sheet_offset - roll->ui_offset_x) /
+ sheet_scale +
+ .5);
+
+ Roll_Note *p = roll->notes + roll->grid_note;
+
+ if (t >= 0) {
+ if (roll->grid_time <= t) {
+ p->time = (roll->grid_time * SAMPLE_RATE) / roll->rate;
+ p->duration = ((1 + t - roll->grid_time) * SAMPLE_RATE) /
+ roll->rate;
+ } else {
+ p->time = (t * SAMPLE_RATE) / roll->rate;
+ p->duration = ((1 + roll->grid_time - t) * SAMPLE_RATE) /
+ roll->rate;
+ }
+ }
+
+ for (i32 n = 0; n < SHEET_SIZE; n++) {
+ if (n == roll->grid_note)
+ continue;
+ Roll_Note *q = roll->notes + n;
+ if (!q->enabled || q->pitch != p->pitch)
+ continue;
+ if (q->time < (roll->grid_time * SAMPLE_RATE) / roll->rate &&
+ q->time + q->duration > p->time) {
+ p->time = q->time + q->duration;
+ p->duration = (roll->grid_time * SAMPLE_RATE) / roll->rate >
+ p->time
+ ? ((roll->grid_time + 1) * SAMPLE_RATE) /
+ roll->rate -
+ p->time
+ : SAMPLE_RATE / roll->rate;
+ }
+ if (q->time > (roll->grid_time * SAMPLE_RATE) / roll->rate &&
+ q->time < p->time + p->duration) {
+ p->time = (roll->grid_time * SAMPLE_RATE) / roll->rate;
+ p->duration = q->time -
+ (roll->grid_time * SAMPLE_RATE) / roll->rate;
+ assert(p->duration > 0);
+ }
+ }
+ } else
+ roll->grid_input = 0;
+ }
+
+ // Playback indicator
+ //
+ {
+ i64 frame = playback_frame;
+
+ if (frame >= roll->time && frame < roll->time + roll->duration &&
+ roll->loop_duration > 0)
+ frame -= ((frame - roll->time) / roll->loop_duration) *
+ roll->loop_duration;
+
+ f64 x = x0 + pianokey_width + sheet_offset + roll->ui_offset_x -
+ border * 2. +
+ ((frame - roll->time) * roll->rate * sheet_scale) /
+ SAMPLE_RATE;
+ f64 w = border * 4;
+
+ if (x >= x0 + pianokey_width + sheet_offset - border * 2. &&
+ x < x0 + width) {
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x, y0 + text_height, w, height - text_height);
+ nvgFillColor(nvg, nvgRGBAf(.9f, .9f, .2f, .7f));
+ nvgFill(nvg);
+ }
+ }
+
+ // Cursor indicator
+ //
+ if (mouse_on &&
+ mouse_x >= x0 + pianokey_width + sheet_offset &&
+ mouse_x < x0 + width && mouse_y >= y0 &&
+ mouse_y < y0 + height) {
+ f64 dx = x0 + pianokey_width + sheet_offset + roll->ui_offset_x;
+ f64 x = dx + floor((mouse_x - dx) / sheet_scale + .5) *
+ sheet_scale;
+ f64 w = border * 4.;
+
+ nvgBeginPath(nvg);
+ nvgRect(nvg, x - w * .5, y0 + text_height, w,
+ height - text_height);
+ nvgFillColor(nvg, nvgRGBAf(.2f, .2f, .8f, .7f));
+ nvgFill(nvg);
+ }
+}
+
+// ================================================================
+//
+// Event handling
+//
+// ================================================================
+
+void init_audio(void) {
+ ma_device_config config = ma_device_config_init(
+ ma_device_type_playback);
+
+ config.playback.format = ma_format_f32;
+ config.playback.channels = CHANNEL_COUNT;
+ config.sampleRate = SAMPLE_RATE;
+ config.dataCallback = audio_callback;
+ config.pUserData = NULL;
+
+ if (ma_device_init(NULL, &config, &audio_device) !=
+ MA_SUCCESS) {
+ printf("ma_device_init failed.\n");
+ fflush(stdout);
+ return;
+ }
+
+ ma_device_start(&audio_device);
+}
+
+void init(void) {
+ // Init globals
+ //
+
+ memset(key_pressed, 0, sizeof key_pressed);
+ memset(drop_file_name, 0, sizeof drop_file_name);
+ memset(&drop_file_data, 0, sizeof drop_file_data);
+ memset(playback_buffer, 0, sizeof playback_buffer);
+ memset(playback_temp, 0, sizeof playback_temp);
+
+ // Init RNG
+ //
+
+ u64 rng_seed;
+ secure_random(sizeof rng_seed, &rng_seed);
+ mt64_init(&rng_mt64, rng_seed);
+ mt64_rotate(&rng_mt64);
+
+ // Init NanoVG
+ //
+
+#ifdef SOKOL_GLCORE33
+ nvg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES);
+#else
+ nvg = nvgCreateGLES3(NVG_ANTIALIAS | NVG_STENCIL_STROKES);
+#endif
+
+ // Init playback
+ //
+
+ if (mtx_init(&playback_mutex, mtx_plain) != thrd_success) {
+ printf("mtx_init failed.\n");
+ fflush(stdout);
+ }
+
+ // Load fonts
+ //
+
+ font_text = nvgCreateFontMem(nvg, "", (u8 *) ttf_text,
+ TTF_TEXT_SIZE, 0);
+
+ if (font_text == -1) {
+ printf("nvgCreateFontMem failed.\n");
+ fflush(stdout);
+ }
+
+ font_icons = nvgCreateFontMem(nvg, "", (u8 *) ttf_icons,
+ TTF_ICONS_SIZE, 0);
+
+ if (font_icons == -1) {
+ printf("nvgCreateFontMem failed.\n");
+ fflush(stdout);
+ }
+
+ // Init Saw state
+ //
+
+ // FIXME
+ // Add init routines
+
+ static f64 tuning[PITCH_COUNT];
+
+ tuning_equal_temperament(tuning);
+
+ for (i32 i = 0; i < ROLL_COUNT; i++) {
+ rolls[i] = (Roll) {
+ .enabled = (i == 0),
+ .track = 0,
+ .pitch_turned_off = { 0 },
+ .tuning = { 0 },
+ .tuning_tag = TUNING_EQUAL_TEMPERAMENT,
+ .mark_pitch = REFERENCE_PITCH_INDEX,
+ .rate = ROLL_DEFAULT_RATE,
+ .notes = { 0 },
+ .time = 0,
+ .duration = (48 * SAMPLE_RATE) / ROLL_DEFAULT_RATE,
+ .loop_duration = 0,
+ .ui_offset_x = 0,
+ .ui_offset_y = ROLL_DEFAULT_UI_OFFSET_Y,
+ };
+
+ memcpy(rolls[i].tuning, tuning, sizeof tuning);
+ }
+
+ for (i32 i = 0; i < TRACK_COUNT; i++)
+ tracks[i] = (Track) {
+ .instrument = INSTRUMENT_OSCILLATOR,
+ .oscillator = {
+ .wave = WAVE_SINE,
+ .warp = .0,
+ .phase = .0,
+ .stereo_width = .2,
+ .volume = 1.,
+ .envelope = {
+ .sustain = .15,
+ .attack = .007,
+ .decay = .3,
+ .release = .4,
+ },
+ }
+ };
+
+ // Determine the project file name
+ //
+
+ if (project_file.size == 0) {
+ c8 arena_buf[10000];
+ kit_allocator_t arena = kit_alloc_buffer(sizeof arena_buf,
+ arena_buf);
+
+ // No need to deallocate memory with arena.
+ str_builder_t cache = path_join(WRAP_STR(path_cache(&arena)),
+ SZ("saw"), &arena);
+
+ s32 s = folder_create_recursive(WRAP_STR(cache));
+
+ if (s != KIT_OK) {
+ printf("Failed to create cache folder: %s\n (code %d)",
+ BS(cache), (i32) s);
+ fflush(stdout);
+ } else {
+ c8 project_name[] = "quick.saw";
+
+ assert(cache.size + 1 + sizeof project_name <
+ sizeof project_file_buf);
+ if (cache.size + 1 + sizeof project_name >=
+ sizeof project_file_buf) {
+ printf("File name too big\n");
+ fflush(stdout);
+ return;
+ }
+
+ memcpy(project_file_buf, cache.values, cache.size);
+ project_file_buf[cache.size] = PATH_DELIM_C;
+ memcpy(project_file_buf + cache.size + 1, project_name,
+ sizeof project_name);
+
+ project_file.size = strlen(project_file_buf);
+ project_file.values = project_file_buf;
+ }
+ }
+
+ printf("Project file: %s\n", BS(project_file));
+ fflush(stdout);
+
+ // Load the project from a file
+ //
+
+ project_parse_file(project_file);
+}
+
+void frame(void) {
+ // TODO
+ // - Check how much time passed to see if we need to adjust the
+ // buffer size.
+
+ (void) ui_value_int;
+
+ audio_render();
+
+ i64 frame_width = sapp_width();
+ i64 frame_height = sapp_height();
+
+ glViewport(0, 0, frame_width, frame_height);
+ glClearColor(.11f, .09f, .08f, 1.f);
+ glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+ // Process input
+ //
+ {
+ // Playback
+ //
+ if (key_pressed[SAPP_KEYCODE_SPACE])
+ playback_on = !playback_on;
+ if (key_pressed[SAPP_KEYCODE_ENTER])
+ playback_frame = 0;
+
+ // Duplicate
+ //
+ if (key_pressed[SAPP_KEYCODE_D] ||
+ (edit_mode == EDIT_MODE_CLONE && lbutton_click))
+ compose.duplicate_input = 1;
+
+ // Panning control
+ //
+ if (key_pressed[SAPP_KEYCODE_ESCAPE])
+ ui_reset_offset();
+ for (i64 i = 0; i < ROLL_COUNT; i++) {
+ if (rolls[i].ui_offset_x_input)
+ rolls[i].ui_offset_x += mouse_dx;
+ if (rolls[i].ui_offset_y_input)
+ rolls[i].ui_offset_y += mouse_dy;
+ }
+ if (compose.ui_offset_input) {
+ compose.ui_offset_x += mouse_dx;
+ compose.ui_offset_y += mouse_dy;
+ }
+
+ // Tabs
+ //
+ if (key_pressed[SAPP_KEYCODE_TAB])
+ ui_tab = (ui_tab + 1) % UI_TAB_COUNT;
+ }
+
+ // Render UI
+ //
+
+ nvgBeginFrame(nvg, frame_width, frame_height, sapp_dpi_scale());
+ {
+ // We have to reset intermediate state for UI input to work
+ // correctly.
+ ui_begin();
+
+ switch (ui_tab) {
+ case UI_MAIN: {
+ // Adjust UI layout
+ //
+ i64 track_width = frame_width / 5;
+ if (track_width < 330)
+ track_width = 330;
+ if (track_width > frame_width / 2)
+ track_width = frame_width / 2;
+
+ i64 header_height = 80;
+ i64 roll_height = (frame_height * 3) / 5;
+ i64 compose_height = frame_height - roll_height -
+ header_height;
+ i64 track_height = frame_height - header_height;
+
+ // Render header
+ //
+
+ ui_header(0, // x0
+ 0, // y0
+ frame_width, // width
+ header_height // height
+ );
+
+ // Render compose view
+ //
+ ui_compose(0, // x0
+ header_height, // y0
+ frame_width - track_width, // width
+ compose_height // height
+ );
+
+ if (current_track != -1) {
+ // Render track view
+ //
+
+ c8 buf[64];
+ sprintf(buf, "Track %lld", current_track + 1);
+
+ ui_track(tracks + current_track, // track
+ frame_width - track_width, // x0
+ header_height, // y0
+ track_width, // width
+ track_height, // height
+ kit_str(strlen(buf), buf) // label
+ );
+ }
+
+ if (current_roll != -1) {
+ // Render roll view
+ //
+
+ c8 buf[64];
+ sprintf(buf, "Sheet %lld", current_roll + 1);
+
+ ui_roll(rolls + current_roll, // roll
+ 0, // x0
+ header_height + compose_height, // y0
+ frame_width - track_width, // width
+ roll_height, // height
+ kit_str(strlen(buf), buf) // label
+ );
+ }
+
+ } break;
+
+ case UI_DEV: {
+ // Grid widget testing
+ //
+
+ static const i64 time_rate = 256;
+ static UI_Grid_Item items[100] = { 0 };
+
+ static UI_Grid grid = {
+ .x0 = 0.,
+ .y0 = 0.,
+ .width = 0.,
+ .height = 0.,
+ .offset_y = 0.,
+ .scale_x = 10.,
+ .scale_y = 10.,
+ .time_begin = 0,
+ .time_end = time_rate * 10,
+ .time_cursor = 0,
+ .time_offset = 0,
+ .time_rate = time_rate,
+ .meter_num = 4,
+ .meter_den = 4,
+ .items_size = sizeof items / sizeof *items,
+ .items = items,
+ };
+
+ grid.x0 = 20.;
+ grid.y0 = 20.;
+ grid.width = frame_width - 40.;
+ grid.height = frame_height - 40.;
+ grid.time_cursor = (playback_frame * time_rate) /
+ SAMPLE_RATE;
+
+ ui_grid(&grid);
+ } break;
+
+ default:;
+ }
+
+ ui_end();
+ }
+ nvgEndFrame(nvg);
+
+ // Cleanup input state.
+ //
+ {
+ mouse_dx = 0;
+ mouse_dy = 0;
+ lbutton_click = 0;
+ rbutton_click = 0;
+ mbutton_click = 0;
+ shift_on = 0;
+ ctrl_on = 0;
+
+ memset(key_pressed, 0, sizeof key_pressed);
+
+ if (drop_file_data.size != 0) {
+ memset(drop_file_name, 0, sizeof drop_file_name);
+ DA_DESTROY(drop_file_data);
+ }
+ }
+}
+
+void cleanup(void) {
+ ma_device_uninit(&audio_device);
+
+ mtx_destroy(&playback_mutex);
+
+#ifdef SOKOL_GLCORE33
+ nvgDeleteGL3(nvg);
+#else
+ nvgDeleteGLES3(nvg);
+#endif
+
+ // Save the project to a file
+ //
+
+ if (project_file.size != 0)
+ project_print_to_file(project_file);
+
+ // Cleanup samplers
+ //
+ for (i64 i = 0; i < TRACK_COUNT; i++)
+ if (tracks[i].instrument == INSTRUMENT_SAMPLER)
+ sampler_cleanup(&tracks[i].sampler);
+
+ // Cleanup buffers
+ //
+ if (drop_file_data.size != 0)
+ DA_DESTROY(drop_file_data);
+}
+
+#ifdef __EMSCRIPTEN__
+void fetch_drop(
+ sapp_html5_fetch_response const *response) {
+ assert(response != NULL);
+
+ if (response == NULL || !response->succeeded) {
+ printf("Unable to fetch the dropped file\n");
+ fflush(stdout);
+ return;
+ }
+
+ assert(drop_file_data.size >= response->data.size);
+
+ DA_RESIZE(drop_file_data, response->data.size);
+}
+#endif
+
+void event(sapp_event const *event) {
+ // Resume the audio only after a user action. This is required
+ // for the browser compatibility.
+ //
+ {
+ if (playback_suspended &&
+ (event->type == SAPP_EVENTTYPE_MOUSE_DOWN ||
+ event->type == SAPP_EVENTTYPE_TOUCHES_BEGAN ||
+ event->type == SAPP_EVENTTYPE_KEY_DOWN)) {
+ init_audio();
+ playback_suspended = 0;
+ }
+ }
+
+ switch (event->type) {
+ case SAPP_EVENTTYPE_MOUSE_MOVE:
+ mouse_on = 1;
+ shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0;
+ ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0;
+ mouse_dx += event->mouse_dx;
+ mouse_dy += event->mouse_dy;
+ mouse_x = event->mouse_x;
+ mouse_y = event->mouse_y;
+ break;
+
+ case SAPP_EVENTTYPE_MOUSE_DOWN:
+ shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0;
+ ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0;
+ switch (event->mouse_button) {
+ case SAPP_MOUSEBUTTON_LEFT:
+ lbutton_down = 1;
+ lbutton_click = 1;
+ break;
+ case SAPP_MOUSEBUTTON_RIGHT:
+ rbutton_down = 1;
+ rbutton_click = 1;
+ break;
+ case SAPP_MOUSEBUTTON_MIDDLE:
+ mbutton_down = 1;
+ mbutton_click = 1;
+ break;
+ default:;
+ }
+ break;
+
+ case SAPP_EVENTTYPE_MOUSE_UP:
+ shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0;
+ ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0;
+ switch (event->mouse_button) {
+ case SAPP_MOUSEBUTTON_LEFT:
+ lbutton_down = 0;
+ break;
+ case SAPP_MOUSEBUTTON_RIGHT:
+ rbutton_down = 0;
+ break;
+ case SAPP_MOUSEBUTTON_MIDDLE:
+ mbutton_down = 0;
+ break;
+ default:;
+ }
+ break;
+
+ case SAPP_EVENTTYPE_MOUSE_LEAVE:
+ mouse_on = 0;
+ shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0;
+ ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0;
+ lbutton_down = 0;
+ rbutton_down = 0;
+ mbutton_down = 0;
+ break;
+
+ case SAPP_EVENTTYPE_KEY_DOWN:
+ shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0;
+ ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0;
+ if (!event->key_repeat && event->key_code >= 0 &&
+ event->key_code <
+ sizeof key_pressed / sizeof *key_pressed)
+ key_pressed[event->key_code] = 1;
+ break;
+
+ // Touch events.
+ // We treat touch as left mouse button and cursor.
+ //
+
+ case SAPP_EVENTTYPE_TOUCHES_BEGAN:
+ if (event->num_touches >= 1) {
+ mouse_x = event->touches[0].pos_x;
+ mouse_y = event->touches[0].pos_y;
+ lbutton_down = 1;
+ lbutton_click = 1;
+ mouse_on = 1;
+ }
+ break;
+
+ case SAPP_EVENTTYPE_TOUCHES_MOVED:
+ if (event->num_touches >= 1) {
+ i64 x = event->touches[0].pos_x;
+ i64 y = event->touches[0].pos_y;
+ mouse_dx += x - mouse_x;
+ mouse_dy += y - mouse_y;
+ mouse_x = x;
+ mouse_y = y;
+ }
+ break;
+
+ case SAPP_EVENTTYPE_TOUCHES_ENDED:
+ if (event->num_touches >= 1) {
+ i64 x = event->touches[0].pos_x;
+ i64 y = event->touches[0].pos_y;
+ mouse_dx += x - mouse_x;
+ mouse_dy += y - mouse_y;
+ mouse_x = x;
+ mouse_y = y;
+ }
+ lbutton_down = 0;
+ mouse_on = 0;
+ break;
+
+ case SAPP_EVENTTYPE_TOUCHES_CANCELLED:
+ lbutton_down = 0;
+ mouse_on = 0;
+ break;
+
+ case SAPP_EVENTTYPE_FILES_DROPPED: {
+ // Get the drop cursor position
+ //
+
+ mouse_x = event->mouse_x;
+ mouse_y = event->mouse_y;
+
+ // Get the dropped file name
+ //
+
+ i32 drop_count = sapp_get_num_dropped_files();
+ if (drop_count <= 0)
+ break;
+
+ c8 const *file_name = sapp_get_dropped_file_path(0);
+
+ i64 len = strlen(file_name);
+
+ assert(len > 0);
+ if (len <= 0)
+ break;
+
+ if (len >= (i64) sizeof drop_file_name)
+ len = sizeof drop_file_name - 1;
+
+ memcpy(drop_file_name, file_name, len);
+ drop_file_name[len] = '\0';
+
+ // Read the file data into the buffer
+ //
+
+#ifdef __EMSCRIPTEN__
+ i64 size = sapp_html5_get_dropped_file_size(0);
+#else
+ i64 size = file_info(str(len, file_name)).size;
+#endif
+
+ assert(size > 0);
+ if (size <= 0)
+ break;
+
+ if (drop_file_data.size > 0)
+ DA_DESTROY(drop_file_data);
+
+ DA_INIT(drop_file_data, size, NULL);
+
+ assert(drop_file_data.size == size);
+ if (drop_file_data.size != size) {
+ printf("Bad alloc\n");
+ fflush(stdout);
+ break;
+ }
+
+#ifdef __EMSCRIPTEN__
+ sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request) {
+ .dropped_file_index = 0,
+ .callback = fetch_drop,
+ .buffer = { .ptr = drop_file_data.values,
+ .size = size },
+ });
+#else
+ FILE *f = fopen(file_name, "rb");
+
+ assert(f != NULL);
+ if (f == NULL) {
+ printf("Unable to open file `%s`\n", file_name);
+ fflush(stdout);
+ break;
+ }
+
+ size = fread(drop_file_data.values, 1, size, f);
+ fclose(f);
+
+ assert(size > 0);
+ if (size <= 0) {
+ printf("Unable to read file `%s`\n", file_name);
+ fflush(stdout);
+ break;
+ }
+
+ DA_RESIZE(drop_file_data, size);
+#endif
+ } break;
+
+ default:;
+ }
+}
+
+c8 const *__lsan_default_suppressions(void) {
+ // There is leaks in NVidia driver on Linux.
+ return "leak:nvidia";
+}
+
+void logger(
+ c8 const *tag,
+ u32 log_level,
+ u32 log_item_id,
+ c8 const *message_or_null,
+ u32 line_nr,
+ c8 const *filename_or_null,
+ void * user_data
+) {
+ (void) tag;
+ (void) log_level;
+ (void) log_item_id;
+ (void) line_nr;
+ (void) filename_or_null;
+ (void) user_data;
+
+ if (message_or_null != NULL)
+ printf("%s", message_or_null);
+ printf("\n");
+ fflush(stdout);
+}
+
+sapp_desc sokol_main(i32 argc, c8 **argv) {
+ b8 print_version = 0;
+ b8 print_help = 0;
+
+ for (i32 i = 0; i < argc; i++)
+ if (strcmp(argv[i], "--version") == 0)
+ print_version = 1;
+ else if (strcmp(argv[i], "--help") == 0)
+ print_help = 1;
+ else if (argv[i][0] == '-' && argv[i][1] != '-' &&
+ argv[i][1] != '\0') {
+ for (i32 k = 1; argv[i][k] != '\0'; k++)
+ if (argv[i][k] == 'V')
+ print_version = 1;
+ else if (argv[i][k] == 'H')
+ print_help = 1;
+ else
+ printf("Unknown command line argument: \"-%c\"\n",
+ argv[i][k]);
+ } else if (i > 0 && project_file.size == 0)
+ project_file = kit_str(strlen(argv[i]), argv[i]);
+ else if (i > 0)
+ printf("Unknown command line argument: \"%s\"\n", argv[i]);
+
+ if (print_version)
+ printf("Saw v%d.%d.%d"
+#if VERSION_DEV
+ "_dev"
+#endif
+#ifndef NDEBUG
+ " (Debug)"
+#endif
+ " - Music sequencer standalone application.\n",
+ VERSION_MAJOR, VERSION_MINOR, VERSION_BABY);
+
+ if (print_help)
+ printf("Usage:\n%s [PROJECT_FILE]\n", argv[0]);
+
+ fflush(stdout);
+
+ if (print_version || print_help)
+ exit(0);
+
+ return (sapp_desc) {
+ .window_title = "Saw",
+ .width = 1280,
+ .height = 720,
+ .enable_dragndrop = 1,
+ .max_dropped_file_path_length = sizeof drop_file_name,
+ .init_cb = init,
+ .frame_cb = frame,
+ .cleanup_cb = cleanup,
+ .event_cb = event,
+ .logger.func = logger,
+ };
+}
+
+// ================================================================
+//
+// TESTS
+//
+// ================================================================
+
+#elif TESTS
+
+#include "kit/types.h"
+
+#define KIT_TEST_IMPLEMENTATION
+#include "kit/test.h"
+
+i32 main(i32 argc, char **argv) {
+ return run_tests(argc, argv);
+}
+
+// ================================================================
+
+#else
+#error Define either of: BUILDME=1, DEPENDENCIES=1, TESTS=1, EXE=1
+#endif