#if 0 /* #/ ================================================================ #/ #/ saw.c #/ #/ Music sequencer and audio editor. #/ Experimental, work in progress. #/ #/ ---------------------------------------------------------------- #/ #/ Qualities #/ #/ - Single source file #/ - All dependencies are included with the code #/ - No configuration required #/ - Cross-platform thanks to sokol and miniaudio #/ #/ NOTE #/ You can build the project easily using this two commands: #/ > gcc -o buildme -DBUILDME saw.c #/ > ./buildme #/ Or, on Windows, with MSVC compiler: #/ > cl.exe -Febuildme.exe -DBUILDME saw.c #/ > buildme.exe #/ #/ To-Do list #/ #/ - Code #/ - Self-compiling single-source code #/ - Fetching dependencies #/ - 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 #/ - Show an indicator when a track is playing without an attached synth #/ - 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 #/ - Clipboard integration #/ - 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 #/ - Different build types don't work without manual full rebuild because they store object files in the same folder #/ #/ 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 #/ #/ ---------------------------------------------------------------- #/ #/ (C) 2024 Mitya Selivanov , MIT License #/ #/ ================================================================ #/ #/ Self-compilation shell script #/ SRC=${0##*./} gcc -o buildme -DBUILDME=1 $SRC && ./buildme $@ && rm buildme exit $? # */ #endif // ================================================================ // // GLOBAL COMPILATION OPTIONS // // ================================================================ // Define one of the options for the linter #if !BUILDME && \ !DEPENDENCIES && \ !EXE && \ !TESTS #define EXE 1 #endif #define _GNU_SOURCE // ================================================================ // // BUILD SYSTEM // // ================================================================ #if BUILDME typedef int i32; typedef long long i64; typedef unsigned u32; typedef char c8; typedef signed char b8; #include #include #include #include #include #if defined(_WIN32) && !defined(__CYGWIN__) # define WIN32_LEAN_AND_MEAN # define NOMINMAX # include #else # include #endif #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(__linux__) || defined(__EMSCRIPTEN__) // Static C runtime is too unstable on Linux. Sad #define STATIC_RUNTIME 0 #else #define STATIC_RUNTIME 1 #endif // TODO Add command line option for the C runtime. 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; } b8 file_exists(c8 *name) { #if defined(_WIN32) && !defined(__CYGWIN__) if (PathFileExistsA(buf)) return 1; #else struct stat info; if (stat(name, &info) == 0 && (S_ISREG(info.st_mode) || S_ISDIR(info.st_mode))) return 1; #endif return 0; } void create_folder_recursive(c8 *path) { #if defined(_WIN32) && !defined(__CYGWIN__) system(fmt("if not exist %s mkdir %s", destination, destination)); #else system(fmt("mkdir %s -p", destination)); #endif } 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 the 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"); create_folder_recursive(destination); // Print info // printf("\nCompiler options: %s\n", flags); printf( "Link options: %s\n\n", link_flags); fflush(stdout); // Code generation // // TODO Add command line option for code generation. if (!file_exists("build_fonts.inl.h")) { 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 (!file_exists(deps)) { // TODO Add command line option for complete rebuild. 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 // Configuration // #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 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, SAMPLER_OUTLINE_SIZE = 64, }; // Constants // enum { 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, 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, NUM_UI_TABS, }; // 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; // FIXME Remove dynamic memory management. 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 // // ================================================================ #define SPACES SZ(" \t\n\r") #define DELIM SZ(" \t\n\r}") #define NUMS SZ(" \t\n\r+-0123456789.") 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 // // ================================================================ 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 *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) % NUM_UI_TABS; } // 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 i64 const 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 handle_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 = handle_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 Build options not provided. Try: gcc -DBUILDME saw.c #endif