Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

209 wiersze
5.3KB

  1. package factory
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "testing"
  7. "time"
  8. "aoiprxkit"
  9. "github.com/jan/fm-rds-tx/internal/audio"
  10. "github.com/jan/fm-rds-tx/internal/config"
  11. "github.com/jan/fm-rds-tx/internal/ingest"
  12. aoipad "github.com/jan/fm-rds-tx/internal/ingest/adapters/aoip"
  13. )
  14. type streamReadCloser struct{ io.Reader }
  15. func (r streamReadCloser) Close() error { return nil }
  16. type stubAES67Receiver struct {
  17. onStart func()
  18. }
  19. func (r *stubAES67Receiver) Start(context.Context) error {
  20. if r.onStart != nil {
  21. r.onStart()
  22. }
  23. return nil
  24. }
  25. func (r *stubAES67Receiver) Stop() error { return nil }
  26. func (r *stubAES67Receiver) Stats() aoiprxkit.Stats {
  27. return aoiprxkit.Stats{}
  28. }
  29. func TestHTTPRawFactoryToRuntimeSmoke(t *testing.T) {
  30. cfg := config.Default()
  31. cfg.Ingest.Kind = "http-raw"
  32. cfg.Ingest.HTTPRaw.SampleRateHz = 44100
  33. cfg.Ingest.HTTPRaw.Channels = 2
  34. src, ingress, err := BuildSource(cfg, Deps{})
  35. if err != nil {
  36. t.Fatalf("build source: %v", err)
  37. }
  38. if src == nil || ingress == nil {
  39. t.Fatalf("expected source and ingress for kind=http-raw")
  40. }
  41. sink := audio.NewStreamSource(128, cfg.Ingest.HTTPRaw.SampleRateHz)
  42. rt := ingest.NewRuntime(sink, src)
  43. if err := rt.Start(context.Background()); err != nil {
  44. t.Fatalf("runtime start: %v", err)
  45. }
  46. defer rt.Stop()
  47. // Two stereo frames: L1,R1,L2,R2 (S16LE).
  48. frames, err := ingress.WritePCM16([]byte{
  49. 0xE8, 0x03, 0x18, 0xFC,
  50. 0xD0, 0x07, 0x30, 0xF8,
  51. })
  52. if err != nil {
  53. t.Fatalf("write pcm16: %v", err)
  54. }
  55. if frames != 2 {
  56. t.Fatalf("frames=%d want 2", frames)
  57. }
  58. waitForSinkFrames(t, sink, 2)
  59. stats := rt.Stats()
  60. if stats.Active.Kind != "http-raw" {
  61. t.Fatalf("active kind=%q want http-raw", stats.Active.Kind)
  62. }
  63. if stats.Source.ChunksIn != 1 {
  64. t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
  65. }
  66. if stats.Source.SamplesIn != 4 {
  67. t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
  68. }
  69. if stats.Runtime.State != "running" {
  70. t.Fatalf("runtime state=%q want running", stats.Runtime.State)
  71. }
  72. if stats.Runtime.LastChunkAt.IsZero() {
  73. t.Fatalf("runtime lastChunkAt should be set")
  74. }
  75. }
  76. func TestSRTFactoryToRuntimeSmoke(t *testing.T) {
  77. var stream bytes.Buffer
  78. if err := aoiprxkit.WritePCM32Packet(&stream, 2, 48000, 2, 1, 480, []int32{11, -11, 22, -22}); err != nil {
  79. t.Fatalf("write packet: %v", err)
  80. }
  81. cfg := config.Default()
  82. cfg.Ingest.Kind = "srt"
  83. cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000?mode=listener"
  84. cfg.Ingest.SRT.SampleRateHz = 48000
  85. cfg.Ingest.SRT.Channels = 2
  86. src, ingress, err := BuildSource(cfg, Deps{
  87. SRTOpener: func(ctx context.Context, srtCfg aoiprxkit.SRTConfig) (io.ReadCloser, error) {
  88. _ = ctx
  89. _ = srtCfg
  90. return streamReadCloser{Reader: bytes.NewReader(stream.Bytes())}, nil
  91. },
  92. })
  93. if err != nil {
  94. t.Fatalf("build source: %v", err)
  95. }
  96. if src == nil {
  97. t.Fatalf("expected source for kind=srt")
  98. }
  99. if ingress != nil {
  100. t.Fatalf("expected no ingress for kind=srt")
  101. }
  102. sink := audio.NewStreamSource(128, cfg.Ingest.SRT.SampleRateHz)
  103. rt := ingest.NewRuntime(sink, src)
  104. if err := rt.Start(context.Background()); err != nil {
  105. t.Fatalf("runtime start: %v", err)
  106. }
  107. defer rt.Stop()
  108. waitForSinkFrames(t, sink, 2)
  109. stats := rt.Stats()
  110. if stats.Active.Kind != "srt" {
  111. t.Fatalf("active kind=%q want srt", stats.Active.Kind)
  112. }
  113. if stats.Source.ChunksIn != 1 {
  114. t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
  115. }
  116. if stats.Source.SamplesIn != 4 {
  117. t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
  118. }
  119. }
  120. func TestAES67FactoryToRuntimeSmoke(t *testing.T) {
  121. cfg := config.Default()
  122. cfg.Ingest.Kind = "aes67"
  123. cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
  124. cfg.Ingest.AES67.Port = 5004
  125. cfg.Ingest.AES67.SampleRateHz = 48000
  126. cfg.Ingest.AES67.Channels = 2
  127. cfg.Ingest.AES67.Encoding = "L24"
  128. cfg.Ingest.AES67.PacketTimeMs = 1
  129. var frameHandler aoiprxkit.FrameHandler
  130. src, ingress, err := BuildSource(cfg, Deps{
  131. AES67ReceiverFactory: func(_ aoiprxkit.Config, onFrame aoiprxkit.FrameHandler) (aoipad.ReceiverClient, error) {
  132. frameHandler = onFrame
  133. return &stubAES67Receiver{
  134. onStart: func() {
  135. frameHandler(aoiprxkit.PCMFrame{
  136. SequenceNumber: 1,
  137. SampleRateHz: 48000,
  138. Channels: 2,
  139. Samples: []int32{7, -7, 9, -9},
  140. ReceivedAt: time.Now(),
  141. })
  142. },
  143. }, nil
  144. },
  145. })
  146. if err != nil {
  147. t.Fatalf("build source: %v", err)
  148. }
  149. if src == nil {
  150. t.Fatalf("expected source for kind=aes67")
  151. }
  152. if ingress != nil {
  153. t.Fatalf("expected no ingress for kind=aes67")
  154. }
  155. sink := audio.NewStreamSource(128, cfg.Ingest.AES67.SampleRateHz)
  156. rt := ingest.NewRuntime(sink, src)
  157. if err := rt.Start(context.Background()); err != nil {
  158. t.Fatalf("runtime start: %v", err)
  159. }
  160. defer rt.Stop()
  161. waitForSinkFrames(t, sink, 2)
  162. stats := rt.Stats()
  163. if stats.Active.Kind != "aes67" {
  164. t.Fatalf("active kind=%q want aes67", stats.Active.Kind)
  165. }
  166. if stats.Source.ChunksIn != 1 {
  167. t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
  168. }
  169. if stats.Source.SamplesIn != 4 {
  170. t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
  171. }
  172. }
  173. func waitForSinkFrames(t *testing.T, sink *audio.StreamSource, minFrames int) {
  174. t.Helper()
  175. deadline := time.Now().Add(1 * time.Second)
  176. for time.Now().Before(deadline) {
  177. if sink.Available() >= minFrames {
  178. return
  179. }
  180. time.Sleep(10 * time.Millisecond)
  181. }
  182. t.Fatalf("timeout waiting for sink frames: have=%d want>=%d", sink.Available(), minFrames)
  183. }