Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

229 Zeilen
6.5KB

  1. package dsp
  2. import "math"
  3. // Biquad is a generic second-order IIR filter (direct form II transposed).
  4. type Biquad struct {
  5. b0, b1, b2 float64
  6. a1, a2 float64
  7. z1, z2 float64
  8. }
  9. // Process filters one sample.
  10. func (f *Biquad) Process(in float64) float64 {
  11. out := f.b0*in + f.z1
  12. f.z1 = f.b1*in - f.a1*out + f.z2
  13. f.z2 = f.b2*in - f.a2*out
  14. return out
  15. }
  16. // Reset clears state.
  17. func (f *Biquad) Reset() { f.z1 = 0; f.z2 = 0 }
  18. // FilterChain cascades multiple biquad sections in series.
  19. // Used for higher-order filters (e.g. 4th-order = 2 biquads).
  20. type FilterChain struct {
  21. Stages []Biquad
  22. }
  23. // Process runs input through all stages in series.
  24. func (c *FilterChain) Process(in float64) float64 {
  25. x := in
  26. for i := range c.Stages {
  27. x = c.Stages[i].Process(x)
  28. }
  29. return x
  30. }
  31. // Reset clears all filter state.
  32. func (c *FilterChain) Reset() {
  33. for i := range c.Stages {
  34. c.Stages[i].Reset()
  35. }
  36. }
  37. // --- Factory functions ---
  38. // NewBiquadLPF creates a 2nd-order Butterworth lowpass (Q = 1/√2).
  39. func NewBiquadLPF(cutoffHz, sampleRate float64) *Biquad {
  40. return newBiquadLPFWithQ(cutoffHz, sampleRate, math.Sqrt2/2)
  41. }
  42. func newBiquadLPFWithQ(cutoffHz, sampleRate, q float64) *Biquad {
  43. if cutoffHz <= 0 || sampleRate <= 0 || cutoffHz >= sampleRate/2 {
  44. return &Biquad{b0: 1} // passthrough
  45. }
  46. omega := 2 * math.Pi * cutoffHz / sampleRate
  47. cosW := math.Cos(omega)
  48. sinW := math.Sin(omega)
  49. alpha := sinW / (2 * q)
  50. a0 := 1 + alpha
  51. return &Biquad{
  52. b0: (1 - cosW) / 2 / a0,
  53. b1: (1 - cosW) / a0,
  54. b2: (1 - cosW) / 2 / a0,
  55. a1: (-2 * cosW) / a0,
  56. a2: (1 - alpha) / a0,
  57. }
  58. }
  59. // NewLPF4 creates a 4th-order Butterworth lowpass (two cascaded biquads).
  60. func NewLPF4(cutoffHz, sampleRate float64) *FilterChain {
  61. q1 := 1.0 / (2 * math.Cos(math.Pi/8)) // ≈ 0.5412
  62. q2 := 1.0 / (2 * math.Cos(3*math.Pi/8)) // ≈ 1.3066
  63. return &FilterChain{
  64. Stages: []Biquad{
  65. *newBiquadLPFWithQ(cutoffHz, sampleRate, q1),
  66. *newBiquadLPFWithQ(cutoffHz, sampleRate, q2),
  67. },
  68. }
  69. }
  70. // NewLPF8 creates an 8th-order Butterworth lowpass (four cascaded biquads).
  71. // Provides -48dB/octave rolloff. At 228kHz with fc=15kHz:
  72. //
  73. // 15kHz: -6dB, 19kHz: -28dB, 38kHz: -72dB, 57kHz: -108dB
  74. func NewLPF8(cutoffHz, sampleRate float64) *FilterChain {
  75. // 8th-order Butterworth pole angles: π/16, 3π/16, 5π/16, 7π/16
  76. q1 := 1.0 / (2 * math.Cos(math.Pi/16)) // ≈ 0.5098
  77. q2 := 1.0 / (2 * math.Cos(3*math.Pi/16)) // ≈ 0.6013
  78. q3 := 1.0 / (2 * math.Cos(5*math.Pi/16)) // ≈ 0.8999
  79. q4 := 1.0 / (2 * math.Cos(7*math.Pi/16)) // ≈ 2.5629
  80. return &FilterChain{
  81. Stages: []Biquad{
  82. *newBiquadLPFWithQ(cutoffHz, sampleRate, q1),
  83. *newBiquadLPFWithQ(cutoffHz, sampleRate, q2),
  84. *newBiquadLPFWithQ(cutoffHz, sampleRate, q3),
  85. *newBiquadLPFWithQ(cutoffHz, sampleRate, q4),
  86. },
  87. }
  88. }
  89. // NewNotch creates a 2nd-order IIR notch (bandstop) filter.
  90. // Q controls width: higher Q = narrower notch.
  91. // Typical: Q=5 → ~4kHz wide at -3dB, Q=10 → ~2kHz wide.
  92. func NewNotch(centerHz, sampleRate, q float64) *Biquad {
  93. if centerHz <= 0 || sampleRate <= 0 || centerHz >= sampleRate/2 {
  94. return &Biquad{b0: 1}
  95. }
  96. omega := 2 * math.Pi * centerHz / sampleRate
  97. cosW := math.Cos(omega)
  98. alpha := math.Sin(omega) / (2 * q)
  99. a0 := 1 + alpha
  100. return &Biquad{
  101. b0: 1 / a0,
  102. b1: -2 * cosW / a0,
  103. b2: 1 / a0,
  104. a1: -2 * cosW / a0,
  105. a2: (1 - alpha) / a0,
  106. }
  107. }
  108. // NewChebyshevI creates an Nth-order Chebyshev Type I lowpass filter.
  109. // Passband ripple in dB (typ. 0.5), then steep rolloff into stopband.
  110. // Much steeper transition band than Butterworth at the same order.
  111. // At 228kHz, 8th-order, 0.5dB ripple, fc=15kHz: -40dB@19kHz (vs -17dB Butterworth).
  112. func NewChebyshevI(order int, rippleDB, cutoffHz, sampleRate float64) *FilterChain {
  113. if order < 2 || order%2 != 0 {
  114. return &FilterChain{Stages: []Biquad{{b0: 1}}}
  115. }
  116. if cutoffHz <= 0 || sampleRate <= 0 || cutoffHz >= sampleRate/2 {
  117. return &FilterChain{Stages: []Biquad{{b0: 1}}}
  118. }
  119. N := order
  120. nSections := N / 2
  121. // Chebyshev parameters
  122. epsilon := math.Sqrt(math.Pow(10, rippleDB/10) - 1)
  123. v := math.Asinh(1/epsilon) / float64(N)
  124. // Bilinear transform constant and frequency pre-warp
  125. c := 2.0 * sampleRate
  126. warp := c * math.Tan(math.Pi*cutoffHz/sampleRate)
  127. stages := make([]Biquad, nSections)
  128. for i := 0; i < nSections; i++ {
  129. // Analog prototype pole (normalized Ωc=1)
  130. angle := float64(2*i+1) * math.Pi / float64(2*N)
  131. sigmaN := -math.Sinh(v) * math.Sin(angle)
  132. omegaN := math.Cosh(v) * math.Cos(angle)
  133. // Scale to actual cutoff frequency
  134. sigma := sigmaN * warp
  135. omega := omegaN * warp
  136. // Analog section: H(s) = A / (s² + Bs + A)
  137. A := sigma*sigma + omega*omega
  138. B := -2 * sigma // positive (sigma is negative)
  139. // Bilinear transform to digital biquad
  140. c2 := c * c
  141. a0 := c2 + B*c + A
  142. stages[i] = Biquad{
  143. b0: A / a0,
  144. b1: 2 * A / a0,
  145. b2: A / a0,
  146. a1: (-2*c2 + 2*A) / a0,
  147. a2: (c2 - B*c + A) / a0,
  148. }
  149. }
  150. // Normalize DC gain to unity (Chebyshev even-order has -ripple at DC)
  151. dcGain := 1.0
  152. for _, s := range stages {
  153. dcGain *= (s.b0 + s.b1 + s.b2) / (1 + s.a1 + s.a2)
  154. }
  155. if dcGain > 0 {
  156. corr := 1.0 / dcGain
  157. stages[0].b0 *= corr
  158. stages[0].b1 *= corr
  159. stages[0].b2 *= corr
  160. }
  161. return &FilterChain{Stages: stages}
  162. }
  163. // --- Broadcast-specific filter factories ---
  164. // NewAudioLPF creates the broadcast-standard audio lowpass at 15kHz.
  165. // 8th-order Chebyshev Type I with 0.5dB passband ripple.
  166. // Flat to 15kHz, then steep wall: -40dB@19kHz (vs -17dB Butterworth).
  167. // Two passes through clip-filter-clip: -80dB broadband at 19kHz.
  168. func NewAudioLPF(sampleRate float64) *FilterChain {
  169. return NewChebyshevI(8, 0.5, 15000, sampleRate)
  170. }
  171. // NewPilotNotch creates a double-cascade 19kHz notch for maximum
  172. // rejection at the pilot frequency. Q=15: only 1.3kHz wide (18.4–19.6kHz).
  173. // The 8th-order LPF handles broadband; this kills the exact 19kHz peak.
  174. func NewPilotNotch(sampleRate float64) *FilterChain {
  175. return &FilterChain{
  176. Stages: []Biquad{
  177. *NewNotch(19000, sampleRate, 15),
  178. *NewNotch(19000, sampleRate, 15),
  179. },
  180. }
  181. }
  182. // NewCompositeProtection creates double-cascade notch filters for the
  183. // composite clipper. Q=10: ~1.9kHz wide at 19kHz, ~5.7kHz wide at 57kHz.
  184. // Narrow enough to preserve audio/stereo, deep enough to protect pilot/RDS.
  185. func NewCompositeProtection(sampleRate float64) (notch19, notch57 *FilterChain) {
  186. notch19 = &FilterChain{
  187. Stages: []Biquad{
  188. *NewNotch(19000, sampleRate, 10),
  189. *NewNotch(19000, sampleRate, 10),
  190. },
  191. }
  192. notch57 = &FilterChain{
  193. Stages: []Biquad{
  194. *NewNotch(57000, sampleRate, 10),
  195. *NewNotch(57000, sampleRate, 10),
  196. },
  197. }
  198. return
  199. }