136 lines
4.1 KiB
Go
136 lines
4.1 KiB
Go
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
|
|
}
|