Files
yoloyolo/encode/mp3.go
T
gurix 3dfcbbeaf5 feat(06-02): add freqCfgs parameter to RunSynthesis
- Change RunSynthesis signature to accept injected config map (D-10)
- Replace hardcoded synth.ClassFreqConfigs with passed-in freqCfgs
- Update all three RunSynthesis calls in encode tests to pass synth.ClassFreqConfigs
2026-03-26 21:02:35 +01:00

68 lines
2.2 KiB
Go

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.
// freqCfgs is the merged config map from config.Load (D-10: injected, not hardcoded).
// 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, freqCfgs map[classify.TrafficClass]synth.FreqConfig) 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, freqCfgs) // 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)
}