package oggvorbis import ( "context" "fmt" "io" "math" "time" "github.com/jan/fm-rds-tx/internal/ingest" "github.com/jan/fm-rds-tx/internal/ingest/decoder" libvorbis "github.com/jfreymuth/oggvorbis" ) type Decoder struct{} func New() *Decoder { return &Decoder{} } func (d *Decoder) Name() string { return "oggvorbis-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: ogg/vorbis decoder stream reader is nil", decoder.ErrUnsupported) } if emit == nil { return fmt.Errorf("%w: ogg/vorbis decoder emit callback is nil", decoder.ErrUnsupported) } dec, err := libvorbis.NewReader(r) if err != nil { return fmt.Errorf("%w: ogg/vorbis decoder init: %v", decoder.ErrUnsupported, err) } channels := dec.Channels() if channels <= 0 { if meta.Channels > 0 { channels = meta.Channels } else { return fmt.Errorf("%w: ogg/vorbis decoder invalid channel count", decoder.ErrUnsupported) } } sampleRate := dec.SampleRate() if sampleRate <= 0 { if meta.SampleRateHz > 0 { sampleRate = meta.SampleRateHz } else { sampleRate = 44100 } } const chunkFrames = 1024 buf := make([]float32, chunkFrames*channels) seq := uint64(0) for { select { case <-ctx.Done(): return nil default: } n, readErr := dec.Read(buf) if n > 0 { chunk := ingest.PCMChunk{ Samples: float32ToPCM32(buf[:n]), Channels: channels, SampleRateHz: sampleRate, Sequence: seq, Timestamp: time.Now(), SourceID: meta.SourceID, } if err := emit(chunk); err != nil { return err } seq++ } if readErr != nil { if readErr == io.EOF { return nil } return fmt.Errorf("ogg/vorbis decoder read pcm: %w", readErr) } } } func float32ToPCM32(in []float32) []int32 { out := make([]int32, len(in)) for i, sample := range in { if sample > 1 { sample = 1 } else if sample < -1 { sample = -1 } if sample == -1 { out[i] = math.MinInt32 continue } out[i] = int32(sample * math.MaxInt32) } return out }