Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

76 lines
1.7KB

  1. package mp3
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. gomp3 "github.com/hajimehoshi/go-mp3"
  7. "github.com/jan/fm-rds-tx/internal/ingest"
  8. "github.com/jan/fm-rds-tx/internal/ingest/decoder"
  9. )
  10. type Decoder struct{}
  11. func New() *Decoder { return &Decoder{} }
  12. func (d *Decoder) Name() string { return "mp3-native" }
  13. func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.StreamMeta, emit func(ingest.PCMChunk) error) error {
  14. if r == nil {
  15. return fmt.Errorf("%w: mp3 decoder stream reader is nil", decoder.ErrUnsupported)
  16. }
  17. if emit == nil {
  18. return fmt.Errorf("%w: mp3 decoder emit callback is nil", decoder.ErrUnsupported)
  19. }
  20. dec, err := gomp3.NewDecoder(r)
  21. if err != nil {
  22. return fmt.Errorf("%w: mp3 decoder init: %v", decoder.ErrUnsupported, err)
  23. }
  24. const channels = 2 // go-mp3 always decodes to stereo s16le
  25. sampleRate := decoder.ResolveSampleRate(dec.SampleRate(), meta)
  26. const chunkFrames = 1024
  27. const frameBytes = channels * 2
  28. buf := make([]byte, chunkFrames*frameBytes)
  29. seq := uint64(0)
  30. for {
  31. select {
  32. case <-ctx.Done():
  33. return nil
  34. default:
  35. }
  36. n, readErr := io.ReadAtLeast(dec, buf, frameBytes)
  37. if readErr != nil {
  38. if readErr == io.EOF || readErr == io.ErrUnexpectedEOF {
  39. if n > 0 {
  40. if err := emitChunk(buf[:n], seq, sampleRate, meta.SourceID, emit); err != nil {
  41. return err
  42. }
  43. }
  44. return nil
  45. }
  46. return fmt.Errorf("mp3 decoder read pcm: %w", readErr)
  47. }
  48. if err := emitChunk(buf[:n], seq, sampleRate, meta.SourceID, emit); err != nil {
  49. return err
  50. }
  51. seq++
  52. }
  53. }
  54. func emitChunk(data []byte, seq uint64, sampleRate int, sourceID string, emit func(ingest.PCMChunk) error) error {
  55. return emit(decoder.BuildChunk(
  56. decoder.PCM16LEToPCM32(data),
  57. 2,
  58. sampleRate,
  59. seq,
  60. sourceID,
  61. ))
  62. }