Merge pull request #177 from UniversityRadioYork/mstratford-duck-when-honking

Add auto player duck on mic live. Closes #70
This commit is contained in:
Matthew Stratford 2021-01-24 20:14:21 +00:00 committed by GitHub
commit ed8e27547f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 175 additions and 52 deletions

View file

@ -36,8 +36,10 @@ interface PlayerState {
loadError: boolean;
state: PlayerStateEnum;
volume: number;
volumeEnum: VolumePresetEnum;
gain: number;
trim: number;
micAutoDuck: boolean;
pfl: boolean;
timeCurrent: number;
timeRemaining: number;
@ -67,7 +69,9 @@ const BasePlayerState: PlayerState = {
loading: -1,
state: "stopped",
volume: 1,
volumeEnum: "full",
gain: 0,
micAutoDuck: false,
trim: defaultTrimDB,
pfl: false,
timeCurrent: 0,
@ -87,6 +91,7 @@ const mixerState = createSlice({
mic: {
open: false,
volume: 1,
volumeEnum: "full",
gain: 1,
baseGain: 0,
openError: null,
@ -147,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,
@ -169,6 +177,15 @@ const mixerState = createSlice({
) {
state.players[action.payload.player].trim = action.payload.trim;
},
setPlayerMicAutoDuck(
state,
action: PayloadAction<{
player: number;
enabled: boolean;
}>
) {
state.players[action.payload.player].micAutoDuck = action.payload.enabled;
},
setPlayerPFL(
state,
action: PayloadAction<{
@ -665,6 +682,8 @@ export const {
toggleAutoAdvance,
togglePlayOnLoad,
toggleRepeat,
setTracklistItemID,
setPlayerMicAutoDuck,
} = mixerState.actions;
export const redrawWavesurfers = (): AppThunk => () => {
@ -673,8 +692,6 @@ export const redrawWavesurfers = (): AppThunk => () => {
});
};
export const { setTracklistItemID } = mixerState.actions;
const FADE_TIME_SECONDS = 1;
export const setVolume = (
player: number,
@ -711,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;
}
@ -726,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;
}
@ -745,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)
@ -844,15 +879,34 @@ export const setMicProcessingEnabled = (enabled: boolean): AppThunk => async (
};
export const setMicVolume = (level: MicVolumePresetEnum): AppThunk => (
dispatch
dispatch,
getState
) => {
const players = getState().mixer.players;
// no tween fuckery here, just cut the level
const levelVal = level === "full" ? 1 : 0;
// actually, that's a lie - if we're turning it off we delay it a little to compensate for
// processing latency
if (levelVal !== 0) {
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 &&
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 && players[player].volumeEnum === "bed") {
dispatch(setVolume(player, "full"));
}
}
window.setTimeout(() => {
dispatch(mixerState.actions.setMicLevels({ volume: levelVal }));
// latency, plus a little buffer

View file

@ -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) {

View file

@ -78,7 +78,7 @@ const SessionHandler: React.FC = function() {
);
}
return <p></p>;
return <></>;
};
export default SessionHandler;

View file

@ -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 (
<span className="card-title">
<strong>
{loadedItem !== null && loading === -1
? loadedItem.title
: loading !== -1
? `LOADING`
: loadError
? "LOAD FAILED"
: "No Media Selected"}
</strong>
<small
className={
"border rounded border-danger text-danger p-1 m-1" +
(loadedItem !== null &&
loading === -1 &&
"clean" in loadedItem &&
!loadedItem.clean
? ""
: " d-none")
}
>
Explicit
</small>
</span>
);
}
export function Player({ id }: { id: number }) {
// Define time remaining (secs) when the play icon should flash.
const SECS_REMAINING_WARNING = 20;
@ -302,30 +342,7 @@ export function Player({ id }: { id: number }) {
</div>
{settings.proMode && !customOutput && <ProModeButtons channel={id} />}
<div className="card-body p-0">
<span className="card-title">
<strong>
{playerState.loadedItem !== null && playerState.loading === -1
? playerState.loadedItem.title
: playerState.loading !== -1
? `LOADING`
: playerState.loadError
? "LOAD FAILED"
: "No Media Selected"}
</strong>
<small
className={
"border rounded border-danger text-danger p-1 m-1" +
(playerState.loadedItem !== null &&
playerState.loading === -1 &&
"clean" in playerState.loadedItem &&
!playerState.loadedItem.clean
? ""
: " d-none")
}
>
Explicit
</small>
</span>
<LoadedTrackInfo id={id} />
<br />
<span className="text-muted">
{playerState.loadedItem !== null && playerState.loading === -1

View file

@ -1,16 +1,29 @@
import React, { useState } from "react";
import { FaHeadphonesAlt, FaTachometerAlt } from "react-icons/fa";
import {
FaHeadphonesAlt,
FaMicrophoneAlt,
FaTachometerAlt,
} from "react-icons/fa";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../rootReducer";
import { setChannelTrim, setChannelPFL } from "../mixer/state";
import {
setChannelPFL,
setChannelTrim,
setPlayerMicAutoDuck,
} from "../mixer/state";
type ButtonIds = "trim" | "pfl";
type ButtonIds = "trim" | "pfl" | "autoDuck";
export default function ProModeButtons({ channel }: { channel: number }) {
const [activeButton, setActiveButton] = useState<ButtonIds | null>(null);
const trimVal = useSelector(
(state: RootState) => state.mixer.players[channel]?.trim
);
const micAutoDuck = useSelector(
(state: RootState) => state.mixer.players[channel]?.micAutoDuck
);
const pflState = useSelector(
(state: RootState) => state.mixer.players[channel]?.pfl
);
@ -41,6 +54,20 @@ export default function ProModeButtons({ channel }: { channel: number }) {
>
<FaHeadphonesAlt />
</button>
<button
className={
"mr-1 btn " + (micAutoDuck ? "btn-info" : "btn-outline-dark")
}
title="Auto Duck on Mic Live"
onClick={() => {
dispatch(
setPlayerMicAutoDuck({ player: channel, enabled: !micAutoDuck })
);
setActiveButton("autoDuck");
}}
>
<FaMicrophoneAlt />
</button>
{activeButton === "trim" && (
<>
<input
@ -63,6 +90,11 @@ export default function ProModeButtons({ channel }: { channel: number }) {
Pre Fader Listen:&nbsp;<strong>{pflState ? "Yes" : "No"}</strong>
</span>
)}
{activeButton === "autoDuck" && (
<span className="mt-2 ml-2">
Duck on Mic:&nbsp;<strong>{micAutoDuck ? "Yes" : "No"}</strong>
</span>
)}
</div>
</>
);

View file

@ -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 (
<LoadingDialogue
title="Getting Show Plan..."
subtitle={planLoading ? "Hang on a sec..." : ""}
error={planLoadError}
percent={100}
/>
);
if (!isShowplan) {
return <GettingShowPlanScreen />;
}
return (
<div className="sp-container m-0">
<CombinedNavAlertBar />
<div className="sp">
<DragDropContext onDragEnd={onDragEnd}>
<div className="channels">
<Channel id={0} data={showplan} />
<Channel id={1} data={showplan} />
<Channel id={2} data={showplan} />
</div>
<ChannelStrips />
<span
id="sidebar-toggle"
className="btn btn-outline-dark btn-sm mb-0"
@ -462,6 +452,20 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
);
};
function GettingShowPlanScreen() {
const { planLoading, planLoadError } = useSelector(
(state: RootState) => state.showplan
);
return (
<LoadingDialogue
title="Getting Show Plan..."
subtitle={planLoading ? "Hang on a sec..." : ""}
error={planLoadError}
percent={100}
/>
);
}
export function LoadingDialogue({
title,
subtitle,
@ -509,4 +513,16 @@ export function LoadingDialogue({
);
}
function ChannelStrips() {
const showplan = useSelector((state: RootState) => state.showplan.plan!);
return (
<div className="channels">
<Channel id={0} data={showplan} />
<Channel id={1} data={showplan} />
<Channel id={2} data={showplan} />
</div>
);
}
export default Showplanner;