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, )) }