Add sample rate/bitrate back to WebDJ, optimize reactivity of WebDJ components.

This commit is contained in:
Buster Neece 2023-01-01 14:26:09 -06:00
parent 62d68f5a7c
commit 5a292d4330
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
8 changed files with 345 additions and 146 deletions

View File

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

View File

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

View File

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

View File

@ -64,36 +64,161 @@
</div>
<div class="card-body">
<div class="form-group">
<label
for="dj_username"
class="mb-2"
>
{{ $gettext('Streamer/DJ Username') }}
</label>
<div class="controls">
<input
id="dj_username"
v-model="djUsername"
type="text"
class="form-control"
>
<div class="form-row">
<div class="col md-6">
<div class="form-group">
<label
for="dj_username"
class="mb-2"
>
{{ $gettext('Username') }}
</label>
<div class="controls">
<input
id="dj_username"
v-model="djUsername"
type="text"
class="form-control"
>
</div>
</div>
</div>
<div class="col md-6">
<div class="form-group">
<label
for="dj_password"
class="mb-2"
>
{{ $gettext('Password') }}
</label>
<div class="controls">
<input
id="dj_password"
v-model="djPassword"
type="password"
class="form-control"
>
</div>
</div>
</div>
</div>
<div class="form-group">
<label
for="dj_password"
class="mb-2"
>
{{ $gettext('Streamer/DJ Password') }}
</label>
<div class="controls">
<input
id="dj_password"
v-model="djPassword"
type="password"
class="form-control"
>
<div class="form-row">
<div class="col md-6">
<div class="form-group">
<label
for="select_samplerate"
class="mb-2"
>
{{ $gettext('Sample Rate') }}
</label>
<div class="controls">
<select
id="select_samplerate"
v-model.number="sampleRate"
class="form-control"
>
<option value="8000">
8 kHz
</option>
<option value="11025">
11.025 kHz
</option>
<option value="12000">
12 kHz
</option>
<option value="16000">
16 kHz
</option>
<option value="22050">
22.05 kHz
</option>
<option value="24000">
24 kHz
</option>
<option value="32000">
32 kHz
</option>
<option value="44100">
44.1 kHz
</option>
<option value="48000">
48 kHz
</option>
</select>
</div>
</div>
</div>
<div class="col md-6">
<div class="form-group">
<label
for="select_bitrate"
class="mb-2"
>
{{ $gettext('Bit Rate') }}
</label>
<div class="controls">
<select
id="select_bitrate"
v-model.number="bitrate"
class="form-control"
>
<option value="8">
8 kbps
</option>
<option value="16">
16 kbps
</option>
<option value="24">
24 kbps
</option>
<option value="32">
32 kbps
</option>
<option value="40">
40 kbps
</option>
<option value="48">
48 kbps
</option>
<option value="56">
56 kbps
</option>
<option value="64">
64 kbps
</option>
<option value="80">
80 kbps
</option>
<option value="96">
96 kbps
</option>
<option value="112">
112 kbps
</option>
<option value="128">
128 kbps
</option>
<option value="144">
144 kbps
</option>
<option value="160">
160 kbps
</option>
<option value="192">
192 kbps
</option>
<option value="224">
224 kbps
</option>
<option value="256">
256 kbps
</option>
<option value="320">
320 kbps
</option>
</select>
</div>
</div>
</div>
</div>
</div>
@ -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();

View File

@ -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
};
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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;