voice-changer/client/lib/src/VoiceChangerClient.ts

301 lines
12 KiB
TypeScript
Raw Normal View History

import { VoiceChangerWorkletNode, VoiceChangerWorkletListener } from "./VoiceChangerWorkletNode";
2023-01-04 20:28:36 +03:00
// @ts-ignore
import workerjs from "raw-loader!../worklet/dist/index.js";
import { VoiceFocusDeviceTransformer, VoiceFocusTransformDevice } from "amazon-chime-sdk-js";
2023-01-08 03:22:22 +03:00
import { createDummyMediaStream, validateUrl } from "./util";
2023-02-19 20:21:51 +03:00
import { DefaultVoiceChangerClientSetting, ServerSettingKey, VoiceChangerClientSetting, WorkletNodeSetting, WorkletSetting } from "./const";
2023-01-08 10:18:20 +03:00
import { ServerConfigurator } from "./ServerConfigurator";
2023-01-04 20:28:36 +03:00
// オーディオデータの流れ
2023-02-19 20:21:51 +03:00
// input node(mic or MediaStream) -> [vf node] -> [vc node] ->
// sio/rest server -> [vc node] -> output node
2023-01-04 20:28:36 +03:00
2023-01-29 03:42:45 +03:00
import { BlockingQueue } from "./utils/BlockingQueue";
2023-01-04 20:28:36 +03:00
2023-01-11 22:52:01 +03:00
export class VoiceChangerClient {
2023-01-08 10:18:20 +03:00
private configurator: ServerConfigurator
2023-01-04 20:28:36 +03:00
private ctx: AudioContext
private vfEnable = false
private vf: VoiceFocusDeviceTransformer | null = null
private currentDevice: VoiceFocusTransformDevice | null = null
private currentMediaStream: MediaStream | null = null
private currentMediaStreamAudioSourceNode: MediaStreamAudioSourceNode | null = null
2023-02-12 12:19:22 +03:00
private inputGainNode: GainNode | null = null
private outputGainNode: GainNode | null = null
2023-03-15 00:39:33 +03:00
private vcInNode!: VoiceChangerWorkletNode
private vcOutNode!: VoiceChangerWorkletNode
2023-01-04 20:28:36 +03:00
private currentMediaStreamAudioDestinationNode!: MediaStreamAudioDestinationNode
2023-02-12 12:19:22 +03:00
2023-01-04 20:28:36 +03:00
private promiseForInitialize: Promise<void>
2023-01-05 05:45:42 +03:00
private _isVoiceChanging = false
2023-01-04 20:28:36 +03:00
2023-02-19 08:20:37 +03:00
private setting: VoiceChangerClientSetting = DefaultVoiceChangerClientSetting
2023-01-08 11:58:27 +03:00
private sslCertified: string[] = []
2023-01-29 03:42:45 +03:00
private sem = new BlockingQueue<number>();
2023-02-19 20:21:51 +03:00
constructor(ctx: AudioContext, vfEnable: boolean, voiceChangerWorkletListener: VoiceChangerWorkletListener) {
2023-01-29 03:42:45 +03:00
this.sem.enqueue(0);
2023-01-08 10:18:20 +03:00
this.configurator = new ServerConfigurator()
2023-01-04 20:28:36 +03:00
this.ctx = ctx
this.vfEnable = vfEnable
this.promiseForInitialize = new Promise<void>(async (resolve) => {
const scriptUrl = URL.createObjectURL(new Blob([workerjs], { type: "text/javascript" }));
await this.ctx.audioWorklet.addModule(scriptUrl)
2023-03-15 00:39:33 +03:00
this.vcInNode = new VoiceChangerWorkletNode(this.ctx, voiceChangerWorkletListener); // vc node
// const ctx44k = new AudioContext({ sampleRate: 44100 }) // これでもプチプチが残る
const ctx44k = new AudioContext({ sampleRate: 48000 }) // 結局これが一番まし。
console.log("audio out:", ctx44k)
await ctx44k.audioWorklet.addModule(scriptUrl)
this.vcOutNode = new VoiceChangerWorkletNode(ctx44k, voiceChangerWorkletListener); // vc node
this.currentMediaStreamAudioDestinationNode = ctx44k.createMediaStreamDestination() // output node
this.outputGainNode = ctx44k.createGain()
2023-02-19 08:20:37 +03:00
this.outputGainNode.gain.value = this.setting.outputGain
2023-03-15 00:39:33 +03:00
this.vcOutNode.connect(this.outputGainNode) // vc node -> output node
2023-02-12 12:19:22 +03:00
this.outputGainNode.connect(this.currentMediaStreamAudioDestinationNode)
2023-01-04 20:28:36 +03:00
if (this.vfEnable) {
this.vf = await VoiceFocusDeviceTransformer.create({ variant: 'c20' })
const dummyMediaStream = createDummyMediaStream(this.ctx)
this.currentDevice = (await this.vf.createTransformDevice(dummyMediaStream)) || null;
}
resolve()
})
}
2023-01-29 03:42:45 +03:00
private lock = async () => {
const num = await this.sem.dequeue();
return num;
};
private unlock = (num: number) => {
this.sem.enqueue(num + 1);
};
2023-01-04 20:28:36 +03:00
isInitialized = async () => {
if (this.promiseForInitialize) {
await this.promiseForInitialize
}
return true
}
/////////////////////////////////////////////////////
// オペレーション
/////////////////////////////////////////////////////
/// Operations ///
2023-02-19 08:20:37 +03:00
setup = async () => {
2023-01-29 03:42:45 +03:00
const lockNum = await this.lock()
2023-02-17 22:15:34 +03:00
2023-02-19 08:20:37 +03:00
console.log(`Input Setup=> echo: ${this.setting.echoCancel}, noise1: ${this.setting.noiseSuppression}, noise2: ${this.setting.noiseSuppression2}`)
2023-01-04 20:28:36 +03:00
// condition check
2023-03-15 00:39:33 +03:00
if (!this.vcInNode) {
2023-01-04 20:28:36 +03:00
console.warn("vc node is not initialized.")
throw "vc node is not initialized."
}
// Main Process
//// shutdown & re-generate mediastream
if (this.currentMediaStream) {
this.currentMediaStream.getTracks().forEach(x => { x.stop() })
this.currentMediaStream = null
}
2023-02-17 22:15:34 +03:00
//// Input デバイスがnullの時はmicStreamを止めてリターン
2023-02-19 08:20:37 +03:00
if (!this.setting.audioInput) {
2023-02-17 22:15:34 +03:00
console.log(`Input Setup=> client mic is disabled.`)
2023-03-15 00:39:33 +03:00
this.vcInNode.stop()
2023-02-17 22:15:34 +03:00
await this.unlock(lockNum)
return
}
2023-02-19 08:20:37 +03:00
if (typeof this.setting.audioInput == "string") {
try {
this.currentMediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: this.setting.audioInput,
channelCount: 1,
sampleRate: this.setting.sampleRate,
sampleSize: 16,
autoGainControl: false,
echoCancellation: this.setting.echoCancel,
noiseSuppression: this.setting.noiseSuppression
}
})
} catch (e) {
console.warn(e)
throw e
}
2023-02-14 23:02:51 +03:00
// this.currentMediaStream.getAudioTracks().forEach((x) => {
// console.log("MIC Setting(cap)", x.getCapabilities())
// console.log("MIC Setting(const)", x.getConstraints())
// console.log("MIC Setting(setting)", x.getSettings())
// })
2023-01-04 20:28:36 +03:00
} else {
2023-02-19 08:20:37 +03:00
this.currentMediaStream = this.setting.audioInput
2023-01-04 20:28:36 +03:00
}
// connect nodes.
2023-02-12 12:50:10 +03:00
this.currentMediaStreamAudioSourceNode = this.ctx.createMediaStreamSource(this.currentMediaStream)
this.inputGainNode = this.ctx.createGain()
2023-02-19 08:20:37 +03:00
this.inputGainNode.gain.value = this.setting.inputGain
2023-02-12 12:50:10 +03:00
this.currentMediaStreamAudioSourceNode.connect(this.inputGainNode)
2023-02-19 08:20:37 +03:00
if (this.currentDevice && this.setting.noiseSuppression2) {
2023-01-04 20:28:36 +03:00
this.currentDevice.chooseNewInnerDevice(this.currentMediaStream)
const voiceFocusNode = await this.currentDevice.createAudioNode(this.ctx); // vf node
2023-02-12 12:19:22 +03:00
this.inputGainNode.connect(voiceFocusNode.start) // input node -> vf node
2023-03-15 00:39:33 +03:00
voiceFocusNode.end.connect(this.vcInNode)
2023-01-04 20:28:36 +03:00
} else {
2023-02-19 22:49:34 +03:00
// console.log("input___ media stream", this.currentMediaStream)
// this.currentMediaStream.getTracks().forEach(x => {
// console.log("input___ media stream set", x.getSettings())
// console.log("input___ media stream con", x.getConstraints())
// console.log("input___ media stream cap", x.getCapabilities())
// })
// console.log("input___ media node", this.currentMediaStreamAudioSourceNode)
// console.log("input___ gain node", this.inputGainNode.channelCount, this.inputGainNode)
2023-03-15 00:39:33 +03:00
this.inputGainNode.connect(this.vcInNode)
2023-01-05 12:35:56 +03:00
}
2023-03-15 00:39:33 +03:00
this.vcInNode.setOutputNode(this.vcOutNode)
2023-02-16 20:11:03 +03:00
console.log("Input Setup=> success")
2023-01-29 03:42:45 +03:00
await this.unlock(lockNum)
2023-01-04 20:28:36 +03:00
}
get stream(): MediaStream {
return this.currentMediaStreamAudioDestinationNode.stream
}
2023-01-05 05:45:42 +03:00
start = () => {
2023-03-15 00:39:33 +03:00
this.vcInNode.start()
2023-01-05 05:45:42 +03:00
this._isVoiceChanging = true
}
stop = () => {
2023-03-15 00:39:33 +03:00
this.vcInNode.stop()
2023-01-05 05:45:42 +03:00
this._isVoiceChanging = false
}
2023-02-19 20:21:51 +03:00
2023-01-05 05:45:42 +03:00
get isVoiceChanging(): boolean {
return this._isVoiceChanging
}
////////////////////////
/// 設定
//////////////////////////////
2023-01-08 10:18:20 +03:00
setServerUrl = (serverUrl: string, openTab: boolean = false) => {
2023-01-08 03:22:22 +03:00
const url = validateUrl(serverUrl)
const pageUrl = `${location.protocol}//${location.host}`
2023-01-08 14:28:57 +03:00
if (url != pageUrl && url.length != 0 && location.protocol == "https:" && this.sslCertified.includes(url) == false) {
2023-01-08 03:22:22 +03:00
if (openTab) {
const value = window.confirm("MMVC Server is different from this page's origin. Open tab to open ssl connection. OK? (You can close the opened tab after ssl connection succeed.)");
if (value) {
window.open(url, '_blank')
2023-01-08 11:58:27 +03:00
this.sslCertified.push(url)
2023-01-08 03:22:22 +03:00
} else {
alert("Your voice conversion may fail...")
}
}
}
2023-03-15 00:39:33 +03:00
this.vcInNode.updateSetting({ ...this.vcInNode.getSettings(), serverUrl: url })
2023-01-08 10:18:20 +03:00
this.configurator.setServerUrl(url)
2023-01-04 20:28:36 +03:00
}
2023-02-19 08:20:37 +03:00
updateClientSetting = (setting: VoiceChangerClientSetting) => {
console.log(`[VoiceChangerClient] Updating Client Setting,`, this.setting, setting)
let reconstructInputRequired = false
if (
this.setting.audioInput != setting.audioInput ||
this.setting.echoCancel != setting.echoCancel ||
this.setting.noiseSuppression != setting.noiseSuppression ||
this.setting.noiseSuppression2 != setting.noiseSuppression2 ||
this.setting.sampleRate != setting.sampleRate
) {
reconstructInputRequired = true
}
2023-02-19 13:22:00 +03:00
if (this.setting.inputGain != setting.inputGain) {
this.setInputGain(setting.inputGain)
}
if (this.setting.outputGain != setting.outputGain) {
this.setOutputGain(setting.outputGain)
}
2023-02-19 08:20:37 +03:00
this.setting = setting
if (reconstructInputRequired) {
this.setup()
}
}
setInputGain = (val: number) => {
2023-02-19 08:20:37 +03:00
this.setting.inputGain = val
if (!this.inputGainNode) {
return
}
this.inputGainNode.gain.value = val
2023-01-04 20:28:36 +03:00
}
setOutputGain = (val: number) => {
if (!this.outputGainNode) {
return
}
this.outputGainNode.gain.value = val
2023-02-18 14:53:15 +03:00
}
2023-01-05 05:45:42 +03:00
/////////////////////////////////////////////////////
// コンポーネント設定、操作
/////////////////////////////////////////////////////
//## Server ##//
updateServerSettings = (key: ServerSettingKey, val: string) => {
return this.configurator.updateSettings(key, val)
}
uploadFile = (buf: ArrayBuffer, filename: string, onprogress: (progress: number, end: boolean) => void) => {
return this.configurator.uploadFile(buf, filename, onprogress)
}
concatUploadedFile = (filename: string, chunkNum: number) => {
return this.configurator.concatUploadedFile(filename, chunkNum)
}
loadModel = (configFilename: string, pyTorchModelFilename: string | null, onnxModelFilename: string | null, clusterTorchModelFilename: string | null, hubertTorchModelFilename: string | null) => {
// !! 注意!! hubertTorchModelは固定値で上書きされるため、設定しても効果ない。
return this.configurator.loadModel(configFilename, pyTorchModelFilename, onnxModelFilename, clusterTorchModelFilename, hubertTorchModelFilename)
}
//## Worklet ##//
2023-01-11 22:52:01 +03:00
configureWorklet = (setting: WorkletSetting) => {
2023-03-15 00:39:33 +03:00
this.vcInNode.configure(setting)
this.vcOutNode.configure(setting)
}
2023-02-19 22:49:34 +03:00
startOutputRecording = () => {
2023-03-15 00:39:33 +03:00
this.vcOutNode.startOutputRecording()
2023-01-11 22:52:01 +03:00
}
2023-02-19 22:49:34 +03:00
stopOutputRecording = () => {
2023-03-15 00:39:33 +03:00
return this.vcOutNode.stopOutputRecording()
}
2023-02-28 05:54:40 +03:00
trancateBuffer = () => {
2023-03-15 00:39:33 +03:00
this.vcOutNode.trancateBuffer()
2023-02-28 05:54:40 +03:00
}
2023-02-19 20:21:51 +03:00
//## Worklet Node ##//
updateWorkletNodeSetting = (setting: WorkletNodeSetting) => {
2023-03-15 00:39:33 +03:00
this.vcInNode.updateSetting(setting)
this.vcOutNode.updateSetting(setting)
2023-02-12 12:19:22 +03:00
}
/////////////////////////////////////////////////////
// 情報取得
/////////////////////////////////////////////////////
2023-01-08 10:18:20 +03:00
// Information
getClientSettings = () => {
2023-03-15 00:39:33 +03:00
return this.vcInNode.getSettings()
2023-01-08 10:18:20 +03:00
}
getServerSettings = () => {
return this.configurator.getSettings()
}
2023-02-17 22:15:34 +03:00
getSocketId = () => {
2023-03-15 00:39:33 +03:00
return this.vcInNode.getSocketId()
2023-02-17 22:15:34 +03:00
}
2023-01-08 10:18:20 +03:00
2023-01-04 20:28:36 +03:00
}