// TODO // - Always use OpenGL ES in both sokol and nanovg. // - Custom OpenGL loader. // #include "profiler.h" #include "../kit/math.h" #include "../kit/time.h" #ifdef __EMSCRIPTEN__ # include # define SOKOL_GLES3 # define NANOVG_GLES3 1 #else # include # define SOKOL_GLCORE33 # define NANOVG_GL3 1 #endif #include "../thirdparty/sokol_app.h" #include "../thirdparty/miniaudio.h" #include "../thirdparty/nanovg.h" #include "../thirdparty/nanovg_gl.h" #include enum { SAW_CHANNEL_COUNT = 2, SAW_SAMPLE_RATE = 44100, VOICE_COUNT = 16, ROLL_SIZE = 40, SHEET_SIZE = 200 }; typedef struct { i8 enabled; i64 time; i64 duration; i64 pitch; } saw_sheet_note_t; typedef struct { i64 rate; saw_sheet_note_t notes[SHEET_SIZE]; } saw_sheet_t; static struct NVGcontext *saw_nvg; static ma_device saw_ma; static i32 saw_mouse_x = 0; static i32 saw_mouse_y = 0; static i8 saw_lbutton_click = 0; static i8 saw_lbutton_down = 0; static i8 saw_rbutton_click = 0; static i8 saw_rbutton_down = 0; static i8 saw_voice_on[VOICE_COUNT] = { 0 }; static i32 saw_voice_pitch[VOICE_COUNT] = { 0 }; static i64 saw_voice_time[VOICE_COUNT] = { 0 }; static f64 saw_voice_duration[VOICE_COUNT] = { 0 }; static i32 saw_roll_last_index = -1; static i8 saw_roll_turned_off[ROLL_SIZE] = { 0 }; static saw_sheet_t saw_roll_sheet = { .rate = 6, .notes = { 0 } }; static i8 saw_roll_playing = 0; static i64 saw_roll_frame = 0; static i8 saw_grid_input = 0; static i32 saw_grid_note = 0; static i32 saw_grid_pitch = 0; static i32 saw_grid_time = 0; #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_envelope(f64 t, f64 attack, f64 decay, f64 sustain, f64 duration, f64 release) { // FIXME // Apply low-pass filter for the envelope curve to prevent // clicking. // if (t < attack) return t / attack; else if (t < attack + decay) return 1. - (1. - sustain) * (t - attack) / decay; else if (t < duration) return sustain; else if (t < duration + release) return sustain * (duration + release - t) / release; else return 0.; } static void saw_roll_playback(i32 frame_count) { if (!saw_roll_playing) return; for (i32 i = 0; i < SHEET_SIZE; i++) { saw_sheet_note_t *p = saw_roll_sheet.notes + i; if (!p->enabled) continue; i64 frame = (p->time * SAW_SAMPLE_RATE) / saw_roll_sheet.rate; if (saw_roll_frame + frame_count <= frame || saw_roll_frame > frame) continue; if (saw_voice_on[VOICE_COUNT - 1]) continue; for (i32 n = VOICE_COUNT - 1; n > 0; --n) { saw_voice_on[n] = saw_voice_on[n - 1]; saw_voice_pitch[n] = saw_voice_pitch[n - 1]; saw_voice_time[n] = saw_voice_time[n - 1]; saw_voice_duration[n] = saw_voice_duration[n - 1]; } saw_voice_on[0] = 1; saw_voice_pitch[0] = p->pitch; saw_voice_time[0] = 0; saw_voice_duration[0] = ((f64) p->duration) / saw_roll_sheet.rate; } saw_roll_frame += frame_count; } static void saw_audio(ma_device *device, void *void_out_, void const *void_in_, ma_uint32 frame_count) { saw_roll_playback(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; } for (i32 n = 0; n < VOICE_COUNT; n++) { if (!saw_voice_on[n]) continue; f64 period = M_PI * 2.; f64 frequency = pow(2., 7.6 + saw_voice_pitch[n] / 12.); f64 amplitude = .2; // envelope f64 attack = .007; f64 decay = .3; f64 sustain = .15; f64 duration = saw_voice_duration[n]; f64 release = .4; for (i64 i = 0; i < frame_count; i++) { f64 t = (f64) saw_voice_time[n] / (f64) SAW_SAMPLE_RATE; f64 a = amplitude * saw_envelope(t, attack, decay, sustain, duration, release); f64 k = period * frequency; out[i * 2] += (f32) (sin(k * t) * a); out[i * 2 + 1] += (f32) (sin(k * t) * a); saw_voice_time[n]++; if (t > duration + release) saw_voice_on[n] = 0; } } } #ifdef __GNUC__ # pragma GCC pop_options # pragma GCC diagnostic pop #endif static void saw_init(void) { sapp_set_window_title("saw"); #ifdef SOKOL_GLCORE33 saw_nvg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); #else saw_nvg = nvgCreateGLES3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); #endif ma_device_config config = ma_device_config_init( ma_device_type_playback); config.playback.format = ma_format_f32; config.playback.channels = SAW_CHANNEL_COUNT; config.sampleRate = SAW_SAMPLE_RATE; config.dataCallback = saw_audio; config.pUserData = NULL; if (ma_device_init(NULL, &config, &saw_ma) != MA_SUCCESS) { printf("ma_device_init failed.\n"); return; } ma_device_start(&saw_ma); } static void saw_frame(void) { i32 width = sapp_width(); i32 height = sapp_height(); glViewport(0, 0, width, height); glClearColor(.23f, .19f, .16f, 1.f); glClearDepth(1.); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgBeginFrame(saw_nvg, width, height, sapp_dpi_scale()); // Piano roll // i32 x0 = 20; i32 y0 = 20; i32 pianokey_height = 35; i32 pianokey_width = 100; i32 roll_border = 2; i32 sheet_offset = 40; i32 sheet_scale = 20; i8 hover_any = 0; for (i32 pitch = 0; pitch < ROLL_SIZE; pitch++) { i32 x = x0 + roll_border; i32 y = height - y0 - (pitch + 1) * pianokey_height + roll_border; i32 w = pianokey_width - roll_border * 2; i32 h = pianokey_height - roll_border * 2; if (y > height - pianokey_height) continue; if (y < 0) break; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x, y, w, h); i8 has_cursor = saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h; nvgFillColor(saw_nvg, saw_roll_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 (!saw_roll_turned_off[pitch] && (saw_lbutton_click || (saw_lbutton_down && saw_roll_last_index != pitch)) && !saw_voice_on[VOICE_COUNT - 1]) { for (i32 n = VOICE_COUNT - 1; n > 0; --n) { saw_voice_on[n] = saw_voice_on[n - 1]; saw_voice_pitch[n] = saw_voice_pitch[n - 1]; saw_voice_time[n] = saw_voice_time[n - 1]; saw_voice_duration[n] = saw_voice_duration[n - 1]; } saw_voice_on[0] = 1; saw_voice_pitch[0] = pitch; saw_voice_time[0] = 0; saw_voice_duration[0] = .6; } if (saw_rbutton_click) saw_roll_turned_off[pitch] = !saw_roll_turned_off[pitch]; saw_roll_last_index = pitch; } } if (!hover_any) saw_roll_last_index = -1; // Draw music sheet // for (i32 pitch = 0; pitch < ROLL_SIZE; pitch++) { i32 y = height - y0 - (pitch + 1) * pianokey_height; if (y > height - pianokey_height) continue; if (y < 0) break; i32 h = pianokey_height; for (i32 t = 0;; t++) { i32 x = x0 + pianokey_width + sheet_offset + t * sheet_scale; if (x >= width - roll_border) break; i32 note = -1; for (i32 n = 0; n < SHEET_SIZE; n++) { saw_sheet_note_t *p = saw_roll_sheet.notes + n; if (p->enabled && p->pitch == pitch && t >= p->time && t < p->time + p->duration) { note = n; break; } } i8 has_cursor = 0; if (note == -1) { // Draw empty cell // i32 w = sheet_scale; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + roll_border, y + roll_border, w - roll_border * 2, h - roll_border * 2); has_cursor = saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h; nvgFillColor(saw_nvg, saw_roll_turned_off[pitch] ? nvgRGBA(160, 150, 120, 100) : has_cursor ? nvgRGBA(180, 180, 220, 160) : nvgRGBA(170, 160, 140, 150)); nvgFill(saw_nvg); // Empty cell input // if (!saw_grid_input && !saw_roll_turned_off[pitch] && has_cursor && saw_lbutton_click) { for (i32 n = 0; n < SHEET_SIZE; n++) if (!saw_roll_sheet.notes[n].enabled) { saw_roll_sheet.notes[n] = (saw_sheet_note_t) { .enabled = 1, .time = t, .duration = 1, .pitch = pitch }; saw_grid_input = 1; saw_grid_note = n; saw_grid_pitch = pitch; saw_grid_time = t; break; } } } else { i32 w = sheet_scale * saw_roll_sheet.notes[note].duration; has_cursor = saw_mouse_x >= x && saw_mouse_x < x + w && saw_mouse_y >= y && saw_mouse_y < y + h; if (t == saw_roll_sheet.notes[note].time) { // Draw note // nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + roll_border, y + roll_border, w - roll_border * 2, h - roll_border * 2); nvgFillColor(saw_nvg, has_cursor ? nvgRGBA(180, 180, 220, 255) : nvgRGBA(180, 180, 180, 255)); nvgFill(saw_nvg); // Note input // if (has_cursor && saw_rbutton_down) saw_roll_sheet.notes[note].enabled = 0; } } } } // Note stretching input // if (saw_grid_input) { if (saw_lbutton_down) { i32 t = (saw_mouse_x - x0 - pianokey_width - sheet_offset) / sheet_scale; if (t >= 0) { saw_sheet_note_t *p = saw_roll_sheet.notes + saw_grid_note; if (saw_grid_time <= t) { p->time = saw_grid_time; p->duration = 1 + t - saw_grid_time; } else { p->time = t; p->duration = saw_grid_time - t; } } saw_sheet_note_t *p = saw_roll_sheet.notes + saw_grid_note; for (i32 n = 0; n < SHEET_SIZE; n++) { if (n == saw_grid_note) continue; saw_sheet_note_t *q = saw_roll_sheet.notes + n; if (!q->enabled || q->pitch != p->pitch) continue; if (q->time < saw_grid_time && q->time + q->duration > p->time) { p->time = q->time + q->duration; p->duration = saw_grid_time > p->time ? saw_grid_time - p->time : 1; } if (q->time > saw_grid_time && q->time < p->time + p->duration) { p->time = saw_grid_time; p->duration = q->time - saw_grid_time; assert(p->duration > 0); } } } else saw_grid_input = 0; } // Playback indicator // f64 playback_time = ((f64) saw_roll_frame) / SAW_SAMPLE_RATE; i32 x = x0 + pianokey_width + sheet_offset - roll_border * 2 + (i32) (playback_time * saw_roll_sheet.rate * sheet_scale + .5); i32 y = 0; i32 w = roll_border * 4; i32 h = height - y0; nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x, y, w, h); nvgFillColor(saw_nvg, nvgRGBA(240, 240, 80, 220)); nvgFill(saw_nvg); nvgEndFrame(saw_nvg); // Cleanup input state. // saw_lbutton_click = 0; saw_rbutton_click = 0; } static void saw_cleanup(void) { ma_device_uninit(&saw_ma); #ifdef SOKOL_GLCORE33 nvgDeleteGL3(saw_nvg); #else nvgDeleteGLES3(saw_nvg); #endif } static void saw_event(sapp_event const *event) { switch (event->type) { case SAPP_EVENTTYPE_MOUSE_MOVE: saw_mouse_x = event->mouse_x; saw_mouse_y = event->mouse_y; break; case SAPP_EVENTTYPE_MOUSE_DOWN: 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; default:; } break; case SAPP_EVENTTYPE_MOUSE_UP: switch (event->mouse_button) { case SAPP_MOUSEBUTTON_LEFT: saw_lbutton_down = 0; break; case SAPP_MOUSEBUTTON_RIGHT: saw_rbutton_down = 0; break; default:; } break; case SAPP_EVENTTYPE_KEY_DOWN: if (!event->key_repeat) { switch (event->key_code) { case SAPP_KEYCODE_SPACE: saw_roll_playing = !saw_roll_playing; break; case SAPP_KEYCODE_ENTER: saw_roll_frame = 0; break; default:; } } break; default:; } } char const *__lsan_default_suppressions() { // There is leaks in NVidia driver on Linux. return "leak:nvidia"; } sapp_desc sokol_main(int argc, char **argv) { return (sapp_desc) { .width = 1280, .height = 720, .init_cb = saw_init, .frame_cb = saw_frame, .cleanup_cb = saw_cleanup, .event_cb = saw_event, }; }