2026-03-26 12:06:41 +01:00
|
|
|
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.
|
2026-03-26 21:02:35 +01:00
|
|
|
// freqCfgs is the merged config map from config.Load (D-10: injected, not hardcoded).
|
2026-03-26 12:06:41 +01:00
|
|
|
// 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.
|
2026-03-26 21:02:35 +01:00
|
|
|
func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error {
|
2026-03-26 12:06:41 +01:00
|
|
|
// 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
|
2026-03-26 21:02:35 +01:00
|
|
|
bank := synth.NewBank(1.0, freqCfgs) // tau=1.0s per D-07
|
2026-03-26 12:06:41 +01:00
|
|
|
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)
|
|
|
|
|
}
|