// 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 . 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 }