Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

88 рядки
2.1KB

  1. package dsp
  2. import (
  3. "math"
  4. "github.com/jan/fm-rds-tx/internal/output"
  5. )
  6. // FMPhaseUpsampler performs FM modulation + upsampling via phase-domain
  7. // interpolation. Maintains continuous phase across successive calls.
  8. type FMPhaseUpsampler struct {
  9. srcRate float64
  10. dstRate float64
  11. maxDeviation float64
  12. ratio float64
  13. phase float64 // persistent across calls
  14. }
  15. func NewFMPhaseUpsampler(srcRate, dstRate, maxDeviation float64) *FMPhaseUpsampler {
  16. return &FMPhaseUpsampler{
  17. srcRate: srcRate,
  18. dstRate: dstRate,
  19. maxDeviation: maxDeviation,
  20. ratio: dstRate / srcRate,
  21. }
  22. }
  23. func (u *FMPhaseUpsampler) Process(frame *output.CompositeFrame) *output.CompositeFrame {
  24. if frame == nil || len(frame.Samples) == 0 {
  25. return frame
  26. }
  27. srcLen := len(frame.Samples)
  28. // Accumulate phase at source rate, continuing from previous chunk
  29. phases := make([]float64, srcLen)
  30. for i, s := range frame.Samples {
  31. u.phase += 2 * math.Pi * float64(s.I) * u.maxDeviation / u.srcRate
  32. phases[i] = u.phase
  33. }
  34. // Keep phase bounded
  35. if u.phase > 1e9 {
  36. offset := math.Floor(u.phase/(2*math.Pi)) * 2 * math.Pi
  37. u.phase -= offset
  38. for i := range phases {
  39. phases[i] -= offset
  40. }
  41. }
  42. // Interpolate phase to target rate
  43. dstLen := int(float64(srcLen) * u.ratio)
  44. dst := make([]output.IQSample, dstLen)
  45. step := 1.0 / u.ratio
  46. pos := 0.0
  47. for i := 0; i < dstLen; i++ {
  48. idx := int(pos)
  49. frac := pos - float64(idx)
  50. var p float64
  51. if idx+1 < srcLen {
  52. p = phases[idx]*(1-frac) + phases[idx+1]*frac
  53. } else if idx < srcLen {
  54. p = phases[idx]
  55. }
  56. dst[i] = output.IQSample{
  57. I: float32(math.Cos(p)),
  58. Q: float32(math.Sin(p)),
  59. }
  60. pos += step
  61. }
  62. return &output.CompositeFrame{
  63. Samples: dst,
  64. SampleRateHz: u.dstRate,
  65. Timestamp: frame.Timestamp,
  66. Sequence: frame.Sequence,
  67. }
  68. }
  69. // UpsampleFMPhase is the stateless version for offline/test use.
  70. func UpsampleFMPhase(frame *output.CompositeFrame, targetRate, maxDeviation float64) *output.CompositeFrame {
  71. u := NewFMPhaseUpsampler(frame.SampleRateHz, targetRate, maxDeviation)
  72. return u.Process(frame)
  73. }