diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md index 5e80ccc..b03c7a0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ All tempo changes are taken into account, so the timecode should be as accurate Usage ----- - +Score Timecode  wFrames **Offset time**\ Enter the timecode for the first measure, in minutes, seconds and milliseconds. Only numbers may be entered, and the correct formatting will be forced by the plugin. A maximum of 99 minutes can be entered.\ @@ -23,6 +23,9 @@ Check this box to _always_ include the minutes component when writing the timeco - When this box is _unchecked_ (default) the minutes will only be included once it is required, and the seconds will be written using one or two digits as required. - When this box _checked_ the minutes will always be included in the timecode (even when 0), and the seconds will always be written using two digits. +**Show frames not millisecs**\ +Check this box to show frames instead of milliseconds. Enter a frame rate below. The plugin automatically uses drop/non-drop frames according to standard timecode conventions. + **Style**\ Set various text style parameters for the timecode. Bold, Italic, Undescore and Square border may be combined as required. - **B** - Make the timecode text appear in **bold** @@ -59,5 +62,6 @@ To use the plugin: Release History ------------- -**v1.0** - 24/4/2025 - Initial Release +**v1.0** - 24/4/2025 - Initial Release +**v1.5** - 7/5/2025 - Add millisec -> frame/dropframe display options diff --git a/ScoreTimeCode.qml b/ScoreTimeCode.qml index 75c9dab..30d4466 100644 --- a/ScoreTimeCode.qml +++ b/ScoreTimeCode.qml @@ -19,20 +19,21 @@ import Muse.UiComponents // // Change History // v1.0 - Initial development +// v1.5 - Add millisec -> frame/dropframe display options (by Eric Warren - https://github.com/eakwarren/ScoreTimecode) // //============================================================================= MuseScore { - version: "1.0" + version: "1.5" title: "Score Timecode" - description: "This plug-in adds text to each measure to display the elapsed time in minutes, seconds, and milliseconds." + description: "This plug-in adds text to each measure to display the elapsed time in minutes, seconds, and milliseconds or frames." pluginType: "dialog" thumbnailName: "ScoreTimecodeIcon.png" implicitHeight: 350; - implicitWidth: 300; + implicitWidth: 350; //============================================================================= @@ -47,8 +48,12 @@ MuseScore property var boldText: false; property var borderText: false; property var underlineText: false; - property var aboveText: false - + property var aboveText: false + + property bool useFps: false; // defaults + property var fps: 24; + property var dropFrames: false; + //============================================================================= // Main UI Layout // @@ -107,18 +112,28 @@ MuseScore CheckBox { id: excludeFirstMeasureCheckbox - Layout.columnSpan:2 + Layout.columnSpan:1 text: "Exclude the first measure" checked: excludeFirstMeasure onClicked: { excludeFirstMeasure = !excludeFirstMeasure; } } + + CheckBox + { + id: useFpsField + Layout.columnSpan:1 + text: "Show fps not millisecs" + + checked: useFps + onClicked: { useFps = !useFps; } + } CheckBox { id: alwaysIncludeMinuteCheckbox - Layout.columnSpan:2 + Layout.columnSpan:1 text: "Always include minutes" checked: alwaysIncludeMinutes onClicked: { @@ -126,6 +141,48 @@ MuseScore } } + RowLayout + { + id: fpsInfo + Layout.columnSpan:1 + Layout.alignment: Qt.AlignRight + + Label + { + id: fpsDesc + font.italic: true + font.pointSize: 12 + color: palette.text + text: "24, 29.97, 60, etc. " + } + + TextField + { + id: fpsField + Layout.alignment: Qt.AlignRight + + text: fps + + placeholderText: "24" + color: ui.theme.fontPrimaryColor + // Set the placeholder with dynamic opacity when empty + placeholderTextColor: Qt.rgba( + ui.theme.fontPrimaryColor.r, + ui.theme.fontPrimaryColor.g, + ui.theme.fontPrimaryColor.b, + text.length > 1 ? 1.0 : 0.5 + ) + + implicitHeight: 24 + Layout.maximumWidth:50 + horizontalAlignment: TextInput.AlignRight + + validator: RegularExpressionValidator { regularExpression: /^((\d{1,2}).(\d{1,3}))$/ || /^((\d{1,2})$/ } + + enabled: useFps ? 1 : 0 + } + } + Label { id: styleLabel @@ -319,9 +376,9 @@ MuseScore onClicked: aboutDialog.open() } - } - } - + } + } + //============================================================================= // About Dialog @@ -470,7 +527,7 @@ console.log("Error: More than one staff selected") cursor.nextMeasure() - while ((cursor.tick != 0) && (cursor.tick < endTick)) + while ((cursor.tick !== 0) && (cursor.tick < endTick)) { var measureSeconds = 0; @@ -520,7 +577,6 @@ console.log("Error: More than one staff selected") curScore.endCmd() } - //============================================================================= // @@ -559,28 +615,40 @@ console.log("Error: More than one staff selected") function formatTime(seconds) { - var minutes = Math.floor(seconds / 60); - - seconds = seconds - (minutes*60); + var minutes = Math.floor(seconds / 60); + var remainingSeconds = seconds - (minutes * 60); - var secondsPart = Math.floor(seconds); - secondsPart = (((minutes > 0 || alwaysIncludeMinutes) && secondsPart < 10) ? "0" : "") + secondsPart; - - seconds = seconds - Math.floor(seconds); - var milliseconds = Math.round(seconds * 1000) - - while (milliseconds.toString().length < 3) milliseconds = "0" + milliseconds; - - var result = secondsPart + "." + milliseconds + '"'; - - if (minutes > 0 || alwaysIncludeMinutes) - { - result = minutes - + (colonsButton.checked ? ":" : "'") - + result; - } - - return result; + var secondsPart = Math.floor(remainingSeconds); + secondsPart = (((minutes > 0 || alwaysIncludeMinutes) && secondsPart < 10) ? "0" : "") + secondsPart; + + var result = ""; + + if (useFps) { + var fpsVal = parseFloat(fpsField.text); + var dropFrames = fpsVal === 29.97 || fpsVal === 59.94 || fpsVal === 23.976 || fpsVal === 23.98; + + // total milliseconds for the entire time (minutes + seconds + milliseconds) + var totalMilliseconds = Math.round(seconds * 1000); + + // use totalMilliseconds in formatTimecode + var frameCode = formatTimecode(totalMilliseconds, fpsVal, dropFrames); + + result = secondsPart + frameCode; + + } else { + // Non-frame format fallback (e.g., 01:23.456") + var fractionalPart = remainingSeconds - Math.floor(remainingSeconds); + var milliseconds = Math.round(fractionalPart * 1000); + while (milliseconds.toString().length < 3) milliseconds = "0" + milliseconds; + + result = secondsPart + "." + milliseconds + '"'; + } + + if (minutes > 0 || alwaysIncludeMinutes) { + result = minutes + (colonsButton.checked ? ":" : "'") + result; + } + + return result; } @@ -698,6 +766,46 @@ console.log("Error: More than one staff selected") { elapsedTime = 0.0; - } + } + +//============================================================================= + + function formatTimecode(ms, fps, drop) { + const isDrop = drop && (fps === 29.97 || fps === 59.94 || fps === 23.976 || fps === 23.98); + const frInt = Math.round(fps); // 30 or 60 or 24 + const dropFrames = (fps === 29.97) ? 2 : + (fps === 59.94) ? 4 : + (fps === 23.976 || fps === 23.98) ? 1 : 0; + + const totalSeconds = ms / 1000; + let totalFrames = Math.floor(totalSeconds * frInt); // use integer fps (30, 60) + + if (isDrop && dropFrames > 0) { + const framesPer10Minutes = frInt * 60 * 10; + const d = totalFrames; + + const tenMinuteChunks = Math.floor(d / framesPer10Minutes); + const framesSinceLast10 = d % framesPer10Minutes; + const minutesSinceLast10 = Math.floor(framesSinceLast10 / (frInt * 60)); + + const dropCount = dropFrames * (tenMinuteChunks * 9 + Math.max(0, minutesSinceLast10 - Math.floor(minutesSinceLast10 / 10))); + totalFrames += dropCount; + } + + const hours = Math.floor(totalFrames / (frInt * 3600)); + const minutes = Math.floor((totalFrames % (frInt * 3600)) / (frInt * 60)); + const seconds = Math.floor((totalFrames % (frInt * 60)) / frInt); + const frames = totalFrames % frInt; + + const separator = isDrop ? ";" : ":"; + + function pad(n) { + return n < 10 ? "0" + n : "" + n; + } + + return separator + pad(frames); + } + + } diff --git a/ScoreTimecodeMainForm.png b/ScoreTimecodeMainForm.png index ee5b0a2..70863a9 100644 Binary files a/ScoreTimecodeMainForm.png and b/ScoreTimecodeMainForm.png differ