From 5a292d4330accee96d10e5697c9d1642269caeab Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Sun, 1 Jan 2023 14:26:09 -0600 Subject: [PATCH] Add sample rate/bitrate back to WebDJ, optimize reactivity of WebDJ components. --- frontend/vue/components/Public/WebDJ.vue | 3 +- .../Public/WebDJ/MicrophonePanel.vue | 6 +- .../components/Public/WebDJ/PlaylistPanel.vue | 12 +- .../components/Public/WebDJ/SettingsPanel.vue | 195 +++++++++++++++--- .../components/Public/WebDJ/useWebDjNode.js | 163 ++++++--------- .../components/Public/WebDJ/useWebDjSource.js | 69 +++++++ .../components/Public/WebDJ/useWebDjTrack.js | 31 +-- .../components/Public/WebDJ/useWebcaster.js | 12 +- 8 files changed, 345 insertions(+), 146 deletions(-) create mode 100644 frontend/vue/components/Public/WebDJ/useWebDjSource.js diff --git a/frontend/vue/components/Public/WebDJ.vue b/frontend/vue/components/Public/WebDJ.vue index f4d01f804..95463ef68 100644 --- a/frontend/vue/components/Public/WebDJ.vue +++ b/frontend/vue/components/Public/WebDJ.vue @@ -43,7 +43,7 @@ import PlaylistPanel from './WebDJ/PlaylistPanel.vue'; import SettingsPanel from './WebDJ/SettingsPanel.vue'; import {useProvideWebDjNode, useWebDjNode} from "~/components/Public/WebDJ/useWebDjNode"; import {ref} from "vue"; -import {useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster"; +import {useProvideWebcaster, useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster"; import {useProvideMixer} from "~/components/Public/WebDJ/useMixerValue"; import {useProvidePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync"; @@ -56,6 +56,7 @@ const props = defineProps({ }); const webcaster = useWebcaster(props); +useProvideWebcaster(webcaster); const node = useWebDjNode(webcaster); useProvideWebDjNode(node); diff --git a/frontend/vue/components/Public/WebDJ/MicrophonePanel.vue b/frontend/vue/components/Public/WebDJ/MicrophonePanel.vue index a01d2d9f9..5012556da 100644 --- a/frontend/vue/components/Public/WebDJ/MicrophonePanel.vue +++ b/frontend/vue/components/Public/WebDJ/MicrophonePanel.vue @@ -84,9 +84,9 @@ import {useDevicesList} from "@vueuse/core"; import {ref, watch} from "vue"; import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack"; import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync"; +import {useWebDjSource} from "~/components/Public/WebDJ/useWebDjSource"; const { - createMicrophoneSource, source, isPlaying, trackGain, @@ -96,6 +96,10 @@ const { stop } = useWebDjTrack(); +const { + createMicrophoneSource +} = useWebDjSource(); + usePassthroughSync(trackPassThrough, 'microphone'); const {audioInputs} = useDevicesList({ diff --git a/frontend/vue/components/Public/WebDJ/PlaylistPanel.vue b/frontend/vue/components/Public/WebDJ/PlaylistPanel.vue index f07efc352..c427031b4 100644 --- a/frontend/vue/components/Public/WebDJ/PlaylistPanel.vue +++ b/frontend/vue/components/Public/WebDJ/PlaylistPanel.vue @@ -177,6 +177,8 @@ import {useTranslate} from "~/vendor/gettext"; import {forEach} from "lodash"; import {useInjectMixer} from "~/components/Public/WebDJ/useMixerValue"; import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync"; +import {useWebDjSource} from "~/components/Public/WebDJ/useWebDjSource"; +import {useInjectWebcaster} from "~/components/Public/WebDJ/useWebcaster"; const props = defineProps({ id: { @@ -190,8 +192,6 @@ const isLeftPlaylist = computed(() => { }); const { - createAudioSource, - sendMetadata, source, isPlaying, isPaused, @@ -204,6 +204,14 @@ const { stop } = useWebDjTrack(); +const { + createAudioSource +} = useWebDjSource(); + +const { + sendMetadata +} = useInjectWebcaster(); + usePassthroughSync(trackPassThrough, props.id); const fileIndex = ref(-1); diff --git a/frontend/vue/components/Public/WebDJ/SettingsPanel.vue b/frontend/vue/components/Public/WebDJ/SettingsPanel.vue index 17a949c52..b64df4ab1 100644 --- a/frontend/vue/components/Public/WebDJ/SettingsPanel.vue +++ b/frontend/vue/components/Public/WebDJ/SettingsPanel.vue @@ -64,36 +64,161 @@
-
- -
- +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
-
- -
- +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
@@ -130,6 +255,7 @@ import {computed, ref, watch} from "vue"; import {useTranslate} from "~/vendor/gettext"; import {useInjectWebDjNode} from "~/components/Public/WebDJ/useWebDjNode"; import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync"; +import {useInjectWebcaster} from "~/components/Public/WebDJ/useWebcaster"; const props = defineProps({ stationName: { @@ -143,13 +269,18 @@ const djPassword = ref(null); const { doPassThrough, - isConnected, + bitrate, + sampleRate, startStream, - stopStream, - metadata, - sendMetadata + stopStream } = useInjectWebDjNode(); +const { + metadata, + sendMetadata, + isConnected +} = useInjectWebcaster(); + usePassthroughSync(doPassThrough, 'global'); const {$gettext} = useTranslate(); diff --git a/frontend/vue/components/Public/WebDJ/useWebDjNode.js b/frontend/vue/components/Public/WebDJ/useWebDjNode.js index 714869841..8194e9053 100644 --- a/frontend/vue/components/Public/WebDJ/useWebDjNode.js +++ b/frontend/vue/components/Public/WebDJ/useWebDjNode.js @@ -1,4 +1,4 @@ -import {inject, provide, ref} from "vue"; +import {computed, inject, provide, ref} from "vue"; const injectKey = "webDjNode"; @@ -11,55 +11,88 @@ export function useProvideWebDjNode(node) { } export function useWebDjNode(webcaster) { - const {isConnected, connect: connectSocket, metadata, sendMetadata} = webcaster; + const {connect: connectSocket} = webcaster; const doPassThrough = ref(false); - const context = new AudioContext({ - sampleRate: 44100 + const bitrate = ref(128); + const sampleRate = ref(44100); + const channelCount = ref(2); + const bufferSize = ref(256); + + const context = computed(() => { + return new AudioContext({ + sampleRate: sampleRate.value + }); }); - const sink = context.createScriptProcessor(256, 2, 2); + const sink = computed(() => { + let currentContext = context.value; - sink.onaudioprocess = (buf) => { - for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) { - let channelData = buf.inputBuffer.getChannelData(channel); - buf.outputBuffer.getChannelData(channel).set(channelData); - } - }; + let sink = currentContext.createScriptProcessor( + bufferSize.value, + channelCount.value, + channelCount.value + ); - const passThrough = context.createScriptProcessor(256, 2, 2); - - passThrough.onaudioprocess = (buf) => { - for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) { - let channelData = buf.inputBuffer.getChannelData(channel); - - if (doPassThrough.value) { + sink.onaudioprocess = (buf) => { + for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) { + let channelData = buf.inputBuffer.getChannelData(channel); buf.outputBuffer.getChannelData(channel).set(channelData); - } else { - buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length)); } - } - }; + }; - sink.connect(passThrough); - passThrough.connect(context.destination); + return sink; + }); - const streamNode = context.createMediaStreamDestination(); - streamNode.channelCount = 2; + const passThrough = computed(() => { + let currentContext = context.value; - sink.connect(streamNode); + let passThrough = currentContext.createScriptProcessor( + bufferSize.value, + channelCount.value, + channelCount.value + ); + + passThrough.onaudioprocess = (buf) => { + for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) { + let channelData = buf.inputBuffer.getChannelData(channel); + + if (doPassThrough.value) { + buf.outputBuffer.getChannelData(channel).set(channelData); + } else { + buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length)); + } + } + }; + + sink.value.connect(passThrough); + passThrough.value.connect(currentContext.destination); + + return passThrough; + }); + + const streamNode = computed(() => { + let currentContext = context.value; + + const streamNode = currentContext.createMediaStreamDestination(); + streamNode.channelCount = channelCount.value; + + sink.value.connect(streamNode); + + return streamNode; + }); let mediaRecorder; const startStream = (username = null, password = null) => { - context.resume(); + context.value.resume(); mediaRecorder = new MediaRecorder( - streamNode.stream, + streamNode.value.stream, { mimeType: "audio/webm;codecs=opus", - audioBitsPerSecond: 128 * 1000 + audioBitsPerSecond: bitrate.value * 1000 } ); @@ -72,77 +105,17 @@ export function useWebDjNode(webcaster) { mediaRecorder?.stop(); }; - const createAudioSource = ({file, audio}, cb, onEnd) => { - const el = new Audio(URL.createObjectURL(file)); - el.controls = false; - el.autoplay = false; - el.loop = false; - - let source = null; - - el.addEventListener("ended", () => { - if (typeof onEnd === "function") { - onEnd(); - } - }); - - el.addEventListener("canplay", () => { - if (source) { - return; - } - - source = context.createMediaElementSource(el); - - source.play = () => el.play() - source.position = () => el.currentTime; - source.duration = () => el.duration; - source.paused = () => el.paused; - source.stop = () => { - el.pause(); - return el.remove(); - }; - source.pause = () => el.pause; - source.seek = (percent) => { - let time = percent * parseFloat(audio.length); - el.currentTime = time; - return time; - }; - - return cb(source); - }); - }; - - const createMicrophoneSource = (audioDeviceId, cb) => { - navigator.mediaDevices.getUserMedia({ - video: false, - audio: { - deviceId: audioDeviceId - } - }).then((stream) => { - let source = context.createMediaStreamSource(stream); - source.stop = () => { - let ref = stream.getAudioTracks(); - return (ref !== null) - ? ref[0].stop() - : 0; - } - - return cb(source); - }); - }; - return { doPassThrough, - isConnected, + bitrate, + sampleRate, + channelCount, + bufferSize, context, sink, passThrough, streamNode, startStream, - stopStream, - createAudioSource, - createMicrophoneSource, - metadata, - sendMetadata + stopStream }; } diff --git a/frontend/vue/components/Public/WebDJ/useWebDjSource.js b/frontend/vue/components/Public/WebDJ/useWebDjSource.js new file mode 100644 index 000000000..876afbf4f --- /dev/null +++ b/frontend/vue/components/Public/WebDJ/useWebDjSource.js @@ -0,0 +1,69 @@ +import {useInjectWebDjNode} from "~/components/Public/WebDJ/useWebDjNode"; + +export function useWebDjSource() { + const {context} = useInjectWebDjNode(); + + const createAudioSource = ({file, audio}, cb, onEnd) => { + const el = new Audio(URL.createObjectURL(file)); + el.controls = false; + el.autoplay = false; + el.loop = false; + + let source = null; + + el.addEventListener("ended", () => { + if (typeof onEnd === "function") { + onEnd(); + } + }); + + el.addEventListener("canplay", () => { + if (source) { + return; + } + + source = context.value.createMediaElementSource(el); + + source.play = () => el.play() + source.position = () => el.currentTime; + source.duration = () => el.duration; + source.paused = () => el.paused; + source.stop = () => { + el.pause(); + return el.remove(); + }; + source.pause = () => el.pause; + source.seek = (percent) => { + let time = percent * parseFloat(audio.length); + el.currentTime = time; + return time; + }; + + return cb(source); + }); + }; + + const createMicrophoneSource = (audioDeviceId, cb) => { + navigator.mediaDevices.getUserMedia({ + video: false, + audio: { + deviceId: audioDeviceId + } + }).then((stream) => { + let source = context.value.createMediaStreamSource(stream); + source.stop = () => { + let ref = stream.getAudioTracks(); + return (ref !== null) + ? ref[0].stop() + : 0; + } + + return cb(source); + }); + }; + + return { + createAudioSource, + createMicrophoneSource + } +} diff --git a/frontend/vue/components/Public/WebDJ/useWebDjTrack.js b/frontend/vue/components/Public/WebDJ/useWebDjTrack.js index 127aa35d3..6e610f02c 100644 --- a/frontend/vue/components/Public/WebDJ/useWebDjTrack.js +++ b/frontend/vue/components/Public/WebDJ/useWebDjTrack.js @@ -5,9 +5,8 @@ export function useWebDjTrack() { const { context, sink, - createMicrophoneSource, - createAudioSource, - sendMetadata + channelCount, + bufferSize } = useInjectWebDjNode(); const trackGain = ref(55); @@ -18,11 +17,14 @@ export function useWebDjTrack() { let source = ref(null); const createControlsNode = () => { - const bufferSize = 4096; - const bufferLog = Math.log(parseFloat(bufferSize)); + const bufferLog = Math.log(parseFloat(bufferSize.value)); const log10 = 2.0 * Math.log(10); - let newSource = context.createScriptProcessor(bufferSize, 2, 2); + let newSource = context.value.createScriptProcessor( + bufferSize.value, + channelCount.value, + channelCount.value + ); newSource.onaudioprocess = (buf) => { if (typeof (source.value?.position) === "function") { @@ -47,7 +49,11 @@ export function useWebDjTrack() { }; const createPassThrough = () => { - let newSource = context.createScriptProcessor(256, 2, 2); + let newSource = context.value.createScriptProcessor( + bufferSize.value, + channelCount.value, + channelCount.value + ); newSource.onaudioprocess = (buf) => { for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) { @@ -79,17 +85,17 @@ export function useWebDjTrack() { const prepare = () => { controlsNode = createControlsNode(); - controlsNode.connect(sink); + controlsNode.connect(sink.value); - trackGainNode = context.createGain(); + trackGainNode = context.value.createGain(); trackGainNode.gain.value = parseFloat(trackGain.value) / 100.0; trackGainNode.connect(controlsNode); passThroughNode = createPassThrough(); - passThroughNode.connect(context.destination); + passThroughNode.connect(context.value.destination); trackGainNode.connect(passThroughNode); - context.resume(); + context.value.resume(); return trackGainNode; } @@ -130,9 +136,6 @@ export function useWebDjTrack() { }; return { - createMicrophoneSource, - createAudioSource, - sendMetadata, source, trackGain, trackPassThrough, diff --git a/frontend/vue/components/Public/WebDJ/useWebcaster.js b/frontend/vue/components/Public/WebDJ/useWebcaster.js index 66bfcbeef..b216c8602 100644 --- a/frontend/vue/components/Public/WebDJ/useWebcaster.js +++ b/frontend/vue/components/Public/WebDJ/useWebcaster.js @@ -1,4 +1,4 @@ -import {ref, shallowRef} from "vue"; +import {inject, provide, ref, shallowRef} from "vue"; import {useNotify} from "~/vendor/bootstrapVue"; import {useTranslate} from "~/vendor/gettext"; @@ -9,6 +9,16 @@ export const webcasterProps = { } }; +const injectKey = 'webDjWebcaster'; + +export function useProvideWebcaster(webcaster) { + provide(injectKey, webcaster); +} + +export function useInjectWebcaster() { + return inject(injectKey); +} + export function useWebcaster(props) { const {baseUri} = props;