From b79a67686d4730b501822252b68e93d98f994df1 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 11 Oct 2020 01:10:41 +0100 Subject: [PATCH 01/45] Allow mic source channels to be selected. --- src/mixer/audio.ts | 39 ++++++++++++++++++++++++++++++++++++-- src/mixer/state.ts | 12 ++++++------ src/optionsMenu/MicTab.tsx | 24 ++++++++++++++++++++++- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 37bbbe2..9d8a8a6 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -233,6 +233,13 @@ export type LevelsSource = | "player-1" | "player-2"; +export type ChannelMapping = + | "stereo-normal" + | "stereo-flipped" + | "mono-left" + | "mono-right" + | "mono-both"; + // Setting this directly affects the performance of .getFloatTimeDomainData() // Must be a power of 2. const ANALYSIS_FFT_SIZE = 2048; @@ -380,7 +387,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.players[number] = undefined; } - async openMic(deviceId: string) { + async openMic(deviceId: string, channelMapping: ChannelMapping) { if (this.micSource !== null && this.micMedia !== null) { this.micMedia.getAudioTracks()[0].stop(); this.micSource.disconnect(); @@ -400,8 +407,36 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.micSource = this.audioContext.createMediaStreamSource(this.micMedia); - this.micSource.connect(this.micCalibrationGain); + // Handle stereo mic sources. + const splitterNode = this.audioContext.createChannelSplitter(2); + const mergerNode = this.audioContext.createChannelMerger(2); + this.micSource.connect(splitterNode); + switch (channelMapping) { + case "stereo-normal": + splitterNode.connect(mergerNode, 0, 0); + splitterNode.connect(mergerNode, 1, 1); + break; + case "stereo-flipped": + splitterNode.connect(mergerNode, 1, 0); + splitterNode.connect(mergerNode, 0, 1); + break; + case "mono-left": + splitterNode.connect(mergerNode, 0, 0); + splitterNode.connect(mergerNode, 0, 1); + break; + case "mono-right": + splitterNode.connect(mergerNode, 1, 0); + splitterNode.connect(mergerNode, 1, 1); + break; + case "mono-both": + default: + splitterNode.connect(mergerNode, 0, 0); + splitterNode.connect(mergerNode, 1, 0); + splitterNode.connect(mergerNode, 0, 1); + splitterNode.connect(mergerNode, 1, 1); + } + mergerNode.connect(this.micCalibrationGain); this.emit("micOpen"); } diff --git a/src/mixer/state.ts b/src/mixer/state.ts index e9d3df7..58f6f91 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -12,7 +12,7 @@ import Keys from "keymaster"; import { Track, MYRADIO_NON_API_BASE, AuxItem } from "../api"; import { AppThunk } from "../store"; import { RootState } from "../rootReducer"; -import { audioEngine } from "./audio"; +import { audioEngine, ChannelMapping } from "./audio"; import * as TheNews from "./the_news"; const playerGainTweens: Array<{ @@ -691,10 +691,10 @@ export const setChannelTrim = (player: number, val: number): AppThunk => async ( audioEngine.players[player]?.setTrim(val); }; -export const openMicrophone = (micID: string): AppThunk => async ( - dispatch, - getState -) => { +export const openMicrophone = ( + micID: string, + micMapping: ChannelMapping +): AppThunk => async (dispatch, getState) => { // TODO: not sure why this is here, and I have a hunch it may break shit, so disabling // File a ticket if it breaks stuff. -Marks // if (getState().mixer.mic.open) { @@ -711,7 +711,7 @@ export const openMicrophone = (micID: string): AppThunk => async ( return; } try { - await audioEngine.openMic(micID); + await audioEngine.openMic(micID, micMapping); } catch (e) { if (e instanceof DOMException) { switch (e.message) { diff --git a/src/optionsMenu/MicTab.tsx b/src/optionsMenu/MicTab.tsx index 0d2040f..39285be 100644 --- a/src/optionsMenu/MicTab.tsx +++ b/src/optionsMenu/MicTab.tsx @@ -4,6 +4,7 @@ import { RootState } from "../rootReducer"; import * as MixerState from "../mixer/state"; import { VUMeter } from "./helpers/VUMeter"; +import { ChannelMapping } from "../mixer/audio"; type MicErrorEnum = | "NO_PERMISSION" @@ -26,6 +27,9 @@ export function MicTab() { const [micList, setMicList] = useState(null); const dispatch = useDispatch(); const [nextMicSource, setNextMicSource] = useState("$NONE"); + const [nextMicMapping, setNextMicMapping] = useState( + "mono-both" + ); const [openError, setOpenError] = useState(null); async function fetchMicNames() { @@ -65,7 +69,12 @@ export function MicTab() { function setMicSource(sourceId: string) { setNextMicSource(sourceId); - dispatch(MixerState.openMicrophone(sourceId)); + dispatch(MixerState.openMicrophone(sourceId, nextMicMapping)); + } + + function setMicMapping(mapping: ChannelMapping) { + setNextMicMapping(mapping); + dispatch(MixerState.openMicrophone(nextMicSource, mapping)); } return ( @@ -110,6 +119,19 @@ export function MicTab() { : "An error occurred when opening the microphone. Please try again."} )} + +

Calibration

From 6b03054b59a471c9167d53f37ff1715e91a9464b Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 11 Oct 2020 02:24:17 +0100 Subject: [PATCH 02/45] Allow enabling/disabling of mic processing. --- src/mixer/audio.ts | 19 +++++++++++++++---- src/mixer/state.ts | 21 +++++++++++++++------ src/optionsMenu/MicTab.tsx | 13 +++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 9d8a8a6..ca235a3 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -336,10 +336,9 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.micMixGain = this.audioContext.createGain(); this.micMixGain.gain.value = 1; - this.micCalibrationGain.connect(this.micPrecompAnalyser); - this.micCalibrationGain - .connect(this.micCompressor) - .connect(this.micMixGain) + // We run setMicProcessingEnabled() later to either patch to the compressor, or bypass it to the mixGain node. + this.micCompressor.connect(this.micMixGain); + this.micMixGain .connect(this.micFinalAnalyser) // we don't run the mic into masterAnalyser to ensure it doesn't go to audioContext.destination .connect(this.streamingAnalyser); @@ -449,6 +448,18 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.micMixGain.gain.value = value; } + setMicProcessingEnabled(value: boolean) { + // Disconnect whatever was connected before. + this.micCalibrationGain.disconnect(); + this.micCalibrationGain.connect(this.micPrecompAnalyser); + console.log("Setting mic processing to: ", value); + if (value) { + this.micCalibrationGain.connect(this.micCompressor); + } else { + this.micCalibrationGain.connect(this.micMixGain); + } + } + getLevels(source: LevelsSource, stereo: boolean): [number, number] { switch (source) { case "mic-precomp": diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 58f6f91..faf6dd5 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -53,6 +53,7 @@ interface MicState { volume: 1 | 0; baseGain: number; id: string | null; + processing: boolean; } interface MixerState { @@ -88,6 +89,7 @@ const mixerState = createSlice({ baseGain: 0, openError: null, id: "None", + processing: true, }, } as MixerState, reducers: { @@ -210,6 +212,9 @@ const mixerState = createSlice({ setMicBaseGain(state, action: PayloadAction) { state.mic.baseGain = action.payload; }, + setMicProcessingEnabled(state, action: PayloadAction) { + state.mic.processing = action.payload; + }, setTimeCurrent( state, action: PayloadAction<{ @@ -695,11 +700,6 @@ export const openMicrophone = ( micID: string, micMapping: ChannelMapping ): AppThunk => async (dispatch, getState) => { - // TODO: not sure why this is here, and I have a hunch it may break shit, so disabling - // File a ticket if it breaks stuff. -Marks - // if (getState().mixer.mic.open) { - // micSource?.disconnect(); - // } if (audioEngine.audioContext.state !== "running") { console.log("Resuming AudioContext because Chrome bad"); await audioEngine.audioContext.resume(); @@ -730,10 +730,19 @@ export const openMicrophone = ( const state = getState().mixer.mic; audioEngine.setMicCalibrationGain(state.baseGain); audioEngine.setMicVolume(state.volume); - + // Now to patch in the Mic to the Compressor, or Bypass it. + audioEngine.setMicProcessingEnabled(state.processing); dispatch(mixerState.actions.micOpen(micID)); }; +export const setMicProcessingEnabled = (enabled: boolean): AppThunk => async ( + dispatch, + _ +) => { + dispatch(mixerState.actions.setMicProcessingEnabled(enabled)); + audioEngine.setMicProcessingEnabled(enabled); +}; + export const setMicVolume = (level: MicVolumePresetEnum): AppThunk => ( dispatch ) => { diff --git a/src/optionsMenu/MicTab.tsx b/src/optionsMenu/MicTab.tsx index 39285be..bc43605 100644 --- a/src/optionsMenu/MicTab.tsx +++ b/src/optionsMenu/MicTab.tsx @@ -132,6 +132,19 @@ export function MicTab() {
From b4149038f2e42bf57b6bc9af2608cb84bc912d1a Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 12 Oct 2020 22:36:25 +0100 Subject: [PATCH 07/45] Add some user description. --- src/optionsMenu/AdvancedTab.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/optionsMenu/AdvancedTab.tsx b/src/optionsMenu/AdvancedTab.tsx index 2de09f0..579ad8c 100644 --- a/src/optionsMenu/AdvancedTab.tsx +++ b/src/optionsMenu/AdvancedTab.tsx @@ -165,6 +165,12 @@ export function AdvancedTab() {

Channel Outputs

+

+ Select a sound output for each channel. Internal routes + directly to the WebStudio stream/recorder. Other outputs will disable + ProMode ™ features.{" "} + Routing will apply upon loading a new item. +

From 489af270457325bf3daf15a86ab4d5b60999df60 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 12 Oct 2020 23:01:34 +0100 Subject: [PATCH 08/45] Handle missing devices in output selections --- src/optionsMenu/AdvancedTab.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/optionsMenu/AdvancedTab.tsx b/src/optionsMenu/AdvancedTab.tsx index 579ad8c..dd34915 100644 --- a/src/optionsMenu/AdvancedTab.tsx +++ b/src/optionsMenu/AdvancedTab.tsx @@ -21,18 +21,20 @@ function ChannelOutputSelect({ outputList: MediaDeviceInfo[] | null; channel: number; }) { - const settings = useSelector((state: RootState) => state.settings); + const outputIds = useSelector( + (state: RootState) => state.settings.channelOutputIds + ); + const outputId = outputIds[channel]; const dispatch = useDispatch(); - return (
- {trimVal} dB + {trimVal} dB )} + {activeButton === "autoDuck" && ( + + Duck on Mic: {micAutoDuck ? "Yes" : "No"} + + )}
); From 938d491d2659362eccef015c993acd84ca519124 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 15:52:24 +0000 Subject: [PATCH 15/45] Missing space. Co-authored-by: Marks Polakovs --- src/mixer/audio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 9279fcf..65665da 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -236,7 +236,7 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { try { instance.setOutputDevice(outputId); } catch (e) { - console.error("Failed to set channel " + player + " output." + e); + console.error("Failed to set channel " + player + " output. " + e); } } else { (wavesurfer as any).backend.gainNode.disconnect(); From 335e4f8eb07ff23a7146ae04b05095b3b7f27747 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 19:18:35 +0000 Subject: [PATCH 16/45] Updated mic meter green area --- src/optionsMenu/MicTab.tsx | 2 +- src/showplanner/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/optionsMenu/MicTab.tsx b/src/optionsMenu/MicTab.tsx index 3934c85..1702345 100644 --- a/src/optionsMenu/MicTab.tsx +++ b/src/optionsMenu/MicTab.tsx @@ -156,7 +156,7 @@ export function MicTab() { height={40} source="mic-precomp" range={[-70, 0]} - greenRange={state.processing ? [-14, -10] : [-10, -5]} + greenRange={state.processing ? [-16, -6] : [-32, -5]} stereo={true} /> diff --git a/src/showplanner/index.tsx b/src/showplanner/index.tsx index 426c541..4909e5c 100644 --- a/src/showplanner/index.tsx +++ b/src/showplanner/index.tsx @@ -248,7 +248,7 @@ function MicControl() { height={40} source="mic-final" range={[-40, 3]} - greenRange={[-10, -5]} + greenRange={[-16, -6]} stereo={proMode} /> From febc96cedc02a6409917ac4beafc1d92d67f521a Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 19:21:23 +0000 Subject: [PATCH 17/45] Make Mic VU meter follow stereo metering. --- src/showplanner/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/showplanner/index.tsx b/src/showplanner/index.tsx index 4909e5c..1935f37 100644 --- a/src/showplanner/index.tsx +++ b/src/showplanner/index.tsx @@ -194,6 +194,9 @@ function LibraryColumn() { function MicControl() { const state = useSelector((state: RootState) => state.mixer.mic); const proMode = useSelector((state: RootState) => state.settings.proMode); + const stereo = useSelector( + (state: RootState) => state.settings.channelVUsStereo + ); const dispatch = useDispatch(); return ( @@ -249,7 +252,7 @@ function MicControl() { source="mic-final" range={[-40, 3]} greenRange={[-16, -6]} - stereo={proMode} + stereo={proMode && stereo} />
From ebb9e6cfd4a541d5be90aca07e2f7370f30ac998 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 19:42:23 +0000 Subject: [PATCH 18/45] Use a ternary for channel VUs --- src/showplanner/Player.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/showplanner/Player.tsx b/src/showplanner/Player.tsx index bb580c7..43d8062 100644 --- a/src/showplanner/Player.tsx +++ b/src/showplanner/Player.tsx @@ -394,12 +394,11 @@ export function Player({ id }: { id: number }) { {settings.proMode && settings.channelVUs && (
- {customOutput && ( + {customOutput ? ( Custom audio output disables VU meters. - )} - {!customOutput && ( + ) : ( Date: Mon, 2 Nov 2020 20:07:19 +0000 Subject: [PATCH 19/45] Only reset played when going live, fixes #180 --- src/broadcast/rtc_streamer.ts | 3 +++ src/broadcast/state.ts | 2 +- src/broadcast/streamer.ts | 1 + src/mixer/the_news.ts | 3 +-- src/navbar/index.tsx | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/broadcast/rtc_streamer.ts b/src/broadcast/rtc_streamer.ts index 4dcdbd6..8479bd2 100644 --- a/src/broadcast/rtc_streamer.ts +++ b/src/broadcast/rtc_streamer.ts @@ -120,6 +120,9 @@ export class WebRTCStreamer extends Streamer { this.state = "ANSWER"; break; case "ACTIVATED": + if (!this.isActive) { + this.onStateChange("GOING_LIVE"); // Starting show. + } this.isActive = true; this.onStateChange("LIVE"); this.dispatch(BroadcastState.setTracklisting(true)); diff --git a/src/broadcast/state.ts b/src/broadcast/state.ts index e4a94ac..80590ac 100644 --- a/src/broadcast/state.ts +++ b/src/broadcast/state.ts @@ -316,7 +316,7 @@ export const goOnAir = (): AppThunk => async (dispatch, getState) => { } else if (state === "CONNECTED") { // okay, we've connected dispatch(registerForShow()); - } else if (state === "LIVE") { + } else if (state === "GOING_LIVE") { dispatch(setItemPlayed({ itemId: "all", played: false })); } }); diff --git a/src/broadcast/streamer.ts b/src/broadcast/streamer.ts index c898f30..2ad3435 100644 --- a/src/broadcast/streamer.ts +++ b/src/broadcast/streamer.ts @@ -4,6 +4,7 @@ export type ConnectionStateEnum = | "CONNECTED" | "CONNECTION_LOST" | "CONNECTION_LOST_RECONNECTING" + | "GOING_LIVE" | "LIVE"; export type ConnectionStateListener = (val: ConnectionStateEnum) => any; diff --git a/src/mixer/the_news.ts b/src/mixer/the_news.ts index 13ffe81..926a2e0 100644 --- a/src/mixer/the_news.ts +++ b/src/mixer/the_news.ts @@ -52,8 +52,7 @@ const considerDoingTheNews = (getState: () => RootState) => async () => { await actuallyDoTheNews(); } else if (state.settings.doTheNews === "while_live") { if ( - state.broadcast.connectionState === "CONNECTED" || - state.broadcast.connectionState === "LIVE" + state.broadcast.connectionState in ["CONNECTED", "GOING_LIVE", "LIVE"] ) { const transition = await broadcastApiRequest<{ autoNews: boolean; diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index 3348aa8..85245e4 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -40,6 +40,7 @@ function nicifyConnectionState(state: ConnectionStateEnum): string { case "NOT_CONNECTED": return "Not Connected"; case "LIVE": + case "GOING_LIVE": return "Live!"; default: console.warn("unhandled", state); From 779ddf2ff7b3fc322e812e500583a9c7a19fb619 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 20:19:14 +0000 Subject: [PATCH 20/45] How about this? --- src/mixer/the_news.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/mixer/the_news.ts b/src/mixer/the_news.ts index 926a2e0..676966d 100644 --- a/src/mixer/the_news.ts +++ b/src/mixer/the_news.ts @@ -51,17 +51,19 @@ const considerDoingTheNews = (getState: () => RootState) => async () => { if (state.settings.doTheNews === "always") { await actuallyDoTheNews(); } else if (state.settings.doTheNews === "while_live") { - if ( - state.broadcast.connectionState in ["CONNECTED", "GOING_LIVE", "LIVE"] - ) { - const transition = await broadcastApiRequest<{ - autoNews: boolean; - selSource: number; - switchAudioAtMin: number; - }>("/nextTransition", "GET", {}); - if (transition.autoNews) { - await actuallyDoTheNews(); - } + switch (state.broadcast.connectionState) { + case "CONNECTED": + case "GOING_LIVE": + case "LIVE": + const transition = await broadcastApiRequest<{ + autoNews: boolean; + selSource: number; + switchAudioAtMin: number; + }>("/nextTransition", "GET", {}); + if (transition.autoNews) { + await actuallyDoTheNews(); + } + break; } } }; From 5d424cf96fc901a99daf3b45fe3714aceaa660fd Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 20:19:14 +0000 Subject: [PATCH 21/45] Revert "How about this?" This reverts commit 779ddf2ff7b3fc322e812e500583a9c7a19fb619. --- src/mixer/the_news.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/mixer/the_news.ts b/src/mixer/the_news.ts index 676966d..926a2e0 100644 --- a/src/mixer/the_news.ts +++ b/src/mixer/the_news.ts @@ -51,19 +51,17 @@ const considerDoingTheNews = (getState: () => RootState) => async () => { if (state.settings.doTheNews === "always") { await actuallyDoTheNews(); } else if (state.settings.doTheNews === "while_live") { - switch (state.broadcast.connectionState) { - case "CONNECTED": - case "GOING_LIVE": - case "LIVE": - const transition = await broadcastApiRequest<{ - autoNews: boolean; - selSource: number; - switchAudioAtMin: number; - }>("/nextTransition", "GET", {}); - if (transition.autoNews) { - await actuallyDoTheNews(); - } - break; + if ( + state.broadcast.connectionState in ["CONNECTED", "GOING_LIVE", "LIVE"] + ) { + const transition = await broadcastApiRequest<{ + autoNews: boolean; + selSource: number; + switchAudioAtMin: number; + }>("/nextTransition", "GET", {}); + if (transition.autoNews) { + await actuallyDoTheNews(); + } } } }; From ccd9a8311ab2ad82165be69773195e41c17d051b Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Mon, 2 Nov 2020 20:34:30 +0000 Subject: [PATCH 22/45] Reduce Item renders Item used to subscribe to the full state of the player of its column, which meant it re-rendered whenever the player state updated (many times per second) - when all it needed is the ID of the loaded item. This commit scopes the selector more tightly. --- src/showplanner/Item.tsx | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/showplanner/Item.tsx b/src/showplanner/Item.tsx index c0242ba..a0202c0 100644 --- a/src/showplanner/Item.tsx +++ b/src/showplanner/Item.tsx @@ -25,14 +25,13 @@ export const Item = memo(function Item({ const isReal = "timeslotitemid" in x; const isGhost = "ghostid" in x; - const playerState = useSelector((state: RootState) => - column > -1 ? state.mixer.players[column] : undefined + const loadedItem = useSelector( + (state: RootState) => + column > -1 ? state.mixer.players[column]?.loadedItem : null, + (a, b) => a === null || b === null || itemId(a) === itemId(b) ); - const isLoaded = - playerState && - playerState.loadedItem !== null && - itemId(playerState.loadedItem) === id; + const isLoaded = loadedItem !== null ? itemId(loadedItem) === id : false; const showDebug = useSelector( (state: RootState) => state.settings.showDebugInfo @@ -59,14 +58,7 @@ export const Item = memo(function Item({ "item " + ("played" in x ? (x.played ? "played " : "") : "") + x.type + - `${ - column >= 0 && - playerState && - playerState.loadedItem !== null && - itemId(playerState.loadedItem) === id - ? " active" - : "" - }` + `${column >= 0 && isLoaded ? " active" : ""}` } onClick={triggerClick} {...provided.draggableProps} From 09b77eb7f5b3fc9290ac5c8c7c5a6a29d2ff7020 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 20:35:56 +0000 Subject: [PATCH 23/45] Don't bother resetting, the user can do it. --- src/broadcast/state.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/broadcast/state.ts b/src/broadcast/state.ts index 80590ac..e6497a7 100644 --- a/src/broadcast/state.ts +++ b/src/broadcast/state.ts @@ -316,8 +316,6 @@ export const goOnAir = (): AppThunk => async (dispatch, getState) => { } else if (state === "CONNECTED") { // okay, we've connected dispatch(registerForShow()); - } else if (state === "GOING_LIVE") { - dispatch(setItemPlayed({ itemId: "all", played: false })); } }); await streamer.start(); From 37f0748f1fec1a919568526185a8a9b73b2e2d5b Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 20:40:29 +0000 Subject: [PATCH 24/45] Remove unused import. --- src/broadcast/state.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/broadcast/state.ts b/src/broadcast/state.ts index e6497a7..ed86b3c 100644 --- a/src/broadcast/state.ts +++ b/src/broadcast/state.ts @@ -7,7 +7,6 @@ import * as NavbarState from "../navbar/state"; import { ConnectionStateEnum } from "./streamer"; import { RecordingStreamer } from "./recording_streamer"; import { audioEngine } from "../mixer/audio"; -import { setItemPlayed } from "../showplanner/state"; export let streamer: WebRTCStreamer | null = null; From 39a0dca8da209bf91eaa934a3a7fb279c312cd7a Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 2 Nov 2020 20:49:56 +0000 Subject: [PATCH 25/45] Revert "Only reset played when going live, fixes #180" This reverts commit 126d43aaa5ccb5423d3b722d34745c95d1496db9. --- src/broadcast/rtc_streamer.ts | 3 --- src/broadcast/streamer.ts | 1 - src/mixer/the_news.ts | 3 ++- src/navbar/index.tsx | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/broadcast/rtc_streamer.ts b/src/broadcast/rtc_streamer.ts index 8479bd2..4dcdbd6 100644 --- a/src/broadcast/rtc_streamer.ts +++ b/src/broadcast/rtc_streamer.ts @@ -120,9 +120,6 @@ export class WebRTCStreamer extends Streamer { this.state = "ANSWER"; break; case "ACTIVATED": - if (!this.isActive) { - this.onStateChange("GOING_LIVE"); // Starting show. - } this.isActive = true; this.onStateChange("LIVE"); this.dispatch(BroadcastState.setTracklisting(true)); diff --git a/src/broadcast/streamer.ts b/src/broadcast/streamer.ts index 2ad3435..c898f30 100644 --- a/src/broadcast/streamer.ts +++ b/src/broadcast/streamer.ts @@ -4,7 +4,6 @@ export type ConnectionStateEnum = | "CONNECTED" | "CONNECTION_LOST" | "CONNECTION_LOST_RECONNECTING" - | "GOING_LIVE" | "LIVE"; export type ConnectionStateListener = (val: ConnectionStateEnum) => any; diff --git a/src/mixer/the_news.ts b/src/mixer/the_news.ts index 926a2e0..13ffe81 100644 --- a/src/mixer/the_news.ts +++ b/src/mixer/the_news.ts @@ -52,7 +52,8 @@ const considerDoingTheNews = (getState: () => RootState) => async () => { await actuallyDoTheNews(); } else if (state.settings.doTheNews === "while_live") { if ( - state.broadcast.connectionState in ["CONNECTED", "GOING_LIVE", "LIVE"] + state.broadcast.connectionState === "CONNECTED" || + state.broadcast.connectionState === "LIVE" ) { const transition = await broadcastApiRequest<{ autoNews: boolean; diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index 85245e4..3348aa8 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -40,7 +40,6 @@ function nicifyConnectionState(state: ConnectionStateEnum): string { case "NOT_CONNECTED": return "Not Connected"; case "LIVE": - case "GOING_LIVE": return "Live!"; default: console.warn("unhandled", state); From 1ca07d75348a0541bea84fe5038eaaffd04b65c3 Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Mon, 2 Nov 2020 23:16:46 +0000 Subject: [PATCH 26/45] Bump to 1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68c0020..3883dd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webstudio", - "version": "1.3.0", + "version": "1.4.0", "private": true, "dependencies": { "@babel/core": "7.6.0", From aed8a4ef35f88e793afa6491f201039c7ad376a6 Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Sun, 8 Nov 2020 13:22:52 +0000 Subject: [PATCH 27/45] Fix loaded item BG --- src/showplanner/Item.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/showplanner/Item.tsx b/src/showplanner/Item.tsx index a0202c0..8e672c2 100644 --- a/src/showplanner/Item.tsx +++ b/src/showplanner/Item.tsx @@ -28,7 +28,9 @@ export const Item = memo(function Item({ const loadedItem = useSelector( (state: RootState) => column > -1 ? state.mixer.players[column]?.loadedItem : null, - (a, b) => a === null || b === null || itemId(a) === itemId(b) + (a, b) => + (a === null && b === null) || + (a !== null && b !== null && itemId(a) === itemId(b)) ); const isLoaded = loadedItem !== null ? itemId(loadedItem) === id : false; From 19ca99fbd2fc9dc5864695a2fe7912eb63e51315 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 10 Nov 2020 21:22:43 +0000 Subject: [PATCH 28/45] Split loaded track info out with separate selectors. --- src/showplanner/Player.tsx | 65 ++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/showplanner/Player.tsx b/src/showplanner/Player.tsx index e1ed3e0..1d9d4e0 100644 --- a/src/showplanner/Player.tsx +++ b/src/showplanner/Player.tsx @@ -197,6 +197,46 @@ function TimingButtons({ id }: { id: number }) { ); } +function LoadedTrackInfo({ id }: { id: number }) { + const dispatch = useDispatch(); + const loadedItem = useSelector( + (state: RootState) => state.mixer.players[id].loadedItem + ); + const loading = useSelector( + (state: RootState) => state.mixer.players[id].loading + ); + const loadError = useSelector( + (state: RootState) => state.mixer.players[id].loadError + ); + + return ( + + + {loadedItem !== null && loading === -1 + ? loadedItem.title + : loading !== -1 + ? `LOADING` + : loadError + ? "LOAD FAILED" + : "No Media Selected"} + + + Explicit + + + ); +} + export function Player({ id }: { id: number }) { // Define time remaining (secs) when the play icon should flash. const SECS_REMAINING_WARNING = 20; @@ -307,30 +347,7 @@ export function Player({ id }: { id: number }) {
{proMode && }
- - - {playerState.loadedItem !== null && playerState.loading === -1 - ? playerState.loadedItem.title - : playerState.loading !== -1 - ? `LOADING` - : playerState.loadError - ? "LOAD FAILED" - : "No Media Selected"} - - - Explicit - - +
{playerState.loadedItem !== null && playerState.loading === -1 From 45627021665ce681de0149b7dc97c55cf6d4dbba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Dec 2020 11:43:51 +0000 Subject: [PATCH 29/45] Bump ini from 1.3.5 to 1.3.8 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 71155b1..66b4a1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6228,9 +6228,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@6.5.0: version "6.5.0" From 8a66987876ec14ae89be3659deefea50dd333ad1 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 20:23:51 +0000 Subject: [PATCH 30/45] MVP UI/state for PFL buttons Also copied some bits from duck-when-honking branch --- src/mixer/audio.ts | 10 +++++++ src/mixer/state.ts | 19 ++++++++++++ src/showplanner/ProModeButtons.tsx | 48 ++++++++++++++++++++---------- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index b62d014..611af98 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -25,6 +25,7 @@ const PlayerEmitter: StrictEmitter< class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { private volume = 0; private trim = 0; + private pfl = false; private constructor( private readonly engine: AudioEngine, private wavesurfer: WaveSurfer, @@ -140,6 +141,11 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { this._applyVolume(); } + setPFL(enabled: boolean) { + this.pfl = enabled; + this._applyPFL(); + } + setOutputDevice(sinkId: string) { if (!this.customOutput) { throw Error( @@ -169,6 +175,10 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { } } + _applyPFL() { + console.log("Do something"); + } + public static create( engine: AudioEngine, player: number, diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 6c1847c..d004196 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -38,6 +38,7 @@ interface PlayerState { volume: number; gain: number; trim: number; + pfl: boolean; timeCurrent: number; timeRemaining: number; timeLength: number; @@ -68,6 +69,7 @@ const BasePlayerState: PlayerState = { volume: 1, gain: 0, trim: defaultTrimDB, + pfl: false, timeCurrent: 0, timeRemaining: 0, timeLength: 0, @@ -167,6 +169,15 @@ const mixerState = createSlice({ ) { state.players[action.payload.player].trim = action.payload.trim; }, + setPlayerPFL( + state, + action: PayloadAction<{ + player: number; + enabled: boolean; + }> + ) { + state.players[action.payload.player].pfl = action.payload.enabled; + }, setLoadedItemIntro( state, action: PayloadAction<{ @@ -730,6 +741,14 @@ export const setChannelTrim = (player: number, val: number): AppThunk => async ( audioEngine.players[player]?.setTrim(val); }; +export const setChannelPFL = ( + player: number, + enabled: boolean +): AppThunk => async (dispatch) => { + dispatch(mixerState.actions.setPlayerPFL({ player, enabled: enabled })); + audioEngine.players[player]?.setPFL(enabled); +}; + export const openMicrophone = ( micID: string, micMapping: ChannelMapping diff --git a/src/showplanner/ProModeButtons.tsx b/src/showplanner/ProModeButtons.tsx index 1303d6a..d08b7e7 100644 --- a/src/showplanner/ProModeButtons.tsx +++ b/src/showplanner/ProModeButtons.tsx @@ -1,33 +1,46 @@ import React, { useState } from "react"; -import { FaTachometerAlt } from "react-icons/fa"; +import { FaHeadphonesAlt, FaTachometerAlt } from "react-icons/fa"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../rootReducer"; -import { setChannelTrim } from "../mixer/state"; +import { setChannelTrim, setChannelPFL } from "../mixer/state"; -type ButtonIds = "trim"; +type ButtonIds = "trim" | "pfl"; export default function ProModeButtons({ channel }: { channel: number }) { const [activeButton, setActiveButton] = useState(null); const trimVal = useSelector( (state: RootState) => state.mixer.players[channel]?.trim ); + const pflState = useSelector( + (state: RootState) => state.mixer.players[channel]?.pfl + ); const dispatch = useDispatch(); return ( <>
Pro Mode™ - {(activeButton === null || activeButton === "trim") && ( - - )} + + {activeButton === "trim" && ( <> - {trimVal} dB + {trimVal} dB )} + {activeButton === "pfl" && ( + + Pre Fader Listen: {pflState ? "Yes" : "No"} + + )}
); From 85d4d723ff510aadcf3d6b5a80d545583e286158 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 20:56:20 +0000 Subject: [PATCH 31/45] Handle PFL in audioengine, all channels affected --- src/mixer/audio.ts | 42 ++++++++++++++++++++++++++++++++++++------ src/mixer/state.ts | 4 ++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 611af98..eb7cf83 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -131,6 +131,10 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { return this.volume; } + getPFL() { + return this.pfl; + } + setVolume(val: number) { this.volume = val; this._applyVolume(); @@ -143,7 +147,6 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { setPFL(enabled: boolean) { this.pfl = enabled; - this._applyPFL(); } setOutputDevice(sinkId: string) { @@ -175,10 +178,6 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { } } - _applyPFL() { - console.log("Do something"); - } - public static create( engine: AudioEngine, player: number, @@ -337,6 +336,9 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { newsEndCountdownEl: HTMLAudioElement; newsEndCountdownNode: MediaElementAudioSourceNode; + // Headphones + headphonesNode: GainNode; + constructor() { super(); @@ -412,6 +414,9 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.newsStartCountdownEl ); + // Headphones (for PFL / Monitoring) + this.headphonesNode = this.audioContext.createGain(); + // Routing the above bits together // Mic Source gets routed to micCompressor or micMixGain. @@ -426,7 +431,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { .connect(this.streamingAnalyser); // Send the final compressor (all players and guests) to the headphones. - this.finalCompressor.connect(this.audioContext.destination); + this.finalCompressor.connect(this.headphonesNode); // Also send the final compressor to the streaming analyser on to the stream. this.finalCompressor.connect(this.streamingAnalyser); @@ -437,6 +442,9 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { // Feed the news in/out reminders to the headphones too. this.newsStartCountdownNode.connect(this.audioContext.destination); this.newsEndCountdownNode.connect(this.audioContext.destination); + + // Send the headphones feed to the headphones. + this.headphonesNode.connect(this.audioContext.destination); } public createPlayer(number: number, outputId: string, url: string) { @@ -462,6 +470,28 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.players[number] = undefined; } + public setPFL(number: number, enabled: boolean) { + var routeMainOut = true; + var player = this.getPlayer(number); + + if (player) { + player.setPFL(enabled); + } + + var i; + for (i = 0; i < this.players.length; i++) { + player = this.getPlayer(i); + if (player?.getPFL()) { + // PFL is enabled on this channel, so we're not routing the regular output to H/Ps. + routeMainOut = false; + console.log("Player", i, "is PFL'd."); + } else { + console.log("Player", i, "isn't PFL'd."); + } + } + console.log("Routing main out?", routeMainOut); + } + async openMic(deviceId: string, channelMapping: ChannelMapping) { if (this.micSource !== null && this.micMedia !== null) { this.micMedia.getAudioTracks()[0].stop(); diff --git a/src/mixer/state.ts b/src/mixer/state.ts index d004196..d3f6709 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -745,8 +745,8 @@ export const setChannelPFL = ( player: number, enabled: boolean ): AppThunk => async (dispatch) => { - dispatch(mixerState.actions.setPlayerPFL({ player, enabled: enabled })); - audioEngine.players[player]?.setPFL(enabled); + dispatch(mixerState.actions.setPlayerPFL({ player, enabled })); + audioEngine.setPFL(player, enabled); }; export const openMicrophone = ( From 67be4cde7dcb16e9892b6b0240a226846b9c62d9 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 23:24:34 +0000 Subject: [PATCH 32/45] Actually do the PFLing. --- src/mixer/audio.ts | 65 +++++++++++++++++++++++++++++++++++----------- src/mixer/state.ts | 2 ++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index eb7cf83..98ff9f4 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -147,6 +147,7 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { setPFL(enabled: boolean) { this.pfl = enabled; + this._connectPFL(); } setOutputDevice(sinkId: string) { @@ -165,15 +166,30 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { _applyVolume() { const level = this.volume + this.trim; const linear = Math.pow(10, level / 20); - if (linear < 1) { - this.wavesurfer.setVolume(linear); - if (!this.customOutput) { - (this.wavesurfer as any).backend.gainNode.gain.value = 1; - } + + // Actually adjust the wavesurfer gain node gain instead, so we can tap off analyser for PFL. + this.wavesurfer.setVolume(1); + if (!this.customOutput) { + (this.wavesurfer as any).backend.gainNode.gain.value = linear; + } + } + + _connectPFL() { + if (this.pfl) { + console.log("Connecting PFL"); + + // In this case, we just want to route the player output to the headphones direct. + // Tap it from analyser to avoid the player volume. + (this.wavesurfer as any).backend.analyser.connect( + this.engine.headphonesNode + ); } else { - this.wavesurfer.setVolume(1); - if (!this.customOutput) { - (this.wavesurfer as any).backend.gainNode.gain.value = linear; + try { + (this.wavesurfer as any).backend.analyser.disconnect( + this.engine.headphonesNode + ); + } catch (e) { + console.log("Didn't disconnect."); } } } @@ -182,6 +198,7 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { engine: AudioEngine, player: number, outputId: string, + pfl: boolean, url: string ) { // If we want to output to a custom audio device, we're gonna need to do things differently. @@ -257,6 +274,7 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { (wavesurfer as any).backend.gainNode.connect( engine.playerAnalysers[player] ); + instance.setPFL(pfl); } return instance; @@ -430,11 +448,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { .connect(this.micFinalAnalyser) .connect(this.streamingAnalyser); - // Send the final compressor (all players and guests) to the headphones. - this.finalCompressor.connect(this.headphonesNode); - - // Also send the final compressor to the streaming analyser on to the stream. - this.finalCompressor.connect(this.streamingAnalyser); + this._connectFinalCompressor(true); // Send the streaming analyser to the Streamer! this.streamingAnalyser.connect(this.streamingDestination); @@ -444,11 +458,30 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.newsEndCountdownNode.connect(this.audioContext.destination); // Send the headphones feed to the headphones. + const db = -12; // DB gain on headphones (-6 to match default trim) + this.headphonesNode.gain.value = Math.pow(10, db / 20); this.headphonesNode.connect(this.audioContext.destination); } - public createPlayer(number: number, outputId: string, url: string) { - const player = Player.create(this, number, outputId, url); + _connectFinalCompressor(headphones: boolean) { + this.finalCompressor.disconnect(); + + if (headphones) { + // Send the final compressor (all players and guests) to the headphones. + this.finalCompressor.connect(this.headphonesNode); + } + + // Also send the final compressor to the streaming analyser on to the stream. + this.finalCompressor.connect(this.streamingAnalyser); + } + + public createPlayer( + number: number, + outputId: string, + pfl: boolean, + url: string + ) { + const player = Player.create(this, number, outputId, pfl, url); this.players[number] = player; return player; } @@ -490,6 +523,8 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { } } console.log("Routing main out?", routeMainOut); + + this._connectFinalCompressor(routeMainOut); } async openMic(deviceId: string, channelMapping: ChannelMapping) { diff --git a/src/mixer/state.ts b/src/mixer/state.ts index d3f6709..af2141e 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -370,6 +370,7 @@ export const load = ( const shouldResetTrim = getState().settings.resetTrimOnLoad; const customOutput = getState().settings.channelOutputIds[player] !== "internal"; + const isPFL = getState().mixer.players[player].pfl; dispatch( mixerState.actions.loadItem({ @@ -438,6 +439,7 @@ export const load = ( const playerInstance = await audioEngine.createPlayer( player, channelOutputId, + isPFL, objectUrl ); From e5358c7cffe3dfe66451c9a405cf4d7430ea2d65 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 23:41:14 +0000 Subject: [PATCH 33/45] Automate play on PFL and disable on fader. --- src/mixer/state.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index af2141e..f0637b3 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -663,7 +663,8 @@ export const { setTracklistItemID } = mixerState.actions; const FADE_TIME_SECONDS = 1; export const setVolume = ( player: number, - level: VolumePresetEnum + level: VolumePresetEnum, + fade: boolean = true ): AppThunk => (dispatch, getState) => { let volume: number; let uiLevel: number; @@ -701,6 +702,18 @@ export const setVolume = ( } } + // If we're fading up the volume, disable the PFL. + if (level !== "off") { + dispatch(setChannelPFL(player, false)); + } + + // If not fading, just do it. + if (!fade) { + dispatch(mixerState.actions.setPlayerVolume({ player, volume: uiLevel })); + dispatch(mixerState.actions.setPlayerGain({ player, gain: volume })); + return; + } + const state = getState().mixer.players[player]; const currentLevel = state.volume; @@ -747,6 +760,12 @@ export const setChannelPFL = ( player: number, enabled: boolean ): AppThunk => async (dispatch) => { + if (typeof audioEngine.players[player] !== "undefined") { + if (!audioEngine.players[player]?.isPlaying) { + dispatch(setVolume(player, "off", false)); + dispatch(play(player)); + } + } dispatch(mixerState.actions.setPlayerPFL({ player, enabled })); audioEngine.setPFL(player, enabled); }; From 658ef5f1eb3905089bfa8772cb3fbcae5352a9b3 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 23:47:32 +0000 Subject: [PATCH 34/45] Only play automatically on enabling PFL. --- src/mixer/state.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index f0637b3..a3b28ca 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -760,11 +760,13 @@ export const setChannelPFL = ( player: number, enabled: boolean ): AppThunk => async (dispatch) => { - if (typeof audioEngine.players[player] !== "undefined") { - if (!audioEngine.players[player]?.isPlaying) { - dispatch(setVolume(player, "off", false)); - dispatch(play(player)); - } + if ( + enabled && + typeof audioEngine.players[player] !== "undefined" && + !audioEngine.players[player]?.isPlaying + ) { + dispatch(setVolume(player, "off", false)); + dispatch(play(player)); } dispatch(mixerState.actions.setPlayerPFL({ player, enabled })); audioEngine.setPFL(player, enabled); From cc5f24580dc392ec6cb7f950bb4a15e9b16640a2 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 5 Jan 2021 23:57:14 +0000 Subject: [PATCH 35/45] Only tracklist if we're actually audible. --- src/mixer/state.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index a3b28ca..963ee45 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -584,10 +584,21 @@ export const play = (player: number): AppThunk => async ( } audioEngine.players[player]?.play(); + // If we're starting off audible, try and tracklist. + if (state.volume > 0) { + dispatch(attemptTracklist(player)); + } +}; + +const attemptTracklist = (player: number): AppThunk => async ( + dispatch, + getState +) => { + const state = getState().mixer.players[player]; if (state.loadedItem && "album" in state.loadedItem) { //track console.log("potentially tracklisting", state.loadedItem); - if (getState().mixer.players[player].tracklistItemID === -1) { + if (state.tracklistItemID === -1) { dispatch(BroadcastState.tracklistStart(player, state.loadedItem.trackid)); } else { console.log("not tracklisting because already tracklisted"); @@ -702,9 +713,11 @@ export const setVolume = ( } } - // If we're fading up the volume, disable the PFL. if (level !== "off") { + // If we're fading up the volume, disable the PFL. dispatch(setChannelPFL(player, false)); + // Also catch a tracklist if we started with the channel off. + dispatch(attemptTracklist(player)); } // If not fading, just do it. From 796f1ffcb8ed6ba02ae339611e2cfc7fee023c22 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Fri, 15 Jan 2021 23:15:03 +0000 Subject: [PATCH 36/45] Add a clear PFL button --- src/mixer/state.ts | 12 ++++++++++-- src/navbar/index.tsx | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 963ee45..1b6b6b7 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -781,8 +781,16 @@ export const setChannelPFL = ( dispatch(setVolume(player, "off", false)); dispatch(play(player)); } - dispatch(mixerState.actions.setPlayerPFL({ player, enabled })); - audioEngine.setPFL(player, enabled); + // If the player number is -1, do all channels. + if (player === -1) { + for (let i = 0; i < audioEngine.players.length; i++) { + dispatch(mixerState.actions.setPlayerPFL({ player: i, enabled: false })); + audioEngine.setPFL(i, false); + } + } else { + dispatch(mixerState.actions.setPlayerPFL({ player, enabled })); + audioEngine.setPFL(player, enabled); + } }; export const openMicrophone = ( diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index 3348aa8..faf9ecc 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -11,6 +11,7 @@ import { FaSpinner, FaExclamationTriangle, FaCog, + FaHeadphonesAlt, } from "react-icons/fa"; import { RootState } from "../rootReducer"; @@ -26,6 +27,7 @@ import { VUMeter } from "../optionsMenu/helpers/VUMeter"; import { getShowplan, setItemPlayed } from "../showplanner/state"; import * as OptionsMenuState from "../optionsMenu/state"; +import { setChannelPFL } from "../mixer/state"; function nicifyConnectionState(state: ConnectionStateEnum): string { switch (state) { @@ -288,6 +290,14 @@ export function NavBarMain() { > Options + {settings.proMode && ( +
  • dispatch(setChannelPFL(-1, false))} + > + Clear PFL +
  • + )}
  • Date: Sat, 16 Jan 2021 00:02:06 +0000 Subject: [PATCH 37/45] Make PFL meter. --- src/mixer/audio.ts | 12 ++++++++++++ src/navbar/index.tsx | 17 ++++++++++++----- src/navbar/navbar.scss | 5 +++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 98ff9f4..70e7975 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -294,6 +294,7 @@ export type LevelsSource = | "mic-precomp" | "mic-final" | "master" + | "pfl" | "player-0" | "player-1" | "player-2"; @@ -356,6 +357,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { // Headphones headphonesNode: GainNode; + pflAnalyser: typeof StereoAnalyserNode; constructor() { super(); @@ -434,6 +436,9 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { // Headphones (for PFL / Monitoring) this.headphonesNode = this.audioContext.createGain(); + this.pflAnalyser = new StereoAnalyserNode(this.audioContext); + this.pflAnalyser.fftSize = ANALYSIS_FFT_SIZE; + this.pflAnalyser.maxDecibels = 0; // Routing the above bits together @@ -461,6 +466,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { const db = -12; // DB gain on headphones (-6 to match default trim) this.headphonesNode.gain.value = Math.pow(10, db / 20); this.headphonesNode.connect(this.audioContext.destination); + this.headphonesNode.connect(this.pflAnalyser); } _connectFinalCompressor(headphones: boolean) { @@ -627,6 +633,12 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.analysisBuffer2 ); break; + case "pfl": + this.pflAnalyser.getFloatTimeDomainData( + this.analysisBuffer, + this.analysisBuffer2 + ); + break; case "player-0": this.playerAnalysers[0].getFloatTimeDomainData( this.analysisBuffer, diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index faf9ecc..65c409f 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -164,6 +164,13 @@ export function NavBarMain() { const dispatch = useDispatch(); const broadcastState = useSelector((state: RootState) => state.broadcast); const settings = useSelector((state: RootState) => state.settings); + const playerState = useSelector((state: RootState) => state.mixer.players); + + let playerPFLs: boolean[] = []; + playerState.forEach((player) => { + playerPFLs.push(player.pfl); + }); + const isPFL = useSelector((state) => playerPFLs).some((x) => x === true); const [connectButtonAnimating, setConnectButtonAnimating] = useState(false); @@ -290,20 +297,20 @@ export function NavBarMain() { > Options
  • - {settings.proMode && ( + {settings.proMode && isPFL && (
  • dispatch(setChannelPFL(-1, false))} > Clear PFL
  • )} -
  • +
  • diff --git a/src/navbar/navbar.scss b/src/navbar/navbar.scss index f1b08df..27c087a 100644 --- a/src/navbar/navbar.scss +++ b/src/navbar/navbar.scss @@ -305,3 +305,8 @@ .nav-link.connect { min-width: 90px; } + +.pfl-live.nav-vu { + box-shadow: inset 0 0 3px 6px #dc3545; + padding-top: 6px; +} From df155c89b143168e073c30053412612d897fc217 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 16 Jan 2021 00:55:50 +0000 Subject: [PATCH 38/45] Fix height of VU. --- src/navbar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index 65c409f..902a214 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -309,7 +309,7 @@ export function NavBarMain() {
  • Date: Sat, 16 Jan 2021 00:58:08 +0000 Subject: [PATCH 39/45] Tidy if Co-authored-by: Marks Polakovs --- src/mixer/audio.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 70e7975..7046215 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -517,8 +517,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { player.setPFL(enabled); } - var i; - for (i = 0; i < this.players.length; i++) { + for (let i = 0; i < this.players.length; i++) { player = this.getPlayer(i); if (player?.getPFL()) { // PFL is enabled on this channel, so we're not routing the regular output to H/Ps. From 085745d756e02d7dbb565115fe12b02f53368586 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 16 Jan 2021 01:03:50 +0000 Subject: [PATCH 40/45] Tidies --- src/mixer/audio.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 70e7975..30465eb 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -176,8 +176,6 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { _connectPFL() { if (this.pfl) { - console.log("Connecting PFL"); - // In this case, we just want to route the player output to the headphones direct. // Tap it from analyser to avoid the player volume. (this.wavesurfer as any).backend.analyser.connect( @@ -189,7 +187,7 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { this.engine.headphonesNode ); } catch (e) { - console.log("Didn't disconnect."); + // This connection wasn't connected anyway, ignore. } } } @@ -469,10 +467,11 @@ export class AudioEngine extends ((EngineEmitter as unknown) as { this.headphonesNode.connect(this.pflAnalyser); } - _connectFinalCompressor(headphones: boolean) { + // Routes the final compressor (all players) to the stream, and optionally headphones. + _connectFinalCompressor(masterToHeadphones: boolean) { this.finalCompressor.disconnect(); - if (headphones) { + if (masterToHeadphones) { // Send the final compressor (all players and guests) to the headphones. this.finalCompressor.connect(this.headphonesNode); } From 9613363e203ed08022fd24c5217e3d5ec2ca8e1e Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 23 Jan 2021 22:07:53 +0000 Subject: [PATCH 41/45] Only try and tracklist if track is actually playing. --- src/mixer/state.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 1b6b6b7..33d88b3 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -595,7 +595,11 @@ const attemptTracklist = (player: number): AppThunk => async ( getState ) => { const state = getState().mixer.players[player]; - if (state.loadedItem && "album" in state.loadedItem) { + if ( + state.loadedItem && + "album" in state.loadedItem && + audioEngine.players[player]?.isPlaying + ) { //track console.log("potentially tracklisting", state.loadedItem); if (state.tracklistItemID === -1) { From 81d443123a24a11ecd99aa941dba4d0e40727dd6 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 23 Jan 2021 23:40:08 +0000 Subject: [PATCH 42/45] Split out navbar so that it doesn't rerender everything so much. --- src/navbar/index.tsx | 290 +++++++++++++++++++++++++------------------ 1 file changed, 169 insertions(+), 121 deletions(-) diff --git a/src/navbar/index.tsx b/src/navbar/index.tsx index 902a214..dcaa96b 100644 --- a/src/navbar/index.tsx +++ b/src/navbar/index.tsx @@ -1,5 +1,5 @@ import React, { useRef, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { shallowEqual, useDispatch, useSelector } from "react-redux"; import Clock from "react-live-clock"; import Stopwatch from "react-stopwatch"; @@ -161,31 +161,6 @@ export function NavBarMyRadio() { } export function NavBarMain() { - const dispatch = useDispatch(); - const broadcastState = useSelector((state: RootState) => state.broadcast); - const settings = useSelector((state: RootState) => state.settings); - const playerState = useSelector((state: RootState) => state.mixer.players); - - let playerPFLs: boolean[] = []; - playerState.forEach((player) => { - playerPFLs.push(player.pfl); - }); - const isPFL = useSelector((state) => playerPFLs).some((x) => x === true); - - const [connectButtonAnimating, setConnectButtonAnimating] = useState(false); - - const prevRegistrationStage = useRef(broadcastState.stage); - useEffect(() => { - if (broadcastState.stage !== prevRegistrationStage.current) { - setConnectButtonAnimating(false); - } - prevRegistrationStage.current = broadcastState.stage; - }, [broadcastState.stage]); - - const { planSaveError, planSaving } = useSelector( - (state: RootState) => state.showplan - ); - return ( <>
      @@ -207,115 +182,188 @@ export function NavBarMain() { timezone={"europe/london"} /> - {planSaving && ( -
    • - Saving show plan... -
    • - )} - {planSaveError && ( -
    • - - {planSaveError} -
    • - )} +
    -
      -
    • -
      - {nicifyConnectionState(broadcastState.connectionState)} -
      + + + + +
    + + ); +} +function SavingAlert() { + const { planSaveError, planSaving } = useSelector( + (state: RootState) => state.showplan + ); + return ( + <> + {planSaving && ( +
  • + Saving show plan...
  • + )} + {planSaveError && ( +
  • + + {planSaveError} +
  • + )} + + ); +} +function RegisterButton() { + const dispatch = useDispatch(); + const broadcastState = useSelector((state: RootState) => state.broadcast); + const [connectButtonAnimating, setConnectButtonAnimating] = useState(false); + + const prevRegistrationStage = useRef(broadcastState.stage); + useEffect(() => { + if (broadcastState.stage !== prevRegistrationStage.current) { + setConnectButtonAnimating(false); + } + prevRegistrationStage.current = broadcastState.stage; + }, [broadcastState.stage]); + + return ( + <> +
  • +
    + {nicifyConnectionState(broadcastState.connectionState)} +
    +
  • +
  • { + setConnectButtonAnimating(true); + switch (broadcastState.stage) { + case "NOT_REGISTERED": + dispatch(BroadcastState.goOnAir()); + break; + case "REGISTERED": + dispatch(BroadcastState.cancelTimeslot()); + break; + } + }} + > + {connectButtonAnimating ? ( + <> + + + + ) : ( + <> + + {broadcastState.stage === "NOT_REGISTERED" && "Register"} + {broadcastState.stage === "REGISTERED" && "Stop"} + + )} +
  • + + ); +} +function RecordingButton() { + const recordingState = useSelector( + (state: RootState) => state.broadcast.recordingState + ); + const enableRecording = useSelector( + (state: RootState) => state.settings.enableRecording + ); + const dispatch = useDispatch(); + return ( + <> + {enableRecording && (
  • { - setConnectButtonAnimating(true); - switch (broadcastState.stage) { - case "NOT_REGISTERED": - dispatch(BroadcastState.goOnAir()); - break; - case "REGISTERED": - dispatch(BroadcastState.cancelTimeslot()); - break; - } - }} + className={ + "btn rounded-0 pt-2 pb-1 nav-item nav-link " + + (recordingState === "CONNECTED" + ? "btn-outline-danger active" + : "btn-outline-light") + } + onClick={() => + dispatch( + recordingState === "NOT_CONNECTED" + ? BroadcastState.startRecording() + : BroadcastState.stopRecording() + ) + } > - {connectButtonAnimating ? ( - <> - - - + {" "} + {recordingState === "CONNECTED" ? ( + { + return {formatted}; + }} + /> ) : ( - <> - - {broadcastState.stage === "NOT_REGISTERED" && "Register"} - {broadcastState.stage === "REGISTERED" && "Stop"} - + "Record" )}
  • - {settings.enableRecording && ( -
  • - dispatch( - broadcastState.recordingState === "NOT_CONNECTED" - ? BroadcastState.startRecording() - : BroadcastState.stopRecording() - ) - } - > - {" "} - {broadcastState.recordingState === "CONNECTED" ? ( - { - return {formatted}; - }} - /> - ) : ( - "Record" - )} -
  • - )} -
  • dispatch(OptionsMenuState.open())} - > - Options -
  • - {settings.proMode && isPFL && ( -
  • dispatch(setChannelPFL(-1, false))} - > - Clear PFL -
  • - )} + )} + + ); +} +function OptionsButton() { + const dispatch = useDispatch(); + return ( +
  • dispatch(OptionsMenuState.open())} + > + Options +
  • + ); +} -
  • +function MeterBridge() { + const dispatch = useDispatch(); + const proMode = useSelector((state: RootState) => state.settings.proMode); + const playerPFLs = useSelector( + (state: RootState) => state.mixer.players.map((x) => x.pfl), + shallowEqual + ); + const isPFL = useSelector((state) => playerPFLs).some((x) => x === true); + + return ( + <> + {proMode && isPFL && ( +
  • dispatch(setChannelPFL(-1, false))} + > + Clear PFL +
  • + )} + +
  • + {isPFL && ( -
  • - + )} + {!isPFL && ( + + )} + ); } From f1c637120caa54c5d913f9cd0973a5ff2ffb2ed6 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 24 Jan 2021 00:27:34 +0000 Subject: [PATCH 43/45] Rejig the main ShowPlanner component to reduce rerenders --- src/session/index.tsx | 2 +- src/showplanner/index.tsx | 50 ++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/session/index.tsx b/src/session/index.tsx index 7e52f66..caf0136 100644 --- a/src/session/index.tsx +++ b/src/session/index.tsx @@ -78,7 +78,7 @@ const SessionHandler: React.FC = function() { ); } - return

    ; + return <>; }; export default SessionHandler; diff --git a/src/showplanner/index.tsx b/src/showplanner/index.tsx index 26c4e8e..cd5d0a7 100644 --- a/src/showplanner/index.tsx +++ b/src/showplanner/index.tsx @@ -23,7 +23,7 @@ import { ResponderProvided, } from "react-beautiful-dnd"; -import { useSelector, useDispatch } from "react-redux"; +import { useSelector, useDispatch, shallowEqual } from "react-redux"; import { RootState } from "../rootReducer"; import { PlanItem, @@ -292,8 +292,9 @@ function incrReducer(state: number, action: any) { } const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { - const { plan: showplan, planLoadError, planLoading } = useSelector( - (state: RootState) => state.showplan + const isShowplan = useSelector( + (state: RootState) => state.showplan.plan !== null, + shallowEqual ); // Tell Modals that #root is the main page content, for accessability reasons. @@ -408,26 +409,15 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { }; }, [dispatch, session.currentTimeslot]); - if (showplan === null) { - return ( - - ); + if (!isShowplan) { + return ; } return (
    -
    - - - -
    + = function({ timeslotId }) { ); }; +function GettingShowPlanScreen() { + const { planLoading, planLoadError } = useSelector( + (state: RootState) => state.showplan + ); + return ( + + ); +} + export function LoadingDialogue({ title, subtitle, @@ -509,4 +513,16 @@ export function LoadingDialogue({ ); } +function ChannelStrips() { + const showplan = useSelector((state: RootState) => state.showplan.plan!); + + return ( +
    + + + +
    + ); +} + export default Showplanner; From cd0956b21946ed3cf5f50d06ec70a56bd01db59b Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 24 Jan 2021 00:56:13 +0000 Subject: [PATCH 44/45] Limit FPS of VU meters to help weedy CPUs. --- src/optionsMenu/helpers/VUMeter.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/optionsMenu/helpers/VUMeter.tsx b/src/optionsMenu/helpers/VUMeter.tsx index 97c07d1..0e7791b 100644 --- a/src/optionsMenu/helpers/VUMeter.tsx +++ b/src/optionsMenu/helpers/VUMeter.tsx @@ -27,6 +27,8 @@ export function VUMeter(props: VUMeterProps) { const isMic = props.source.substr(0, 3) === "mic"; + const FPS = 30; // Limit the FPS so that lower spec machines have a better time juggling CPU. + useEffect(() => { const animate = () => { if (!isMic || isMicOpen) { @@ -38,7 +40,9 @@ export function VUMeter(props: VUMeterProps) { if (props.stereo) { setPeakR(result[1]); } - rafRef.current = requestAnimationFrame(animate); + setTimeout((current = rafRef.current, a = animate) => { + current = requestAnimationFrame(a); + }, 1000 / FPS); } }; if (!isMic || isMicOpen) { From bc8a06729c3a25167d206faea024b62e506db89f Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 24 Jan 2021 20:06:33 +0000 Subject: [PATCH 45/45] Store the volumeEnum per player So that autoduck knows when it's not supposed to meddle. --- src/mixer/state.ts | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 274995f..48c95b7 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -36,6 +36,7 @@ interface PlayerState { loadError: boolean; state: PlayerStateEnum; volume: number; + volumeEnum: VolumePresetEnum; gain: number; trim: number; micAutoDuck: boolean; @@ -68,6 +69,7 @@ const BasePlayerState: PlayerState = { loading: -1, state: "stopped", volume: 1, + volumeEnum: "full", gain: 0, micAutoDuck: false, trim: defaultTrimDB, @@ -89,6 +91,7 @@ const mixerState = createSlice({ mic: { open: false, volume: 1, + volumeEnum: "full", gain: 1, baseGain: 0, openError: null, @@ -149,9 +152,12 @@ const mixerState = createSlice({ action: PayloadAction<{ player: number; volume: number; + volumeEnum: VolumePresetEnum; }> ) { state.players[action.payload.player].volume = action.payload.volume; + state.players[action.payload.player].volumeEnum = + action.payload.volumeEnum; }, setPlayerGain( state, @@ -722,7 +728,13 @@ export const setVolume = ( playerGainTweens[player].tweens.forEach((tween) => tween.pause()); if (playerGainTweens[player].target === level) { delete playerGainTweens[player]; - dispatch(mixerState.actions.setPlayerVolume({ player, volume: uiLevel })); + dispatch( + mixerState.actions.setPlayerVolume({ + player, + volume: uiLevel, + volumeEnum: level, + }) + ); dispatch(mixerState.actions.setPlayerGain({ player, gain: volume })); return; } @@ -737,7 +749,13 @@ export const setVolume = ( // If not fading, just do it. if (!fade) { - dispatch(mixerState.actions.setPlayerVolume({ player, volume: uiLevel })); + dispatch( + mixerState.actions.setPlayerVolume({ + player, + volume: uiLevel, + volumeEnum: level, + }) + ); dispatch(mixerState.actions.setPlayerGain({ player, gain: volume })); return; } @@ -756,7 +774,13 @@ export const setVolume = ( const volumeTween = new Between(currentLevel, uiLevel) .time(FADE_TIME_SECONDS * 1000) .on("update", (val: number) => { - dispatch(mixerState.actions.setPlayerVolume({ player, volume: val })); + dispatch( + mixerState.actions.setPlayerVolume({ + player, + volume: val, + volumeEnum: level, + }) + ); }); const gainTween = new Between(currentGain, volume) .time(FADE_TIME_SECONDS * 1000) @@ -869,14 +893,17 @@ export const setMicVolume = (level: MicVolumePresetEnum): AppThunk => ( dispatch(mixerState.actions.setMicLevels({ volume: levelVal })); for (let player = 0; player < players.length; player++) { // If we have auto duck enabled on this channel player, tell it to fade down. - if (players[player].micAutoDuck) { + if ( + players[player].micAutoDuck && + players[player].volumeEnum === "full" + ) { dispatch(setVolume(player, "bed")); } } } else { for (let player = 0; player < players.length; player++) { // If we have auto duck enabled on this channel player, tell it to fade back up. - if (players[player].micAutoDuck) { + if (players[player].micAutoDuck && players[player].volumeEnum === "bed") { dispatch(setVolume(player, "full")); } }