From d413f25118efd4afff6c474cc23ed63445cab47f Mon Sep 17 00:00:00 2001 From: Mitch Clifford Date: Fri, 28 Mar 2025 14:22:11 +1100 Subject: [PATCH 1/2] Improved audio meter Improved the log10() function to better scale audio meter values Enhanced the updateVU() function Improved audio level processing in vMixRefresh() Updated generateVU() to ensure meters are visible by default If one of the multiview boxes is called "Output", show master audio meter. --- script.js | 135 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 27 deletions(-) diff --git a/script.js b/script.js index f0f4eca..be00910 100644 --- a/script.js +++ b/script.js @@ -157,12 +157,24 @@ var lastResponse; var vMixInputs = [] function log10(x) { - if(Math.log(x)*20 == -Infinity){ - return 0; - } - else{ - return Math.log(x)*21.5; + // Convert to dB scale with proper reference level for audio meters + // Audio meters typically show from 0dB to -60dB (or lower) + if (x <= 0) { + return 0; // Silent } + + // Convert to decibels (20 * log10(x)) with proper scaling + // x is typically between 0 and 1 from vMix + const dB = 20 * Math.log10(x); + + // Map from dB range to percentage (0 to 100) + // 0dB -> 100%, -60dB -> 0% + const minDb = -60; // Minimum dB to display + + // Calculate percentage: 0dB = 100%, minDb = 0% + const percentage = Math.max(0, Math.min(100, (dB - minDb) * 100 / Math.abs(minDb))); + + return percentage; } var vMixStatuses = [ @@ -182,19 +194,44 @@ function vMixRefresh(data){ vMixSettings.previewKey = data.querySelector(`[number="${vMixSettings.previewNumber}"]`).getAttribute("key") vMixSettings.programKey = data.querySelector(`[number="${vMixSettings.programNumber}"]`).getAttribute("key") + // Get master volume data + const masterVolume = data.getElementsByTagName("master")[0]; + const masterMeterF1 = masterVolume ? masterVolume.getAttribute("meterF1") : null; + const masterMeterF2 = masterVolume ? masterVolume.getAttribute("meterF2") : null; + const masterMuted = masterVolume ? masterVolume.getAttribute("muted") === "True" : false; + + // Add master audio data to vMixInputs array + vMixInputs.master = { + audio: true, + muted: masterMuted, + audioL: masterMeterF1 !== null ? log10(parseFloat(masterMeterF1)) : 0, + audioR: masterMeterF2 !== null ? log10(parseFloat(masterMeterF2)) : 0 + }; + Array.prototype.slice.call(data.getElementsByTagName("input"), 0 ).forEach(function(input, i){ vMixInputs[i] = {} vMixInputs[i].title = input.getAttribute("title"); vMixInputs[i].key = input.getAttribute("key"); - if(input.getAttribute("muted")){ - vMixInputs[i].audio = true - vMixInputs[i].muted = input.getAttribute("muted") == "True" - vMixInputs[i].audioL = log10(input.getAttribute("meterF1") * 100) - vMixInputs[i].audioR = log10(input.getAttribute("meterF2") * 100) - } - else{ - vMixInputs[i].audio = false + + // Detect if this is an "Output" input for master audio + vMixInputs[i].isOutput = vMixInputs[i].title === "Output"; + + const meterF1 = input.getAttribute("meterF1"); + const meterF2 = input.getAttribute("meterF2"); + + if (meterF1 !== null && meterF2 !== null) { + vMixInputs[i].audio = true; + vMixInputs[i].muted = input.getAttribute("muted") === "True"; + + const leftLevel = parseFloat(meterF1); + const rightLevel = parseFloat(meterF2); + + vMixInputs[i].audioL = log10(leftLevel); + vMixInputs[i].audioR = log10(rightLevel); + } else { + vMixInputs[i].audio = false; } + if(input.getAttribute("duration")){ vMixInputs[i].video = true vMixInputs[i].duration = input.getAttribute("duration") @@ -245,6 +282,7 @@ function vMixRefresh(data){ panY, inputKey, inputName, + isOutput: lastResponse.querySelector(`input[key="${inputKey}"]`).getAttribute("title") === "Output" } } @@ -269,34 +307,63 @@ function vMixRefresh(data){ function updateVU(){ overlayArray.forEach((overlay, i) => { - if(vMixInputs[vMixInputs.findIndex(vMixInputs => vMixInputs.key === overlay.getAttribute("key"))].audio){ - $($(".outerContainer").children()[i]).find(".actualMeterL").css("clip-path", "inset(" + (Math.floor((100 - vMixInputs[vMixInputs.findIndex(vMixInputs => vMixInputs.key === overlay.getAttribute("key"))].audioL)/3.125)*3.125) + "% 0% 0% 0%)"); - $($(".outerContainer").children()[i]).find(".actualMeterR").css("clip-path", "inset(" + (Math.floor((100 - vMixInputs[vMixInputs.findIndex(vMixInputs => vMixInputs.key === overlay.getAttribute("key"))].audioR)/3.125)*3.125) + "% 0% 0% 0%)"); - if(vMixInputs[vMixInputs.findIndex(vMixInputs => vMixInputs.key === overlay.getAttribute("key"))].muted){ + const inputIndex = vMixInputs.findIndex(input => input.key === overlay.getAttribute("key")); + + // Check if multiViewOverlays[i] exists before accessing its properties + const outputBox = i < multiViewOverlays.length && multiViewOverlays[i] ? + multiViewOverlays[i].isOutput : false; + + // Show VU meters if the input has audio OR if it's the Output box (for master audio) + if ((inputIndex >= 0 && vMixInputs[inputIndex].audio) || outputBox) { + // For output box, always use master audio + // For regular inputs with audio, use their own levels + const useValues = outputBox && vMixInputs.master ? + { + audioL: vMixInputs.master.audioL, + audioR: vMixInputs.master.audioR, + muted: vMixInputs.master.muted + } : + { + audioL: inputIndex >= 0 ? vMixInputs[inputIndex].audioL : 0, + audioR: inputIndex >= 0 ? vMixInputs[inputIndex].audioR : 0, + muted: inputIndex >= 0 ? vMixInputs[inputIndex].muted : false + }; + + // The clip path should match the percentage directly + const leftClipPercent = Math.max(0, 100 - useValues.audioL); + const rightClipPercent = Math.max(0, 100 - useValues.audioR); + + // Apply clip path for proper visual display + $($(".outerContainer").children()[i]).find(".actualMeterL").css("clip-path", `inset(${leftClipPercent}% 0% 0% 0%)`); + $($(".outerContainer").children()[i]).find(".actualMeterR").css("clip-path", `inset(${rightClipPercent}% 0% 0% 0%)`); + + // Make sure meters are visible with proper opacity + $($(".outerContainer").children()[i]).find(".actualMeterL").css("opacity", "0.7"); + $($(".outerContainer").children()[i]).find(".actualMeterR").css("opacity", "0.7"); + + if (useValues.muted) { $($(".outerContainer").children()[i]).find(".actualMeterR").css("filter", "grayscale(100%)"); $($(".outerContainer").children()[i]).find(".actualMeterL").css("filter", "grayscale(100%)"); - } - else{ + } else { $($(".outerContainer").children()[i]).find(".actualMeterR").css("filter", "grayscale(0%)"); $($(".outerContainer").children()[i]).find(".actualMeterL").css("filter", "grayscale(0%)"); - } } }); } -function generateVU(bool, overlay){ +function generateVU(bool, overlay, isMaster = false){ if(bool){ return(`
-
+
-
+
0
@@ -337,19 +404,33 @@ function refresh(){ saveArray[i][setting] = extra[setting] }) }); - $("#saveURL").val("https://multiviewbeta.multicam.media?Settings=" + encodeURIComponent(JSON.stringify(saveArray))) + $("#saveURL").val("https://multiview.multicam.media?Settings=" + encodeURIComponent(JSON.stringify(saveArray))) clockTick(); $(".outerContainer").html("") multiViewOverlays.forEach(overlay => { + // Determine if this is an Output overlay + const isOutput = overlay.isOutput; + + // Find the input index - we'll need this to check for audio + const inputIndex = vMixInputs.findIndex(input => input.key === overlay.inputKey); + + // Generate HTML for the multiview box $(".outerContainer").append(` -
${lastResponse.querySelector(`input[key="${overlay.inputKey}"]`).getAttribute("title")}
- ${generateVU(vMixInputs[vMixInputs.findIndex(vMixInputs => vMixInputs.key === overlay.inputKey)].audio, overlay)} + ${generateVU( + // Show VU meters if: + // 1. The input has audio OR + // 2. It's the Output box (for master audio) + (inputIndex >= 0 && vMixInputs[inputIndex]?.audio) || isOutput, + overlay, + isOutput + )}
`) }); @@ -499,4 +580,4 @@ extras.forEach(extra => { extra.color = e.target.value; refresh(); }); -}) \ No newline at end of file +}) From c56b9a79a593fb0867693fcbc00942a956a280e6 Mon Sep 17 00:00:00 2001 From: Mitch Clifford Date: Fri, 28 Mar 2025 14:23:39 +1100 Subject: [PATCH 2/2] Adjust audio meter color for visibility --- style.css | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/style.css b/style.css index 27180d5..65b960f 100644 --- a/style.css +++ b/style.css @@ -89,10 +89,11 @@ div#clock { } .actualMeter { + transition-duration: 0s; height: 90%; width: 22%; /* transition: all 0.05s ease; */ - background: linear-gradient(0deg, #4CAF50 0%, #4CAF50 53.2%, #FFEB3B 36%, #FFEB3B 85%, #F44336 59%, rgb(221 81 81 / 1) 100%); + background: linear-gradient(0deg, #0ceb14 0%, #4aff50 53.2%, #FFEB3B 36%, #FFEB3B 85%, #F44336 59%, rgb(221 81 81 / 1) 100%); background-position: center top; background-repeat: no-repeat; background-size: contain; @@ -102,12 +103,12 @@ div#clock { /* clip-path: inset(0% 0% 0% 0%) !important; */ -webkit-mask-position: bottom; position: absolute; - opacity: 0.7; + opacity: 1; } .leftAlignContainer { height: 74%; - background-color: rgba(0,0,0,0.3); + background-color: rgba(0,0,0,0.8); border-radius: 5px; position: absolute; left: 4px; @@ -115,6 +116,12 @@ div#clock { bottom: 8px; } +.outputBox .leftAlignContainer { + /* Make the VU meters more prominent for the output box */ + width: 10%; + height: 80%; +} + .VUlabel { font-size: 1.4rem; color: gainsboro; @@ -196,10 +203,10 @@ div#clock { } .settings { - position: absolute; + /* position: absolute; */ bottom: 0; width: 100%; - height: 71%; + height: 51%; display: flex; z-index: 20; padding: 10px 0; @@ -308,7 +315,7 @@ div#countdown { position: absolute; bottom: 0; width: 100%; - height: 15%; + height: 17%; background-color: rgba(0,0,0,0.5); opacity: 0; z-index: 100; @@ -316,10 +323,14 @@ div#countdown { .settingsURL { display: flex; - width: 100%; - margin: 1%; + /* width: 100%; */ + padding: 0.8% 0; + margin: 0 1.8%; justify-content: center; align-items: center; + border-bottom-style: solid; + border-color: white; + border-width: 0.5px; } .hideContainer:hover { @@ -338,4 +349,4 @@ input#saveURL { font-weight: bold; text-shadow: 1px 1px 2px black; padding-right: 10px; -} \ No newline at end of file +}