package httpraw import ( "context" "encoding/binary" "fmt" "sync/atomic" "time" "github.com/jan/fm-rds-tx/internal/ingest" ) type Source struct { id string sampleRate int channels int chunks chan ingest.PCMChunk errs chan error sequence atomic.Uint64 state atomic.Value // string chunksIn atomic.Uint64 samplesIn atomic.Uint64 discontinuities atomic.Uint64 lastChunkAtUnix atomic.Int64 lastError atomic.Value // string } func New(id string, sampleRate, channels int) *Source { if id == "" { id = "http-raw" } if sampleRate <= 0 { sampleRate = 44100 } if channels <= 0 { channels = 2 } s := &Source{ id: id, sampleRate: sampleRate, channels: channels, chunks: make(chan ingest.PCMChunk, 32), errs: make(chan error, 8), } s.state.Store("idle") return s } func (s *Source) Descriptor() ingest.SourceDescriptor { return ingest.SourceDescriptor{ ID: s.id, Kind: "http-raw", Family: "raw", Transport: "http", Codec: "pcm_s16le", Channels: s.channels, SampleRateHz: s.sampleRate, Detail: "HTTP push /audio/stream", } } func (s *Source) Start(_ context.Context) error { s.state.Store("running") return nil } func (s *Source) Stop() error { s.state.Store("stopped") return nil } func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks } func (s *Source) Errors() <-chan error { return s.errs } func (s *Source) Stats() ingest.SourceStats { state, _ := s.state.Load().(string) last := s.lastChunkAtUnix.Load() errStr, _ := s.lastError.Load().(string) var lastChunkAt time.Time if last > 0 { lastChunkAt = time.Unix(0, last) } return ingest.SourceStats{ State: state, Connected: state == "running", LastChunkAt: lastChunkAt, ChunksIn: s.chunksIn.Load(), SamplesIn: s.samplesIn.Load(), Discontinuities: s.discontinuities.Load(), LastError: errStr, } } func (s *Source) WritePCM16(data []byte) (int, error) { if s.channels != 1 && s.channels != 2 { return 0, fmt.Errorf("unsupported configured channels: %d", s.channels) } if len(data) == 0 { return 0, nil } frameBytes := s.channels * 2 usable := len(data) - (len(data) % frameBytes) if usable == 0 { return 0, nil } samples := make([]int32, 0, usable/2) for i := 0; i+1 < usable; i += 2 { v := int16(binary.LittleEndian.Uint16(data[i : i+2])) samples = append(samples, int32(v)<<16) } seq := s.sequence.Add(1) - 1 chunk := ingest.PCMChunk{ Samples: samples, Channels: s.channels, SampleRateHz: s.sampleRate, Sequence: seq, Timestamp: time.Now(), SourceID: s.id, } select { case s.chunks <- chunk: default: s.discontinuities.Add(1) return 0, fmt.Errorf("http raw ingress overflow") } frames := usable / frameBytes s.chunksIn.Add(1) s.samplesIn.Add(uint64(len(samples))) s.lastChunkAtUnix.Store(time.Now().UnixNano()) return frames, nil }