Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 108 additions & 27 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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")
Expand Down Expand Up @@ -245,6 +282,7 @@ function vMixRefresh(data){
panY,
inputKey,
inputName,
isOutput: lastResponse.querySelector(`input[key="${inputKey}"]`).getAttribute("title") === "Output"
}

}
Expand All @@ -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(`
<div class="leftAlignContainer">
<div class="audioMeterContainer">
<div class="audioMeter">
<div class="meterOutline"></div>
<div class="actualMeter actualMeterL"></div>
<div class="actualMeter actualMeterL" style="opacity: 0.7;"></div>
</div>
<div class="audioMeter">
<div class="meterOutline"></div>
<div class="actualMeter actualMeterR"></div>
<div class="actualMeter actualMeterR" style="opacity: 0.7;"></div>
</div>
<div class="VUlabels">
<div class="VUlabel" style="transform: scale(${overlay.zoomY / 100})">0</div>
Expand Down Expand Up @@ -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(`
<div class="multiViewBox" style="
<div class="multiViewBox${isOutput ? ' outputBox' : ''}" style="
height: ${overlay.zoomY}%;
width: ${overlay.zoomX}%;
left: ${((overlay.panX)/2) + 50}%;
top: ${((overlay.panY*-1)/2) + 50}%;
">
<div class="inputTitle">${lastResponse.querySelector(`input[key="${overlay.inputKey}"]`).getAttribute("title")}</div>
${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
)}
</div>
`)
});
Expand Down Expand Up @@ -499,4 +580,4 @@ extras.forEach(extra => {
extra.color = e.target.value;
refresh();
});
})
})
29 changes: 20 additions & 9 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -102,19 +103,25 @@ 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;
width: 7%;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -308,18 +315,22 @@ 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;
}

.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 {
Expand All @@ -338,4 +349,4 @@ input#saveURL {
font-weight: bold;
text-shadow: 1px 1px 2px black;
padding-right: 10px;
}
}