| 1 | // vtest build: !openbsd |
| 2 | import os |
| 3 | import time |
| 4 | import sokol.audio |
| 5 | |
| 6 | struct Player { |
| 7 | mut: |
| 8 | sample_rate int |
| 9 | samples []f32 |
| 10 | pos int |
| 11 | finished bool |
| 12 | } |
| 13 | |
| 14 | fn 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 | |
| 23 | fn 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 | // |
| 42 | fn 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 | |
| 58 | fn (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 | |
| 68 | fn (mut p Player) stop() { |
| 69 | audio.shutdown() |
| 70 | p.free() |
| 71 | } |
| 72 | |
| 73 | fn (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 | |
| 85 | fn (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). |
| 99 | struct RIFFHeader { |
| 100 | riff [4]u8 |
| 101 | file_size u32 |
| 102 | form_type [4]u8 |
| 103 | } |
| 104 | |
| 105 | struct RIFFChunkHeader { |
| 106 | chunk_type [4]u8 |
| 107 | chunk_size u32 |
| 108 | chunk_data voidptr |
| 109 | } |
| 110 | |
| 111 | struct 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 | |
| 124 | fn 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 | |