Files

136 lines
4.6 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package sound
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
)
// Fenrir's cues are short PCM WAV files. Validate RIFF chunk bounds before
// decoding, then resample once to the persistent stream's stereo rate.
func decode_wav(reader io.Reader) (*clip, error) {
data, err := io.ReadAll(io.LimitReader(reader, (4<<20)+1))
if err != nil {
return nil, err
}
if len(data) > 4<<20 {
return nil, fmt.Errorf("WAV exceeds 4 MiB")
}
if len(data) < 12 || string(data[:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
return nil, fmt.Errorf("not a RIFF WAV file")
}
end := uint64(binary.LittleEndian.Uint32(data[4:8])) + 8
if end < 12 || end > uint64(len(data)) {
return nil, fmt.Errorf("truncated WAV container")
}
var format, audio []byte
for offset := uint64(12); offset < end; {
if end-offset < 8 {
return nil, fmt.Errorf("truncated WAV chunk header")
}
name := string(data[offset : offset+4])
size := uint64(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
offset += 8
if size > end-offset {
return nil, fmt.Errorf("truncated WAV %s chunk", name)
}
switch name {
case "fmt ":
if format != nil {
return nil, fmt.Errorf("duplicate WAV format")
}
format = data[offset : offset+size]
case "data":
if audio != nil {
return nil, fmt.Errorf("duplicate WAV data")
}
audio = data[offset : offset+size]
}
offset += size + size%2
if offset > end {
return nil, fmt.Errorf("missing WAV padding")
}
}
if len(format) < 16 || len(audio) == 0 {
return nil, fmt.Errorf("WAV needs format and audio data")
}
encoding := binary.LittleEndian.Uint16(format)
channels := int(binary.LittleEndian.Uint16(format[2:]))
rate := int(binary.LittleEndian.Uint32(format[4:]))
align := int(binary.LittleEndian.Uint16(format[12:]))
bits := int(binary.LittleEndian.Uint16(format[14:]))
if encoding == 0xfffe {
// WAVEFORMATEXTENSIBLE stores PCM's subtype and valid-bit count in
// the extended header. Fenrir's installed default pack uses this.
pcmGUID := []byte{1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71}
if len(format) < 40 || binary.LittleEndian.Uint16(format[16:]) < 22 || !bytes.Equal(format[24:40], pcmGUID) {
return nil, fmt.Errorf("unsupported extended WAV format")
}
validBits := int(binary.LittleEndian.Uint16(format[18:]))
if validBits < 1 || validBits > bits {
return nil, fmt.Errorf("invalid WAV valid-bit count")
}
encoding = 1
}
if encoding != 1 || channels < 1 || channels > 2 || rate < 8000 || rate > 192000 || (bits != 8 && bits != 16 && bits != 24 && bits != 32) {
return nil, fmt.Errorf("unsupported PCM WAV format")
}
bytesPerSample := bits / 8
if align != channels*bytesPerSample || len(audio)%align != 0 {
return nil, fmt.Errorf("invalid WAV frame alignment")
}
frames := len(audio) / align
if frames > rate*int(maxDuration.Seconds()) {
return nil, fmt.Errorf("sound exceeds ten seconds")
}
readSample := func(frame, channel int) float32 {
if channels == 1 {
channel = 0
}
value := audio[frame*align+channel*bytesPerSample:]
switch bits {
case 8:
return (float32(value[0]) - 128) / 128
case 16:
return float32(int16(binary.LittleEndian.Uint16(value))) / 32768
case 24:
signed := int32(value[0]) | int32(value[1])<<8 | int32(value[2])<<16
return float32(signed<<8>>8) / 8388608
default:
return float32(int32(binary.LittleEndian.Uint32(value))) / 2147483648
}
}
count := int((int64(frames)*sampleRate + int64(rate) - 1) / int64(rate))
samples := make([]float32, count*2)
for frame := range count {
position := float64(frame) * float64(rate) / sampleRate
left := min(int(position), frames-1)
right := min(left+1, frames-1)
fraction := float32(position - math.Floor(position))
for channel := range 2 {
first, second := readSample(left, channel), readSample(right, channel)
samples[frame*2+channel] = first + (second-first)*fraction
}
}
return &clip{samples: samples}, nil
}