Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

121 lignes
2.8KB

  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. wbuf []byte // reusable write buffer
  20. }
  21. // NewFileBackend creates a writer that appends float32 interleaved I/Q pairs to the named file.
  22. func NewFileBackend(path string, order binary.ByteOrder, info BackendInfo) (*FileBackend, error) {
  23. if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
  24. return nil, fmt.Errorf("create output dir: %w", err)
  25. }
  26. f, err := os.Create(path)
  27. if err != nil {
  28. return nil, fmt.Errorf("open output file: %w", err)
  29. }
  30. if info.Name == "" {
  31. info.Name = path
  32. }
  33. if info.Capabilities.MaxSamplesPerWrite == 0 {
  34. info.Capabilities.MaxSamplesPerWrite = 4096
  35. }
  36. info.Capabilities.SupportsComposite = true
  37. info.Capabilities.FixedRate = true
  38. return &FileBackend{
  39. file: f,
  40. order: order,
  41. info: info,
  42. }, nil
  43. }
  44. // Configure stores the requested configuration, but the file backend simply preserves the values.
  45. func (fb *FileBackend) Configure(_ context.Context, cfg BackendConfig) error {
  46. fb.mu.Lock()
  47. defer fb.mu.Unlock()
  48. if fb.closed {
  49. return ErrBackendClosed
  50. }
  51. fb.cfg = cfg
  52. return nil
  53. }
  54. // Write emits the provided frame as binary interleaved float32 I/Q samples.
  55. // Serializes the entire frame into a byte buffer and writes in one syscall.
  56. func (fb *FileBackend) Write(ctx context.Context, frame *CompositeFrame) (int, error) {
  57. if err := ctx.Err(); err != nil {
  58. return 0, err
  59. }
  60. fb.mu.Lock()
  61. defer fb.mu.Unlock()
  62. if fb.closed {
  63. return 0, ErrBackendClosed
  64. }
  65. if frame == nil || len(frame.Samples) == 0 {
  66. return 0, nil
  67. }
  68. // Grow write buffer if needed, reuse across calls
  69. need := len(frame.Samples) * 8
  70. if cap(fb.wbuf) < need {
  71. fb.wbuf = make([]byte, need)
  72. }
  73. buf := fb.wbuf[:need]
  74. for i, sample := range frame.Samples {
  75. off := i * 8
  76. fb.order.PutUint32(buf[off:], math.Float32bits(sample.I))
  77. fb.order.PutUint32(buf[off+4:], math.Float32bits(sample.Q))
  78. }
  79. if _, err := fb.file.Write(buf); err != nil {
  80. return 0, fmt.Errorf("write sample data: %w", err)
  81. }
  82. return len(frame.Samples), nil
  83. }
  84. // Flush commits the current file buffer to disk.
  85. func (fb *FileBackend) Flush(_ context.Context) error {
  86. fb.mu.Lock()
  87. defer fb.mu.Unlock()
  88. if fb.closed {
  89. return ErrBackendClosed
  90. }
  91. return fb.file.Sync()
  92. }
  93. // Close finalizes the file handle.
  94. func (fb *FileBackend) Close(_ context.Context) error {
  95. fb.mu.Lock()
  96. defer fb.mu.Unlock()
  97. if fb.closed {
  98. return ErrBackendClosed
  99. }
  100. fb.closed = true
  101. return fb.file.Close()
  102. }
  103. // Info returns the backend metadata.
  104. func (fb *FileBackend) Info() BackendInfo {
  105. fb.mu.Lock()
  106. defer fb.mu.Unlock()
  107. return fb.info
  108. }