| @@ -0,0 +1,58 @@ | |||||
| package decoder | |||||
| import ( | |||||
| "encoding/binary" | |||||
| "math" | |||||
| "time" | |||||
| "github.com/jan/fm-rds-tx/internal/ingest" | |||||
| ) | |||||
| const defaultSampleRateHz = 44100 | |||||
| func ResolveSampleRate(decodedSampleRateHz int, meta StreamMeta) int { | |||||
| if decodedSampleRateHz > 0 { | |||||
| return decodedSampleRateHz | |||||
| } | |||||
| if meta.SampleRateHz > 0 { | |||||
| return meta.SampleRateHz | |||||
| } | |||||
| return defaultSampleRateHz | |||||
| } | |||||
| func BuildChunk(samples []int32, channels, sampleRateHz int, seq uint64, sourceID string) ingest.PCMChunk { | |||||
| return ingest.PCMChunk{ | |||||
| Samples: samples, | |||||
| Channels: channels, | |||||
| SampleRateHz: sampleRateHz, | |||||
| Sequence: seq, | |||||
| Timestamp: time.Now(), | |||||
| SourceID: sourceID, | |||||
| } | |||||
| } | |||||
| func PCM16LEToPCM32(in []byte) []int32 { | |||||
| out := make([]int32, 0, len(in)/2) | |||||
| for i := 0; i+1 < len(in); i += 2 { | |||||
| v := int16(binary.LittleEndian.Uint16(in[i : i+2])) | |||||
| out = append(out, int32(v)<<16) | |||||
| } | |||||
| return out | |||||
| } | |||||
| 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 | |||||
| } | |||||
| @@ -2,10 +2,8 @@ package mp3 | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/binary" | |||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "time" | |||||
| gomp3 "github.com/hajimehoshi/go-mp3" | gomp3 "github.com/hajimehoshi/go-mp3" | ||||
| "github.com/jan/fm-rds-tx/internal/ingest" | "github.com/jan/fm-rds-tx/internal/ingest" | ||||
| @@ -32,14 +30,7 @@ func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.St | |||||
| } | } | ||||
| const channels = 2 // go-mp3 always decodes to stereo s16le | 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 | |||||
| } | |||||
| } | |||||
| sampleRate := decoder.ResolveSampleRate(dec.SampleRate(), meta) | |||||
| const chunkFrames = 1024 | const chunkFrames = 1024 | ||||
| const frameBytes = channels * 2 | const frameBytes = channels * 2 | ||||
| @@ -74,17 +65,11 @@ func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.St | |||||
| } | } | ||||
| func emitChunk(data []byte, seq uint64, sampleRate int, sourceID string, emit func(ingest.PCMChunk) error) error { | 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, | |||||
| }) | |||||
| return emit(decoder.BuildChunk( | |||||
| decoder.PCM16LEToPCM32(data), | |||||
| 2, | |||||
| sampleRate, | |||||
| seq, | |||||
| sourceID, | |||||
| )) | |||||
| } | } | ||||
| @@ -4,8 +4,6 @@ import ( | |||||
| "context" | "context" | ||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "math" | |||||
| "time" | |||||
| "github.com/jan/fm-rds-tx/internal/ingest" | "github.com/jan/fm-rds-tx/internal/ingest" | ||||
| "github.com/jan/fm-rds-tx/internal/ingest/decoder" | "github.com/jan/fm-rds-tx/internal/ingest/decoder" | ||||
| @@ -40,14 +38,7 @@ func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.St | |||||
| } | } | ||||
| } | } | ||||
| sampleRate := dec.SampleRate() | |||||
| if sampleRate <= 0 { | |||||
| if meta.SampleRateHz > 0 { | |||||
| sampleRate = meta.SampleRateHz | |||||
| } else { | |||||
| sampleRate = 44100 | |||||
| } | |||||
| } | |||||
| sampleRate := decoder.ResolveSampleRate(dec.SampleRate(), meta) | |||||
| const chunkFrames = 1024 | const chunkFrames = 1024 | ||||
| buf := make([]float32, chunkFrames*channels) | buf := make([]float32, chunkFrames*channels) | ||||
| @@ -62,14 +53,13 @@ func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.St | |||||
| n, readErr := dec.Read(buf) | n, readErr := dec.Read(buf) | ||||
| if n > 0 { | if n > 0 { | ||||
| chunk := ingest.PCMChunk{ | |||||
| Samples: float32ToPCM32(buf[:n]), | |||||
| Channels: channels, | |||||
| SampleRateHz: sampleRate, | |||||
| Sequence: seq, | |||||
| Timestamp: time.Now(), | |||||
| SourceID: meta.SourceID, | |||||
| } | |||||
| chunk := decoder.BuildChunk( | |||||
| decoder.Float32ToPCM32(buf[:n]), | |||||
| channels, | |||||
| sampleRate, | |||||
| seq, | |||||
| meta.SourceID, | |||||
| ) | |||||
| if err := emit(chunk); err != nil { | if err := emit(chunk); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| @@ -84,20 +74,3 @@ func (d *Decoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.St | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| 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 | |||||
| } | |||||