| @@ -15,6 +15,7 @@ | |||||
| Current no-hardware sources: | Current no-hardware sources: | ||||
| - generated stereo tones via config | - generated stereo tones via config | ||||
| - 16-bit PCM WAV file input via `audio.inputPath` | - 16-bit PCM WAV file input via `audio.inputPath` | ||||
| - basic sample-rate adaptation for WAV sources into the composite generation path | |||||
| ### Tone configuration | ### Tone configuration | ||||
| @@ -52,5 +52,12 @@ func RunSimulatedTransmit(cfg cfgpkg.Config, outPath string, duration time.Durat | |||||
| if err := backend.Flush(context.Background()); err != nil { | if err := backend.Flush(context.Background()); err != nil { | ||||
| return "", err | return "", err | ||||
| } | } | ||||
| return fmt.Sprintf("simulated transmit: backend=%s output=%s duration=%s", backend.Info().Name, outPath, duration), nil | |||||
| return fmt.Sprintf("simulated transmit: backend=%s output=%s duration=%s input=%s", backend.Info().Name, outPath, duration, inputLabel(cfg)), nil | |||||
| } | |||||
| func inputLabel(cfg cfgpkg.Config) string { | |||||
| if cfg.Audio.InputPath != "" { | |||||
| return cfg.Audio.InputPath | |||||
| } | |||||
| return "tones" | |||||
| } | } | ||||
| @@ -0,0 +1,28 @@ | |||||
| package audio | |||||
| type ResampledSource struct { | |||||
| src *WAVSource | |||||
| ratio float64 | |||||
| position float64 | |||||
| } | |||||
| func NewResampledSource(src *WAVSource, targetSampleRate float64) *ResampledSource { | |||||
| ratio := 1.0 | |||||
| if src != nil && src.SampleRate > 0 && targetSampleRate > 0 { | |||||
| ratio = float64(src.SampleRate) / targetSampleRate | |||||
| } | |||||
| return &ResampledSource{src: src, ratio: ratio} | |||||
| } | |||||
| func (s *ResampledSource) NextFrame() Frame { | |||||
| if s.src == nil || len(s.src.frames) == 0 { | |||||
| return NewFrame(0, 0) | |||||
| } | |||||
| idx := int(s.position) % len(s.src.frames) | |||||
| frame := s.src.frames[idx] | |||||
| s.position += s.ratio | |||||
| for s.position >= float64(len(s.src.frames)) { | |||||
| s.position -= float64(len(s.src.frames)) | |||||
| } | |||||
| return frame | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package audio | |||||
| import "testing" | |||||
| func TestResampledSource(t *testing.T) { | |||||
| src := &WAVSource{frames: []Frame{NewFrame(0.1, 0.1), NewFrame(0.2, 0.2)}, SampleRate: 48000} | |||||
| rs := NewResampledSource(src, 96000) | |||||
| a := rs.NextFrame() | |||||
| b := rs.NextFrame() | |||||
| if a == (Frame{}) || b == (Frame{}) { | |||||
| t.Fatal("expected frames") | |||||
| } | |||||
| } | |||||
| @@ -31,7 +31,7 @@ func NewGenerator(cfg cfgpkg.Config) *Generator { | |||||
| func (g *Generator) sourceFor(sampleRate float64) frameSource { | func (g *Generator) sourceFor(sampleRate float64) frameSource { | ||||
| if g.cfg.Audio.InputPath != "" { | if g.cfg.Audio.InputPath != "" { | ||||
| if src, err := audio.LoadWAVSource(g.cfg.Audio.InputPath); err == nil { | if src, err := audio.LoadWAVSource(g.cfg.Audio.InputPath); err == nil { | ||||
| return src | |||||
| return audio.NewResampledSource(src, sampleRate) | |||||
| } | } | ||||
| } | } | ||||
| return audio.NewConfiguredToneSource(sampleRate, g.cfg.Audio.ToneLeftHz, g.cfg.Audio.ToneRightHz, g.cfg.Audio.ToneAmplitude) | return audio.NewConfiguredToneSource(sampleRate, g.cfg.Audio.ToneLeftHz, g.cfg.Audio.ToneRightHz, g.cfg.Audio.ToneAmplitude) | ||||