wav.go
2492 bytes
1// Package tts is the audio.cpp speech client, a minimal WAV decoder, and
2// non-blocking oto/v3 playback. It is an HTTP client only and never starts or
3// manages any model service.
4package tts
5
6import (
7 "encoding/binary"
8 "errors"
9 "fmt"
10)
11
12// WAVSpec describes the decoded PCM payload of a WAV file.
13type WAVSpec struct {
14 SampleRate int
15 Channels int
16 BitsPerSample int
17 Data []byte // raw little-endian PCM samples
18}
19
20var errNotWAV = errors.New("tts: not a valid WAV file")
21
22// ParseWAV decodes a RIFF/WAVE file into its PCM payload. It supports 8- and
23// 16-bit PCM only.
24func ParseWAV(b []byte) (*WAVSpec, error) {
25 if len(b) < 44 {
26 return nil, fmt.Errorf("%w: too short (%d bytes)", errNotWAV, len(b))
27 }
28 if string(b[0:4]) != "RIFF" {
29 return nil, fmt.Errorf("%w: bad RIFF magic", errNotWAV)
30 }
31 if string(b[8:12]) != "WAVE" {
32 return nil, fmt.Errorf("%w: not a WAVE form", errNotWAV)
33 }
34
35 var (
36 sampleRate int
37 channels int
38 bits int
39 dataOffset int
40 dataSize int
41 foundFmt bool
42 foundData bool
43 )
44
45 pos := 12
46 for pos+8 <= len(b) {
47 chunkID := string(b[pos : pos+4])
48 chunkSize := int(binary.LittleEndian.Uint32(b[pos+4 : pos+8]))
49 body := pos + 8
50 if body+chunkSize > len(b) {
51 return nil, fmt.Errorf("%w: truncated chunk %q", errNotWAV, chunkID)
52 }
53 switch chunkID {
54 case "fmt ":
55 fb := b[body : body+chunkSize]
56 if len(fb) < 16 {
57 return nil, fmt.Errorf("%w: short fmt chunk", errNotWAV)
58 }
59 if binary.LittleEndian.Uint16(fb[0:2]) != 1 { // 1 = PCM
60 return nil, fmt.Errorf("%w: not PCM format", errNotWAV)
61 }
62 channels = int(binary.LittleEndian.Uint16(fb[2:4]))
63 sampleRate = int(binary.LittleEndian.Uint32(fb[4:8]))
64 bits = int(binary.LittleEndian.Uint16(fb[14:16]))
65 foundFmt = true
66 case "data":
67 dataOffset = body
68 dataSize = chunkSize
69 foundData = true
70 }
71 pos = body + chunkSize
72 if chunkSize&1 == 1 { // RIFF chunks are word-aligned; skip the pad byte
73 pos++
74 }
75 }
76
77 if !foundFmt || !foundData {
78 return nil, fmt.Errorf("%w: missing fmt or data chunk", errNotWAV)
79 }
80 if channels < 1 || sampleRate <= 0 {
81 return nil, fmt.Errorf("%w: invalid channel count or sample rate", errNotWAV)
82 }
83 switch bits {
84 case 8, 16:
85 default:
86 return nil, fmt.Errorf("%w: unsupported bit depth %d (want 8 or 16)", errNotWAV, bits)
87 }
88
89 return &WAVSpec{
90 SampleRate: sampleRate,
91 Channels: channels,
92 BitsPerSample: bits,
93 Data: b[dataOffset : dataOffset+dataSize],
94 }, nil
95}