// ================================================================ // // 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/threads.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 "../miniaudio/miniaudio.h" #include "../nanovg/nanovg.h" #include "../nanovg/nanovg_gl.h" #include #include // Fonts data #include "fonts.inl.h" // ================================================================ // // Definitions // // ================================================================ // Program version // #define VERSION_MAJOR 0 #define VERSION_MINOR 0 #define VERSION_BABY 2 #define VERSION_DEV 1 // Constants // enum { // TODO // Use 28224000 for time rate, divisible by common sample rates // like 192000, 44100 etc. CHANNEL_COUNT = 2, SAMPLE_RATE = 44100, BUFFER_SIZE = 2048, TRACK_COUNT = 16, ROLL_COUNT = 32, PITCH_COUNT = 80, VOICE_COUNT = 32, UNISON_COUNT = 100, SHEET_SIZE = 200, ROLL_DEFAULT_RATE = 8, ROLL_DEFAULT_UI_OFFSET_Y = 710, INSTRUMENT_SINE = 0, INSTRUMENT_SAW_UP, INSTRUMENT_SAW_DOWN, INSTRUMENT_SQUARE_UP, INSTRUMENT_SQUARE_DOWN, INSTRUMENT_KICK, INSTRUMENT_COUNT, TRACK_INPUT_NONE = 0, TRACK_INPUT_INSTRUMENT, TRACK_INPUT_WARP, TRACK_INPUT_PHASE, TRACK_INPUT_UNISON, TRACK_INPUT_SPREAD, TRACK_INPUT_STEREO_WIDTH, TRACK_INPUT_VOLUME, TRACK_INPUT_SUSTAIN, TRACK_INPUT_ATTACK, TRACK_INPUT_DECAY, TRACK_INPUT_RELEASE, EDIT_MODE_HAND = 0, EDIT_MODE_ERASE, EDIT_MODE_PAN, EDIT_MODE_CLONE, }; // Data types // typedef struct { b8 enabled; i64 time; f64 duration; f64 frequency; f64 amplitude; f64 phase[2]; i64 track; } saw_voice_t; typedef struct { b8 enabled; i64 time; i64 duration; i64 pitch; } saw_roll_note_t; typedef struct { b8 enabled; i64 track; b8 pitch_turned_off[PITCH_COUNT]; i64 rate; saw_roll_note_t notes[SHEET_SIZE]; i64 time; i64 duration; i64 loop_duration; i64 ui_offset_x; i64 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; } saw_roll_t; typedef struct { f64 sustain; f64 attack; f64 decay; f64 release; } saw_envelope_t; typedef struct { i32 instrument; f64 warp; f64 phase; i32 unison; f64 spread; f64 stereo_width; f64 volume; saw_envelope_t envelope; // dynamic properties // i32 value_input; f64 value_buffer; } saw_track_t; typedef struct { i64 rolls[ROLL_COUNT]; // dynamic properties // b8 grid_input; i32 grid_roll; i32 grid_cell; b8 ui_offset_input; i64 ui_offset_x; i64 ui_offset_y; b8 duplicate_input; } saw_compose_t; // ================================================================ // // Global state // // ================================================================ static struct NVGcontext *saw_nvg; static ma_device saw_audio_device; // Input events // static i64 saw_mouse_x = 0; static i64 saw_mouse_y = 0; static i64 saw_mouse_dx = 0; static i64 saw_mouse_dy = 0; static b8 saw_lbutton_click = 0; static b8 saw_lbutton_down = 0; static b8 saw_rbutton_click = 0; static b8 saw_rbutton_down = 0; static b8 saw_mbutton_click = 0; static b8 saw_mbutton_down = 0; static b8 saw_shift_on = 0; static b8 saw_ctrl_on = 0; static b8 saw_key_pressed[512] = { 0 }; static b8 saw_playback_on = 0; static i64 saw_playback_frame = 0; static i64 saw_playback_lookahead = 0; static i64 saw_playback_offset_read = 0; static i64 saw_playback_offset_write = 0; static f32 saw_playback_buffer[BUFFER_SIZE] = { 0.f }; static mtx_t saw_playback_mutex; static i64 saw_current_track = 0; static i64 saw_current_roll = 0; static i64 saw_edit_mode = EDIT_MODE_HAND; static char saw_project_file_buf[4096]; static str_t saw_project_file; static i32 saw_font_text = -1; static i32 saw_font_icons = -1; static mt64_state_t saw_rng_mt64; static saw_voice_t saw_voices[VOICE_COUNT] = { 0 }; static saw_roll_t saw_rolls[ROLL_COUNT]; static saw_track_t saw_tracks[TRACK_COUNT]; static saw_compose_t saw_compose = { .rolls = { -1 }, .grid_input = 0, .grid_roll = 0, .grid_cell = 0, .ui_offset_input = 0, .ui_offset_x = 0, .ui_offset_y = 0, .duplicate_input = 0, }; // ================================================================ // // Sound // // ================================================================ #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" # pragma GCC diagnostic ignored "-Wunknown-pragmas" # pragma GCC push_options # pragma GCC optimize("O3") #endif static f64 saw_random(f64 min, f64 max) { if (max - min < .000001) return min; u64 x = mt64_generate(&saw_rng_mt64); u64 range = (u64) ((max - min) * 10000 + 0.5); return min + (max - min) * ((1.0 / range) * (x % (range + 1))); } static f64 saw_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; } static f64 saw_pitch_frequency(i64 pitch) { return pow(2., 5.5 + pitch / 12.); } static f64 saw_pitch_amplitude(i64 pitch) { return .2 / exp(0.02 * pitch); } static void saw_play_voice(saw_track_t *track, saw_roll_t *roll, i64 pitch, i64 duration) { if (saw_voices[VOICE_COUNT - 1].enabled) return; for (i32 n = VOICE_COUNT - 1; n > 0; --n) saw_voices[n] = saw_voices[n - 1]; f64 s = track->stereo_width / 8; saw_voices[0] = (saw_voice_t) { .enabled = 1, .time = 0, .duration = (f64) duration / (f64) SAMPLE_RATE, .frequency = saw_pitch_frequency(pitch), .amplitude = saw_pitch_amplitude(pitch) * track->volume, .phase = { saw_random(-s, s), saw_random(-s, s), }, .track = roll->track, }; } static f64 saw_oscillator(i32 type, f64 frequency, f64 phase, f64 warp, f64 t) { if (type == INSTRUMENT_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 INSTRUMENT_SINE: return sin(t * (M_PI * 2)); case INSTRUMENT_SAW_UP: return -1. + t * 2.; case INSTRUMENT_SAW_DOWN: return 1. - t * 2.; case INSTRUMENT_SQUARE_UP: return t < .5 + warp / 2. ? -1. : 1.; case INSTRUMENT_SQUARE_DOWN: return t < .5 + warp / 2. ? 1. : -1.; default:; } return 0.; } static void saw_audio_render(void) { if (mtx_lock(&saw_playback_mutex) != thrd_success) { assert(0); return; } i64 frame_count = (BUFFER_SIZE / CHANNEL_COUNT) - saw_playback_lookahead; for (i64 i = 0; i < frame_count; i++) { if (saw_playback_on) { // Note triggers // for (i32 k = 0; k < ROLL_COUNT; k++) { saw_roll_t *roll = saw_rolls + k; if (!roll->enabled || saw_playback_frame + 1 <= roll->time || saw_playback_frame + 1 > roll->time + roll->duration) continue; i64 play_frame = roll->loop_duration == 0 ? saw_playback_frame : saw_playback_frame - ((saw_playback_frame + 1 - roll->time) / roll->loop_duration) * roll->loop_duration; for (i32 i = 0; i < SHEET_SIZE; i++) { saw_roll_note_t *note = roll->notes + i; if (!note->enabled) continue; i64 note_frame = roll->time + note->time; if (play_frame + 1 <= note_frame || play_frame > note_frame) continue; saw_play_voice(saw_tracks + roll->track, roll, note->pitch, note->duration); } } ++saw_playback_frame; } // Oscillators // { i64 k = (saw_playback_offset_write + i * CHANNEL_COUNT) % BUFFER_SIZE; saw_playback_buffer[k] = 0.f; saw_playback_buffer[k + 1] = 0.f; for (i32 n = 0; n < VOICE_COUNT; n++) { if (!saw_voices[n].enabled) continue; saw_track_t *track = saw_tracks + saw_voices[n].track; i32 wave_type = track->instrument; f64 warp = track->warp; f64 frequency = saw_voices[n].frequency; f64 amplitude = saw_voices[n].amplitude; f64 phase_l = track->phase + saw_voices[n].phase[0]; f64 phase_r = track->phase + saw_voices[n].phase[1]; f64 attack = track->envelope.attack; f64 decay = track->envelope.decay; f64 sustain = track->envelope.sustain; f64 duration = saw_voices[n].duration; f64 release = track->envelope.release; f64 t = (f64) saw_voices[n].time / (f64) SAMPLE_RATE; f64 a = amplitude * saw_envelope(t, attack, decay, sustain, duration, release); saw_playback_buffer[k] += (f32) (saw_oscillator( wave_type, frequency, phase_l, warp, t) * a); saw_playback_buffer[k + 1] += (f32) (saw_oscillator( wave_type, frequency, phase_r, warp, t) * a); saw_voices[n].time++; if (t > duration + release) saw_voices[n].enabled = 0; } } } saw_playback_offset_write = (saw_playback_offset_write + frame_count * CHANNEL_COUNT) % BUFFER_SIZE; saw_playback_lookahead += frame_count; mtx_unlock(&saw_playback_mutex); } static void saw_audio_callback(ma_device *device, void *void_out_, void const *void_in_, ma_uint32 frame_count) { 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(&saw_playback_mutex) != thrd_success) { assert(0); return; } i64 n = frame_count < saw_playback_lookahead ? frame_count : saw_playback_lookahead; for (i64 i = 0; i < n; i++) { i64 k = (saw_playback_offset_read + i * CHANNEL_COUNT) % BUFFER_SIZE; out[i * 2] = saw_playback_buffer[k]; out[i * 2 + 1] = saw_playback_buffer[k + 1]; } saw_playback_offset_read = (saw_playback_offset_read + n * CHANNEL_COUNT) % BUFFER_SIZE; saw_playback_lookahead -= n; mtx_unlock(&saw_playback_mutex); } #ifdef __GNUC__ # pragma GCC pop_options # pragma GCC diagnostic pop #endif // ================================================================ // // UI // // ================================================================ static void saw_reset_ui_offset(void) { if (saw_current_roll != -1) { saw_rolls[saw_current_roll].ui_offset_x = 0; saw_rolls[saw_current_roll].ui_offset_y = ROLL_DEFAULT_UI_OFFSET_Y; } saw_compose.ui_offset_x = 0; saw_compose.ui_offset_y = 0; } static void saw_ui_header(i64 x0, u64 y0, u64 width, i64 height) { i64 frame_height = sapp_height(); i64 border = 14; nvgFontSize(saw_nvg, height - border * 2); nvgFontFaceId(saw_nvg, saw_font_icons); i64 x = x0 + border; i64 h = frame_height - y0 - height + height - border; char backward_fast[] = "\uf049"; char play[] = "\uf04b"; char stop[] = "\uf04d"; char anchor[] = "\uf13d"; char hand_pointer[] = "\uf25a"; char eraser[] = "\uf12d"; char panning[] = "\uf047"; char clone[] = "\uf24d"; u8 color_active[] = { 220, 220, 220, 255 }; u8 color_hover[] = { 255, 255, 220, 255 }; u8 color_selected[] = { 255, 255, 255, 255 }; b8 has_cursor = 0; #define ICON_(i_, selected_) \ do { \ has_cursor = saw_mouse_x >= x && saw_mouse_x < x + height && \ saw_mouse_y >= frame_height - y0 - height && \ saw_mouse_y < frame_height - y0; \ if (has_cursor) \ nvgFillColor(saw_nvg, \ nvgRGBA(color_hover[0], color_hover[1], \ color_hover[2], color_hover[3])); \ else if (selected_) \ nvgFillColor(saw_nvg, \ nvgRGBA(color_selected[0], color_selected[1], \ color_selected[2], color_selected[3])); \ else \ nvgFillColor(saw_nvg, \ nvgRGBA(color_active[0], color_active[1], \ color_active[2], color_active[3])); \ nvgText(saw_nvg, x, h, (i_), (i_) + (sizeof(i_) - 1)); \ x += height; \ } while (0) // Global actions // ICON_(backward_fast, 0); if (has_cursor && saw_lbutton_click) saw_playback_frame = 0; if (saw_playback_on) ICON_(stop, 0); else ICON_(play, 0); if (has_cursor && saw_lbutton_click) saw_playback_on = !saw_playback_on; ICON_(anchor, 0); if (has_cursor && saw_lbutton_click) saw_reset_ui_offset(); // Editing mode // x += height / 2; color_active[0] = 220; color_active[1] = 180; color_active[2] = 80; color_active[3] = 160; color_selected[0] = 220; color_selected[1] = 180; color_selected[2] = 80; color_selected[3] = 255; ICON_(hand_pointer, saw_edit_mode == EDIT_MODE_HAND); if (has_cursor && saw_lbutton_click) saw_edit_mode = EDIT_MODE_HAND; ICON_(eraser, saw_edit_mode == EDIT_MODE_ERASE); if (has_cursor && saw_lbutton_click) saw_edit_mode = EDIT_MODE_ERASE; ICON_(panning, saw_edit_mode == EDIT_MODE_PAN); if (has_cursor && saw_lbutton_click) saw_edit_mode = EDIT_MODE_PAN; ICON_(clone, saw_edit_mode == EDIT_MODE_CLONE); if (has_cursor && saw_lbutton_click) saw_edit_mode = EDIT_MODE_CLONE; #undef ICON_ } static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { i64 frame_height = sapp_height(); i64 track_height = 60; i64 grid_scale = 50; i64 grid_rate = 3; i64 border = 2; // Time bar // nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x0, frame_height - y0 - height + border, width, track_height / 5 - border * 2); nvgRect(saw_nvg, x0, frame_height - y0 - height + border + (track_height * 4) / 5, width, track_height / 5 - border * 2); nvgFillColor(saw_nvg, nvgRGBA(180, 140, 120, 160)); nvgFill(saw_nvg); // Tracks // b8 hover_any = 0; for (i64 i = 0; i < ROLL_COUNT; i++) { if (saw_compose.rolls[i] == -1) continue; saw_roll_t *roll = saw_rolls + saw_compose.rolls[i]; i64 top = frame_height - y0 - height + track_height; i64 bottom = frame_height - y0; i64 dx = x0 + saw_compose.ui_offset_x; i64 l = dx + (roll->time * grid_scale) / SAMPLE_RATE; i64 r = l + (roll->duration * grid_scale) / SAMPLE_RATE; i64 u = frame_height - y0 - height + track_height + saw_compose.ui_offset_y + roll->track * track_height; i64 d = u + track_height; i64 s = grid_scale / grid_rate; if (l < x0) l = dx + ((x0 - dx + (s - 1)) / s) * s; if (r >= x0 + width) r = dx + ((x0 + width - dx) / s) * s; if (u < top) u = top; if (d > bottom) d = bottom; if (l >= r || u >= d) continue; i64 x = l; i64 w = r - l; i64 y = u; i64 h = d - u; b8 is_choosen = (saw_current_roll == i); b8 is_playing = saw_playback_on && saw_playback_frame >= roll->time && saw_playback_frame < roll->time + roll->duration; b8 has_cursor = (saw_compose.grid_input && saw_compose.grid_roll == i) || (saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h); nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + border, y + border, w - border * 2, h - border * 2); nvgFillColor(saw_nvg, is_choosen ? nvgRGBA(240, 230, 200, 255) : is_playing ? nvgRGBA(240, 210, 180, 240) : has_cursor ? nvgRGBA(210, 210, 255, 255) : nvgRGBA(180, 180, 180, 220)); nvgFill(saw_nvg); if (has_cursor) { if (saw_rbutton_down || (saw_edit_mode == EDIT_MODE_ERASE && saw_lbutton_down)) { if (saw_current_roll == saw_compose.rolls[i]) saw_current_roll = -1; saw_compose.rolls[i] = -1; roll->enabled = 0; } else { if (saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_click) { if (saw_current_roll == saw_compose.rolls[i]) { i64 cell = ((saw_mouse_x - saw_compose.ui_offset_x) * grid_rate) / grid_scale; i64 c0 = (roll->time * grid_rate) / SAMPLE_RATE; i64 c1 = c0 + (roll->duration * grid_rate) / SAMPLE_RATE; saw_compose.grid_input = 1; saw_compose.grid_roll = saw_current_roll; if (cell - c0 > c1 - cell) { saw_compose.grid_cell = c0; roll->duration = ((cell - c0 + 1) * SAMPLE_RATE) / grid_rate; } else { saw_compose.grid_cell = c1 - 1; roll->duration = ((c1 - cell + 1) * SAMPLE_RATE) / grid_rate; } } else { saw_current_roll = saw_compose.rolls[i]; saw_current_track = roll->track; } } hover_any = 1; } } } // Placing new sheet // if (!hover_any && saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_down && saw_mouse_x >= x0 + saw_compose.ui_offset_x && saw_mouse_x < x0 + width) { if (!saw_compose.grid_input && saw_mouse_y >= frame_height - y0 - height && saw_mouse_y < frame_height - y0 - height + track_height) saw_playback_frame = ((saw_mouse_x - saw_compose.ui_offset_x) * SAMPLE_RATE) / grid_scale; else if (saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_click && saw_mouse_y >= frame_height - y0 - height + track_height && saw_mouse_y < frame_height - y0) { i64 track = (saw_mouse_y - saw_compose.ui_offset_y - frame_height + y0 + height) / track_height - 1; i64 cell = ((saw_mouse_x - saw_compose.ui_offset_x) * grid_rate) / grid_scale; i64 frame = (cell * SAMPLE_RATE) / grid_rate; i64 n = -1; for (i64 i = 0; i < ROLL_COUNT; i++) if (!saw_rolls[i].enabled) { n = i; break; } i64 x = x0 + saw_compose.ui_offset_x + (frame * grid_scale) / SAMPLE_RATE; i64 y = frame_height - y0 - height + track_height + saw_compose.ui_offset_y + track * track_height; if (track < 0 || track >= TRACK_COUNT || x < x0 || x >= x0 + width || y < frame_height - y0 - height || y + track_height >= frame_height - y0) n = -1; if (n != -1) { for (i64 i = 0; i < ROLL_COUNT; i++) if (saw_compose.rolls[i] == -1) { saw_compose.rolls[i] = n; saw_compose.grid_input = 1; saw_compose.grid_roll = n; saw_compose.grid_cell = cell; break; } saw_rolls[n] = (saw_roll_t) { .enabled = 1, .track = track, .pitch_turned_off = { 0 }, .rate = ROLL_DEFAULT_RATE, .notes = { 0 }, .time = frame, .duration = (ROLL_DEFAULT_RATE * SAMPLE_RATE) / grid_rate, .loop_duration = 0, .ui_offset_x = 0, .ui_offset_y = ROLL_DEFAULT_UI_OFFSET_Y, .last_index = -1, .grid_input = 0, .grid_note = 0, .grid_time = 0, .ui_offset_x_input = 0, .ui_offset_y_input = 0, .loop_input = 0, }; saw_current_roll = n; saw_current_track = track; } } } // Duplicate selected sheet // if (saw_compose.duplicate_input) { saw_compose.duplicate_input = 0; do { if (saw_current_roll == -1) break; i64 track = (saw_mouse_y - saw_compose.ui_offset_y - frame_height + y0 + height) / track_height - 1; i64 cell = ((saw_mouse_x - saw_compose.ui_offset_x) * grid_rate) / grid_scale; if (cell < 0 || track < 0 || track >= TRACK_COUNT) break; i64 frame = (cell * SAMPLE_RATE) / grid_rate; i64 n = -1; saw_roll_t *roll = saw_rolls + saw_current_roll; b8 collision = 0; for (i64 i = 0; i < ROLL_COUNT; i++) { saw_roll_t *p = saw_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 (!saw_rolls[i].enabled) { n = i; break; } if (n == -1) break; for (i64 i = 0; i < ROLL_COUNT; i++) if (saw_compose.rolls[i] == -1) { saw_compose.rolls[i] = n; break; } saw_rolls[n] = *roll; saw_rolls[n].track = track; saw_rolls[n].time = frame; } while (0); } // Panning input // if (saw_mbutton_click || (saw_edit_mode == EDIT_MODE_PAN && saw_lbutton_click)) { if (saw_mouse_x >= x0 && saw_mouse_y >= frame_height - y0 - height + track_height && saw_mouse_x < x0 + width && saw_mouse_y < frame_height - y0) saw_compose.ui_offset_input = 1; } if (!(saw_mbutton_down || (saw_edit_mode == EDIT_MODE_PAN && saw_lbutton_down))) saw_compose.ui_offset_input = 0; // Track stretching input // if (saw_compose.grid_input) { if (saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_down) { i64 cell = ((saw_mouse_x - saw_compose.ui_offset_x) * grid_rate) / grid_scale; saw_roll_t *p = saw_rolls + saw_compose.grid_roll; if (cell >= 0) { if (saw_compose.grid_cell <= cell) { p->time = (saw_compose.grid_cell * SAMPLE_RATE) / grid_rate; p->duration = ((1 + cell - saw_compose.grid_cell) * SAMPLE_RATE) / grid_rate; } else { p->time = (cell * SAMPLE_RATE) / grid_rate; p->duration = ((1 + saw_compose.grid_cell - cell) * SAMPLE_RATE) / grid_rate; } } for (i64 i = 0; i < ROLL_COUNT; i++) { if (i == saw_compose.grid_roll) continue; saw_roll_t *q = saw_rolls + i; if (!q->enabled || p->track != q->track) continue; i64 q_cell = (q->time * grid_rate) / SAMPLE_RATE; i64 q_size = (q->duration * grid_rate) / SAMPLE_RATE; if (saw_compose.grid_cell < q_cell && cell >= q_cell) { cell = q_cell - 1; p->time = (saw_compose.grid_cell * SAMPLE_RATE) / grid_rate; p->duration = ((q_cell - saw_compose.grid_cell) * SAMPLE_RATE) / grid_rate; } if (saw_compose.grid_cell > q_cell && cell < q_cell + q_size) { cell = q_cell + q_size; p->time = ((q_cell + q_size) * SAMPLE_RATE) / grid_rate; p->duration = ((1 + saw_compose.grid_cell - q_cell - q_size) * SAMPLE_RATE) / grid_rate; } } if (p->duration <= 0) p->duration = SAMPLE_RATE / grid_rate; } else saw_compose.grid_input = 0; } // Playback indicator // i64 x = x0 + saw_compose.ui_offset_x - border * 2 + (saw_playback_frame * grid_scale + SAMPLE_RATE / 2) / SAMPLE_RATE; i64 w = border * 4; if (x >= x0 - border * 2 && x < x0 + width) { nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x, frame_height - y0 - height, w, height); nvgFillColor(saw_nvg, nvgRGBA(240, 240, 80, 180)); nvgFill(saw_nvg); } // Draw cursor // if (!hover_any && !saw_compose.grid_input && saw_mouse_x >= x0 + saw_compose.ui_offset_x) { i64 track = (saw_mouse_y - saw_compose.ui_offset_y - frame_height + y0 + height) / track_height - 1; i64 cell = ((saw_mouse_x - saw_compose.ui_offset_x) * grid_rate) / grid_scale; i64 x = x0 + saw_compose.ui_offset_x + (cell * grid_scale) / grid_rate; i64 y = frame_height - y0 - height + track_height + saw_compose.ui_offset_y + track * track_height; i64 w = grid_scale / grid_rate; if (track >= 0 && track < TRACK_COUNT && x >= x0 && x + w < x0 + width && y >= frame_height - y0 - height + track_height && y + track_height < frame_height - y0) { nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + border, y + border, w - border * 2, track_height - border * 2); nvgFillColor(saw_nvg, nvgRGBA(180, 160, 140, 160)); nvgFill(saw_nvg); } } // Cursor indicator // if (saw_mouse_x >= x0 + saw_compose.ui_offset_x && saw_mouse_x < x0 + width && saw_mouse_y >= frame_height - y0 - height && saw_mouse_y < frame_height - y0) { i64 dx = x0 + saw_compose.ui_offset_x; i64 s = grid_scale / grid_rate; i64 x = dx + ((saw_mouse_x - dx + s / 2) / s) * s; i64 w = border * 4; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x - w / 2, frame_height - y0 - height, w, height); nvgFillColor(saw_nvg, nvgRGBA(80, 80, 240, 160)); nvgFill(saw_nvg); } } static void saw_ui_track(saw_track_t *track, i64 x0, i64 y0, i64 width, i64 height, str_t title) { i64 frame_height = sapp_height(); i64 text_height = 33; i64 header_offset = 60; i64 border = 2; i64 column_width = 200; i64 next_y = header_offset; // Values input and highlight // if (!saw_lbutton_down && track->value_input != TRACK_INPUT_NONE) { track->value_input = TRACK_INPUT_NONE; track->value_buffer = 0; #ifndef __EMSCRIPTEN__ sapp_lock_mouse(0); #endif } for (i64 input_index = TRACK_INPUT_INSTRUMENT; input_index <= TRACK_INPUT_RELEASE; ++input_index) { // TODO // Implement Unison and Spread. if (input_index == TRACK_INPUT_UNISON || input_index == TRACK_INPUT_SPREAD) continue; if ((track->value_input == TRACK_INPUT_NONE || track->value_input == input_index) && saw_mouse_x >= x0 && saw_mouse_x < x0 + width && saw_mouse_y >= frame_height - y0 - height + next_y && saw_mouse_y < frame_height - y0 - height + next_y + text_height) { i64 x = x0 + column_width; i64 y = frame_height - y0 - height + next_y; i64 w = width - column_width; i64 h = text_height; if (w > 0) { nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + border, y, w - border * 2, h); nvgFillColor(saw_nvg, nvgRGBA(200, 240, 200, 80)); nvgFill(saw_nvg); } if (saw_lbutton_click && track->value_input == TRACK_INPUT_NONE) { #ifndef __EMSCRIPTEN__ sapp_lock_mouse(1); #endif track->value_input = input_index; switch (input_index) { case TRACK_INPUT_INSTRUMENT: track->value_buffer = track->instrument * 500; break; case TRACK_INPUT_UNISON: track->value_buffer = track->unison * 100; break; case TRACK_INPUT_WARP: track->value_buffer = track->warp * 10000; break; case TRACK_INPUT_PHASE: track->value_buffer = track->phase * 10000; break; case TRACK_INPUT_SPREAD: track->value_buffer = track->spread * 10000; break; case TRACK_INPUT_STEREO_WIDTH: track->value_buffer = track->stereo_width * 10000; break; case TRACK_INPUT_VOLUME: track->value_buffer = track->volume * 10000; break; case TRACK_INPUT_SUSTAIN: track->value_buffer = track->envelope.sustain * 10000; break; case TRACK_INPUT_ATTACK: track->value_buffer = track->envelope.attack * 100000; break; case TRACK_INPUT_DECAY: track->value_buffer = track->envelope.decay * 100000; break; case TRACK_INPUT_RELEASE: track->value_buffer = track->envelope.release * 100000; break; default:; } } } if (input_index == TRACK_INPUT_VOLUME) next_y += header_offset; next_y += text_height; } // Draw text // nvgBeginPath(saw_nvg); nvgFontSize(saw_nvg, text_height - border); nvgFontFaceId(saw_nvg, saw_font_text); nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + text_height - border * 2, title.values, title.values + title.size); next_y = header_offset + text_height; char buf_instr[][100] = { "Sine", "Saw up", "Saw down", "Sqr up", "Sqr down", "Kick" }; nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Instr.", 0); if (track->instrument >= 0 && track->instrument < INSTRUMENT_COUNT) nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf_instr[track->instrument], 0); next_y += text_height; char buf[100]; sprintf(buf, "%.3f", (f32) track->warp); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Warp", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.3f", (f32) track->phase); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Phase", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; /* sprintf(buf, "%d", (i32) track->unison); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Unison", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.3f", (f32) track->spread); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Spread", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; */ sprintf(buf, "%.3f", (f32) track->stereo_width); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Stereo", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.3f", (f32) track->volume); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Volume", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += header_offset; nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + next_y - border * 2, "Envelope", 0); next_y += text_height; sprintf(buf, "%.3f", (f32) track->envelope.sustain); nvgText(saw_nvg, x0 + column_width / 4 + border * 2, frame_height - y0 - height + next_y - border * 2, "Sustain", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.1f ms", (f32) (track->envelope.attack * 1000)); nvgText(saw_nvg, x0 + column_width / 4 + border * 2, frame_height - y0 - height + next_y - border * 2, "Attack", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.1f ms", (f32) (track->envelope.decay * 1000)); nvgText(saw_nvg, x0 + column_width / 4 + border * 2, frame_height - y0 - height + next_y - border * 2, "Decay", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; sprintf(buf, "%.1f ms", (f32) (track->envelope.release * 1000)); nvgText(saw_nvg, x0 + column_width / 4 + border * 2, frame_height - y0 - height + next_y - border * 2, "Release", 0); nvgText(saw_nvg, x0 + column_width + border * 2, frame_height - y0 - height + next_y - border * 2, buf, 0); next_y += text_height; nvgFill(saw_nvg); } static void saw_ui_roll(saw_roll_t *roll, i64 x0, i64 y0, i64 width, i64 height, str_t title) { i64 frame_height = sapp_height(); i64 text_height = 35; i64 header_height = 35; i64 pianokey_height = 35; i64 pianokey_width = 100; i64 border = 2; i64 sheet_offset = 40; i64 sheet_scale = (40 * SAMPLE_RATE) / (10000 * roll->rate); // Title text // nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x0, frame_height - y0 - height, width, text_height); nvgFillColor(saw_nvg, nvgRGBA(80, 60, 50, 160)); nvgFill(saw_nvg); nvgBeginPath(saw_nvg); nvgFontSize(saw_nvg, text_height); nvgFontFaceId(saw_nvg, saw_font_text); nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); nvgText(saw_nvg, x0 + border * 2, frame_height - y0 - height + text_height - border * 2, title.values, title.values + title.size); nvgFill(saw_nvg); // Loop control // { i64 x = x0 + pianokey_width + sheet_offset; i64 y = frame_height - y0 - height + text_height; i64 w = width - pianokey_width - sheet_offset; i64 h = header_height / 5; char repeat[] = "\uf363"; i64 border = 5; nvgFontSize(saw_nvg, header_height - border * 2); nvgFontFaceId(saw_nvg, saw_font_icons); if (roll->loop_duration == 0) nvgFillColor(saw_nvg, nvgRGBA(80, 80, 80, 160)); else nvgFillColor(saw_nvg, nvgRGBA(180, 80, 40, 200)); nvgText(saw_nvg, x - header_height * 2 + border, y + header_height - border, repeat, repeat + (sizeof repeat - 1)); i64 rw = (roll->loop_duration * roll->rate * sheet_scale + SAMPLE_RATE / 2) / 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(saw_nvg); nvgRect(saw_nvg, x + rx, y, rw, h); nvgRect(saw_nvg, x + rx, y + h * 4, rw, h); nvgFillColor(saw_nvg, nvgRGBA(180, 80, 40, 160)); nvgFill(saw_nvg); } nvgBeginPath(saw_nvg); if (rx > 0) { nvgRect(saw_nvg, x, y, rx, h); nvgRect(saw_nvg, x, y + h * 4, rx, h); } nvgRect(saw_nvg, x + rx + rw, y, w - rx - rw, h); nvgRect(saw_nvg, x + rx + rw, y + h * 4, w - rx - rw, h); nvgFillColor(saw_nvg, nvgRGBA(80, 80, 80, 160)); nvgFill(saw_nvg); if (saw_mouse_x >= x && saw_mouse_y >= y && saw_mouse_x < x + w && saw_mouse_y < y + header_height && !roll->loop_input && saw_lbutton_click) roll->loop_input = 1; if (roll->loop_input && saw_lbutton_down) { i64 t = (saw_mouse_x - x0 - pianokey_width - sheet_offset - roll->ui_offset_x + sheet_scale / 2) / sheet_scale; if (t <= 0) roll->loop_duration = 0; else roll->loop_duration = (t * SAMPLE_RATE + roll->rate / 2) / roll->rate; } } if (!saw_lbutton_down) roll->loop_input = 0; // Piano roll // b8 hover_any = 0; for (i64 pitch = 0; pitch < PITCH_COUNT; pitch++) { i64 x = x0 + border; i64 y = frame_height - y0 - (pitch + 1) * pianokey_height + border + roll->ui_offset_y; i64 w = pianokey_width - border * 2; i64 h = pianokey_height - border * 2; if (y > frame_height - y0 - pianokey_height) continue; if (y < frame_height - y0 - height + text_height + header_height) break; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x, y, w, h); b8 has_cursor = saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h; nvgFillColor(saw_nvg, roll->pitch_turned_off[pitch] ? nvgRGBA(220, 220, 220, 160) : has_cursor ? nvgRGBA(200, 200, 255, 255) : nvgRGBA(220, 220, 220, 255)); nvgFill(saw_nvg); // Piano roll input // if (has_cursor) { hover_any = 1; if (!roll->pitch_turned_off[pitch] && ((saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_click) || (saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_down && roll->last_index != pitch))) saw_play_voice(saw_tracks + roll->track, roll, pitch, SAMPLE_RATE / roll->rate); if (saw_rbutton_click || (saw_edit_mode == EDIT_MODE_ERASE && saw_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 (saw_mbutton_click || (saw_edit_mode == EDIT_MODE_PAN && saw_lbutton_click && !roll->loop_input)) { if (saw_mouse_x >= x0 + pianokey_width + sheet_offset && saw_mouse_y >= frame_height - y0 - height + text_height && saw_mouse_x < x0 + width && saw_mouse_y < frame_height - y0) roll->ui_offset_x_input = 1; if (saw_mouse_x >= x0 && saw_mouse_y >= frame_height - y0 - height + text_height && saw_mouse_x < x0 + width && saw_mouse_y < frame_height - y0) roll->ui_offset_y_input = 1; } if (!(saw_mbutton_down || (saw_edit_mode == EDIT_MODE_PAN && saw_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++) { i64 y = frame_height - y0 - (pitch + 1) * pianokey_height + roll->ui_offset_y; if (y > frame_height - y0 - pianokey_height) continue; if (y < frame_height - y0 - height + text_height + header_height) break; i64 h = pianokey_height; for (i64 t = 0; t < (roll->duration * roll->rate) / SAMPLE_RATE; t++) { i64 x = x0 + pianokey_width + sheet_offset + t * sheet_scale + roll->ui_offset_x; if (x >= x0 + width - sheet_scale - border) break; i64 note = -1; for (i64 n = 0; n < SHEET_SIZE; n++) { saw_roll_note_t *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; i64 w = sheet_scale; nvgBeginPath(saw_nvg); nvgRect(saw_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 && saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h; nvgFillColor(saw_nvg, turned_off ? nvgRGBA(160, 150, 120, 100) : has_cursor ? nvgRGBA(180, 180, 220, 160) : nvgRGBA(170, 160, 140, 150)); nvgFill(saw_nvg); // Empty cell input // if (!roll->grid_input && !turned_off && has_cursor && saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_click) for (i64 n = 0; n < SHEET_SIZE; n++) if (!roll->notes[n].enabled) { roll->notes[n] = (saw_roll_note_t) { .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; saw_play_voice(saw_tracks + roll->track, roll, pitch, SAMPLE_RATE / roll->rate); break; } } } // Draw notes // for (i64 n = 0; n < SHEET_SIZE; n++) { saw_roll_note_t *note = roll->notes + n; if (!note->enabled) continue; i64 y = frame_height - y0 - (note->pitch + 1) * pianokey_height + roll->ui_offset_y; if (y + pianokey_height > frame_height - y0) continue; if (y < frame_height - y0 - height + text_height + header_height) continue; i64 x = x0 + pianokey_width + sheet_offset + roll->ui_offset_x + (note->time * roll->rate * sheet_scale + SAMPLE_RATE / 2) / SAMPLE_RATE; i64 w = (note->duration * roll->rate * sheet_scale + SAMPLE_RATE / 2) / SAMPLE_RATE; i64 h = pianokey_height; b8 has_cursor = (roll->grid_input && roll->grid_note == n) || (saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h); if (x >= x0 + width) continue; if (x + w - sheet_scale < x0 + pianokey_width + sheet_offset) continue; // Draw note // i64 frame = saw_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 = saw_playback_on && frame >= roll->time + note->time && frame < roll->time + note->time + note->duration; i64 underflow = (x0 + pianokey_width + sheet_offset + sheet_scale - 1 - x) / sheet_scale; i64 overflow = (x + w + sheet_scale + 1 - x0 - width) / sheet_scale; if (underflow > 0) { x += underflow * sheet_scale; w -= underflow * sheet_scale; } if (overflow > 0) w -= overflow * sheet_scale; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + border, y + border, w - border * 2, h - border * 2); nvgFillColor(saw_nvg, is_playing ? nvgRGBA(255, 230, 200, 255) : has_cursor ? nvgRGBA(190, 190, 230, 255) : nvgRGBA(180, 180, 180, 255)); nvgFill(saw_nvg); // Note input // if (has_cursor && (saw_rbutton_down || (saw_edit_mode == EDIT_MODE_ERASE && saw_lbutton_down))) note->enabled = 0; } // Note stretching input // if (roll->grid_input) { if (saw_edit_mode == EDIT_MODE_HAND && saw_lbutton_down) { i64 t = (saw_mouse_x - x0 - pianokey_width - sheet_offset - roll->ui_offset_x) / sheet_scale; saw_roll_note_t *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; saw_roll_note_t *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 = saw_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; i64 x = x0 + pianokey_width + sheet_offset + roll->ui_offset_x - border * 2 + ((frame - roll->time) * roll->rate * sheet_scale + SAMPLE_RATE / 2) / SAMPLE_RATE; i64 w = border * 4; if (x >= x0 + pianokey_width + sheet_offset - border * 2 && x < x0 + width) { nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x, frame_height - y0 - height + text_height, w, height - text_height); nvgFillColor(saw_nvg, nvgRGBA(240, 240, 80, 180)); nvgFill(saw_nvg); } } // Cursor indicator // if (saw_mouse_x >= x0 + pianokey_width + sheet_offset && saw_mouse_x < x0 + width && saw_mouse_y >= frame_height - y0 - height && saw_mouse_y < frame_height - y0) { i64 dx = x0 + pianokey_width + sheet_offset + roll->ui_offset_x; i64 x = dx + ((saw_mouse_x - dx + sheet_scale / 2) / sheet_scale) * sheet_scale; i64 w = border * 4; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x - w / 2, frame_height - y0 - height + text_height, w, height - text_height); nvgFillColor(saw_nvg, nvgRGBA(80, 80, 240, 160)); nvgFill(saw_nvg); } } // ================================================================ // // Event handling // // ================================================================ static void saw_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 = saw_audio_callback; config.pUserData = NULL; if (ma_device_init(NULL, &config, &saw_audio_device) != MA_SUCCESS) { printf("ma_device_init failed.\n"); fflush(stdout); return; } ma_device_start(&saw_audio_device); } static void saw_init(void) { // Init RNG // u64 rng_seed; secure_random(sizeof rng_seed, &rng_seed); mt64_init(&saw_rng_mt64, rng_seed); mt64_rotate(&saw_rng_mt64); // Init NanoVG // #ifdef SOKOL_GLCORE33 saw_nvg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); #else saw_nvg = nvgCreateGLES3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); #endif // Init audio // #ifndef __EMSCRIPTEN__ saw_init_audio(); #endif if (mtx_init(&saw_playback_mutex, mtx_plain) != thrd_success) { printf("mtx_init failed.\n"); fflush(stdout); } // Load fonts // saw_font_text = nvgCreateFontMem(saw_nvg, "", (u8 *) saw_ttf_text, SAW_TTF_TEXT_SIZE, 0); if (saw_font_text == -1) { printf("nvgCreateFontMem failed.\n"); fflush(stdout); } saw_font_icons = nvgCreateFontMem(saw_nvg, "", (u8 *) saw_ttf_icons, SAW_TTF_ICONS_SIZE, 0); if (saw_font_icons == -1) { printf("nvgCreateFontMem failed.\n"); fflush(stdout); } // Init Saw state // for (i32 i = 0; i < ROLL_COUNT; i++) { saw_compose.rolls[i] = (i == 0 ? 0 : -1); saw_rolls[i] = (saw_roll_t) { .enabled = (i == 0), .track = 0, .last_index = -1, .pitch_turned_off = { 0 }, .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, .grid_input = 0, .grid_note = 0, .grid_time = 0, .ui_offset_x_input = 0, .ui_offset_y_input = 0, .loop_input = 0, }; } for (i32 i = 0; i < TRACK_COUNT; i++) saw_tracks[i] = (saw_track_t) { .instrument = INSTRUMENT_SINE, .warp = .0, .phase = .0, .unison = 1, .spread = .1, .stereo_width = .2, .volume = 1., .envelope = { .sustain = .15, .attack = .007, .decay = .3, .release = .4, }, .value_input = TRACK_INPUT_NONE, }; // Determine the project file name // if (saw_project_file.size == 0) { char 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 { memcpy(saw_project_file_buf, cache.values, cache.size); saw_project_file_buf[cache.size] = PATH_DELIM_C; memcpy(saw_project_file_buf + cache.size + 1, "last", 5); saw_project_file.size = strlen(saw_project_file_buf); saw_project_file.values = saw_project_file_buf; } } printf("Project file: %s\n", BS(saw_project_file)); fflush(stdout); // Load the project from a file // if (path_type(saw_project_file) == PATH_FILE) { FILE *f = fopen(BS(saw_project_file), "rb"); if (f == NULL) { printf("Failed to read file: %s\n", BS(saw_project_file)); fflush(stdout); return; } #define SCAN_(format_, num_, ...) \ do { \ if (fscanf(f, format_, __VA_ARGS__) != num_) { \ printf("Invalid syntax at \"%s\"\n", format_); \ fflush(stdout); \ fclose(f); \ return; \ } \ } while (0) i32 total_rolls; SCAN_(" compose_rolls %d", 1, &total_rolls); if (total_rolls < 0 || total_rolls > ROLL_COUNT) { printf("Invalid roll count: %d\n", total_rolls); fflush(stdout); fclose(f); return; } for (i64 i = 0; i < total_rolls; i++) SCAN_(" %lld", 1, saw_compose.rolls + i); SCAN_(" rolls %d", 1, &total_rolls); for (i64 i = 0; i < total_rolls; i++) { saw_roll_t *roll = saw_rolls + i; i32 enabled; SCAN_(" enabled %d", 1, &enabled); roll->enabled = enabled ? 1 : 0; SCAN_(" track %lld", 1, &roll->track); i32 pitch_count; SCAN_(" pitch_turned_off %d", 1, &pitch_count); if (pitch_count < 0 || pitch_count > PITCH_COUNT) { printf("Invalid pitch count: %d\n", pitch_count); fflush(stdout); fclose(f); return; } for (i64 pitch = 0; pitch < pitch_count; pitch++) { i32 flag; SCAN_(" %d", 1, &flag); roll->pitch_turned_off[pitch] = flag ? 1 : 0; } SCAN_(" rate %lld", 1, &roll->rate); i32 sheet_size; SCAN_(" notes %d", 1, &sheet_size); if (sheet_size < 0 || sheet_size > SHEET_SIZE) { printf("Invalid note count: %d\n", sheet_size); fflush(stdout); fclose(f); return; } for (i64 n = 0; n < sheet_size; n++) { i32 flag; SCAN_(" %d %lld %lld %lld", 4, &flag, &roll->notes[n].time, &roll->notes[n].duration, &roll->notes[n].pitch); roll->notes[n].enabled = flag ? 1 : 0; } SCAN_(" time %lld", 1, &roll->time); SCAN_(" duration %lld", 1, &roll->duration); SCAN_(" loop_duration %lld", 1, &roll->loop_duration); SCAN_(" ui_offset %lld %lld", 2, &roll->ui_offset_x, &roll->ui_offset_y); } i32 total_tracks; SCAN_(" tracks %d", 1, &total_tracks); for (i64 i = 0; i < total_tracks; i++) { saw_track_t *track = saw_tracks + i; i64 warp, phase, spread, stereo_width, volume, sustain, attack, decay, release; SCAN_(" instrument %d", 1, &track->instrument); SCAN_(" warp %lld", 1, &warp); SCAN_(" phase %lld", 1, &phase); SCAN_(" unison %d", 1, &track->unison); SCAN_(" spread %lld", 1, &spread); SCAN_(" stereo_width %lld", 1, &stereo_width); SCAN_(" volume %lld", 1, &volume); SCAN_(" sustain %lld", 1, &sustain); SCAN_(" attack %lld", 1, &attack); SCAN_(" decay %lld", 1, &decay); SCAN_(" release %lld", 1, &release); track->warp = warp * 0.0001 - 1.; track->phase = phase * 0.0001; track->spread = spread * 0.0001; track->stereo_width = stereo_width * 0.0001; track->volume = volume * 0.0001; track->envelope.sustain = sustain * 0.0001; track->envelope.attack = attack * 0.0001; track->envelope.decay = decay * 0.0001; track->envelope.release = release * 0.0001; } #undef SCAN_ fclose(f); } } static void saw_frame(void) { // TODO // Adjust sleep depending on how much compute we need // Sleep to prevent high CPU load thrd_sleep(&(struct timespec) { .tv_nsec = 10000000 }, NULL); saw_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); #ifdef SOKOL_GLCORE33 glClearDepth(1.); #else glClearDepthf(1.f); #endif glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // Process input // { if (saw_key_pressed[SAPP_KEYCODE_SPACE]) saw_playback_on = !saw_playback_on; if (saw_key_pressed[SAPP_KEYCODE_ENTER]) saw_playback_frame = 0; if (saw_key_pressed[SAPP_KEYCODE_ESCAPE]) saw_reset_ui_offset(); if (saw_key_pressed[SAPP_KEYCODE_D] || (saw_edit_mode == EDIT_MODE_CLONE && saw_lbutton_click)) saw_compose.duplicate_input = 1; // Panning control // for (i64 i = 0; i < ROLL_COUNT; i++) { if (saw_rolls[i].ui_offset_x_input) saw_rolls[i].ui_offset_x += saw_mouse_dx; if (saw_rolls[i].ui_offset_y_input) saw_rolls[i].ui_offset_y += saw_mouse_dy; } if (saw_compose.ui_offset_input) { saw_compose.ui_offset_x += saw_mouse_dx; saw_compose.ui_offset_y += saw_mouse_dy; } if (saw_current_track != -1 && saw_tracks[saw_current_track].value_input != TRACK_INPUT_NONE) { // Value input // saw_track_t *track = saw_tracks + saw_current_track; track->value_buffer -= saw_shift_on ? saw_mouse_dy * 300 : saw_ctrl_on ? saw_mouse_dy : saw_mouse_dy * 20; // TODO // Unify value input logic. // Change input value buffer for selected value. // switch (track->value_input) { case TRACK_INPUT_INSTRUMENT: track->instrument = (i64) (track->value_buffer * .002 + 0.5); if (track->instrument < 0) track->instrument = 0; if (track->instrument >= INSTRUMENT_COUNT) track->instrument = INSTRUMENT_COUNT - 1; break; case TRACK_INPUT_WARP: track->warp = track->value_buffer * .0001; if (track->warp < -1.) track->warp = -1.; if (track->warp > 1.) track->warp = 1.; break; case TRACK_INPUT_PHASE: track->value_buffer = (f64) ((i64) track->value_buffer % 10000); while (track->value_buffer < 0) track->value_buffer += 10000; track->phase = track->value_buffer * .0001; break; case TRACK_INPUT_UNISON: track->unison = (i64) (track->value_buffer * .01 + 0.5); if (track->unison < 1) track->unison = 1; if (track->unison > UNISON_COUNT) track->unison = UNISON_COUNT; break; case TRACK_INPUT_SPREAD: track->spread = track->value_buffer * .0001; if (track->spread < 0.) track->spread = 0.; if (track->spread > 1.) track->spread = 1.; break; case TRACK_INPUT_STEREO_WIDTH: track->stereo_width = track->value_buffer * .0001; if (track->stereo_width < 0.) track->stereo_width = 0.; if (track->stereo_width > 2.) track->stereo_width = 2.; break; case TRACK_INPUT_VOLUME: track->volume = track->value_buffer * .0001; if (track->volume < 0.) track->volume = 0.; if (track->volume > 2.) track->volume = 2.; break; case TRACK_INPUT_SUSTAIN: track->envelope.sustain = track->value_buffer * .0001; if (track->envelope.sustain < 0.) track->envelope.sustain = 0.; if (track->envelope.sustain > 1.) track->envelope.sustain = 1.; break; case TRACK_INPUT_ATTACK: track->envelope.attack = track->value_buffer * .00001; if (track->envelope.attack < 0.) track->envelope.attack = 0.; if (track->envelope.attack > 60.) track->envelope.attack = 60.; break; case TRACK_INPUT_DECAY: track->envelope.decay = track->value_buffer * .00001; if (track->envelope.decay < 0.) track->envelope.decay = 0.; if (track->envelope.decay > 60.) track->envelope.decay = 60.; break; case TRACK_INPUT_RELEASE: track->envelope.release = track->value_buffer * .00001; if (track->envelope.release < 0.) track->envelope.release = 0.; if (track->envelope.release > 60.) track->envelope.release = 60.; break; default:; } } } // Render UI // nvgBeginFrame(saw_nvg, frame_width, frame_height, sapp_dpi_scale()); { // 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 // saw_ui_header(0, // x0 frame_height - header_height, // y0 frame_width, // width header_height // height ); // Render compose view // saw_ui_compose(0, // x0 roll_height, // y0 frame_width - track_width, // width compose_height // height ); if (saw_current_track != -1) { // Render track view // char buf[64]; sprintf(buf, "Track %lld", saw_current_track + 1); saw_ui_track(saw_tracks + saw_current_track, // track frame_width - track_width, // x0 0, // y0 track_width, // width track_height, // height kit_str(strlen(buf), buf) // label ); } if (saw_current_roll != -1) { // Render roll view // char buf[64]; sprintf(buf, "Sheet %lld", saw_current_roll + 1); saw_ui_roll(saw_rolls + saw_current_roll, // roll 0, // x0 0, // y0 frame_width - track_width, // width roll_height, // height kit_str(strlen(buf), buf) // label ); } } nvgEndFrame(saw_nvg); // Cleanup input state. // { saw_mouse_dx = 0; saw_mouse_dy = 0; saw_lbutton_click = 0; saw_rbutton_click = 0; saw_mbutton_click = 0; saw_shift_on = 0; saw_ctrl_on = 0; memset(saw_key_pressed, 0, sizeof saw_key_pressed); } } static void saw_cleanup(void) { ma_device_uninit(&saw_audio_device); mtx_destroy(&saw_playback_mutex); #ifdef SOKOL_GLCORE33 nvgDeleteGL3(saw_nvg); #else nvgDeleteGLES3(saw_nvg); #endif // Save the project to a file // if (saw_project_file.size == 0) return; printf("Save project: %s\n", BS(saw_project_file)); fflush(stdout); FILE *f = fopen(BS(saw_project_file), "wb"); if (f == NULL) { printf("Failed to write file: %s\n", BS(saw_project_file)); fflush(stdout); return; } // Save the compose // fprintf(f, "compose_rolls %d", ROLL_COUNT); for (i64 i = 0; i < ROLL_COUNT; i++) fprintf(f, " %lld", saw_compose.rolls[i]); fprintf(f, "\n\n"); // Save rolls // i32 total_rolls = 0; for (i64 i = 0; i < ROLL_COUNT; i++) if (saw_rolls[i].enabled) total_rolls = i + 1; fprintf(f, "rolls %d\n\n", total_rolls); for (i64 i = 0; i < total_rolls; i++) { saw_roll_t *roll = saw_rolls + i; fprintf(f, "enabled %d\n", (i32) roll->enabled); fprintf(f, "track %lld\n", roll->track); fprintf(f, "pitch_turned_off %d\n ", PITCH_COUNT); for (i64 pitch = 0; pitch < PITCH_COUNT; pitch++) fprintf(f, " %d", (i32) roll->pitch_turned_off[pitch]); fprintf(f, "\n"); 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 %d\n", total_notes); for (i32 n = 0; n < total_notes; n++) fprintf(f, " %d %4lld %4lld %4lld\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 %lld %lld\n\n", roll->ui_offset_x, roll->ui_offset_y); } // Save tracks // fprintf(f, "tracks %d\n\n", TRACK_COUNT); for (i64 i = 0; i < TRACK_COUNT; i++) { saw_track_t *track = saw_tracks + i; fprintf(f, "instrument %d\n", track->instrument); fprintf(f, "warp %lld\n", (i64) ((1. + track->warp) * 10000 + .5)); fprintf(f, "phase %lld\n", (i64) (track->phase * 10000 + .5)); fprintf(f, "unison %d\n", track->unison); fprintf(f, "spread %lld\n", (i64) (track->spread * 10000 + .5)); fprintf(f, "stereo_width %lld\n", (i64) (track->stereo_width * 10000 + .5)); fprintf(f, "volume %lld\n", (i64) (track->volume * 10000 + .5)); fprintf(f, "sustain %lld\n", (i64) (track->envelope.sustain * 10000 + .5)); fprintf(f, "attack %lld\n", (i64) (track->envelope.attack * 10000 + .5)); fprintf(f, "decay %lld\n", (i64) (track->envelope.decay * 10000 + .5)); fprintf(f, "release %lld\n\n", (i64) (track->envelope.release * 10000 + .5)); } fclose(f); } static void saw_event(sapp_event const *event) { #ifdef __EMSCRIPTEN__ // In browser, resume the audio only after a user action. static b8 is_audio_suspended = 1; if (is_audio_suspended && (event->type == SAPP_EVENTTYPE_MOUSE_DOWN || event->type == SAPP_EVENTTYPE_TOUCHES_BEGAN)) { saw_init_audio(); is_audio_suspended = 0; } #endif switch (event->type) { case SAPP_EVENTTYPE_MOUSE_MOVE: saw_shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0; saw_ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0; saw_mouse_dx += (i64) floor(event->mouse_dx + .5); saw_mouse_dy += (i64) floor(event->mouse_dy + .5); saw_mouse_x = (i64) floor(event->mouse_x + .5); saw_mouse_y = (i64) floor(event->mouse_y + .5); break; case SAPP_EVENTTYPE_MOUSE_DOWN: saw_shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0; saw_ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0; switch (event->mouse_button) { case SAPP_MOUSEBUTTON_LEFT: saw_lbutton_down = 1; saw_lbutton_click = 1; break; case SAPP_MOUSEBUTTON_RIGHT: saw_rbutton_down = 1; saw_rbutton_click = 1; break; case SAPP_MOUSEBUTTON_MIDDLE: saw_mbutton_down = 1; saw_mbutton_click = 1; break; default:; } break; case SAPP_EVENTTYPE_MOUSE_UP: saw_shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0; saw_ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0; switch (event->mouse_button) { case SAPP_MOUSEBUTTON_LEFT: saw_lbutton_down = 0; break; case SAPP_MOUSEBUTTON_RIGHT: saw_rbutton_down = 0; break; case SAPP_MOUSEBUTTON_MIDDLE: saw_mbutton_down = 0; break; default:; } break; case SAPP_EVENTTYPE_MOUSE_LEAVE: saw_shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0; saw_ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0; saw_lbutton_down = 0; saw_rbutton_down = 0; saw_mbutton_down = 0; break; case SAPP_EVENTTYPE_KEY_DOWN: saw_shift_on = (event->modifiers & SAPP_MODIFIER_SHIFT) != 0; saw_ctrl_on = (event->modifiers & SAPP_MODIFIER_CTRL) != 0; if (!event->key_repeat && event->key_code >= 0 && event->key_code < sizeof saw_key_pressed / sizeof *saw_key_pressed) saw_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) { saw_mouse_x = (i64) floor(event->touches[0].pos_x + .5); saw_mouse_y = (i64) floor(event->touches[0].pos_y + .5); saw_lbutton_down = 1; saw_lbutton_click = 1; } break; case SAPP_EVENTTYPE_TOUCHES_MOVED: if (event->num_touches >= 1) { i64 x = (i64) floor(event->touches[0].pos_x + .5); i64 y = (i64) floor(event->touches[0].pos_y + .5); saw_mouse_dx += x - saw_mouse_x; saw_mouse_dy += y - saw_mouse_y; saw_mouse_x = x; saw_mouse_y = y; } break; case SAPP_EVENTTYPE_TOUCHES_ENDED: if (event->num_touches >= 1) { i64 x = (i64) floor(event->touches[0].pos_x + .5); i64 y = (i64) floor(event->touches[0].pos_y + .5); saw_mouse_dx += x - saw_mouse_x; saw_mouse_dy += y - saw_mouse_y; saw_mouse_x = x; saw_mouse_y = y; } saw_lbutton_down = 0; break; case SAPP_EVENTTYPE_TOUCHES_CANCELLED: saw_lbutton_down = 0; break; default:; } } char const *__lsan_default_suppressions() { // There is leaks in NVidia driver on Linux. return "leak:nvidia"; } static void log_(char const *tag, u32 log_level, u32 log_item_id, char const *message_or_null, u32 line_nr, char const *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, char **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 && saw_project_file.size == 0) saw_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, .init_cb = saw_init, .frame_cb = saw_frame, .cleanup_cb = saw_cleanup, .event_cb = saw_event, .logger.func = log_, }; }