|
- package mp3
-
- import (
- "context"
- "encoding/binary"
- "fmt"
- "io"
- "time"
-
- 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 := dec.SampleRate()
- if sampleRate <= 0 {
- if meta.SampleRateHz > 0 {
- sampleRate = meta.SampleRateHz
- } else {
- sampleRate = 44100
- }
- }
-
- 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 {
- samples := make([]int32, 0, len(data)/2)
- for i := 0; i+1 < len(data); i += 2 {
- v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
- samples = append(samples, int32(v)<<16)
- }
- return emit(ingest.PCMChunk{
- Samples: samples,
- Channels: 2,
- SampleRateHz: sampleRate,
- Sequence: seq,
- Timestamp: time.Now(),
- SourceID: sourceID,
- })
- }
|