// Package ffmpeg implements driven.AudioEncoder: WAV framing done by hand, // then a shell-out to the system ffmpeg binary to transcode to AAC/M4A, // matching uma-tts-api's pydub `.export(..., format="ipod")` step // (mimetype audio/aac, despite the "ipod" format name being an M4A/AAC // container alias). package ffmpeg import ( "bytes" "context" "fmt" "os" "os/exec" ) type Encoder struct{} func NewEncoder() *Encoder { return &Encoder{} } // Encode transcodes PCM samples to AAC. The output goes to a temp file // rather than a stdout pipe because the M4A/MP4 muxer needs a seekable // destination to write its moov atom. func (e *Encoder) Encode(ctx context.Context, pcm []float32, sampleRate int) ([]byte, string, error) { wavBytes := encodeWAV(pcm, sampleRate) tmpFile, err := os.CreateTemp("", "tts-gateway-*.m4a") if err != nil { return nil, "", fmt.Errorf("ffmpeg: create temp output file: %w", err) } tmpPath := tmpFile.Name() _ = tmpFile.Close() defer os.Remove(tmpPath) cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-f", "wav", "-i", "pipe:0", "-f", "ipod", tmpPath, ) cmd.Stdin = bytes.NewReader(wavBytes) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return nil, "", fmt.Errorf("ffmpeg: transcode failed: %w: %s", err, stderr.String()) } data, err := os.ReadFile(tmpPath) if err != nil { return nil, "", fmt.Errorf("ffmpeg: read transcoded output: %w", err) } return data, "audio/aac", nil }