package output import ( "context" "encoding/binary" "fmt" "math" "os" "sync" ) // FileBackend streams composite samples to disk so that playback or offline tooling can consume them. type FileBackend struct { mu sync.Mutex file *os.File order binary.ByteOrder info BackendInfo cfg BackendConfig closed bool } // NewFileBackend creates a writer that appends float32 interleaved I/Q pairs to the named file. func NewFileBackend(path string, order binary.ByteOrder, info BackendInfo) (*FileBackend, error) { f, err := os.Create(path) if err != nil { return nil, fmt.Errorf("open output file: %w", err) } if info.Name == "" { info.Name = path } if info.Capabilities.MaxSamplesPerWrite == 0 { info.Capabilities.MaxSamplesPerWrite = 4096 } info.Capabilities.SupportsComposite = true info.Capabilities.FixedRate = true return &FileBackend{ file: f, order: order, info: info, }, nil } // Configure stores the requested configuration, but the file backend simply preserves the values. func (fb *FileBackend) Configure(_ context.Context, cfg BackendConfig) error { fb.mu.Lock() defer fb.mu.Unlock() if fb.closed { return ErrBackendClosed } fb.cfg = cfg return nil } // Write emits the provided frame as binary interleaved float32 I/Q samples. func (fb *FileBackend) Write(ctx context.Context, frame *CompositeFrame) (int, error) { if err := ctx.Err(); err != nil { return 0, err } fb.mu.Lock() defer fb.mu.Unlock() if fb.closed { return 0, ErrBackendClosed } if frame == nil || len(frame.Samples) == 0 { return 0, nil } buf := make([]byte, 8) written := 0 for _, sample := range frame.Samples { if err := ctx.Err(); err != nil { return written, err } fb.order.PutUint32(buf[0:], math.Float32bits(sample.I)) fb.order.PutUint32(buf[4:], math.Float32bits(sample.Q)) if _, err := fb.file.Write(buf); err != nil { return written, fmt.Errorf("write sample data: %w", err) } written++ } return written, nil } // Flush commits the current file buffer to disk. func (fb *FileBackend) Flush(_ context.Context) error { fb.mu.Lock() defer fb.mu.Unlock() if fb.closed { return ErrBackendClosed } return fb.file.Sync() } // Close finalizes the file handle. func (fb *FileBackend) Close(_ context.Context) error { fb.mu.Lock() defer fb.mu.Unlock() if fb.closed { return ErrBackendClosed } fb.closed = true return fb.file.Close() } // Info returns the backend metadata. func (fb *FileBackend) Info() BackendInfo { fb.mu.Lock() defer fb.mu.Unlock() return fb.info }