After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.
This commit is contained in:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -0,0 +1,135 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user