using System; using System.Globalization; namespace Paca.Core; /// /// Predefined dynamic range compression presets for dialogue clarity and evening listening. /// public enum CompressorPreset { None, NightListening, PodcastSpeech, BroadcastStandard } /// /// Intelligent audio dynamic range compression (DRC) and peak limiter filter generator. /// Balances quiet speech and loud sound effects (gunfire, explosions) for clear, non-fatiguing audio. /// public static class AudioDynamicRangeCompressor { /// /// Builds the ffmpeg acompressor and alimiter filter chain for the given preset. /// public static string BuildFilter(CompressorPreset preset) { return preset switch { CompressorPreset.NightListening => "acompressor=threshold=-24dB:ratio=6:attack=5:release=100:makeup=6dB,alimiter=limit=-1dB", CompressorPreset.PodcastSpeech => "acompressor=threshold=-20dB:ratio=4:attack=10:release=150:makeup=4dB,alimiter=limit=-1dB", CompressorPreset.BroadcastStandard => "acompressor=threshold=-18dB:ratio=8:attack=2:release=80:makeup=8dB,alimiter=limit=0dB", _ => "anull" }; } /// /// Builds custom compressor filter chain with granular acoustic parameters. /// public static string BuildCustomFilter(double thresholdDb, double ratio, double attackMs, double releaseMs, double makeupDb, double limitPeakDb) { if (double.IsNaN(thresholdDb) || double.IsInfinity(thresholdDb)) throw new ArgumentOutOfRangeException(nameof(thresholdDb), "Threshold must be a finite number."); if (double.IsNaN(ratio) || double.IsInfinity(ratio) || ratio <= 0) throw new ArgumentOutOfRangeException(nameof(ratio), "Compression ratio must be a positive finite number."); if (double.IsNaN(attackMs) || double.IsInfinity(attackMs) || attackMs < 0) throw new ArgumentOutOfRangeException(nameof(attackMs), "Attack time must be a non-negative finite number."); if (double.IsNaN(releaseMs) || double.IsInfinity(releaseMs) || releaseMs < 0) throw new ArgumentOutOfRangeException(nameof(releaseMs), "Release time must be a non-negative finite number."); if (double.IsNaN(makeupDb) || double.IsInfinity(makeupDb)) throw new ArgumentOutOfRangeException(nameof(makeupDb), "Makeup gain must be a finite number."); if (double.IsNaN(limitPeakDb) || double.IsInfinity(limitPeakDb)) throw new ArgumentOutOfRangeException(nameof(limitPeakDb), "Peak limiter must be a finite number."); var inv = CultureInfo.InvariantCulture; return $"acompressor=threshold={thresholdDb.ToString("0.##", inv)}dB:ratio={ratio.ToString("0.##", inv)}:attack={attackMs.ToString("0.##", inv)}:release={releaseMs.ToString("0.##", inv)}:makeup={makeupDb.ToString("0.##", inv)}dB,alimiter=limit={limitPeakDb.ToString("0.##", inv)}dB"; } /// /// Builds full ffmpeg command line arguments for applying DRC to video or audio files. /// public static string BuildFfmpegArgs(string inputPath, string outputPath, CompressorPreset preset, bool copyVideo = true, int audioBitrateKbps = 192) { if (string.IsNullOrWhiteSpace(inputPath)) throw new ArgumentException("Input path cannot be null or empty.", nameof(inputPath)); if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Output path cannot be null or empty.", nameof(outputPath)); if (string.Equals(inputPath.Trim(), outputPath.Trim(), StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Input and output paths cannot be identical.", nameof(outputPath)); if (audioBitrateKbps <= 0) audioBitrateKbps = 192; string vCodec = copyVideo ? "-c:v copy" : "-c:v libx264 -crf 20 -preset fast"; var filter = BuildFilter(preset); return $"-hide_banner -y -i \"{inputPath}\" {vCodec} -af \"{filter}\" -c:a aac -b:a {audioBitrateKbps}k \"{outputPath}\""; } }