Hey, I heard you like Stereo VU Meters.

This commit is contained in:
Matthew Stratford 2020-09-20 16:16:34 +01:00
parent 1132862d9a
commit a45aa7a17e
8 changed files with 90 additions and 40 deletions

View file

@ -93,7 +93,9 @@
"resolve-url-loader": "3.1.0",
"sass-loader": "7.2.0",
"sdp-transform": "^2.14.0",
"semver": "6.3.0",
"semver": "7.3.2",
"serialize-javascript": "^5.0.1",
"stereo-analyser-node": "^1.0.0",
"strict-event-emitter-types": "^2.0.0",
"style-loader": "1.0.0",
"terser-webpack-plugin": "1.4.1",

View file

@ -7,6 +7,9 @@ import RegionsPlugin from "wavesurfer.js/dist/plugin/wavesurfer.regions.min.js";
import NewsEndCountdown from "../assets/audio/NewsEndCountdown.wav";
import NewsIntro from "../assets/audio/NewsIntro.wav";
import StereoAnalyserNode from "stereo-analyser-node";
interface PlayerEvents {
loadComplete: (duration: number) => void;
timeChange: (time: number) => void;
@ -183,20 +186,20 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
micMedia: MediaStream | null = null;
micSource: MediaStreamAudioSourceNode | null = null;
micCalibrationGain: GainNode;
micPrecompAnalyser: AnalyserNode;
micPrecompAnalyser: StereoAnalyserNode;
micCompressor: DynamicsCompressorNode;
micMixGain: GainNode;
micFinalAnalyser: AnalyserNode;
micFinalAnalyser: StereoAnalyserNode;
finalCompressor: DynamicsCompressorNode;
streamingDestination: MediaStreamAudioDestinationNode;
player0Analyser: AnalyserNode;
player1Analyser: AnalyserNode;
player2Analyser: AnalyserNode;
playerAnalysers: AnalyserNode[];
player0Analyser: StereoAnalyserNode;
player1Analyser: StereoAnalyserNode;
player2Analyser: StereoAnalyserNode;
playerAnalysers: StereoAnalyserNode[];
streamingAnalyser: AnalyserNode;
streamingAnalyser: StereoAnalyserNode;
newsStartCountdownEl: HTMLAudioElement;
newsStartCountdownNode: MediaElementAudioSourceNode;
@ -204,7 +207,6 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
newsEndCountdownEl: HTMLAudioElement;
newsEndCountdownNode: MediaElementAudioSourceNode;
analysisBuffer: Float32Array;
constructor() {
super();
@ -220,16 +222,21 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
this.finalCompressor.release.value = 0.2;
this.finalCompressor.knee.value = 0;
this.player0Analyser = this.audioContext.createAnalyser();
this.player0Analyser = new StereoAnalyserNode(this.audioContext);
this.player0Analyser.fftSize = ANALYSIS_FFT_SIZE;
this.player1Analyser = this.audioContext.createAnalyser();
this.player1Analyser = new StereoAnalyserNode(this.audioContext);
this.player1Analyser.fftSize = ANALYSIS_FFT_SIZE;
this.player2Analyser = this.audioContext.createAnalyser();
this.player2Analyser = new StereoAnalyserNode(this.audioContext);
this.player2Analyser.fftSize = ANALYSIS_FFT_SIZE;
this.playerAnalysers = [this.player0Analyser, this.player1Analyser, this.player2Analyser];
this.streamingAnalyser = this.audioContext.createAnalyser();
this.streamingAnalyser = new StereoAnalyserNode(this.audioContext);
this.streamingAnalyser.fftSize = ANALYSIS_FFT_SIZE;
// this.streamingAnalyser.maxDecibels = 0;
this.streamingDestination = this.audioContext.createMediaStreamDestination();
@ -242,16 +249,14 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
this.micCalibrationGain = this.audioContext.createGain();
this.micPrecompAnalyser = this.audioContext.createAnalyser();
this.micPrecompAnalyser = new StereoAnalyserNode(this.audioContext);
this.micPrecompAnalyser.fftSize = ANALYSIS_FFT_SIZE;
this.micPrecompAnalyser.maxDecibels = 0;
this.micFinalAnalyser = this.audioContext.createAnalyser();
this.micFinalAnalyser = new StereoAnalyserNode(this.audioContext);
this.micFinalAnalyser.fftSize = ANALYSIS_FFT_SIZE;
this.micFinalAnalyser.maxDecibels = 0;
this.analysisBuffer = new Float32Array(ANALYSIS_FFT_SIZE);
this.micCompressor = this.audioContext.createDynamicsCompressor();
this.micCompressor.ratio.value = 3; // mic compressor - fairly gentle, can be upped
this.micCompressor.threshold.value = -18;
@ -264,6 +269,7 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
this.micCalibrationGain
.connect(this.micPrecompAnalyser)
this.micCalibrationGain
.connect(this.micCompressor)
.connect(this.micMixGain)
.connect(this.micFinalAnalyser)
@ -337,34 +343,46 @@ export class AudioEngine extends ((EngineEmitter as unknown) as {
this.micMixGain.gain.value = value;
}
getLevel(source: LevelsSource) {
getLevels(source: LevelsSource, stereo: boolean) {
let analysisBuffer = new Float32Array(ANALYSIS_FFT_SIZE);
let analysisBuffer2 = new Float32Array(ANALYSIS_FFT_SIZE);
switch (source) {
case "mic-precomp":
this.micPrecompAnalyser.getFloatTimeDomainData(this.analysisBuffer);
this.micPrecompAnalyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
case "mic-final":
this.micFinalAnalyser.getFloatTimeDomainData(this.analysisBuffer);
this.micFinalAnalyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
case "master":
this.streamingAnalyser.getFloatTimeDomainData(this.analysisBuffer);
this.streamingAnalyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
case "player-0":
this.player0Analyser.getFloatTimeDomainData(this.analysisBuffer);
this.player0Analyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
case "player-1":
this.player1Analyser.getFloatTimeDomainData(this.analysisBuffer);
this.player1Analyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
case "player-2":
this.player2Analyser.getFloatTimeDomainData(this.analysisBuffer);
this.player2Analyser.getFloatTimeDomainData(analysisBuffer, analysisBuffer2);
break;
default:
throw new Error("can't getLevel " + source);
}
let peak = 0;
for (let i = 0; i < this.analysisBuffer.length; i++) {
peak = Math.max(peak, Math.abs(this.analysisBuffer[i]));
let peakL = 0;
for (let i = 0; i < analysisBuffer.length; i++) {
peakL = Math.max(peakL, Math.abs(analysisBuffer[i]));
}
return 20 * Math.log10(peak);
peakL = 20 * Math.log10(peakL);
if (stereo) {
let peakR = 0;
for (let i = 0; i < analysisBuffer2.length; i++) {
peakR = Math.max(peakR, Math.abs(analysisBuffer2[i]));
}
peakR = 20 * Math.log10(peakR);
return [peakL, peakR]
}
return [peakL];
}
async playNewsEnd() {

View file

@ -269,7 +269,7 @@ export function NavBarWebStudio() {
</li>
)}
<li className="nav-item px-2 nav-vu">
<VUMeter width={235} height={40} source="master" range={[-40, 3]} />
<VUMeter width={235} height={40} source="master" range={[-40, 3]} stereo={true} />
</li>
</ul>
</>

View file

@ -125,6 +125,7 @@ export function MicTab() {
source="mic-precomp"
range={[-70, 0]}
greenRange={[-14, -10]}
stereo={true}
/>
</div>
<div>

View file

@ -14,6 +14,7 @@ interface VUMeterProps extends HTMLProps<HTMLCanvasElement> {
range: [number, number];
greenRange?: [number, number];
source: LevelsSource;
stereo: boolean;
}
export function VUMeter(props: VUMeterProps) {
@ -22,15 +23,19 @@ export function VUMeter(props: VUMeterProps) {
const isMicOpen = useSelector((state: RootState) => state.mixer.mic.open);
const rafRef = useRef<number | null>(null);
const [peak, setPeak] = useState(-Infinity);
const [peakL, setPeakL] = useState(-Infinity);
const [peakR, setPeakR] = useState(-Infinity);
const isMic = props.source.substr(0, 3) === "mic";
useEffect(() => {
const animate = () => {
if (!isMic || isMicOpen) {
const result = audioEngine.getLevel(props.source);
setPeak(result);
const result = audioEngine.getLevels(props.source, props.stereo);
setPeakL(result[0]);
if (props.stereo) {
setPeakR(result[1]);
}
rafRef.current = requestAnimationFrame(animate);
}
};
@ -43,7 +48,7 @@ export function VUMeter(props: VUMeterProps) {
rafRef.current = null;
}
};
}, [isMicOpen, isMic, props.source]);
}, [isMicOpen, isMic, props.source, props.stereo]);
useLayoutEffect(() => {
if (canvasRef.current) {
@ -76,9 +81,15 @@ export function VUMeter(props: VUMeterProps) {
height
);
if (peak >= props.greenRange[0] && peak <= props.greenRange[1]) {
if (
(peakL >= props.greenRange[0] && peakL <= props.greenRange[1])
|| (props.stereo && peakR >= props.greenRange[0] && peakR <= props.greenRange[1])
) {
ctx.fillStyle = "#00ff00";
} else if (peak < props.greenRange[0]) {
} else if (
(peakL < props.greenRange[0])
|| (props.stereo && peakR < props.greenRange[0])
) {
ctx.fillStyle = "#e8d120";
} else {
ctx.fillStyle = "#ff0000";
@ -87,17 +98,28 @@ export function VUMeter(props: VUMeterProps) {
ctx.fillStyle = "#e8d120";
}
const valueOffset =
(Math.max(peak, props.range[0]) - props.range[0]) / valueRange;
ctx.fillRect(0, 0, valueOffset * width, height - 10);
const valueOffsetL =
(Math.max(peakL, props.range[0]) - props.range[0]) / valueRange;
let valueOffsetR = 0;
if (props.stereo) {
valueOffsetR =
(Math.max(peakR, props.range[0]) - props.range[0]) / valueRange;
} else {
valueOffsetR = valueOffsetL;
}
ctx.fillRect(0, 0, valueOffsetL * width, height/2 - 7);
ctx.fillRect(0, height/2 - 6, valueOffsetR * width, height / 2 - 7);
ctx.fillStyle = "#fff";
for (let i = 0; i < 10; i++) {
const value = (props.range[0] + valueRange * (i / 10)).toFixed(0);
ctx.fillText(value, width * (i / 10), height - 7);
ctx.fillText(value, width * (i / 10), height - 2);
}
}, [peak, props.range, props.greenRange]);
}, [peakL, peakR, props.range, props.greenRange, props.stereo]);
const { value, range, greenRange, source, ...rest } = props;

View file

@ -259,6 +259,7 @@ export function Player({ id }: { id: number }) {
height={40}
source={VUsource(id)}
range={[-40, 0]}
stereo={true}
/>}
</div>
);

View file

@ -172,6 +172,7 @@ function MicControl() {
source="mic-final"
range={[-40, 3]}
greenRange={[-10, -5]}
stereo={true}
/>
</div>
<div className={`mixer-buttons ${!state.open && "disabled"}`}>

View file

@ -10288,6 +10288,11 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stereo-analyser-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stereo-analyser-node/-/stereo-analyser-node-1.0.0.tgz#349f503ece474651e1e6adf299024ba9a18be8c3"
integrity sha1-NJ9QPs5HRlHh5q3ymQJLqaGL6MM=
stream-browserify@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"