Parent directory

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}