|
- 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
- }
|