package encode import ( "fmt" "os" lame "github.com/sjzar/go-lame" "github.com/netsynth/netsynth/classify" "github.com/netsynth/netsynth/synth" ) // EncodeMP3 writes stereo PCM frames to an MP3 file at the given path. // Encoding parameters: sampleRate Hz, 128 kbps, stereo, quality 5. // Per D-13 (44100 Hz CD quality) and D-14 (128 kbps bitrate). // CRITICAL: InitParams() is called after all Set* calls (go-lame Pitfall 1). func EncodeMP3(outputPath string, frames [][2]float64, sampleRate int) error { f, err := os.Create(outputPath) if err != nil { return fmt.Errorf("create output file: %w", err) } defer f.Close() wr := lame.NewWriter(f) wr.Encoder.SetInSamplerate(sampleRate) wr.Encoder.SetOutSamplerate(sampleRate) wr.Encoder.SetNumChannels(2) wr.Encoder.SetBitrate(128) wr.Encoder.SetQuality(5) wr.Encoder.InitParams() // MUST be called after all Set* calls pcmBytes := synth.StereoFramesToInt16Bytes(frames) if _, err := wr.Write(pcmBytes); err != nil { return fmt.Errorf("write PCM to LAME encoder: %w", err) } if err := wr.Close(); err != nil { return fmt.Errorf("finalize MP3: %w", err) } return nil } // RunSynthesis consumes a slice of WindowSnapshots, renders audio via OscillatorBank, // and encodes to MP3 at outputPath. // Returns an error if zero packets were captured (D-16 / OUT-03). // The zero-packet guard runs BEFORE file creation to avoid leaving an empty file on disk. func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string) error { // D-16 / OUT-03: Zero-packet guard — check BEFORE creating output file var totalPackets int64 for _, snap := range snapshots { totalPackets += snap.TotalPackets } if totalPackets == 0 { return fmt.Errorf("no packets captured: output MP3 not written (empty capture produces no audio)") } // Render all windows to stereo frames bank := synth.NewBank(1.0, synth.ClassFreqConfigs) // tau=1.0s per D-07 var allFrames [][2]float64 for _, snap := range snapshots { frames := bank.RenderWindow(snap) allFrames = append(allFrames, frames...) } // Encode to MP3 return EncodeMP3(outputPath, allFrames, synth.SampleRate) }