package main import ( "encoding/json" "fmt" "os/exec" "path/filepath" "strings" ) // ConvertAudioTrack processes a single audio track: extract -> normalize -> encode func ConvertAudioTrack(index int, ch int, lang string, audioTempDir string, sourceFile string, shouldDownmix bool) (string, error) { tempExtracted := filepath.Join(audioTempDir, fmt.Sprintf("track_%d_extracted.flac", index)) tempNormalized := filepath.Join(audioTempDir, fmt.Sprintf("track_%d_normalized.flac", index)) finalOpus := filepath.Join(audioTempDir, fmt.Sprintf("track_%d_final.opus", index)) LogInfo(" - Extracting Audio Track #%d to FLAC...", index) ffmpegArgs := []string{ "-v", "quiet", "-stats", "-y", "-i", sourceFile, "-map", fmt.Sprintf("0:%d", index), "-map_metadata", "-1", } if shouldDownmix && ch >= 6 { if ch == 6 { ffmpegArgs = append(ffmpegArgs, "-af", "pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5") } else if ch == 8 { ffmpegArgs = append(ffmpegArgs, "-af", "pan=stereo|c0=c2+0.30*c0+0.30*c4+0.30*c6|c1=c2+0.30*c1+0.30*c5+0.30*c7") } else { ffmpegArgs = append(ffmpegArgs, "-ac", "2") } } ffmpegArgs = append(ffmpegArgs, "-c:a", "flac", tempExtracted) _, err := RunCmd("ffmpeg", ffmpegArgs, false) if err != nil { return "", fmt.Errorf("failed to extract audio: %w", err) } LogInfo(" - Normalizing Audio Track #%d with ffmpeg (loudnorm 2-pass)...", index) LogInfo(" - Pass 1: Analyzing...") analyzeCmd := []string{ "-v", "info", "-i", tempExtracted, "-af", "loudnorm=I=-23:LRA=7:tp=-1:print_format=json", "-f", "null", "-", } // We need to capture stderr for the JSON output // RunCmd with captureOutput=true captures stdout. ffmpeg prints stats to stderr. // We need a custom run for this to capture stderr. // Re-implementing specific run logic here for simplicity or extending RunCmd? // Let's extend RunCmd logic locally here since it's specific. // Actually, RunCmd returns stdout. We need stderr. // Let's use exec.Command directly. LogInfo("Executing: ffmpeg %s", strings.Join(analyzeCmd, " ")) cmd := exec.Command("ffmpeg", analyzeCmd...) outputBytes, err := cmd.CombinedOutput() // loudnorm json is on stderr, but sometimes mixed. // Actually loudnorm prints to stderr. outputStr := string(outputBytes) if err != nil { return "", fmt.Errorf("loudnorm analysis failed: %w", err) } // Parse JSON from output jsonStart := strings.Index(outputStr, "{") if jsonStart == -1 { return "", fmt.Errorf("could not find JSON start in loudnorm output") } // Find the matching closing brace jsonEnd := -1 braceLevel := 0 for i, char := range outputStr[jsonStart:] { if char == '{' { braceLevel++ } else if char == '}' { braceLevel-- if braceLevel == 0 { jsonEnd = jsonStart + i + 1 break } } } if jsonEnd == -1 { return "", fmt.Errorf("could not find JSON end in loudnorm output") } var stats LoudnormStats err = json.Unmarshal([]byte(outputStr[jsonStart:jsonEnd]), &stats) if err != nil { return "", fmt.Errorf("failed to parse loudnorm stats: %w", err) } LogInfo(" - Pass 2: Applying normalization...") filterComplex := fmt.Sprintf("loudnorm=I=-23:LRA=7:tp=-1:measured_i=%s:measured_lra=%s:measured_tp=%s:measured_thresh=%s:offset=%s", stats.InputI, stats.InputLra, stats.InputTp, stats.InputThresh, stats.TargetOffset) normArgs := []string{ "-v", "quiet", "-stats", "-y", "-i", tempExtracted, "-af", filterComplex, "-c:a", "flac", tempNormalized, } _, err = RunCmd("ffmpeg", normArgs, false) if err != nil { return "", fmt.Errorf("failed to apply normalization: %w", err) } // Bitrate selection bitrate := "96k" isDownmixed := shouldDownmix && ch >= 6 if isDownmixed { bitrate = "128k" } else { switch ch { case 1: bitrate = "64k" case 2: bitrate = "128k" case 6: bitrate = "256k" case 8: bitrate = "384k" default: bitrate = "96k" } } LogInfo(" - Encoding Audio Track #%d to Opus at %s...", index, bitrate) opusArgs := []string{ "--vbr", "--bitrate", bitrate, tempNormalized, finalOpus, } _, err = RunCmd("opusenc", opusArgs, false) if err != nil { return "", fmt.Errorf("failed to encode to opus: %w", err) } return finalOpus, nil }