package audio import ( "context" "encoding/json" "errors" "fmt" "log" "github.com/qvest-digital/go-mxl/mxl" ) func Run( ctx context.Context, inst *mxl.Instance, cfg Config, ) (runErr error) { flowJSON, err := json.Marshal(cfg.Definition) if err != nil { return fmt.Errorf("marshal audio flow definition: %w", err) } writer, isCreated, err := inst.NewWriter(string(flowJSON)) if err != nil { return fmt.Errorf("create audio writer: %w", err) } defer func() { if err := writer.Close(); err != nil { runErr = errors.Join( runErr, fmt.Errorf("close audio writer: %w", err), ) } }() if !isCreated { log.Printf("reusing existing audio flow: %s", cfg.ID()) } writerCfg := writer.Config() if writerCfg.Common.Format != mxl.FormatAudio { return fmt.Errorf( "audio writer has format %s, want audio", writerCfg.Common.Format, ) } if writerCfg.Continuous.ChannelCount != uint32(cfg.Channels()) { return fmt.Errorf( "audio writer has %d channels, configured generator expects %d", writerCfg.Continuous.ChannelCount, cfg.Channels(), ) } rate := writerCfg.Common.GrainRate if rate != cfg.Rate() { return fmt.Errorf( "audio writer has sample rate %d/%d, configured generator expects %d/%d", rate.Num, rate.Den, cfg.Rate().Num, cfg.Rate().Den, ) } const baseFrequency = 1000.0 gen, err := NewSineGenerator(cfg, baseFrequency) if err != nil { return fmt.Errorf("initialize audio generator: %w", err) } batch := batchSize(rate) maxBatch, err := writer.GetMaxWriteLengthSamples() if err != nil { return fmt.Errorf("get maximum audio write length: %w", err) } if maxBatch == 0 { return fmt.Errorf("audio writer reported a maximum write length of zero samples") } if batch > maxBatch { batch = maxBatch } index := mxl.CurrentIndex(rate) if index < batch-1 { return fmt.Errorf("current audio index %d is too small for batch size %d", index, batch) } log.Printf( "writing audio flow sampleRate=%d/%d channels=%d batch=%d starting at idx=%d", rate.Num, rate.Den, cfg.Channels(), batch, index, ) var samplesWritten uint64 for { select { case <-ctx.Done(): log.Printf("stopping audio after %d samples", samplesWritten) return nil default: } access, err := writer.OpenSamples(index, int(batch)) if err != nil { return fmt.Errorf( "open %d audio samples at index %d: %w", batch, index, err, ) } firstSample := index - batch + 1 for channel := uint64(0); channel < access.ChannelCount; channel++ { first, second, err := access.ChannelFragments(channel) if err != nil { return cancelSamples( access, fmt.Errorf( "get fragments for audio channel %d at index %d: %w", channel, index, err, ), ) } if err := gen.Generate( uint(channel), firstSample, first, second, ); err != nil { return cancelSamples( access, fmt.Errorf( "generate audio channel %d at index %d: %w", channel, index, err, ), ) } } if err := access.Commit(); err != nil { return fmt.Errorf( "commit %d audio samples at index %d: %w", batch, index, err, ) } samplesWritten += batch index += batch mxl.SleepNs(mxl.NsUntilIndex(index, rate)) } } func cancelSamples( access *mxl.SamplesWriteAccess, cause error, ) error { if err := access.Cancel(); err != nil { return errors.Join( cause, fmt.Errorf("cancel audio samples: %w", err), ) } return cause } func batchSize(rate mxl.Rational) uint64 { if rate.Num <= 0 || rate.Den <= 0 { return 1 } samples := rate.Num / (100 * rate.Den) if samples < 1 { return 1 } return uint64(samples) }