210 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			V
		
	
	
			
		
		
	
	
			210 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			V
		
	
	
import os
 | 
						|
import time
 | 
						|
import sokol.audio
 | 
						|
 | 
						|
struct Player {
 | 
						|
mut:
 | 
						|
	samples  []f32
 | 
						|
	pos      int
 | 
						|
	finished bool
 | 
						|
}
 | 
						|
 | 
						|
fn main() {
 | 
						|
	if os.args.len < 2 {
 | 
						|
		eprintln('Usage: play_wav file1.wav file2.wav ...')
 | 
						|
		play_sounds([os.resource_abs_path('uhoh.wav')]) ?
 | 
						|
		exit(1)
 | 
						|
	}
 | 
						|
	play_sounds(os.args[1..]) ?
 | 
						|
}
 | 
						|
 | 
						|
fn play_sounds(files []string) ? {
 | 
						|
	mut player := Player{}
 | 
						|
	player.init()
 | 
						|
	for f in files {
 | 
						|
		if !os.exists(f) || os.is_dir(f) {
 | 
						|
			eprintln('skipping "$f" (does not exist)')
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		fext := os.file_ext(f).to_lower()
 | 
						|
		if fext != '.wav' {
 | 
						|
			eprintln('skipping "$f" (not a .wav file)')
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		player.play_wav_file(f) ?
 | 
						|
	}
 | 
						|
	player.stop()
 | 
						|
}
 | 
						|
 | 
						|
//
 | 
						|
fn audio_player_callback(buffer &f32, num_frames int, num_channels int, mut p Player) {
 | 
						|
	if p.finished {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	ntotal := num_channels * num_frames
 | 
						|
	nremaining := p.samples.len - p.pos
 | 
						|
	nsamples := if nremaining < ntotal { nremaining } else { ntotal }
 | 
						|
	if nsamples <= 0 {
 | 
						|
		p.finished = true
 | 
						|
		return
 | 
						|
	}
 | 
						|
	unsafe {C.memcpy(buffer, &p.samples[p.pos], nsamples * int(sizeof(f32)))}
 | 
						|
	p.pos += nsamples
 | 
						|
}
 | 
						|
 | 
						|
fn (mut p Player) init() {
 | 
						|
	audio.setup({
 | 
						|
		num_channels: 2
 | 
						|
		stream_userdata_cb: audio_player_callback
 | 
						|
		user_data: p
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
fn (mut p Player) stop() {
 | 
						|
	audio.shutdown()
 | 
						|
	p.free()
 | 
						|
}
 | 
						|
 | 
						|
fn (mut p Player) play_wav_file(fpath string) ? {
 | 
						|
	println('> play_wav_file: $fpath')
 | 
						|
	samples := read_wav_file_samples(fpath) ?
 | 
						|
	p.finished = true
 | 
						|
	p.samples << samples
 | 
						|
	p.finished = false
 | 
						|
	for !p.finished {
 | 
						|
		time.sleep_ms(16)
 | 
						|
	}
 | 
						|
	p.free()
 | 
						|
}
 | 
						|
 | 
						|
fn (mut p Player) free() {
 | 
						|
	p.finished = false
 | 
						|
	p.samples = []f32{}
 | 
						|
	p.pos = 0
 | 
						|
}
 | 
						|
 | 
						|
// The read_wav_file_samples function below is based on the following sources:
 | 
						|
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
 | 
						|
// http://www.lightlink.com/tjweber/StripWav/WAVE.html
 | 
						|
// http://www.lightlink.com/tjweber/StripWav/Canon.html
 | 
						|
// https://tools.ietf.org/html/draft-ema-vpim-wav-00
 | 
						|
// NB: > The chunks MAY appear in any order except that the Format chunk
 | 
						|
// > MUST be placed before the Sound data chunk (but not necessarily
 | 
						|
// > contiguous to the Sound data chunk).
 | 
						|
struct RIFFHeader {
 | 
						|
	riff      [4]byte
 | 
						|
	file_size u32
 | 
						|
	form_type [4]byte
 | 
						|
}
 | 
						|
 | 
						|
struct RIFFChunkHeader {
 | 
						|
	chunk_type [4]byte
 | 
						|
	chunk_size u32
 | 
						|
	chunk_data voidptr
 | 
						|
}
 | 
						|
 | 
						|
struct RIFFFormat {
 | 
						|
	format_tag            u16 // PCM = 1; Values other than 1 indicate some form of compression.
 | 
						|
	nchannels             u16 // Nc ; 1 = mono ; 2 = stereo
 | 
						|
	sample_rate           u32 // F
 | 
						|
	avg_bytes_per_second  u32 // F * M*Nc
 | 
						|
	nblock_align          u16 // M*Nc
 | 
						|
	bits_per_sample       u16 // 8 * M
 | 
						|
	cbsize                u16 // Size of the extension: 22
 | 
						|
	valid_bits_per_sample u16 // at most 8*M
 | 
						|
	channel_mask          u32 // Speaker position mask
 | 
						|
	sub_format            [16]byte // GUID
 | 
						|
}
 | 
						|
 | 
						|
fn read_wav_file_samples(fpath string) ?[]f32 {
 | 
						|
	mut res := []f32{}
 | 
						|
	// eprintln('> read_wav_file_samples: $fpath -------------------------------------------------')
 | 
						|
	mut bytes := os.read_bytes(fpath) ?
 | 
						|
	mut pbytes := byteptr(bytes.data)
 | 
						|
	mut offset := u32(0)
 | 
						|
	rh := &RIFFHeader(pbytes)
 | 
						|
	// eprintln('rh: $rh')
 | 
						|
	if rh.riff != [byte(`R`), `I`, `F`, `F`]!! {
 | 
						|
		return error('WAV should start with `RIFF`')
 | 
						|
	}
 | 
						|
	if rh.form_type != [byte(`W`), `A`, `V`, `E`]!! {
 | 
						|
		return error('WAV should have `WAVE` form type')
 | 
						|
	}
 | 
						|
	if rh.file_size + 8 != bytes.len {
 | 
						|
		return error('WAV should have valid lenght')
 | 
						|
	}
 | 
						|
	offset += sizeof(RIFFHeader)
 | 
						|
	mut rf := &RIFFFormat(0)
 | 
						|
	for {
 | 
						|
		if offset >= bytes.len {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		//
 | 
						|
		ch := &RIFFChunkHeader(unsafe {pbytes + offset})
 | 
						|
		offset += 8 + ch.chunk_size
 | 
						|
		// eprintln('ch: $ch')
 | 
						|
		// eprintln('p: $pbytes | offset: $offset | bytes.len: $bytes.len')
 | 
						|
		// ////////
 | 
						|
		if ch.chunk_type == [byte(`L`), `I`, `S`, `T`]!! {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		//
 | 
						|
		if ch.chunk_type == [byte(`i`), `d`, `3`, ` `]!! {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		//
 | 
						|
		if ch.chunk_type == [byte(`f`), `m`, `t`, ` `]!! {
 | 
						|
			// eprintln('`fmt ` chunk')
 | 
						|
			rf = &RIFFFormat(&ch.chunk_data)
 | 
						|
			// eprintln('fmt riff format: $rf')
 | 
						|
			if rf.format_tag != 1 {
 | 
						|
				return error('only PCM encoded WAVs are supported')
 | 
						|
			}
 | 
						|
			if rf.nchannels < 1 || rf.nchannels > 2 {
 | 
						|
				return error('only mono or stereo WAVs are supported')
 | 
						|
			}
 | 
						|
			if rf.bits_per_sample !in [u16(8), 16] {
 | 
						|
				return error('only 8 or 16 bits per sample WAVs are supported')
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		//
 | 
						|
		if ch.chunk_type == [byte(`d`), `a`, `t`, `a`]!! {
 | 
						|
			if rf == 0 {
 | 
						|
				return error('`data` chunk should be after `fmt ` chunk')
 | 
						|
			}
 | 
						|
			// eprintln('`fmt ` chunk: $rf\n`data` chunk: $ch')
 | 
						|
			mut doffset := 0
 | 
						|
			mut dp := byteptr(&ch.chunk_data)
 | 
						|
			for doffset < ch.chunk_size {
 | 
						|
				for c := 0; c < rf.nchannels; c++ {
 | 
						|
					mut x := f32(0.0)
 | 
						|
					mut step := 0
 | 
						|
					ppos := unsafe {dp + doffset}
 | 
						|
					if rf.bits_per_sample == 8 {
 | 
						|
						d8 := byteptr(ppos)
 | 
						|
						x = (f32(*d8) - 128) / 128.0
 | 
						|
						step = 1
 | 
						|
						doffset++
 | 
						|
					}
 | 
						|
					if rf.bits_per_sample == 16 {
 | 
						|
						d16 := &i16(ppos)
 | 
						|
						x = f32(*d16) / 32768.0
 | 
						|
						step = 2
 | 
						|
					}
 | 
						|
					doffset += step
 | 
						|
					if doffset < ch.chunk_size {
 | 
						|
						res << x
 | 
						|
						if rf.nchannels == 1 {
 | 
						|
							// Duplicating single channel mono sounds,
 | 
						|
							// produces a stereo sound, simplifying further processing:
 | 
						|
							res << x
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return res
 | 
						|
}
 |