Skip to content

Commit

Permalink
viano v3 (reverse UI edition)
Browse files Browse the repository at this point in the history
  • Loading branch information
spaceface777 committed May 14, 2021
1 parent 3a49b07 commit 6433380
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 178 deletions.
118 changes: 70 additions & 48 deletions main.v
Original file line number Diff line number Diff line change
Expand Up @@ -11,80 +11,75 @@ 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 {
black
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)

Expand All @@ -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>(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 <file.mid>')
exit(1)
}

app.gg.run()
Expand Down
168 changes: 94 additions & 74 deletions midi.v
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
Expand All @@ -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)
}
Loading

0 comments on commit 6433380

Please sign in to comment.