diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js index e9d90c3..7f136d8 100644 --- a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js +++ b/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js @@ -1,10 +1,5 @@ class ShowScene extends MeldStudioPlugin { - errorState = [ - { - image: "assets/sceneOn", - title: "", - }, - ]; + sceneRequested = null; constructor() { super("co.meldstudio.streamdeck.show-scene"); @@ -13,22 +8,37 @@ class ShowScene extends MeldStudioPlugin { const { scene } = this.getSettings(context); if (!scene) return; + this.sceneRequested = scene; + this.updateState(context); + if ($MS.meld?.showScene) $MS.meld.showScene(scene); }); $MS.on("sessionChanged", (session) => { + this.sceneRequested = null; + this.forAllContexts((context, settings) => { - const { scene } = settings; - if (!scene) return; + this.updateState(context); + }); + }); + } - if (!session.items[scene]) - return $SD.setState(context, 0); + updateState(context) { + const { scene } = this.getSettings(context); + const session = $MS?.meld?.session; - const state = session.items[scene].current ? 1 : 0; + const state = (() => { + if (!scene) return 0; + if (!session || !session?.items) return 0; - $SD.setState(context, state); - }); - }); + const item = session.items[scene]; + + if (!item) return 0; + if (scene == this.sceneRequested) return 1; + return item.current ? 1 : 0; + })(); + + $SD.setState(context, state); } } diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js index e6056e1..ec11ee1 100644 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js +++ b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js @@ -13,7 +13,7 @@ class ToggleLayer extends MeldStudioPlugin { this.forAllContexts((context, settings) => { const { layer } = settings; if (!layer) return; - if (!session.items[layer]) + if (!session?.items || !session?.items[layer]) return $SD.setState(context, 0); const state = session.items[layer].visible ? 1 : 0; diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html new file mode 100644 index 0000000..0e5d504 --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html @@ -0,0 +1,46 @@ + + + + + + co.meldstudio.streamdeck.volume-stepper Property Inspector + + + + +
+
+
Track
+ +
+
+
Step Size
+ +
+ + +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js new file mode 100644 index 0000000..7ddbdaf --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js @@ -0,0 +1,195 @@ +class VolumeStepper extends MeldStudioPlugin { + trackInfo = {}; + unregisterCallbacks = {}; + + constructor() { + super("co.meldstudio.streamdeck.volume-stepper"); + + this.action.onKeyUp(({ action, context, device, event, payload }) => { + const { track } = this.getSettings(context); + if (!track) return; + + if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); + }); + + this.action.onDialRotate(({ context, payload }) => { + const { track, stepsize: stepString } = this.getSettings(context); + const stepsize = 0.01 * parseFloat(stepString); + const info = this.trackInfo[track]; + + if (!track) return; + + let gain = +stepsize * payload.ticks + (info?.gain ?? 0.0); + gain = gain < 0 ? 0 : gain; + gain = gain > 1 ? 1 : gain; + + // Store the new gain until the callback fires. + this.trackInfo[track] = { ...info, gain }; + + if ($MS.meld?.setGain) $MS.meld?.setGain(track, gain); + }); + + this.action.onDialPress(({ action, context, device, event, payload }) => { + if (!payload.pressed) return; + + const { track } = this.getSettings(context); + if (!track) return; + + if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); + }); + + this.action.onTouchTap(({ action, context, device, event, payload }) => { + if (payload.hold) return; + + const { tapPos } = payload; + const { track } = this.getSettings(context); + if (!track) return; + + // Indicator starts at x=76 and is 108 pixels wide. + if (tapPos[0] > 80) { + if (tapPos[0] > 180) return; + + const info = this.trackInfo[track]; + const gain = (tapPos[0] - 80) / 100; + + // Store the new gain until the callback fires. + this.trackInfo[track] = { ...info, gain }; + + if ($MS.meld?.setGain) $MS.meld?.setGain(track, gain); + } else { + if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); + } + }); + + this.action.onDidReceiveSettings(({ context, payload }) => { + this.setSettings(context, payload?.settings ?? {}); + + if ($MS.ready) { + this.onReady(context, payload?.settings); + } else { + // If we lose connection, we may need to reinitialize. + $MS.on("ready", () => { + this.onReady(context, payload?.settings); + }); + + this.setGainAndMute(context, { + gain: 0.0, + muted: true, + }); + } + }); + + $MS.on("sessionChanged", (session) => { + this.forAllContexts((context, { track }) => { + if (!track) return; + if (!session?.items) return; + if (!session?.items[track]) return; + + const { name, muted } = session.items[track]; + const state = muted ? 0 : 1; + + this.trackInfo[track] = { ...this.trackInfo[track], name, muted }; + + this.setGainAndMute(context, this.trackInfo[track]); + }); + }); + + $MS.on("gainChanged", ({ trackId, gain, muted }) => { + let info = this.trackInfo[trackId]; + info = { ...info, gain, muted }; + this.trackInfo[trackId] = info; + + this.forAllContexts((context, { track }) => { + if (!track || trackId != track) return; + this.setGainAndMute(context, info); + }); + }); + + $MS.on("closed", () => { + // reset all registrations. + this.unregisterCallbacks = {}; + }); + } + + setGainAndMute(context, { gain, muted, name }) { + // meter colors - + // green: #6DDE92 + // orange: #FB923C + // red: #F04A4A + + const info = (() => { + if (!muted) { + if (gain > 0.4) return { icon: "assets/iconAudioTrack" }; + if (gain > 0.0) return { icon: "assets/audioUnmuted40" }; + } + return { icon: "assets/audioMute" }; + })(); + + $SD.setFeedback(context, { + ...info, + title: name ?? "Adjust Volume", + value: `${parseInt(gain * 100)}%`, + indicator: { + value: gain * 100, + enabled: true, + bar_bg_c: muted ? "0:#666666,1:#666666" : "0:#6DDE92,1:#6DDE92", + }, + }); + } + + register(context, track) { + console.assert($MS.meld); + + const callbackInfo = this.unregisterCallbacks[context]; + // Only register once. + if (callbackInfo?.track === track) return; + + this.maybeUnregister(context, track); + $MS.meld.registerTrackObserver(context, track); + + this.unregisterCallbacks[context] = { + callback: () => { + if ($MS.meld) $MS.meld.unregisterTrackObserver(context, track); + }, + track, + }; + } + + maybeUnregister(context) { + const callbackInfo = this.unregisterCallbacks[context]; + if (!callbackInfo) return; + + callbackInfo.callback(); + this.unregisterCallbacks[context] = undefined; + } + + getNameForTrack(track) { + const defaultName = "Adjust Volume"; + if (!$MS?.meld?.session?.items) return defaultName; + + const name = $MS.meld.session.items[track]?.name; + return name ? name : defaultName; + } + + connectGain(context, track) { + this.trackInfo[track] = { + gain: 0.0, + muted: false, + name: this.getNameForTrack(track), + }; + + this.setGainAndMute(context, this.trackInfo[track]); + this.register(context, track); + + this.action.onWillDisappear(({ context }) => { + this.maybeUnregister(context); + }); + } + + onReady(context, { track }) { + console.assert($MS.ready); + if (this.isEncoder(context)) this.connectGain(context, track); + } +} + +const volumeStepper = new VolumeStepper(); diff --git a/co.meldstudio.streamdeck.sdPlugin/app.html b/co.meldstudio.streamdeck.sdPlugin/app.html index 308114b..7acc506 100644 --- a/co.meldstudio.streamdeck.sdPlugin/app.html +++ b/co.meldstudio.streamdeck.sdPlugin/app.html @@ -33,5 +33,6 @@ + diff --git a/co.meldstudio.streamdeck.sdPlugin/assets/audioMute.svg b/co.meldstudio.streamdeck.sdPlugin/assets/audioMute.svg new file mode 100644 index 0000000..6f960bc --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/assets/audioMute.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40.svg b/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40.svg new file mode 100644 index 0000000..af9f33c --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40.svg @@ -0,0 +1,4 @@ + + + + diff --git a/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40@2x.svg b/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40@2x.svg new file mode 100644 index 0000000..604aa27 --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/assets/audioUnmuted40@2x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker.png b/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker.png new file mode 100644 index 0000000..92e1051 Binary files /dev/null and b/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker.png differ diff --git a/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker@2x.png b/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker@2x.png new file mode 100644 index 0000000..1955990 Binary files /dev/null and b/co.meldstudio.streamdeck.sdPlugin/assets/iconSpeaker@2x.png differ diff --git a/co.meldstudio.streamdeck.sdPlugin/de.json b/co.meldstudio.streamdeck.sdPlugin/de.json index 21da1d7..5ce6177 100644 --- a/co.meldstudio.streamdeck.sdPlugin/de.json +++ b/co.meldstudio.streamdeck.sdPlugin/de.json @@ -26,6 +26,17 @@ "Name": "Go Live", "Tooltip": "Starten oder stoppen Sie einen Livestream." }, + "co.meldstudio.streamdeck.volume-stepper": { + "Name": "Lautstärkeregler", + "Tooltip": "Steuern Sie die Lautstärke einer bestimmten Audiospur.", + "Encoder": { + "TriggerDescription": { + "Rotate": "Lautstärkeregler", + "Touch": "Lautstärkeregler", + "Push": "Spur stumm" + } + } + }, "Localization": { "Scene": "Szene", "Layer": "Layer", diff --git a/co.meldstudio.streamdeck.sdPlugin/en.json b/co.meldstudio.streamdeck.sdPlugin/en.json index e2bc48b..be042e8 100644 --- a/co.meldstudio.streamdeck.sdPlugin/en.json +++ b/co.meldstudio.streamdeck.sdPlugin/en.json @@ -26,11 +26,23 @@ "Name": "Go Live", "Tooltip": "Start or stop a live stream." }, + "co.meldstudio.streamdeck.volume-stepper": { + "Name": "Adjust Volume", + "Tooltip": "Adjust the volume of a specified audio track.", + "Encoder": { + "TriggerDescription": { + "Rotate": "Adjust Volume", + "Touch": "Adjust Volume", + "Push": "Mute Track" + } + } + }, "Localization": { "Scene": "Scene", "Layer": "Layer", "Track": "Track", "Effect": "Effect", + "StepSize": "Step Size", "Connecting": "Connecting to Meld Studio..." } } \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js index 92d6833..0063c17 100644 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js +++ b/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js @@ -106,6 +106,37 @@ class MeldStudioPropertyInspector { $PI.getSettings(); } + initializeText(action, elements) { + for (let id of elements) { + const el = document.getElementById(id); + console.assert(el.id, 'Input element not found'); + if (!el) continue; + + this.settings[el] = ''; + + el.onchange = () => { + if (!this.settings) return; + this.settings = { ...this.settings, [id]: el.value }; + $PI.setSettings(this.settings); + }; + } + + $PI.onDidReceiveSettings(action, ({ action, payload }) => { + const { settings } = payload; + this.settings = settings; + + for (let field of elements) { + const dom_field = document.getElementById(field); + if (settings[field] !== undefined) { + dom_field.value = settings[field]; + } else { + // Use the default value specified by the field. + this.settings[field] = dom_field.value; + } + } + }); + } + watchConnections(id) { $MS.on('connected', () => { document.getElementById(id).style = 'display: none;'; diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js index abd4623..2261105 100644 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js +++ b/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js @@ -42,6 +42,10 @@ class MeldStudio { this.emit('isRecordingChanged', this.meld.isRecording); }); + this.meld.gainUpdated.connect((trackId, gain, muted) => { + this.emit('gainChanged', { trackId, gain, muted }); + }); + this.ready = true; this.emit('ready'); this.emit('sessionChanged', this.meld.session); @@ -73,20 +77,39 @@ class MeldStudioPlugin { constructor(UUID) { this.action = new Action(UUID); - this.action.onWillAppear(({ context }) => { - this.contexts[context] = { settings: {} }; + this.action.onWillAppear(({ context, payload }) => { + this.contexts[context] = { + isEncoder: payload.controller == 'Encoder', + settings: {}, + }; $SD.getSettings(context); }); this.action.onDidReceiveSettings(({ context, payload }) => { + const oldSettings = this.getSettings(context); this.setSettings(context, payload?.settings ?? {}); + + if ($MS.ready) { + this.onReady(context, payload?.settings, oldSettings); + } + + // If we lose connection, we may need to reinitialize. + $MS.on('ready', () => { + this.onReady(context, payload?.settings, oldSettings); + }); }); } + onReady() {} + setSettings(context, settings) { this.contexts[context].settings = settings; } + isEncoder(context) { + return this.contexts[context].isEncoder; + } + getSettings(context) { return this.contexts[context].settings; } diff --git a/co.meldstudio.streamdeck.sdPlugin/manifest.json b/co.meldstudio.streamdeck.sdPlugin/manifest.json index e6dcee0..8e68a38 100644 --- a/co.meldstudio.streamdeck.sdPlugin/manifest.json +++ b/co.meldstudio.streamdeck.sdPlugin/manifest.json @@ -8,7 +8,7 @@ "CodePath": "app.html", "Description": "Control Meld Studio.", "URL": "https://streamwithmeld.com", - "Version": "0.1.0", + "Version": "0.2.0", "OS": [ { "Platform": "mac", @@ -51,6 +51,30 @@ "UUID": "co.meldstudio.streamdeck.toggle-mute", "PropertyInspectorPath": "actions/toggle-mute/inspector.html" }, + { + "Icon": "assets/iconAudioTrack", + "Name": "Adjust Volume", + "States": [ + { + "Image": "assets/iconSpeaker", + "TitleColor": "#FFFFFF" + } + ], + "Controllers": [ + "Encoder" + ], + "Encoder": { + "layout": "$B2", + "TriggerDescription": { + "Rotate": "Adjust Volume", + "Touch": "Adjust Volume", + "Push": "Mute Track" + } + }, + "Tooltip": "Adjust the volume of a specified audio track.", + "UUID": "co.meldstudio.streamdeck.volume-stepper", + "PropertyInspectorPath": "actions/volume-stepper/inspector.html" + }, { "Icon": "assets/iconLayerVisibility", "Name": "Layer Visibility",