|
|
|
@@ -2,9 +2,12 @@ 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" |
|
|
|
) |
|
|
|
@@ -15,6 +18,73 @@ func New() *Decoder { return &Decoder{} } |
|
|
|
|
|
|
|
func (d *Decoder) Name() string { return "mp3-native" } |
|
|
|
|
|
|
|
func (d *Decoder) DecodeStream(_ context.Context, _ io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error { |
|
|
|
return fmt.Errorf("%w: mp3 native decoder not wired yet", decoder.ErrUnsupported) |
|
|
|
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, |
|
|
|
}) |
|
|
|
} |