|
- package mp3
-
- import (
- "context"
- "fmt"
- "io"
-
- gomp3 "github.com/hajimehoshi/go-mp3"
- "github.com/jan/fm-rds-tx/internal/ingest"
- "github.com/jan/fm-rds-tx/internal/ingest/decoder"
- )
-
- type Decoder struct{}
-
- func New() *Decoder { return &Decoder{} }
-
- func (d *Decoder) Name() string { return "mp3-native" }
-
- func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.StreamMeta, emit func(ingest.PCMChunk) error) error {
- if r == nil {
- return fmt.Errorf("%w: mp3 decoder stream reader is nil", decoder.ErrUnsupported)
- }
- if emit == nil {
- return fmt.Errorf("%w: mp3 decoder emit callback is nil", decoder.ErrUnsupported)
- }
-
- dec, err := gomp3.NewDecoder(r)
- if err != nil {
- return fmt.Errorf("%w: mp3 decoder init: %v", decoder.ErrUnsupported, err)
- }
-
- const channels = 2 // go-mp3 always decodes to stereo s16le
- sampleRate := decoder.ResolveSampleRate(dec.SampleRate(), meta)
-
- const chunkFrames = 1024
- const frameBytes = channels * 2
- buf := make([]byte, chunkFrames*frameBytes)
- seq := uint64(0)
-
- for {
- select {
- case <-ctx.Done():
- return nil
- default:
- }
-
- n, readErr := io.ReadAtLeast(dec, buf, frameBytes)
- if readErr != nil {
- if readErr == io.EOF || readErr == io.ErrUnexpectedEOF {
- if n > 0 {
- if err := emitChunk(buf[:n], seq, sampleRate, meta.SourceID, emit); err != nil {
- return err
- }
- }
- return nil
- }
- return fmt.Errorf("mp3 decoder read pcm: %w", readErr)
- }
-
- if err := emitChunk(buf[:n], seq, sampleRate, meta.SourceID, emit); err != nil {
- return err
- }
- seq++
- }
- }
-
- func emitChunk(data []byte, seq uint64, sampleRate int, sourceID string, emit func(ingest.PCMChunk) error) error {
- return emit(decoder.BuildChunk(
- decoder.PCM16LEToPCM32(data),
- 2,
- sampleRate,
- seq,
- sourceID,
- ))
- }
|