Skip to content

Commit 0785cec

Browse files
committed
basic metronome
1 parent e021a74 commit 0785cec

File tree

11 files changed

+6693
-0
lines changed

11 files changed

+6693
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
imgui.ini
2+
*.bin

beeps/ping1.wav

8.56 KB
Binary file not shown.

src/main.odin

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package flowtimer
2+
3+
import "core:slice"
4+
import "core:time"
5+
import rl "vendor:raylib"
6+
import rlimgui "../thirdparty/imgui_impl_raylib"
7+
import imgui "../thirdparty/odin-imgui"
8+
9+
main :: proc() {
10+
rl.InitWindow(640, 360, "flowtimer")
11+
defer rl.CloseWindow()
12+
13+
imgui.CreateContext(nil)
14+
defer imgui.DestroyContext(nil)
15+
16+
rlimgui.init()
17+
defer rlimgui.shutdown()
18+
19+
metronome: Metronome
20+
create_metronome(&metronome)
21+
defer delete_metronome(&metronome)
22+
23+
offsets := []time.Duration{time.Millisecond * 5000, time.Millisecond * 10000}
24+
max_offset := slice.max(offsets)
25+
26+
set_beep(&metronome, "beeps/ping1.wav")
27+
prepare_metronome(&metronome, offsets, time.Millisecond * 500, 5)
28+
29+
start_tick: time.Tick
30+
31+
for !rl.WindowShouldClose() {
32+
rl.BeginDrawing()
33+
rl.ClearBackground(rl.BLACK)
34+
35+
rlimgui.begin()
36+
37+
imgui.Begin("Window")
38+
if imgui.Button("Beep") {
39+
start_tick = time.tick_now()
40+
play_audio(&metronome, &metronome.buffer)
41+
}
42+
43+
time_remaining := max_offset
44+
if start_tick._nsec != 0 {
45+
time_remaining = max_offset - time.tick_since(start_tick)
46+
47+
if time_remaining < 0 {
48+
time_remaining = 0
49+
start_tick._nsec = 0
50+
}
51+
}
52+
53+
54+
imgui.LabelText("##", "%.0f", time.duration_milliseconds(time_remaining))
55+
imgui.End()
56+
57+
rlimgui.end()
58+
59+
rl.EndDrawing()
60+
}
61+
}

src/metronome.odin

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package flowtimer
2+
3+
import "core:mem"
4+
import "core:slice"
5+
import "core:time"
6+
import ma "vendor:miniaudio"
7+
8+
AUDIO_FORMAT :: ma.format.f32
9+
AUDIO_SAMPLE_RATE :: 48000
10+
AUDIO_CHANNELS :: 2
11+
AUDIO_PERIOD_IN_FRAMES :: 16
12+
AUDIO_BYTES_PER_FRAME := ma.get_bytes_per_frame(AUDIO_FORMAT, AUDIO_CHANNELS)
13+
14+
Audio :: struct {
15+
pcm: []u8,
16+
frame_count: u32,
17+
}
18+
19+
Metronome :: struct {
20+
device: ma.device,
21+
22+
queued: ^Audio,
23+
frame_position: u32,
24+
25+
buffer: Audio,
26+
beep: Audio,
27+
}
28+
29+
metronome_data_proc :: proc "c" (device: ^ma.device, sink, _: rawptr, requested_frames: u32) {
30+
mem.zero(sink, int(audio_frames_to_bytes(requested_frames)))
31+
32+
metronome := transmute(^Metronome)device.pUserData
33+
if metronome.queued == nil do return
34+
35+
remaining := metronome.queued.frame_count - metronome.frame_position
36+
frames_to_copy := min(requested_frames, remaining)
37+
38+
if frames_to_copy > 0 {
39+
src_offset := audio_frames_to_bytes(metronome.frame_position)
40+
mem.copy(sink, &metronome.queued.pcm[src_offset], int(audio_frames_to_bytes(frames_to_copy)))
41+
metronome.frame_position += frames_to_copy
42+
}
43+
}
44+
45+
audio_frames_to_bytes :: proc "contextless" (frame_count: u32) -> u32 {
46+
return frame_count * AUDIO_BYTES_PER_FRAME
47+
}
48+
49+
duration_to_audio_frames :: proc "contextless" (duration: time.Duration) -> u32 {
50+
return u32(time.duration_seconds(duration) * f64(AUDIO_SAMPLE_RATE))
51+
}
52+
53+
resize_audio :: proc(audio: ^Audio, frame_count: u32) {
54+
delete(audio.pcm)
55+
audio.pcm = make([]u8, audio_frames_to_bytes(frame_count))
56+
audio.frame_count = frame_count
57+
}
58+
59+
create_metronome :: proc(metronome: ^Metronome) -> bool {
60+
config := ma.device_config_init(.playback)
61+
config.periodSizeInFrames = AUDIO_PERIOD_IN_FRAMES
62+
config.playback.format = AUDIO_FORMAT
63+
config.playback.channels = AUDIO_CHANNELS
64+
config.sampleRate = AUDIO_SAMPLE_RATE
65+
config.dataCallback = metronome_data_proc
66+
config.pUserData = metronome
67+
68+
if ma.device_init(nil, &config, &metronome.device) != .SUCCESS do return false
69+
if ma.device_start(&metronome.device) != .SUCCESS do return false
70+
71+
return true
72+
}
73+
74+
delete_metronome :: proc(metronome: ^Metronome) -> bool {
75+
delete(metronome.buffer.pcm)
76+
delete(metronome.beep.pcm)
77+
78+
if ma.device_stop(&metronome.device) != .SUCCESS do return false
79+
ma.device_uninit(&metronome.device)
80+
81+
return true
82+
}
83+
84+
set_beep :: proc(metronome: ^Metronome, filepath: cstring) -> bool {
85+
config := ma.decoder_config_init(AUDIO_FORMAT, AUDIO_CHANNELS, AUDIO_SAMPLE_RATE)
86+
87+
decoder: ma.decoder
88+
if ma.decoder_init_file(filepath, &config, &decoder) != .SUCCESS do return false
89+
defer ma.decoder_uninit(&decoder)
90+
91+
frame_count: u64
92+
if ma.decoder_get_length_in_pcm_frames(&decoder, &frame_count) != .SUCCESS do return false
93+
resize_audio(&metronome.beep, u32(frame_count))
94+
95+
frames_read: u64
96+
if ma.decoder_read_pcm_frames(&decoder, raw_data(metronome.beep.pcm), u64(metronome.beep.frame_count), &frames_read) != .SUCCESS do return false
97+
if frames_read != frame_count do return false
98+
99+
return true
100+
}
101+
102+
prepare_metronome :: proc(metronome: ^Metronome, offsets: []time.Duration, interval: time.Duration, beeps: int) {
103+
max_offset := slice.max(offsets)
104+
105+
frame_count := duration_to_audio_frames(max_offset) + metronome.beep.frame_count
106+
resize_audio(&metronome.buffer, frame_count)
107+
108+
for offset in offsets {
109+
for i in 0..<beeps {
110+
offset_duration := offset - time.Duration(i) * interval
111+
offset_frames := duration_to_audio_frames(offset_duration)
112+
offset_byte := audio_frames_to_bytes(offset_frames)
113+
copy(metronome.buffer.pcm[offset_byte:], metronome.beep.pcm)
114+
}
115+
}
116+
}
117+
118+
play_audio :: proc(metronome: ^Metronome, audio: ^Audio) {
119+
metronome.queued = audio
120+
metronome.frame_position = 0
121+
}

0 commit comments

Comments
 (0)