|
- package output
-
- import (
- "context"
- "encoding/binary"
- "fmt"
- "math"
- "os"
- "path/filepath"
- "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
- wbuf []byte // reusable write buffer
- }
-
- // 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) {
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- return nil, fmt.Errorf("create output dir: %w", err)
- }
- 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.
- // Serializes the entire frame into a byte buffer and writes in one syscall.
- 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
- }
-
- // Grow write buffer if needed, reuse across calls
- need := len(frame.Samples) * 8
- if cap(fb.wbuf) < need {
- fb.wbuf = make([]byte, need)
- }
- buf := fb.wbuf[:need]
-
- for i, sample := range frame.Samples {
- off := i * 8
- fb.order.PutUint32(buf[off:], math.Float32bits(sample.I))
- fb.order.PutUint32(buf[off+4:], math.Float32bits(sample.Q))
- }
-
- if _, err := fb.file.Write(buf); err != nil {
- return 0, fmt.Errorf("write sample data: %w", err)
- }
- return len(frame.Samples), 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
- }
|