v2 / examples / sokol / sounds / wav_player.v
211 lines · 199 sloc · 5.23 KB · 2332ecff4811b8c97dfda8e825170e9397962519
Raw
1// vtest build: !openbsd
2import os
3import time
4import sokol.audio
5
6struct Player {
7mut:
8 sample_rate int
9 samples []f32
10 pos int
11 finished bool
12}
13
14fn main() {
15 if os.args.len < 2 {
16 eprintln('Usage: play_wav file1.wav file2.wav ...')
17 play_sounds([os.resource_abs_path('uhoh.wav')])!
18 exit(1)
19 }
20 play_sounds(os.args[1..])!
21}
22
23fn play_sounds(files []string) ! {
24 mut player := Player{}
25 player.init()
26 for f in files {
27 if !os.exists(f) || os.is_dir(f) {
28 eprintln('skipping "${f}" (does not exist)')
29 continue
30 }
31 fext := os.file_ext(f).to_lower()
32 if fext != '.wav' {
33 eprintln('skipping "${f}" (not a .wav file)')
34 continue
35 }
36 player.play_wav_file(f)!
37 }
38 player.stop()
39}
40
41//
42fn audio_player_callback(mut buffer &f32, num_frames int, num_channels int, mut p Player) {
43 ntotal := num_channels * num_frames
44 unsafe { vmemset(buffer, 0, ntotal * 4) }
45 if p.finished {
46 return
47 }
48 nremaining := p.samples.len - p.pos
49 nsamples := if nremaining < ntotal { nremaining } else { ntotal }
50 if nsamples <= 0 {
51 p.finished = true
52 return
53 }
54 unsafe { vmemcpy(buffer, &p.samples[p.pos], nsamples * 4) }
55 p.pos += nsamples
56}
57
58fn (mut p Player) init() {
59 audio.setup(
60 num_channels: 1
61 stream_userdata_cb: audio_player_callback
62 user_data: p
63 sample_rate: 44100
64 )
65 p.sample_rate = audio.sample_rate()
66}
67
68fn (mut p Player) stop() {
69 audio.shutdown()
70 p.free()
71}
72
73fn (mut p Player) play_wav_file(fpath string) ! {
74 println('> play_wav_file: ${fpath}')
75 samples := read_wav_file_samples(fpath)!
76 p.finished = true
77 p.samples << samples
78 p.finished = false
79 for !p.finished {
80 time.sleep(16 * time.millisecond)
81 }
82 p.free()
83}
84
85fn (mut p Player) free() {
86 p.finished = false
87 p.samples = []f32{}
88 p.pos = 0
89}
90
91// The read_wav_file_samples function below is based on the following sources:
92// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
93// http://www.lightlink.com/tjweber/StripWav/WAVE.html
94// http://www.lightlink.com/tjweber/StripWav/Canon.html
95// https://tools.ietf.org/html/draft-ema-vpim-wav-00
96// Note: > The chunks MAY appear in any order except that the Format chunk
97// > MUST be placed before the Sound data chunk (but not necessarily
98// > contiguous to the Sound data chunk).
99struct RIFFHeader {
100 riff [4]u8
101 file_size u32
102 form_type [4]u8
103}
104
105struct RIFFChunkHeader {
106 chunk_type [4]u8
107 chunk_size u32
108 chunk_data voidptr
109}
110
111struct RIFFFormat {
112 format_tag u16 // PCM = 1; Values other than 1 indicate some form of compression.
113 nchannels u16 // Nc ; 1 = mono ; 2 = stereo
114 sample_rate u32 // F
115 avg_bytes_per_second u32 // F * M*Nc
116 nblock_align u16 // M*Nc
117 bits_per_sample u16 // 8 * M
118 cbsize u16 // Size of the extension: 22
119 valid_bits_per_sample u16 // at most 8*M
120 channel_mask u32 // Speaker position mask
121 sub_format [16]u8 // GUID
122}
123
124fn read_wav_file_samples(fpath string) ![]f32 {
125 mut res := []f32{}
126 // eprintln('> read_wav_file_samples: ${fpath} -------------------------------------------------')
127 mut bytes := os.read_bytes(fpath)!
128 mut pbytes := &u8(bytes.data)
129 mut offset := u32(0)
130 rh := unsafe { &RIFFHeader(pbytes) }
131 // eprintln('rh: ${rh}')
132 if rh.riff != [u8(`R`), `I`, `F`, `F`]! {
133 return error('WAV should start with `RIFF`')
134 }
135 if rh.form_type != [u8(`W`), `A`, `V`, `E`]! {
136 return error('WAV should have `WAVE` form type')
137 }
138 if rh.file_size + 8 != bytes.len {
139 return error('WAV should have valid length')
140 }
141 offset += sizeof(RIFFHeader)
142 mut rf := &RIFFFormat(unsafe { nil })
143 for {
144 if offset >= bytes.len {
145 break
146 }
147 //
148 ch := unsafe { &RIFFChunkHeader(pbytes + offset) }
149 offset += 8 + ch.chunk_size
150 // eprintln('ch: ${ch}')
151 // eprintln('p: ${pbytes} | offset: ${offset} | bytes.len: ${bytes.len}')
152 // ////////
153 if ch.chunk_type == [u8(`L`), `I`, `S`, `T`]! {
154 continue
155 }
156 //
157 if ch.chunk_type == [u8(`i`), `d`, `3`, ` `]! {
158 continue
159 }
160 //
161 if ch.chunk_type == [u8(`f`), `m`, `t`, ` `]! {
162 // eprintln('`fmt ` chunk')
163 rf = unsafe { &RIFFFormat(&ch.chunk_data) }
164 // eprintln('fmt riff format: ${rf}')
165 if rf.format_tag != 1 {
166 return error('only PCM encoded WAVs are supported')
167 }
168 if rf.nchannels < 1 || rf.nchannels > 2 {
169 return error('only mono or stereo WAVs are supported')
170 }
171 if rf.bits_per_sample !in [u16(8), 16] {
172 return error('only 8 or 16 bits per sample WAVs are supported')
173 }
174 continue
175 }
176 //
177 if ch.chunk_type == [u8(`d`), `a`, `t`, `a`]! {
178 if unsafe { rf == 0 } {
179 return error('`data` chunk should be after `fmt ` chunk')
180 }
181 // eprintln('`fmt ` chunk: ${rf}\n`data` chunk: ${ch}')
182 mut doffset := 0
183 mut dp := unsafe { &u8(&ch.chunk_data) }
184 for doffset < ch.chunk_size {
185 for c := 0; c < rf.nchannels; c++ {
186 mut x := f32(0.0)
187 mut step := 0
188 ppos := unsafe { dp + doffset }
189 if rf.bits_per_sample == 8 {
190 d8 := unsafe { &u8(ppos) }
191 x = (f32(*d8) - 128) / 128.0
192 step = 1
193 doffset++
194 }
195 if rf.bits_per_sample == 16 {
196 d16 := unsafe { &i16(ppos) }
197 x = f32(*d16) / 32768.0
198 step = 2
199 }
200 doffset += step
201 if doffset < ch.chunk_size {
202 for _ in 0 .. (44100 / rf.sample_rate) {
203 res << x
204 }
205 }
206 }
207 }
208 }
209 }
210 return res
211}
212