diff --git a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs index 9946c33..23c6d92 100644 --- a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs +++ b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs @@ -25,7 +25,28 @@ public partial class OptionsViewModel : ObservableObject [ObservableProperty] private bool _videoPreferGpu = true; - /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작). + // ── 영상 인코딩 (출력이 영상 컨테이너일 때) ────────────────────────────────────────────── + [ObservableProperty] private int _videoCodecIndex; // 0=Auto,1=H264,2=H265,3=VP9,4=AV1,5=Copy + [ObservableProperty] private int _rateControlIndex; // 0=CRF,1=ABR,2=ConstrainedCRF,3=CBR,4=2pass + [ObservableProperty] private int _crf = 23; // 0~51 + [ObservableProperty] private int _videoBitrateKbps = 8000; + [ObservableProperty] private int _presetIndex = 5; // 0=UltraFast..8=VerySlow(기본 Medium) + [ObservableProperty] private int _resolutionIndex; // 0=원본,1=2160p,2=1440p,3=1080p,4=720p,5=480p + [ObservableProperty] private int _fpsIndex; // 0=원본,1=24,2=30,3=60 + [ObservableProperty] private int _rotateIndex; // 0=None,1=Cw90,2=Ccw90,3=180,4=FlipH,5=FlipV + [ObservableProperty] private int _deinterlaceIndex; // 0=Off,1=Yadif,2=Bwdif + [ObservableProperty] private bool _fastStart = true; + [ObservableProperty] private bool _spatialAq = true; + + // ── 오디오 인코딩 (영상의 오디오 트랙 + 오디오 전용 출력) ───────────────────────────────── + [ObservableProperty] private int _audioCodecIndex; // 0=Auto,1=AAC,2=MP3,3=Opus,4=Vorbis,5=FLAC,6=PCM,7=Copy + [ObservableProperty] private int _audioBitrateIndex = 2; // 0=96,1=128,2=192,3=256,4=320 + [ObservableProperty] private bool _audioVbr; + [ObservableProperty] private int _sampleRateIndex; // 0=원본,1=44100,2=48000 + [ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오 + [ObservableProperty] private bool _loudnorm; + + /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오). public ConvertOptions ToConvertOptions() { var hasCustom = !string.IsNullOrWhiteSpace(CustomOutputDirectory); @@ -50,6 +71,34 @@ public partial class OptionsViewModel : ObservableObject TargetLanguage = string.IsNullOrWhiteSpace(TargetLanguage) ? null : TargetLanguage.Trim(), }, VideoPreferGpu = VideoPreferGpu, + Video = BuildVideo(), + Audio = BuildAudio(), }; } + + private VideoEncodeOptions BuildVideo() => new() + { + Codec = VideoCodecIndex switch { 1 => VideoCodec.H264, 2 => VideoCodec.H265, 3 => VideoCodec.Vp9, 4 => VideoCodec.Av1, 5 => VideoCodec.Copy, _ => VideoCodec.Auto }, + RateControl = RateControlIndex switch { 1 => RateControlMode.AverageBitrate, 2 => RateControlMode.ConstrainedCrf, 3 => RateControlMode.Cbr, 4 => RateControlMode.TwoPass, _ => RateControlMode.Crf }, + Crf = Math.Clamp(Crf, 0, 63), + VideoBitrateKbps = VideoBitrateKbps, + VideoMaxrateKbps = RateControlIndex == 2 ? VideoBitrateKbps : null, // 제약CRF는 비트레이트 입력을 상한으로 + Preset = (VideoSpeedPreset)Math.Clamp(PresetIndex, 0, 8), + ScaleWidth = ResolutionIndex switch { 1 => 3840, 2 => 2560, 3 => 1920, 4 => 1280, 5 => 854, _ => (int?)null }, + Fps = FpsIndex switch { 1 => 24.0, 2 => 30.0, 3 => 60.0, _ => (double?)null }, + Rotate = (RotateMode)Math.Clamp(RotateIndex, 0, 5), + Deinterlace = (DeinterlaceMode)Math.Clamp(DeinterlaceIndex, 0, 2), + FastStart = FastStart, + SpatialAq = SpatialAq, + }; + + private AudioEncodeOptions BuildAudio() => new() + { + Codec = AudioCodecIndex switch { 1 => AudioCodec.Aac, 2 => AudioCodec.Mp3, 3 => AudioCodec.Opus, 4 => AudioCodec.Vorbis, 5 => AudioCodec.Flac, 6 => AudioCodec.Pcm, 7 => AudioCodec.Copy, _ => AudioCodec.Auto }, + RateMode = AudioVbr ? AudioRateMode.Vbr : AudioRateMode.Bitrate, + AudioBitrateKbps = AudioBitrateIndex switch { 0 => 96, 1 => 128, 3 => 256, 4 => 320, _ => 192 }, + SampleRate = SampleRateIndex switch { 1 => 44100, 2 => 48000, _ => (int?)null }, + Channels = ChannelsIndex switch { 1 => 1, 2 => 2, _ => (int?)null }, + Loudnorm = Loudnorm, + }; } diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml b/src/Everything2Everything.App/Views/MainWindow.xaml index 259136b..eb47f72 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml +++ b/src/Everything2Everything.App/Views/MainWindow.xaml @@ -244,6 +244,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "AVIF QUALITY", _ => "ENCODING QUALITY", }; + + UpdateMediaPanelForFormat(extension); + } + + /// 출력이 영상/오디오일 때만 영상·오디오 인코딩 패널을 노출(영상이면 영상+오디오, 오디오면 오디오만). + private void UpdateMediaPanelForFormat(string? extension) + { + if (MediaPanel is null) return; + var ext = (extension ?? string.Empty).ToLowerInvariant(); + var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi"; + var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav"; + MediaPanel.Visibility = (isVideo || isAudio) ? Visibility.Visible : Visibility.Collapsed; + VideoSubPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed; + AudioSubPanel.Visibility = (isVideo || isAudio) ? Visibility.Visible : Visibility.Collapsed; } private void UpdateOutputDestHint(string? extension)