Mixer sync and passthrough sync.

This commit is contained in:
Buster Neece 2023-01-01 08:16:32 -06:00
parent f161b6c806
commit 2125e320e3
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
11 changed files with 167 additions and 63 deletions

View File

@ -41,9 +41,11 @@ import MixerPanel from './WebDJ/MixerPanel.vue';
import MicrophonePanel from './WebDJ/MicrophonePanel.vue';
import PlaylistPanel from './WebDJ/PlaylistPanel.vue';
import SettingsPanel from './WebDJ/SettingsPanel.vue';
import {useWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
import {provide} from "vue";
import {useProvideWebDjNode, useWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
import {ref} from "vue";
import {useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster";
import {useProvideMixer} from "~/components/Public/WebDJ/useMixerValue";
import {useProvidePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
const props = defineProps({
...webcasterProps,
@ -54,6 +56,13 @@ const props = defineProps({
});
const webcaster = useWebcaster(props);
const node = useWebDjNode(webcaster);
provide('node', node);
useProvideWebDjNode(node);
const mixer = ref(1.0);
useProvideMixer(mixer);
const passthroughSync = ref('');
useProvidePassthroughSync(passthroughSync);
</script>

View File

@ -83,34 +83,48 @@ import VolumeSlider from "~/components/Public/WebDJ/VolumeSlider";
import {useDevicesList} from "@vueuse/core";
import {ref, watch} from "vue";
import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack";
import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
const {node, source, isPlaying, trackGain, trackPassThrough, volume, prepare, stop} = useWebDjTrack();
const {
createMicrophoneSource,
source,
isPlaying,
trackGain,
trackPassThrough,
volume,
prepare,
stop
} = useWebDjTrack();
const {createMicrophoneSource} = node;
usePassthroughSync(trackPassThrough, 'microphone');
const {audioInputs} = useDevicesList({
requestPermissions: true,
constraints: {audio: true, video: false}
});
const device = ref(audioInputs.value[0]?.deviceId);
const device = ref(null);
watch(audioInputs, (inputs) => {
if (device.value === null) {
device.value = inputs[0]?.deviceId;
}
});
let destination = null;
const createSource = () => {
if (source.value != null && destination !== null) {
if (source.value != null) {
source.value.disconnect(destination);
}
createMicrophoneSource(device.value, (newSource) => {
source.value = newSource;
if (destination !== null) {
newSource.connect(destination);
}
newSource.connect(destination);
});
};
watch(device, () => {
if (source.value == null) {
if (source.value === null || destination === null) {
return;
}
createSource();

View File

@ -14,14 +14,14 @@
</div>
<div class="flex-fill px-2">
<input
v-model="mixerValue"
v-model.number="mixer"
type="range"
min="0"
max="1"
step="0.01"
max="2"
step="0.05"
class="custom-range slider"
style="width: 200px; height: 10px;"
@click.right.prevent="mixerValue = 0.5"
@click.right.prevent="mixer = 1.0"
>
</div>
<div class="flex-shrink-0">
@ -35,7 +35,7 @@
</template>
<script setup>
import {useMixerValue} from "~/components/Public/WebDJ/useMixerValue";
import {useInjectMixer} from "~/components/Public/WebDJ/useMixerValue";
const mixerValue = useMixerValue();
const mixer = useInjectMixer();
</script>

View File

@ -8,7 +8,7 @@
</h5>
</div>
<div class="flex-shrink-0 pl-3">
<volume-slider v-model.number="trackGain" />
<volume-slider v-model.number="localGain" />
</div>
</div>
</div>
@ -169,10 +169,12 @@
import Icon from '~/components/Common/Icon';
import VolumeSlider from "~/components/Public/WebDJ/VolumeSlider";
import formatTime from "~/functions/formatTime";
import {computed, ref} from "vue";
import {computed, ref, watch} from "vue";
import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack";
import {useTranslate} from "~/vendor/gettext";
import {forEach} from "lodash";
import {useInjectMixer} from "~/components/Public/WebDJ/useMixerValue";
import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
const props = defineProps({
id: {
@ -186,7 +188,8 @@ const isLeftPlaylist = computed(() => {
});
const {
node,
createAudioSource,
sendMetadata,
source,
isPlaying,
isPaused,
@ -199,7 +202,7 @@ const {
stop
} = useWebDjTrack();
const {createFileSource, sendMetadata} = node;
usePassthroughSync(trackPassThrough, props.id);
const fileIndex = ref(-1);
const files = ref([]);
@ -207,6 +210,28 @@ const duration = ref(0.0);
const loop = ref(false);
const playThrough = ref(false);
// Factor in mixer and local gain to calculate total gain.
const localGain = ref(55);
const mixer = useInjectMixer();
const computedGain = computed(() => {
let multiplier;
if (isLeftPlaylist.value) {
multiplier = (mixer.value > 1)
? 2.0 - (mixer.value)
: 1.0;
} else {
multiplier = (mixer.value < 1)
? mixer.value
: 1.0;
}
return localGain.value * multiplier;
});
watch(computedGain, (newGain) => {
trackGain.value = newGain;
}, {immediate: true});
const {$gettext} = useTranslate();
const langHeader = computed(() => {
@ -272,7 +297,7 @@ const play = (options = {}) => {
let destination = prepare();
createFileSource(file, (newSource) => {
createAudioSource(file, (newSource) => {
source.value = newSource;
newSource.connect(destination);

View File

@ -7,7 +7,7 @@
<small>{{ stationName }}</small>
</h5>
</div>
<div class="card-body pt-0">
<div class="card-body">
<template v-if="isConnected">
<div class="form-group">
<label
@ -113,8 +113,10 @@
</template>
<script setup>
import {computed, inject, ref, watch} from "vue";
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";
const props = defineProps({
stationName: {
@ -133,7 +135,9 @@ const {
stopStream,
metadata,
sendMetadata
} = inject('node');
} = useInjectWebDjNode();
usePassthroughSync(doPassThrough, 'global');
const {$gettext} = useTranslate();

View File

@ -27,7 +27,7 @@ import {useVModel} from "@vueuse/core";
const props = defineProps({
modelValue: {
type: String,
type: Number,
required: true
}
});

View File

@ -1,8 +1,11 @@
import {createGlobalState} from "@vueuse/core";
import {ref} from "vue";
import {inject, provide} from "vue";
export function useMixerValue() {
return createGlobalState(
() => ref(0.5)
);
const injectKey = "webDjMixer";
export function useProvideMixer(mixer) {
provide(injectKey, mixer);
}
export function useInjectMixer() {
return inject(injectKey);
}

View File

@ -0,0 +1,27 @@
import {inject, provide, watch} from "vue";
const injectKey = 'webDjPassthroughSync';
export function useProvidePassthroughSync (passthroughSync) {
provide(injectKey, passthroughSync);
}
export function useInjectPassthroughSync() {
return inject(injectKey);
}
export function usePassthroughSync(thisPassthrough, stringVal) {
const passthroughSync = useInjectPassthroughSync();
watch(passthroughSync, (newVal) => {
if (newVal !== stringVal) {
thisPassthrough.value = false;
}
});
watch(thisPassthrough, (newVal) => {
if (newVal) {
passthroughSync.value = stringVal;
}
});
}

View File

@ -1,5 +1,14 @@
import {ref} from "vue";
import {useUserMedia} from "@vueuse/core";
import {inject, provide, ref} from "vue";
const injectKey = "webDjNode";
export function useInjectWebDjNode() {
return inject(injectKey);
}
export function useProvideWebDjNode(node) {
provide(injectKey, node);
}
export function useWebDjNode(webcaster) {
const {isConnected, connect: connectSocket, metadata, sendMetadata} = webcaster;
@ -103,23 +112,23 @@ export function useWebDjNode(webcaster) {
});
};
const createFileSource = (file, cb, onEnd) => {
return createAudioSource(file, cb, onEnd);
};
const createMicrophoneSource = (audioDeviceId, cb) => {
const {stream} = useUserMedia({
audioDeviceId: audioDeviceId,
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);
});
stream.stop = () => {
let ref = stream.getAudioTracks();
return (ref !== null)
? ref[0].stop()
: 0;
}
return cb(stream);
};
return {
@ -132,7 +141,6 @@ export function useWebDjNode(webcaster) {
startStream,
stopStream,
createAudioSource,
createFileSource,
createMicrophoneSource,
metadata,
sendMetadata

View File

@ -1,7 +1,14 @@
import {computed, inject, ref, shallowRef, watch} from "vue";
import {computed, ref, shallowRef, watch} from "vue";
import {useInjectWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
export function useWebDjTrack() {
const node = inject('node');
const {
context,
sink,
createMicrophoneSource,
createAudioSource,
sendMetadata
} = useInjectWebDjNode();
const trackGain = ref(55);
const trackPassThrough = ref(false);
@ -15,7 +22,7 @@ export function useWebDjTrack() {
const bufferLog = Math.log(parseFloat(bufferSize));
const log10 = 2.0 * Math.log(10);
let newSource = node.context.createScriptProcessor(bufferSize, 2, 2);
let newSource = context.createScriptProcessor(bufferSize, 2, 2);
newSource.onaudioprocess = (buf) => {
position.value = source.value?.position();
@ -38,7 +45,7 @@ export function useWebDjTrack() {
};
const createPassThrough = () => {
let newSource = node.context.createScriptProcessor(256, 2, 2);
let newSource = context.createScriptProcessor(256, 2, 2);
newSource.onaudioprocess = (buf) => {
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
@ -70,16 +77,16 @@ export function useWebDjTrack() {
const prepare = () => {
controlsNode = createControlsNode();
controlsNode.connect(node.sink);
controlsNode.connect(sink);
trackGainNode = node.context.createGain();
trackGainNode = context.createGain();
trackGainNode.connect(controlsNode);
passThroughNode = createPassThrough();
passThroughNode.connect(node.context.destination);
passThroughNode.connect(context.destination);
trackGainNode.connect(passThroughNode);
node.context.resume();
context.resume();
return trackGainNode;
}
@ -120,7 +127,9 @@ export function useWebDjTrack() {
};
return {
node,
createMicrophoneSource,
createAudioSource,
sendMetadata,
source,
trackGain,
trackPassThrough,

View File

@ -53,15 +53,20 @@ export function useWebcaster(props) {
isConnected.value = true;
// Timeout as Liquidsoap won't return any success/failure message, so the only
// way we know if we're still connected is to set a timer.
setTimeout(() => {
if (isConnected.value) {
notifySuccess($gettext('WebDJ connected!'));
if (metadata.value !== null) {
sendMetadata(metadata.value);
socket.send(JSON.stringify({
type: "metadata",
data: metadata.value
}));
}
}
}, 2000);
}, 1000);
};
socket.onerror = () => {
@ -74,13 +79,13 @@ export function useWebcaster(props) {
mediaRecorder.ondataavailable = async (e) => {
const data = await e.data.arrayBuffer();
if (isConnected()) {
if (isConnected.value) {
socket.send(data);
}
};
mediaRecorder.onstop = () => {
if (isConnected()) {
if (isConnected.value) {
socket.close();
}
};