From 59ac08837ca411410385050657942fe981e327db Mon Sep 17 00:00:00 2001 From: Mitya Selivanov Date: Fri, 29 Sep 2023 06:58:11 +0200 Subject: Stereo; Minor UI improvements --- TODO | 11 +- source/saw/_impl.c | 11 +- source/saw/main.c | 363 +++++++++++++++++++++++++++++++++++++++----------- source/saw/profiler.c | 6 +- 4 files changed, 306 insertions(+), 85 deletions(-) diff --git a/TODO b/TODO index dc4b78a..380137b 100644 --- a/TODO +++ b/TODO @@ -9,7 +9,7 @@ To-Do list - Sound: Kick, snare, cymbal - UI: Catalog - State load and store -- WAV export +- Data: WAV export - Sound: Buffering - Build: WebAssembly - Project load and store @@ -31,6 +31,15 @@ To-Do list - Sound: Sample rendering - UI: Matrix view - UI: Graph view +- UI improvements +- Performance optimization +- Parallel computation +- Plugin module +- VST3 wrapper +- LV2 wrapper +- Data: WAV import +- Data: MIDI export +- Data: MIDI import Done diff --git a/source/saw/_impl.c b/source/saw/_impl.c index f405a6b..eb1d2ba 100644 --- a/source/saw/_impl.c +++ b/source/saw/_impl.c @@ -6,6 +6,16 @@ # define NANOVG_GL3 1 #endif +// hotfix for weird GCC bug +#ifdef __linux__ +# define _GNU_SOURCE +# include +#endif + +// kit +// +#include "../kit/_lib.c" + // sokol // #define SOKOL_APP_IMPL @@ -29,4 +39,3 @@ #include "../thirdparty/nanovg.c" #include "../thirdparty/nanovg_gl.c" #include "../thirdparty/android.c" - diff --git a/source/saw/main.c b/source/saw/main.c index 77ec5df..c179564 100644 --- a/source/saw/main.c +++ b/source/saw/main.c @@ -3,6 +3,8 @@ #include "../kit/math.h" #include "../kit/time.h" #include "../kit/string_ref.h" +#include "../kit/mersenne_twister_64.h" +#include "../kit/secure_random.h" #ifdef __EMSCRIPTEN__ # include @@ -31,8 +33,11 @@ enum { ROLL_SIZE = 80, SHEET_SIZE = 200, ROLL_COUNT = 16, + TRACK_COUNT = 8, ROLL_DEFAULT_OFFSET_Y = 710, + + INSTRUMENT_SINE = 0, }; typedef struct { @@ -41,6 +46,7 @@ typedef struct { f64 duration; f64 frequency; f64 amplitude; + f64 stereo_factor; } saw_voice_t; typedef struct { @@ -68,14 +74,30 @@ typedef struct { i64 offset_y; } saw_roll_t; +typedef struct { + f64 sustain; + f64 attack; + f64 decay; + f64 release; +} saw_envelope_t; + +typedef struct { + i32 instrument; + f64 warp; + f64 phase; + f64 stereo_width; + f64 volume; + saw_envelope_t envelope; +} saw_track_t; + typedef struct { i64 rolls[ROLL_COUNT]; - i8 offset_input; - i64 offset_x; - i64 offset_y; i8 grid_input; i32 grid_roll; i32 grid_cell; + i8 offset_input; + i64 offset_x; + i64 offset_y; } saw_compose_t; static struct NVGcontext *saw_nvg; @@ -83,6 +105,8 @@ static ma_device saw_ma; static i32 saw_font = -1; +static mt64_state_t saw_mt64; + static i32 saw_mouse_x = 0; static i32 saw_mouse_y = 0; static i8 saw_lbutton_click = 0; @@ -94,19 +118,21 @@ static i8 saw_mbutton_down = 0; static i8 saw_playback_on = 0; static i64 saw_playback_frame = 0; +static i64 saw_current_track = 0; static i64 saw_current_roll = 0; 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 }, - .offset_input = 0, - .offset_x = 0, - .offset_y = 0, .grid_input = 0, .grid_roll = 0, .grid_cell = 0, + .offset_input = 0, + .offset_x = 0, + .offset_y = 0, }; #ifdef __GNUC__ @@ -117,6 +143,14 @@ static saw_compose_t saw_compose = { # pragma GCC optimize("O3") #endif +static f64 saw_random(f64 min, f64 max) { + if (max < min) + return min; + u64 x = mt64_generate(&saw_mt64); + u64 range = (u64) ((max - min) * 10000 + 0.5); + return min + (max - min) * (range * (x % (range + 1))); +} + static f64 saw_envelope(f64 t, f64 attack, f64 decay, f64 sustain, f64 duration, f64 release) { f64 a = 1.; @@ -169,11 +203,12 @@ static void saw_playback(i32 frame_count) { saw_voices[n] = saw_voices[n - 1]; saw_voices[0] = (saw_voice_t) { - .enabled = 1, - .time = 0, - .duration = ((f64) p->duration) / roll->rate, - .frequency = saw_pitch_frequency(p->pitch), - .amplitude = saw_pitch_amplitude(p->pitch), + .enabled = 1, + .time = 0, + .duration = ((f64) p->duration) / roll->rate, + .frequency = saw_pitch_frequency(p->pitch), + .amplitude = saw_pitch_amplitude(p->pitch), + .stereo_factor = saw_random(-0.01, 0.01), }; } } @@ -196,9 +231,10 @@ static void saw_audio(ma_device *device, void *void_out_, if (!saw_voices[n].enabled) continue; - f64 period = M_PI * 2.; - f64 frequency = saw_voices[n].frequency; - f64 amplitude = saw_voices[n].amplitude; + f64 period = M_PI * 2.; + f64 frequency = saw_voices[n].frequency; + f64 amplitude = saw_voices[n].amplitude; + f64 stereo_factor = saw_voices[n].stereo_factor; // envelope f64 attack = .007; @@ -214,7 +250,7 @@ static void saw_audio(ma_device *device, void *void_out_, f64 k = period * frequency; out[i * 2] += (f32) (sin(k * t) * a); - out[i * 2 + 1] += (f32) (sin(k * t) * a); + out[i * 2 + 1] += (f32) (sin(k * t + stereo_factor) * a); saw_voices[n].time++; @@ -242,7 +278,12 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x0, frame_height - y0 - height + border, width, - frame_height - y0 - height + track_height / 2 - border * 2); + frame_height - y0 - height + track_height / 5 - border * 2); + nvgRect(saw_nvg, x0, + frame_height - y0 - height + border + + (track_height * 4) / 5, + width, + frame_height - y0 - height + track_height / 5 - border * 2); nvgFillColor(saw_nvg, nvgRGBA(180, 140, 120, 160)); nvgFill(saw_nvg); @@ -256,17 +297,33 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { continue; saw_roll_t *roll = saw_rolls + saw_compose.rolls[i]; - i64 x = x0 + saw_compose.offset_x + - (roll->frame * grid_scale) / SAW_SAMPLE_RATE; - i64 w = (roll->size * grid_scale) / roll->rate; - i64 y = frame_height - y0 - height + track_height + + i64 top = frame_height - y0 - height + track_height; + i64 bottom = frame_height - y0; + i64 dx = x0 + saw_compose.offset_x; + i64 l = dx + (roll->frame * grid_scale) / SAW_SAMPLE_RATE; + i64 r = l + (roll->size * grid_scale) / roll->rate; + i64 u = frame_height - y0 - height + track_height + saw_compose.offset_y + roll->track * track_height; - - if (x < x0 || x + w >= x0 + width || - y < frame_height - y0 - height || - y + track_height >= frame_height - y0) + 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; + i8 is_choosen = (saw_current_roll == i); i8 is_playing = saw_playback_on && @@ -278,12 +335,11 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { i8 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 + track_height); + saw_mouse_y >= y && saw_mouse_y < y + h); nvgBeginPath(saw_nvg); nvgRect(saw_nvg, x + border, y + border, w - border * 2, - track_height - border * 2); + h - border * 2); nvgFillColor(saw_nvg, is_choosen ? nvgRGBA(240, 230, 200, 255) : is_playing ? nvgRGBA(255, 230, 200, 255) : has_cursor ? nvgRGBA(210, 210, 255, 255) @@ -313,46 +369,21 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { saw_compose.grid_cell = c1 - 1; roll->size = ((c1 - cell + 1) * roll->rate) / grid_rate; } - } else - saw_current_roll = saw_compose.rolls[i]; + } else { + saw_current_roll = saw_compose.rolls[i]; + saw_current_track = roll->track; + } } hover_any = 1; } } } - // Draw cursor - // - - if (!hover_any && !saw_compose.grid_input) { - i64 track = (saw_mouse_y - saw_compose.offset_y - frame_height + - y0 + height) / - track_height - - 1; - i64 cell = ((saw_mouse_x - saw_compose.offset_x) * grid_rate) / - grid_scale; - i64 x = x0 + saw_compose.offset_x + - (cell * grid_scale) / grid_rate; - i64 y = frame_height - y0 - height + track_height + - saw_compose.offset_y + track * track_height; - i64 w = grid_scale / grid_rate; - - if (track >= 0 && track < ROLL_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); - } - } - - // Placing new track + // Placing new sheet // - if (!hover_any && saw_lbutton_down && saw_mouse_x >= x0 && + if (!hover_any && saw_lbutton_down && + saw_mouse_x >= x0 + saw_compose.offset_x && saw_mouse_x < x0 + width) { if (!saw_compose.grid_input && saw_mouse_y >= frame_height - y0 - height && @@ -385,7 +416,7 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { i64 y = frame_height - y0 - height + track_height + saw_compose.offset_y + track * track_height; - if (track < 0 || track >= ROLL_COUNT || x < x0 || + if (track < 0 || track >= TRACK_COUNT || x < x0 || x >= x0 + width || y < frame_height - y0 - height || y + track_height >= frame_height - y0) n = -1; @@ -418,11 +449,25 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { .offset_y = ROLL_DEFAULT_OFFSET_Y, }; - saw_current_roll = n; + saw_current_roll = n; + saw_current_track = track; } } } + // Offset input + // + + if (saw_mbutton_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.offset_input = 1; + } + + if (!saw_mbutton_down) + saw_compose.offset_input = 0; + // Track stretching input // @@ -492,9 +537,40 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { nvgFill(saw_nvg); } + // Draw cursor + // + + if (!hover_any && !saw_compose.grid_input && + saw_mouse_x >= x0 + saw_compose.offset_x) { + i64 track = (saw_mouse_y - saw_compose.offset_y - frame_height + + y0 + height) / + track_height - + 1; + i64 cell = ((saw_mouse_x - saw_compose.offset_x) * grid_rate) / + grid_scale; + i64 x = x0 + saw_compose.offset_x + + (cell * grid_scale) / grid_rate; + i64 y = frame_height - y0 - height + track_height + + saw_compose.offset_y + track * track_height; + i64 w = grid_scale / grid_rate; + + if (track >= 0 && track < ROLL_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_mouse_x < x0 + width && + + if (saw_mouse_x >= x0 + saw_compose.offset_x && + saw_mouse_x < x0 + width && saw_mouse_y >= frame_height - y0 - height && saw_mouse_y < frame_height - y0) { i32 dx = x0 + saw_compose.offset_x; @@ -510,6 +586,87 @@ static void saw_ui_compose(i64 x0, i64 y0, i64 width, i64 height) { } } +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 = 35; + i64 border = 2; + + // Title + // + + nvgBeginPath(saw_nvg); + nvgFontSize(saw_nvg, text_height); + nvgFontFaceId(saw_nvg, saw_font); + 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); + + // Instrument + // + + char buf_instr[][100] = { "Sine wave" }; + + nvgBeginPath(saw_nvg); + nvgFontSize(saw_nvg, text_height); + nvgFontFaceId(saw_nvg, saw_font); + nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); + nvgText(saw_nvg, x0 + border * 2, + frame_height - y0 - height + text_height * 2 - border * 2, + buf_instr[track->instrument], 0); + nvgFill(saw_nvg); + + // Phase + // + + char buf_phase[100]; + sprintf(buf_phase, "Phase: %d", (int) (track->phase * 100 + 0.5)); + + nvgBeginPath(saw_nvg); + nvgFontSize(saw_nvg, text_height); + nvgFontFaceId(saw_nvg, saw_font); + nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); + nvgText(saw_nvg, x0 + border * 2, + frame_height - y0 - height + text_height * 3 - border * 2, + buf_phase, 0); + nvgFill(saw_nvg); + + // Stereo width + // + + char buf_stereo_width[100]; + sprintf(buf_stereo_width, "Stereo width: %d", + (int) (track->stereo_width * 100 + 0.5)); + + nvgBeginPath(saw_nvg); + nvgFontSize(saw_nvg, text_height); + nvgFontFaceId(saw_nvg, saw_font); + nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); + nvgText(saw_nvg, x0 + border * 2, + frame_height - y0 - height + text_height * 4 - border * 2, + buf_stereo_width, 0); + nvgFill(saw_nvg); + + // Volume + // + + char buf_volume[100]; + sprintf(buf_volume, "Volume: %d", + (int) (track->volume * 100 + 0.5)); + + nvgBeginPath(saw_nvg); + nvgFontSize(saw_nvg, text_height); + nvgFontFaceId(saw_nvg, saw_font); + nvgFillColor(saw_nvg, nvgRGBA(255, 255, 255, 255)); + nvgText(saw_nvg, x0 + border * 2, + frame_height - y0 - height + text_height * 5 - border * 2, + buf_volume, 0); + nvgFill(saw_nvg); +} + static void saw_ui_roll(saw_roll_t *roll, i64 x0, i64 y0, i64 width, i64 height, str_t title) { i32 frame_height = sapp_height(); @@ -583,11 +740,12 @@ static void saw_ui_roll(saw_roll_t *roll, i64 x0, i64 y0, i64 width, saw_voices[n] = saw_voices[n - 1]; saw_voices[0] = (saw_voice_t) { - .enabled = 1, - .time = 0, - .duration = .6, - .frequency = saw_pitch_frequency(pitch), - .amplitude = saw_pitch_amplitude(pitch), + .enabled = 1, + .time = 0, + .duration = .6, + .frequency = saw_pitch_frequency(pitch), + .amplitude = saw_pitch_amplitude(pitch), + .stereo_factor = saw_random(-0.01, 0.01), }; } @@ -602,16 +760,19 @@ static void saw_ui_roll(saw_roll_t *roll, i64 x0, i64 y0, i64 width, roll->last_index = -1; // Offset input + // + if (saw_mbutton_click) { if (saw_mouse_x >= x0 + pianokey_width + sheet_offset && - saw_mouse_y >= frame_height - y0 - height && + saw_mouse_y >= frame_height - y0 - height + text_height && saw_mouse_x < x0 + width && saw_mouse_y < frame_height - y0) roll->offset_x_input = 1; if (saw_mouse_x >= x0 && - saw_mouse_y >= frame_height - y0 - height && + saw_mouse_y >= frame_height - y0 - height + text_height && saw_mouse_x < x0 + width && saw_mouse_y < frame_height - y0) roll->offset_y_input = 1; } + if (!saw_mbutton_down) { roll->offset_x_input = 0; roll->offset_y_input = 0; @@ -835,7 +996,10 @@ static void saw_ui_roll(saw_roll_t *roll, i64 x0, i64 y0, i64 width, } static void saw_init(void) { - sapp_set_window_title("saw"); + u64 rng_seed; + secure_random(sizeof rng_seed, &rng_seed); + mt64_init(&saw_mt64, rng_seed); + mt64_rotate(&saw_mt64); #ifdef SOKOL_GLCORE33 saw_nvg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); @@ -886,6 +1050,21 @@ static void saw_init(void) { .offset_y = ROLL_DEFAULT_OFFSET_Y, }; } + + for (i32 i = 0; i < TRACK_COUNT; i++) + saw_tracks[i] = (saw_track_t) { + .instrument = INSTRUMENT_SINE, + .warp = .5, + .phase = .0, + .stereo_width = .2, + .volume = .2, + .envelope = { + .sustain = .15, + .attack = .007, + .decay = .3, + .release = .4, + }, + }; } static void saw_frame(void) { @@ -900,15 +1079,25 @@ static void saw_frame(void) { nvgBeginFrame(saw_nvg, frame_width, frame_height, sapp_dpi_scale()); - saw_ui_compose(0, (frame_height * 3) / 5, frame_width, + saw_ui_compose(0, (frame_height * 3) / 5, (frame_width * 4) / 5, (frame_height * 2) / 5); + if (saw_current_track != -1) { + char buf[64]; + sprintf(buf, "Track %lld", saw_current_track + 1); + + saw_ui_track(saw_tracks + saw_current_track, + (frame_width * 4) / 5, 0, frame_width / 5, + frame_height, kit_str(strlen(buf), buf)); + } + if (saw_current_roll != -1) { char buf[64]; - sprintf(buf, "Track %lld", saw_current_roll + 1); + sprintf(buf, "Sheet %lld", saw_current_roll + 1); - saw_ui_roll(saw_rolls + saw_current_roll, 0, 0, frame_width, - (frame_height * 3) / 5, kit_str(strlen(buf), buf)); + saw_ui_roll(saw_rolls + saw_current_roll, 0, 0, + (frame_width * 4) / 5, (frame_height * 3) / 5, + kit_str(strlen(buf), buf)); } nvgEndFrame(saw_nvg); @@ -944,6 +1133,11 @@ static void saw_event(sapp_event const *event) { saw_rolls[i].offset_y += event->mouse_dy; } + if (saw_compose.offset_input) { + saw_compose.offset_x += event->mouse_dx; + saw_compose.offset_y += event->mouse_dy; + } + break; case SAPP_EVENTTYPE_MOUSE_DOWN: @@ -979,13 +1173,19 @@ static void saw_event(sapp_event const *event) { case SAPP_KEYCODE_SPACE: saw_playback_on = !saw_playback_on; break; + case SAPP_KEYCODE_ENTER: saw_playback_frame = 0; break; + case SAPP_KEYCODE_ESCAPE: if (saw_current_roll != -1) { saw_rolls[saw_current_roll].offset_x = 0; saw_rolls[saw_current_roll].offset_y = ROLL_DEFAULT_OFFSET_Y; } + + saw_compose.offset_x = 0; + saw_compose.offset_y = 0; + break; default:; } @@ -1002,12 +1202,15 @@ char const *__lsan_default_suppressions() { } sapp_desc sokol_main(int argc, char **argv) { + static char title[] = "saw"; + return (sapp_desc) { - .width = 1280, - .height = 720, - .init_cb = saw_init, - .frame_cb = saw_frame, - .cleanup_cb = saw_cleanup, - .event_cb = saw_event, + .window_title = title, + .width = 1280, + .height = 720, + .init_cb = saw_init, + .frame_cb = saw_frame, + .cleanup_cb = saw_cleanup, + .event_cb = saw_event, }; } diff --git a/source/saw/profiler.c b/source/saw/profiler.c index 14e6b41..2f8e23b 100644 --- a/source/saw/profiler.c +++ b/source/saw/profiler.c @@ -1,8 +1,9 @@ #include "../kit/time.h" +#include "../kit/types.h" #include -long long profiler_time_; +i64 profiler_time_; #ifdef __GNUC__ # pragma GCC diagnostic push @@ -15,8 +16,7 @@ long long profiler_time_; void profile_frame(char const *s) { struct timespec ts; timespec_get(&ts, TIME_UTC); - long long t = ((long long) ts.tv_sec) * 1000 + - ((long long) ts.tv_nsec) / 1000000; + i64 t = ((i64) ts.tv_sec) * 1000 + ((i64) ts.tv_nsec) / 1000000; if (profiler_time_ == 0) printf("%s\n", s); else -- cgit v1.2.3