From 30740ca4131d1f574718262451b4410207dc8d4e Mon Sep 17 00:00:00 2001 From: Mitya Selivanov Date: Sun, 14 Jul 2024 21:12:37 +0200 Subject: Reworking the build system --- saw.c | 4679 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4679 insertions(+) create mode 100644 saw.c (limited to 'saw.c') 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 , 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 +#include +#include +#include +#include + +#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 +#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 +# define SOKOL_GLES3 +# define NANOVG_GLES3 1 +#else +# include +# 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 +#include + +// 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, ¬e_enabled); + next = parse_int(next, ¬e_time); + next = parse_int(next, ¬e_duration); + next = parse_int(next, ¬e_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 -- cgit v1.2.3