package audio import ( "encoding/binary" "fmt" ) // pcmFormat describes raw PCM samples as oto needs them for playback. type pcmFormat struct { SampleRate int Channels int BitsPerCoded int // 8 (unsigned), 16 (signed LE), 32 (IEEE float only) Float bool } // decodeWAV splits RIFF/WAVE bytes into format and PCM data. // // Supported: PCM (AudioFormat 1) with 8/16 bit and IEEE float (AudioFormat // 3) with 32 bit; mono and stereo. Everything else, compressed formats in // particular, is a clear error; the OS TTS paths of osbuiltin deliver // 16-bit PCM WAV exclusively. func decodeWAV(data []byte) (pcmFormat, []byte, error) { var f pcmFormat if len(data) < 12 { return f, nil, fmt.Errorf("wav: too short for a RIFF header (%d bytes)", len(data)) } if string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" { return f, nil, fmt.Errorf("wav: no RIFF/WAVE header") } var haveFmt bool var payload []byte pos := 12 for pos+8 <= len(data) { id := string(data[pos : pos+4]) size := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8])) body := pos + 8 if body+size > len(data) { return f, nil, fmt.Errorf("wav: chunk %q exceeds the data (size %d)", id, size) } switch id { case "fmt ": if size < 16 { return f, nil, fmt.Errorf("wav: fmt chunk too short (%d)", size) } audioFormat := binary.LittleEndian.Uint16(data[body : body+2]) f.Channels = int(binary.LittleEndian.Uint16(data[body+2 : body+4])) f.SampleRate = int(binary.LittleEndian.Uint32(data[body+4 : body+8])) f.BitsPerCoded = int(binary.LittleEndian.Uint16(data[body+14 : body+16])) switch { case audioFormat == 1 && (f.BitsPerCoded == 8 || f.BitsPerCoded == 16): case audioFormat == 3 && f.BitsPerCoded == 32: f.Float = true default: return f, nil, fmt.Errorf("wav: unsupported format (AudioFormat %d, %d bit); allowed: PCM 8/16 bit, float 32 bit", audioFormat, f.BitsPerCoded) } if f.Channels < 1 || f.Channels > 2 { return f, nil, fmt.Errorf("wav: %d channels (allowed: 1, 2)", f.Channels) } if f.SampleRate <= 0 { return f, nil, fmt.Errorf("wav: sample rate %d", f.SampleRate) } haveFmt = true case "data": payload = data[body : body+size] } pos = body + size if size%2 == 1 { // chunks are word-aligned pos++ } } if !haveFmt { return f, nil, fmt.Errorf("wav: no fmt chunk") } if payload == nil { return f, nil, fmt.Errorf("wav: no data chunk") } return f, payload, nil }