Files
aego/audio/codec/codec.go
T

216 lines
4.4 KiB
Go

package codec
import (
"errors"
"io"
"path"
"strings"
"time"
"aego/audio/dsp"
"aego/audio/mix"
)
type Info struct {
SampleRate int
Channels int
Frames int64
}
type Decoder interface {
Info() Info
Read(dst []float32) (frames int, err error)
SeekFrame(frame int64) error
Close() error
}
type Looper interface {
Loop() (start, end int64, ok bool)
}
type Format struct {
Name string
Match func(head []byte) bool
Open func(r io.ReadSeeker) (Decoder, error)
}
type StreamOptions struct {
Loop bool
LoopStart int64
LoopEnd int64
Chunk int
}
const (
MaxMemory = 64 << 20
readFrames = 4096
headBytes = 16
)
var (
ErrUnknown = errors.New("codec: unknown audio format")
ErrMP3 = errors.New("codec: mp3 is not supported; convert with ffmpeg")
ErrTooLarge = errors.New("codec: the sound is larger than 64 MB decoded; stream it instead")
ErrLayout = errors.New("codec: only mono and stereo sounds with a positive sample rate are supported")
formats []Format
)
func Register(f Format) {
formats = append(formats, f)
}
func Formats() []string {
out := make([]string, len(formats))
for i, f := range formats {
out[i] = f.Name
}
return out
}
func Open(r io.ReadSeeker, hint string) (Decoder, error) {
var head [headBytes]byte
n, err := io.ReadFull(r, head[:])
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
return nil, err
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
return nil, err
}
h := head[:n]
for _, f := range formats {
if f.Match(h) {
return f.Open(r)
}
}
if mp3(h) || strings.EqualFold(path.Ext(hint), ".mp3") {
return nil, ErrMP3
}
return nil, ErrUnknown
}
func mp3(h []byte) bool {
return len(h) >= 3 && string(h[:3]) == "ID3" || len(h) >= 2 && h[0] == 0xFF && h[1]&0xE0 == 0xE0
}
func DecodeAll(d Decoder, rate int) (*mix.Memory, error) {
info := d.Info()
ch := info.Channels
if ch < 1 || ch > 2 || info.SampleRate <= 0 {
return nil, ErrLayout
}
limit := MaxMemory / 4
if info.Frames > 0 && info.Frames*int64(ch) > int64(limit) {
return nil, ErrTooLarge
}
var pcm []float32
if info.Frames > 0 {
pcm = make([]float32, 0, info.Frames*int64(ch))
}
buf := make([]float32, readFrames*ch)
for {
n, err := d.Read(buf)
pcm = append(pcm, buf[:n*ch]...)
if len(pcm) > limit {
return nil, ErrTooLarge
}
if errors.Is(err, io.EOF) || (err == nil && n == 0) {
break
}
if err != nil {
return nil, err
}
}
mem := &mix.Memory{PCM: pcm, Rate: info.SampleRate, Channels: ch}
if l, ok := d.(Looper); ok {
if s, e, ok := l.Loop(); ok {
mem.LoopStart, mem.LoopEnd = s, e
}
}
if rate > 0 && rate != info.SampleRate {
mem.PCM = dsp.Resample(pcm, ch, info.SampleRate, rate)
mem.LoopStart = mem.LoopStart * int64(rate) / int64(info.SampleRate)
mem.LoopEnd = mem.LoopEnd * int64(rate) / int64(info.SampleRate)
mem.Rate = rate
}
return mem, nil
}
func Stream(d Decoder, st *mix.Stream, o StreamOptions, cancel <-chan struct{}) <-chan error {
done := make(chan error, 1)
go func() {
done <- feed(d, st, o, cancel)
}()
return done
}
func feed(d Decoder, st *mix.Stream, o StreamOptions, cancel <-chan struct{}) error {
defer st.SetEOF()
ch := st.Channels
if d.Info().Channels != ch {
return errors.New("codec: the stream and the decoder have different channel counts")
}
if o.Loop && o.LoopEnd == 0 {
if l, ok := d.(Looper); ok {
if s, e, ok := l.Loop(); ok {
o.LoopStart, o.LoopEnd = s, e
}
}
}
chunk := o.Chunk
if chunk <= 0 {
chunk = readFrames
}
buf := make([]float32, chunk*ch)
var pending []float32
pos := int64(0)
empty := 0
wait := time.NewTicker(5 * time.Millisecond)
defer wait.Stop()
for {
select {
case <-cancel:
return nil
default:
}
if len(pending) == 0 {
want := chunk
if o.Loop && o.LoopEnd > 0 {
want = int(min(int64(chunk), o.LoopEnd-pos))
}
n, err := 0, error(nil)
if want > 0 {
n, err = d.Read(buf[:want*ch])
}
if err != nil && !errors.Is(err, io.EOF) {
return err
}
if n == 0 {
if !o.Loop {
return nil
}
empty++
if empty > 2 {
return nil
}
if err := d.SeekFrame(o.LoopStart); err != nil {
return err
}
pos = o.LoopStart
continue
}
empty = 0
pos += int64(n)
pending = buf[:n*ch]
}
w := st.Ring.Write(pending)
pending = pending[w*ch:]
if len(pending) > 0 {
select {
case <-cancel:
return nil
case <-wait.C:
}
}
}
}