Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

115 line
2.6KB

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