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

111 рядки
2.5KB

  1. package output
  2. import (
  3. "context"
  4. "encoding/binary"
  5. "fmt"
  6. "math"
  7. "os"
  8. "sync"
  9. )
  10. // FileBackend streams composite samples to disk so that playback or offline tooling can consume them.
  11. type FileBackend struct {
  12. mu sync.Mutex
  13. file *os.File
  14. order binary.ByteOrder
  15. info BackendInfo
  16. cfg BackendConfig
  17. closed bool
  18. }
  19. // NewFileBackend creates a writer that appends float32 interleaved I/Q pairs to the named file.
  20. func NewFileBackend(path string, order binary.ByteOrder, info BackendInfo) (*FileBackend, error) {
  21. f, err := os.Create(path)
  22. if err != nil {
  23. return nil, fmt.Errorf("open output file: %w", err)
  24. }
  25. if info.Name == "" {
  26. info.Name = path
  27. }
  28. if info.Capabilities.MaxSamplesPerWrite == 0 {
  29. info.Capabilities.MaxSamplesPerWrite = 4096
  30. }
  31. info.Capabilities.SupportsComposite = true
  32. info.Capabilities.FixedRate = true
  33. return &FileBackend{
  34. file: f,
  35. order: order,
  36. info: info,
  37. }, nil
  38. }
  39. // Configure stores the requested configuration, but the file backend simply preserves the values.
  40. func (fb *FileBackend) Configure(_ context.Context, cfg BackendConfig) error {
  41. fb.mu.Lock()
  42. defer fb.mu.Unlock()
  43. if fb.closed {
  44. return ErrBackendClosed
  45. }
  46. fb.cfg = cfg
  47. return nil
  48. }
  49. // Write emits the provided frame as binary interleaved float32 I/Q samples.
  50. func (fb *FileBackend) Write(ctx context.Context, frame *CompositeFrame) (int, error) {
  51. if err := ctx.Err(); err != nil {
  52. return 0, err
  53. }
  54. fb.mu.Lock()
  55. defer fb.mu.Unlock()
  56. if fb.closed {
  57. return 0, ErrBackendClosed
  58. }
  59. if frame == nil || len(frame.Samples) == 0 {
  60. return 0, nil
  61. }
  62. buf := make([]byte, 8)
  63. written := 0
  64. for _, sample := range frame.Samples {
  65. if err := ctx.Err(); err != nil {
  66. return written, err
  67. }
  68. fb.order.PutUint32(buf[0:], math.Float32bits(sample.I))
  69. fb.order.PutUint32(buf[4:], math.Float32bits(sample.Q))
  70. if _, err := fb.file.Write(buf); err != nil {
  71. return written, fmt.Errorf("write sample data: %w", err)
  72. }
  73. written++
  74. }
  75. return written, nil
  76. }
  77. // Flush commits the current file buffer to disk.
  78. func (fb *FileBackend) Flush(_ context.Context) error {
  79. fb.mu.Lock()
  80. defer fb.mu.Unlock()
  81. if fb.closed {
  82. return ErrBackendClosed
  83. }
  84. return fb.file.Sync()
  85. }
  86. // Close finalizes the file handle.
  87. func (fb *FileBackend) Close(_ context.Context) error {
  88. fb.mu.Lock()
  89. defer fb.mu.Unlock()
  90. if fb.closed {
  91. return ErrBackendClosed
  92. }
  93. fb.closed = true
  94. return fb.file.Close()
  95. }
  96. // Info returns the backend metadata.
  97. func (fb *FileBackend) Info() BackendInfo {
  98. fb.mu.Lock()
  99. defer fb.mu.Unlock()
  100. return fb.info
  101. }