From 34e613859dcfdf3b82c6e79dafab389d146dcb15 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Wed, 17 Dec 2025 13:22:23 -0500 Subject: [PATCH] Add frame rate controls to merge and convert simple mode - Add mergeFrameRate and mergeMotionInterpolation fields to appState - Add frame rate dropdown and motion interpolation checkbox to merge UI - Pass frame rate settings through merge job config - Implement frame rate conversion in executeMergeJob (for non-DVD formats) - Add frame rate controls to convert module's simple mode Frame rate conversion with optional motion interpolation is now available in: - Convert module (simple and advanced modes) - Merge module - Upscale module All modules support both simple fps conversion (fast) and motion interpolation (slower, smoother) for professional frame rate standardization. --- main.go | 77 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index d42e796..16634d7 100644 --- a/main.go +++ b/main.go @@ -632,14 +632,16 @@ type appState struct { autoCompare bool // Auto-load Compare module after conversion // Merge state - mergeClips []mergeClip - mergeFormat string - mergeOutput string - mergeKeepAll bool - mergeCodecMode string - mergeChapters bool - mergeDVDRegion string // "NTSC" or "PAL" - mergeDVDAspect string // "16:9" or "4:3" + mergeClips []mergeClip + mergeFormat string + mergeOutput string + mergeKeepAll bool + mergeCodecMode string + mergeChapters bool + mergeDVDRegion string // "NTSC" or "PAL" + mergeDVDAspect string // "16:9" or "4:3" + mergeFrameRate string // Source, 24, 30, 60, or custom + mergeMotionInterpolation bool // Use motion interpolation for frame rate changes // Thumbnail module state thumbFile *videoSource @@ -2055,6 +2057,9 @@ func (s *appState) showMergeView() { if s.mergeDVDAspect == "" { s.mergeDVDAspect = "16:9" } + if s.mergeFrameRate == "" { + s.mergeFrameRate = "Source" + } backBtn := widget.NewButton("< MERGE", func() { s.showMainMenu() @@ -2308,6 +2313,23 @@ func (s *appState) showMergeView() { dvdOptionsContainer.Hide() } + // Frame Rate controls + frameRateSelect := widget.NewSelect([]string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}, func(val string) { + s.mergeFrameRate = val + }) + frameRateSelect.SetSelected(s.mergeFrameRate) + + motionInterpCheck := widget.NewCheck("Use Motion Interpolation (slower, smoother)", func(checked bool) { + s.mergeMotionInterpolation = checked + }) + motionInterpCheck.SetChecked(s.mergeMotionInterpolation) + + frameRateRow := container.NewVBox( + widget.NewLabel("Frame Rate"), + frameRateSelect, + motionInterpCheck, + ) + browseOut := widget.NewButton("Browse", func() { dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) { if err != nil || writer == nil { @@ -2375,6 +2397,9 @@ func (s *appState) showMergeView() { widget.NewLabel("Format"), formatSelect, dvdOptionsContainer, + widget.NewSeparator(), + frameRateRow, + widget.NewSeparator(), keepAllCheck, chapterCheck, widget.NewSeparator(), @@ -2435,14 +2460,16 @@ func (s *appState) addMergeToQueue(startNow bool) error { } config := map[string]interface{}{ - "clips": clips, - "format": s.mergeFormat, - "keepAllStreams": s.mergeKeepAll, - "chapters": s.mergeChapters, - "codecMode": s.mergeCodecMode, - "outputPath": s.mergeOutput, - "dvdRegion": s.mergeDVDRegion, - "dvdAspect": s.mergeDVDAspect, + "clips": clips, + "format": s.mergeFormat, + "keepAllStreams": s.mergeKeepAll, + "chapters": s.mergeChapters, + "codecMode": s.mergeCodecMode, + "outputPath": s.mergeOutput, + "dvdRegion": s.mergeDVDRegion, + "dvdAspect": s.mergeDVDAspect, + "frameRate": s.mergeFrameRate, + "useMotionInterpolation": s.mergeMotionInterpolation, } job := &queue.Job{ @@ -2724,6 +2751,21 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress args = append(args, "-c", "copy") } + // Frame rate handling (for non-DVD formats that don't lock frame rate) + frameRate, _ := cfg["frameRate"].(string) + useMotionInterp, _ := cfg["useMotionInterpolation"].(bool) + if frameRate != "" && frameRate != "Source" && format != "dvd" && !strings.HasPrefix(format, "dvd-") { + // Build frame rate filter + var frFilter string + if useMotionInterp { + frFilter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1", frameRate) + } else { + frFilter = "fps=" + frameRate + } + // Add as separate filter + args = append(args, "-vf", frFilter) + } + // Add progress output for live updates (must be before output path) args = append(args, "-progress", "pipe:1", "-nostats") @@ -5458,6 +5500,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { simpleBitrateSelect, widget.NewLabelWithStyle("Target Resolution", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), resolutionSelectSimple, + widget.NewLabelWithStyle("Frame Rate", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + frameRateSelect, + motionInterpCheck, widget.NewLabelWithStyle("Target Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), targetAspectSelectSimple, targetAspectHint,