From 64333809a2df0d1ece3127a35a9996f1e8116636 Mon Sep 17 00:00:00 2001 From: spaceface Date: Fri, 14 May 2021 15:37:20 +0200 Subject: [PATCH] viano v3 (reverse UI edition) --- main.v | 118 +++++++++++++++++++++++----------------- midi.v | 168 ++++++++++++++++++++++++++++++++------------------------- ui.v | 160 +++++++++++++++++++++++++++++++++++------------------- 3 files changed, 268 insertions(+), 178 deletions(-) diff --git a/main.v b/main.v index ce3faba..668dc28 100644 --- a/main.v +++ b/main.v @@ -11,14 +11,39 @@ mut: vidi &vidi.Context = voidptr(0) audio &audio.Context = voidptr(0) sustained bool - win_width int - win_height int - key_width f32 - key_height f32 + win_width int = 1280 + win_height int = 800 + key_width f32 = 60 + key_height f32 = 200 white_key_count int keys [128]Key start_note byte = 36 dragging bool + + // info about the current song: + // the notes that make it up + notes []Note + // the current timestamp (in ns) + t u64 + // the index of the first note currently being played in the song + i u32 + // the current song's length in ns + song_len u64 +} + +struct Note { +mut: + start u64 + len u32 + midi byte + vel byte +} + +fn (n Note) str() string { + a := int(n.midi) + b := f64(n.start) / time.second + c := f64(n.start+n.len) / time.second + return '\n [$a] $b - $c' } enum KeyColor { @@ -26,65 +51,35 @@ enum KeyColor { white } -struct Keypress { +struct Key { mut: - start i64 - end i64 - velocity byte + sidx u32 + sustained bool + pressed bool } -struct Key { -mut: - sustained bool - pressed bool - presses []Keypress +// byte.is_playable returns true if a note is playable using a Boomwhackers set +[inline] +fn is_playable(n byte) bool { + return n >= 48 && n <= 76 } fn (mut app App) play_note(note byte, vol_ byte) { if app.keys[note].pressed { return } - // if a note is being sustained, but it is pressed and released, pause and play it again - if app.sustained && app.keys[note].sustained && app.keys[note].presses.len > 0 { - t := time.ticks() - app.keys[note].presses[app.keys[note].presses.len - 1].end = t - app.keys[note].presses << { start: t, velocity: vol_ } - } else { - app.keys[note].pressed = true - app.keys[note].sustained = app.sustained - app.keys[note].presses << { start: time.ticks(), velocity: vol_ } - } - + app.keys[note].pressed = true vol := f32(vol_) / 127 app.audio.play(note, vol) } fn (mut app App) pause_note(note byte) { - mut key := unsafe { &app.keys[note] } - - if app.sustained { - key.pressed = false - key.sustained = true - } else { - if key.sustained && key.pressed { - key.sustained = false - } else { - key.sustained = false - key.pressed = false - if key.presses.len > 0 && key.presses[key.presses.len - 1].end == 0 { - key.presses[key.presses.len - 1].end = time.ticks() - } - app.audio.pause(note) - } - } + app.keys[note].pressed = false + app.audio.pause(note) } [console] fn main() { mut app := &App{} - // initialize arrays - for mut key in app.keys { - key.presses = [] - } app.audio = audio.new_context(wave_kind: .torgan) @@ -99,16 +94,43 @@ fn main() { event_fn: event user_data: app font_path: gg.system_font_path() - // sample_count: 4 + sample_count: 4 ) if os.args.len > 1 { - app.play_midi_file(os.args[1]) or { + app.parse_midi_file(os.args[1]) or { eprintln('failed to parse midi file `${os.args[1]}`: $err') return } + + mut song_len := u64(0) + mut notes_needed := map[byte]u16{} + for note in app.notes { + if note.start + note.len > song_len { + song_len = note.start + note.len + } + notes_needed[note.midi]++ + } + app.song_len = song_len + println('song length: ${f64(song_len) / 6e+10:.1f} minutes') + + notes_per_second := f64(app.notes.len) / f64(app.song_len) * f64(time.second) + difficulties := ['easy', 'medium', 'hard', 'extreme']! + println('total notes: $app.notes.len (${notes_per_second:.1f} notes/sec, difficulty: ${difficulties[clamp(byte(notes_per_second / 3.3), 0, 3)]})') + + mut keys := notes_needed.keys() + keys.sort() + + println('required notes:') + for k in keys { + v := notes_needed[k] + println(' ${midi2name(k):-20}$v') + } + + go app.play() } else { - app.open_midi_port() ? + eprintln('usage: viano ') + exit(1) } app.gg.run() diff --git a/midi.v b/midi.v index a196062..8a8c763 100644 --- a/midi.v +++ b/midi.v @@ -1,29 +1,13 @@ -import math import time import vidi - [inline] -fn midi2freq(midi byte) f32 { - return int(math.powf(2, f32(midi - 69) / 12) * 440) -} - -fn parse_midi_event(buf []byte, timestamp f64, mut app App) { - if buf.len < 1 { return } - // println('Read $buf.len MIDI bytes: $buf.hex()') - - status, channel := buf[0] & 0xF0, buf[0] & 0x0F - _ = channel - match status { - 0x80 /* note down */, 0x90 /* note up */ { - app.note_down(buf[1] & 0x7F, buf[2] & 0x7F) - } 0xB0 /* control change */ { - app.control_change(buf[1] & 0x7F, buf[2] & 0x7F) - } else { - println('Unknown MIDI event `$status`') - } - } +fn midi2name(midi byte) string { + x := ['bass', 'mid', 'high']! + oct := if is_playable(midi) { x[midi/12 - 4] } else { '(unplayable)' } + note := note_names[midi%12] + return '$oct $note' } fn (mut app App) note_down(note byte, velocity byte) { @@ -34,17 +18,6 @@ fn (mut app App) note_down(note byte, velocity byte) { } } -fn (mut app App) control_change(control byte, value byte) { - match control { - 0x40, 0x17 { - if value > 0x40 { app.sustain() } else { app.unsustain() } - } - else { - println('Control change (control=$control, value=$value)') - } - } -} - fn (mut app App) sustain() { if !app.sustained { app.sustained = true @@ -59,61 +32,108 @@ fn (mut app App) sustain() { fn (mut app App) unsustain() { if app.sustained { app.sustained = false - for midi, _ in app.keys { - app.pause_note(byte(midi)) + for midi in 0 .. byte(app.keys.len) { + app.pause_note(midi) } } } -fn (mut app App) play_midi_file(name string) ? { +fn (mut app App) parse_midi_file(name string) ? { midi := vidi.parse_file(name) ? - // for track in midi.tracks { - // go app.play_midi_track(track, i64(midi.micros_per_tick)) - // } - go app.play_midi_track(midi, 0) -} + mut mpqn := u32(midi.micros_per_tick) + mut cache := [128]Note{} + mut sustained_notes := []Note{} + mut is_sustain := false + _ = is_sustain + mut t := u64(0) + for track in midi.tracks { + for event in track.data { + t += event.delta_time * mpqn * u64(time.microsecond) + match event { + vidi.NoteOn, vidi.NoteOff { + if event.velocity == 0 { + // pause + if cache[event.note].midi != event.note { + eprintln('malformed midi file - releasing paused note') + continue + } + cache[event.note].len = u32(t - cache[event.note].start) + app.notes << cache[event.note] + cache[event.note] = Note{} + } else { + // play + cache[event.note] = { + start: t + midi: event.note + vel: event.velocity + } + } + } + vidi.Controller { + match event.controller_type { + 0x40, 0x17 { + if event.value > 0x40 { + is_sustain = true + } else { + is_sustain = false + for mut note in sustained_notes { + note.len = u32(t - note.start) + } + app.notes << sustained_notes + sustained_notes.clear() + } + } + else { + // println('Control change (control=$control, value=$value)') + } + } -fn (mut app App) play_midi_track(midi vidi.Midi, i int) { - mut mpqn := i64(midi.micros_per_tick) - for event in midi.tracks[i].data { - match event { - vidi.NoteOn, vidi.NoteOff { - sleep := i64(event.delta_time) * mpqn * time.microsecond - time.sleep(sleep) - app.note_down(event.note, event.velocity) - } - vidi.Controller { - app.control_change(event.controller_type, event.value) + } + vidi.SetTempo { + mpqn = u32(midi.mpqn(event.microseconds)) + } + else { + // println(event) + } } - vidi.SetTempo { - mpqn = midi.mpqn(event.microseconds) - } - else {} } + t = 0 } + app.notes.sort(a.start < b.start) } -fn (mut app App) open_midi_port() ? { - app.vidi = vidi.new_ctx(callback: parse_midi_event, user_data: app) ? - port_count := vidi.port_count() - println('There are $port_count ports') - if port_count == 0 { exit(1) } +fn (mut app App) play() { + start_time := time.sys_mono_now() + for app.t < app.song_len { + app.t = (time.sys_mono_now() - start_time) + time.sleep(5*time.microsecond) + mut is_at_start := true + _ = is_at_start + for i := app.i; i < app.notes.len ; i++ { + note := app.notes[i] + key := app.keys[note.midi] + end := note.start + note.len - for i in 0 .. port_count { - info := vidi.port_info(i) - println(' $i: $info.manufacturer $info.name $info.model') - } + lt := app.t - lookahead - if port_count == 1 { - app.vidi.open(0) ? - println('\nOpened port 0, since it was the only available port\n') - } else { - for i in 0 .. port_count { - if _ := app.vidi.open(i) { break } // or {} - else {} + if is_at_start { + if lt < app.t && note.start + note.len < lt { + app.i++ + } else { + is_at_start = false + } + } + + if note.start <= lt && end > lt && !key.pressed { + app.play_note(note.midi, note.vel) + app.keys[note.midi].sidx = i + } + if key.sidx == i && end <= lt && key.pressed { + app.pause_note(note.midi) + } + + if note.start > lt { break } } - // num := os.input('\nEnter port number: ').int() - // app.vidi.open(num) ? - // println('Opened port $num successfully\n') } + exit(1) } diff --git a/ui.v b/ui.v index db8c4e1..be9c2ee 100644 --- a/ui.v +++ b/ui.v @@ -9,6 +9,8 @@ const ( const ( default_key_width = 60 + min_key_height = 128 + max_key_height = 2./3 ) const ( @@ -31,12 +33,9 @@ fn frame(app &App) { app.gg.end() } -// how quickly the note bars will leave the screen -const leave_factor = 100 +// how long a note will fall for before being played +const lookahead = u64(3 * time.second) -// NOTE: there is a "bug" here, which is that once a note has been released, -// it moves up quicker than the leave_factor. This was not the intended behavior -// originally, but it looked nice to me, and I haven't bothered fixing it :)) fn (app &App) draw() { ww, wh := app.win_width, app.win_height kw, kh := app.key_width, app.key_height @@ -44,13 +43,46 @@ fn (app &App) draw() { starty := wh - kh - 5 bar_area_height := wh - kh - 10 - t := time.ticks() - // draw red strip above keyboard app.gg.draw_rect(0, starty - 5, ww, 5, red_strip_color) + + // whites := app.notes[app.i..].filter(octave[it.midi % octave.len] == .white) + // blacks := app.notes[app.i..].filter(octave[it.midi % octave.len] == .black) + + // println('$app.i $whites.len + $blacks.len = ${app.notes[app.i..].len}') + + // draw the note bars + mut i := u32(0) + mut lol := 0 + for i = app.i; i < app.notes.len ; i++ { + lol++ + note := app.notes[i] + if note.start > app.t { break } + h := f32((note.len) * u64(bar_area_height) / lookahead) + y := f32((app.t - note.start) * u64(bar_area_height) / lookahead) + x, w := app.note_pos(note.midi) + app.gg.draw_rect(x, y - h, w, h, note_color(note.midi, 100)) + + + c := text_cfg(note.midi) + app.gg.draw_text(int(x + w / 2), int(y - 20), note_names[note.midi % octave.len], { ...c, color: gx.white }) + + + // if y > wh { break } + // println('$i: $y: ${wh - int(y)}') + + // println(f64(note.start+note.len-app.t) / f64(lookahead)) + + // height := max(int(f64(note.len) / f64(lookahead) * wh), bar_area_height - f32(y)) + // color := note_color(note.midi, 100) + // app.gg.draw_rect(note.midi * 10, f32(y), 10, height, color) + // println('(${note.midi * 10}, $y), (10, $height)') + } + // println('$app.i -> $i') + i = 0 + // draw white keys - mut i := 0 for midi := byte(app.start_note); i < app.white_key_count; midi++ { if octave[midi % octave.len] == .black { midi++ } startx := i * kw @@ -59,27 +91,17 @@ fn (app &App) draw() { pressed := key.pressed || key.sustained color := if pressed { pressed_white_key_color } else { white_key_color } height := if pressed { kh + 5 } else { kh } - app.gg.draw_rounded_rect(startx, starty, kw / 2, height / 2, f32(kw) / 6, color) - app.gg.draw_empty_rounded_rect(startx, starty, kw / 2, height / 2, f32(kw) / 6, gx.black) + app.gg.draw_rounded_rect(startx, starty, kw, height, f32(kw) / 6, color) + app.gg.draw_empty_rounded_rect(startx, starty, kw, height, f32(kw) / 6, gx.black) app.gg.draw_text(int(startx + kw / 2), wh - 30, note_names[midi % octave.len], text_cfg(midi)) - - // draw note bars - for press in key.presses { - end := if press.end == 0 { t } else { press.end } - offset := f32(t - end) - len := f32(end - press.start) - len_px := (leave_factor * len) / bar_area_height - bcolor := note_color(midi, press.velocity) - app.gg.draw_rect(startx, bar_area_height - len_px - offset, f32(kw), len_px, bcolor) - } i++ } + i = 0 - // black key width / black key height + // calculate the black key width / black key height: 2/3 that of a white key bkw, bkh := app.key_width * 2 / 3, app.key_height * 2 / 3 // draw black keys on top - i = 0 for midi := byte(app.start_note); i < app.white_key_count - 1; midi++ { x := octave[(midi + 1) % octave.len] if x == .white { i++ continue } else { midi++ } @@ -91,20 +113,20 @@ fn (app &App) draw() { height := if pressed { bkh + 3 } else { bkh } app.gg.draw_rect(startx, starty, bkw, height, color) app.gg.draw_text(int(startx + kw / 3), int(wh - app.key_height / 3 - 20), note_names[midi % octave.len], text_cfg(midi)) - - // draw note bars - for press in key.presses { - end := if press.end == 0 { t } else { press.end } - offset := f32(t - end) - len := f32(end - press.start) - len_px := (leave_factor * len) / bar_area_height - bcolor := note_color(midi, press.velocity) - app.gg.draw_rect(startx, bar_area_height - len_px - offset, f32(bkw), len_px, bcolor) - } i++ } } +// note_pos returns the x coordinate of a note bar and its width +// TODO +fn (app &App) note_pos(note byte) (f32, f32) { + if octave[note % octave.len] == .white { + return f32(note - app.start_note) * app.win_width / app.white_key_count / 1.75, app.key_width + } else { + return f32(note - app.start_note) * app.win_width / app.white_key_count / 1.75 + 1/2, app.key_width * 2 / 3 + } +} + [inline] fn text_cfg(note byte) gx.TextCfg { size := if octave[note % octave.len] == .black { 18 } else { 32 } @@ -119,7 +141,16 @@ fn text_cfg(note byte) gx.TextCfg { [inline] fn note_color(note byte, vol byte) gx.Color { - return note_colors[note % octave.len] + mut c := note_colors[note % octave.len] + if !is_playable(note) { + // these are outside the playable Boomwhackers range - make them dark + c = gx.Color{ + r: c.r / 3 + g: c.g / 3 + b: c.b / 3 + } + } + return c } fn event(e &gg.Event, mut app App) { @@ -131,7 +162,20 @@ fn event(e &gg.Event, mut app App) { app.resize() } .mouse_scroll { - + if e.scroll_x < 0 { + app.shift_kb(.left) + } else if e.scroll_x > 0 { + app.shift_kb(.right) + } + if e.scroll_y < 0 { + if app.key_height < f64(app.win_height) * max_key_height { + app.key_height += 3 + } + } else { + if app.key_height > 128 { + app.key_height -= 3 + } + } } .mouse_down { if e.mouse_y < app.win_height - app.key_height { return } @@ -167,7 +211,6 @@ fn event(e &gg.Event, mut app App) { if app.dragging { s := sign(e.mouse_dx) if s == -1 { return } // TODO: fix - println(s) mut note, mut i := i8(app.start_note), 0 for { @@ -191,7 +234,6 @@ fn event(e &gg.Event, mut app App) { if octave[note % octave.len] == .black { prev_note -= s } if note != prev_note { - println('$prev_note -> $note') app.pause_note(byte(prev_note)) app.play_note(byte(note), 100) } @@ -203,22 +245,14 @@ fn event(e &gg.Event, mut app App) { exit(0) } .left { - for { - app.start_note++ - if octave[app.start_note % octave.len] == .white { break } - } - app.check_bounds() + app.shift_kb(.left) } .right { - for { - app.start_note-- - if octave[app.start_note % octave.len] == .white { break } - } - app.check_bounds() - } - .space { - if app.sustained { app.unsustain() } else { app.sustain() } + app.shift_kb(.right) } + // .space { + // if app.sustained { app.unsustain() } else { app.sustain() } + // } else {} } } @@ -226,15 +260,32 @@ fn event(e &gg.Event, mut app App) { } } +enum Direction { + left = -1 + right = 1 +} + +fn (mut app App) shift_kb(d Direction) { + for { + app.start_note -= byte(d) + if octave[app.start_note % octave.len] == .white { break } + } + app.check_bounds() +} + fn (mut app App) resize() { + // save previous values to keep proportional scaling + _, ph := app.win_width, app.win_height + s := gg.window_size() - app.win_width, app.win_height = s.width, s.height + ww, wh := s.width, s.height + app.win_width, app.win_height = ww, wh - app.key_height = clamp(app.win_height / 4, 150, 400) + app.key_height = clamp(app.key_height / ph * wh, min_key_height, f32(wh) * max_key_height) // calculate ideal key count/width based on the current window width - app.white_key_count = int(f32(app.win_width) / default_key_width + 0.5) // round - app.key_width = f32(app.win_width) / app.white_key_count // will be 45 ± some decimal + app.white_key_count = int(f32(ww) / default_key_width + 0.5) // round + app.key_width = f32(ww) / app.white_key_count // will be 45 ± some decimal if app.start_note + app.white_key_count >= 127 { app.start_note = byte(128 - app.white_key_count) @@ -254,13 +305,10 @@ fn (mut app App) check_bounds() { } if midi <= 128 { return } - dump('YES') - for midi > 127 { if octave[midi % octave.len] == .black { midi -= 2 } else { midi-- } app.start_note-- } - dump(app.start_note) // ensure the window layout is now valid app.check_bounds()