324 lines
9.9 KiB
Go
Executable File
324 lines
9.9 KiB
Go
Executable File
package encoding
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// AudioStandardizer contains audio standardization parameters
|
|
type AudioStandardizer struct {
|
|
Codec string // "aac" or "opus"
|
|
SkipIfCompatible bool // Default: true
|
|
BitratePerChannel int // Default: 80
|
|
StereoBitrate int // Default: 160
|
|
ChannelMode string // "preserve", "stereo", "mono"
|
|
CreateDownmix bool // Default: false
|
|
DownmixSingleTrack bool // Default: false
|
|
ForceTranscode bool // Default: false
|
|
QualityPreset string // "custom", "high_quality", "balanced", "small_size"
|
|
// Opus-specific
|
|
OpusApplication string // Default: "audio"
|
|
OpusVBR string // Default: "on"
|
|
}
|
|
|
|
// DefaultAudioStandardizer returns default audio standardization parameters
|
|
// Updated to align with Tdarr defaults (AAC)
|
|
func DefaultAudioStandardizer() AudioStandardizer {
|
|
return AudioStandardizer{
|
|
Codec: "aac",
|
|
SkipIfCompatible: true,
|
|
BitratePerChannel: 80,
|
|
StereoBitrate: 160,
|
|
ChannelMode: "preserve",
|
|
CreateDownmix: false,
|
|
DownmixSingleTrack: false,
|
|
ForceTranscode: false,
|
|
QualityPreset: "custom",
|
|
OpusApplication: "audio",
|
|
OpusVBR: "on",
|
|
}
|
|
}
|
|
|
|
// AudioStreamInfo contains information about an audio stream
|
|
type AudioStreamInfo struct {
|
|
Index int
|
|
Codec string
|
|
Channels int
|
|
Bitrate int
|
|
SampleRate int
|
|
Language string
|
|
ChannelLayout string
|
|
}
|
|
|
|
// AnalyzeAudioStreams analyzes audio streams in a video file
|
|
func AnalyzeAudioStreams(file string) ([]AudioStreamInfo, error) {
|
|
// Get all audio stream information using ffprobe
|
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "a",
|
|
"-show_entries", "stream=index,codec_name,channels,bit_rate,sample_rate,channel_layout",
|
|
"-show_entries", "stream_tags=language",
|
|
"-of", "json", file)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse JSON output (simplified - in production, use proper JSON parsing)
|
|
// For now, we'll use a simpler approach with individual queries
|
|
var streams []AudioStreamInfo
|
|
|
|
// Get stream count first
|
|
countCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "a",
|
|
"-show_entries", "stream=index", "-of", "csv=p=0", file)
|
|
countOutput, err := countCmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
indices := strings.Split(strings.TrimSpace(string(countOutput)), "\n")
|
|
for _, idxStr := range indices {
|
|
if idxStr == "" {
|
|
continue
|
|
}
|
|
|
|
stream := AudioStreamInfo{}
|
|
stream.Index, _ = strconv.Atoi(strings.TrimSpace(idxStr))
|
|
|
|
// Get codec
|
|
codecCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream=codec_name", "-of", "csv=p=0", file)
|
|
if codecOut, err := codecCmd.Output(); err == nil {
|
|
stream.Codec = strings.TrimSpace(string(codecOut))
|
|
}
|
|
|
|
// Get channels
|
|
channelsCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream=channels", "-of", "csv=p=0", file)
|
|
if channelsOut, err := channelsCmd.Output(); err == nil {
|
|
stream.Channels, _ = strconv.Atoi(strings.TrimSpace(string(channelsOut)))
|
|
}
|
|
|
|
// Get bitrate
|
|
bitrateCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream=bit_rate", "-of", "csv=p=0", file)
|
|
if bitrateOut, err := bitrateCmd.Output(); err == nil {
|
|
stream.Bitrate, _ = strconv.Atoi(strings.TrimSpace(string(bitrateOut)))
|
|
}
|
|
|
|
// Get sample rate
|
|
sampleRateCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream=sample_rate", "-of", "csv=p=0", file)
|
|
if sampleRateOut, err := sampleRateCmd.Output(); err == nil {
|
|
stream.SampleRate, _ = strconv.Atoi(strings.TrimSpace(string(sampleRateOut)))
|
|
}
|
|
|
|
// Get language
|
|
langCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream_tags=language", "-of", "csv=p=0", file)
|
|
if langOut, err := langCmd.Output(); err == nil {
|
|
stream.Language = strings.TrimSpace(string(langOut))
|
|
}
|
|
|
|
// Get channel layout
|
|
layoutCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", fmt.Sprintf("a:%d", stream.Index),
|
|
"-show_entries", "stream=channel_layout", "-of", "csv=p=0", file)
|
|
if layoutOut, err := layoutCmd.Output(); err == nil {
|
|
stream.ChannelLayout = strings.TrimSpace(string(layoutOut))
|
|
}
|
|
|
|
streams = append(streams, stream)
|
|
}
|
|
|
|
_ = output // Suppress unused variable warning
|
|
return streams, nil
|
|
}
|
|
|
|
// IsCompatibleCodec checks if a codec is compatible with the target
|
|
func IsCompatibleCodec(codec string, targetCodec string) bool {
|
|
codec = strings.ToLower(codec)
|
|
targetCodec = strings.ToLower(targetCodec)
|
|
|
|
// Opus compatibility
|
|
if targetCodec == "opus" {
|
|
return codec == "opus" || codec == "libopus"
|
|
}
|
|
|
|
// AAC compatibility
|
|
if targetCodec == "aac" {
|
|
return codec == "aac"
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// NeedsTranscoding checks if a stream needs transcoding
|
|
func NeedsTranscoding(stream AudioStreamInfo, standardizer AudioStandardizer) bool {
|
|
if standardizer.ForceTranscode {
|
|
return true
|
|
}
|
|
|
|
if standardizer.SkipIfCompatible {
|
|
// Accept both AAC and Opus as compatible
|
|
compatible := []string{"aac", "opus", "libopus"}
|
|
for _, c := range compatible {
|
|
if strings.ToLower(stream.Codec) == c {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Must match exact target codec
|
|
return !IsCompatibleCodec(stream.Codec, standardizer.Codec)
|
|
}
|
|
|
|
// CalculateBitrate calculates target bitrate based on channels
|
|
func CalculateBitrate(standardizer AudioStandardizer, channels int) int {
|
|
// Check for "original" bitrate (handled as 0)
|
|
if standardizer.BitratePerChannel == 0 {
|
|
return 0 // Use original bitrate (don't specify in FFmpeg)
|
|
}
|
|
return standardizer.BitratePerChannel * channels
|
|
}
|
|
|
|
// BuildAudioCodecArgs builds FFmpeg arguments for audio encoding
|
|
func BuildAudioCodecArgs(audioIdx int, standardizer AudioStandardizer, targetBitrate int) string {
|
|
if standardizer.Codec == "opus" {
|
|
args := []string{
|
|
fmt.Sprintf("-c:a:%d", audioIdx),
|
|
"libopus",
|
|
}
|
|
if targetBitrate > 0 {
|
|
args = append(args, fmt.Sprintf("-b:a:%d", audioIdx), fmt.Sprintf("%dk", targetBitrate))
|
|
}
|
|
args = append(args,
|
|
"-vbr", standardizer.OpusVBR,
|
|
"-application", standardizer.OpusApplication,
|
|
"-compression_level", "10")
|
|
return strings.Join(args, " ")
|
|
}
|
|
|
|
// AAC
|
|
args := []string{
|
|
fmt.Sprintf("-c:a:%d", audioIdx),
|
|
"aac",
|
|
}
|
|
if targetBitrate > 0 {
|
|
args = append(args, fmt.Sprintf("-b:a:%d", audioIdx), fmt.Sprintf("%dk", targetBitrate))
|
|
}
|
|
args = append(args, "-strict", "-2")
|
|
return strings.Join(args, " ")
|
|
}
|
|
|
|
// BuildChannelArgs builds FFmpeg arguments for channel handling
|
|
func BuildChannelArgs(audioIdx int, standardizer AudioStandardizer) string {
|
|
switch standardizer.ChannelMode {
|
|
case "stereo":
|
|
return fmt.Sprintf("-ac:a:%d 2", audioIdx)
|
|
case "mono":
|
|
return fmt.Sprintf("-ac:a:%d 1", audioIdx)
|
|
default:
|
|
return "" // preserve
|
|
}
|
|
}
|
|
|
|
// OpusIncompatibleLayouts lists channel layouts incompatible with Opus
|
|
var OpusIncompatibleLayouts = []string{
|
|
"3.0(back)",
|
|
"3.0(front)",
|
|
"4.0",
|
|
"5.0(side)",
|
|
"6.0",
|
|
"6.1",
|
|
"7.0",
|
|
"7.0(front)",
|
|
}
|
|
|
|
// IsOpusIncompatibleLayout checks if a channel layout is incompatible with Opus
|
|
func IsOpusIncompatibleLayout(layout string) bool {
|
|
layout = strings.ToLower(layout)
|
|
for _, incompatible := range OpusIncompatibleLayouts {
|
|
if strings.ToLower(incompatible) == layout {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// QualityPresetConfig contains preset configurations
|
|
type QualityPresetConfig struct {
|
|
AACBitratePerChannel int
|
|
OpusBitratePerChannel int
|
|
StereoBitrate int
|
|
OpusVBR string
|
|
OpusApplication string
|
|
Description string
|
|
}
|
|
|
|
// QualityPresets contains predefined quality configurations
|
|
var QualityPresets = map[string]QualityPresetConfig{
|
|
"high_quality": {
|
|
AACBitratePerChannel: 128,
|
|
OpusBitratePerChannel: 96,
|
|
StereoBitrate: 256,
|
|
OpusVBR: "on",
|
|
OpusApplication: "audio",
|
|
Description: "Maximum quality, larger files",
|
|
},
|
|
"balanced": {
|
|
AACBitratePerChannel: 80,
|
|
OpusBitratePerChannel: 64,
|
|
StereoBitrate: 160,
|
|
OpusVBR: "on",
|
|
OpusApplication: "audio",
|
|
Description: "Good quality, reasonable file sizes",
|
|
},
|
|
"small_size": {
|
|
AACBitratePerChannel: 64,
|
|
OpusBitratePerChannel: 48,
|
|
StereoBitrate: 128,
|
|
OpusVBR: "constrained",
|
|
OpusApplication: "audio",
|
|
Description: "Smaller files, acceptable quality",
|
|
},
|
|
}
|
|
|
|
// ApplyQualityPreset applies quality preset settings to standardizer
|
|
func ApplyQualityPreset(standardizer AudioStandardizer) AudioStandardizer {
|
|
if standardizer.QualityPreset == "custom" {
|
|
return standardizer
|
|
}
|
|
|
|
preset, exists := QualityPresets[standardizer.QualityPreset]
|
|
if !exists {
|
|
return standardizer
|
|
}
|
|
|
|
// Apply preset based on codec
|
|
if standardizer.Codec == "aac" {
|
|
standardizer.BitratePerChannel = preset.AACBitratePerChannel
|
|
} else if standardizer.Codec == "opus" {
|
|
standardizer.BitratePerChannel = preset.OpusBitratePerChannel
|
|
standardizer.OpusVBR = preset.OpusVBR
|
|
standardizer.OpusApplication = preset.OpusApplication
|
|
}
|
|
|
|
standardizer.StereoBitrate = preset.StereoBitrate
|
|
return standardizer
|
|
}
|
|
|
|
// BuildDownmixArgs builds FFmpeg arguments for creating downmix tracks
|
|
func BuildDownmixArgs(audioIdx int, streamIndex int, standardizer AudioStandardizer) string {
|
|
baseArgs := fmt.Sprintf("-map 0:%d", streamIndex)
|
|
|
|
if standardizer.Codec == "opus" {
|
|
return fmt.Sprintf("%s -c:a:%d libopus -b:a:%d %dk -vbr %s -application %s -ac 2 -metadata:s:a:%d title=\"2.0 Downmix\"",
|
|
baseArgs, audioIdx, audioIdx, standardizer.StereoBitrate, standardizer.OpusVBR, standardizer.OpusApplication, audioIdx)
|
|
}
|
|
|
|
return fmt.Sprintf("%s -c:a:%d aac -b:a:%d %dk -strict -2 -ac 2 -metadata:s:a:%d title=\"2.0 Downmix\"",
|
|
baseArgs, audioIdx, audioIdx, standardizer.StereoBitrate, audioIdx)
|
|
}
|