add handling setSinkID

This commit is contained in:
w-okada 2023-11-11 00:05:40 +09:00
parent a35551906a
commit 1e68e01e39
3 changed files with 282 additions and 235 deletions

View File

@ -389,8 +389,12 @@ export const DeviceArea = (_props: DeviceAreaProps) => {
// Server Audio を使う場合はElementから音は出さない。 // Server Audio を使う場合はElementから音は出さない。
audio.volume = 0; audio.volume = 0;
} else if (audioOutputForGUI == "none") { } else if (audioOutputForGUI == "none") {
// @ts-ignore try {
audio.setSinkId(""); // @ts-ignore
audio.setSinkId("");
} catch (e) {
console.error("catch:" + e);
}
if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) { if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) {
audio.volume = 0; audio.volume = 0;
} else { } else {
@ -404,8 +408,12 @@ export const DeviceArea = (_props: DeviceAreaProps) => {
return x.deviceId == audioOutputForGUI; return x.deviceId == audioOutputForGUI;
}); });
if (found) { if (found) {
// @ts-ignore // 例外キャッチできないので事前にIDチェックが必要らしい。 try {
audio.setSinkId(audioOutputForGUI); // @ts-ignore // 例外キャッチできないので事前にIDチェックが必要らしい。
audio.setSinkId(audioOutputForGUI);
} catch (e) {
console.error("catch:" + e);
}
} else { } else {
console.warn("No audio output device. use default"); console.warn("No audio output device. use default");
} }
@ -620,9 +628,13 @@ export const DeviceArea = (_props: DeviceAreaProps) => {
// Server Audio を使う場合はElementから音は出さない。 // Server Audio を使う場合はElementから音は出さない。
audio.volume = 0; audio.volume = 0;
} else if (audioMonitorForGUI == "none") { } else if (audioMonitorForGUI == "none") {
// @ts-ignore try {
audio.setSinkId(""); // @ts-ignore
audio.volume = 0; audio.setSinkId("");
audio.volume = 0;
} catch (e) {
console.error("catch:" + e);
}
} else { } else {
const audioOutputs = mediaDeviceInfos.filter((x) => { const audioOutputs = mediaDeviceInfos.filter((x) => {
return x.kind == "audiooutput"; return x.kind == "audiooutput";
@ -631,9 +643,13 @@ export const DeviceArea = (_props: DeviceAreaProps) => {
return x.deviceId == audioMonitorForGUI; return x.deviceId == audioMonitorForGUI;
}); });
if (found) { if (found) {
// @ts-ignore // 例外キャッチできないので事前にIDチェックが必要らしい。 try {
audio.setSinkId(audioMonitorForGUI); // @ts-ignore // 例外キャッチできないので事前にIDチェックが必要らしい。
audio.volume = 1; audio.setSinkId(audioMonitorForGUI);
audio.volume = 1;
} catch (e) {
console.error("catch:" + e);
}
} else { } else {
console.warn("No audio output device. use default"); console.warn("No audio output device. use default");
} }

View File

@ -1,44 +1,50 @@
import React, { useMemo, useState } from "react" import React, { useMemo, useState } from "react";
import { useAppState } from "../../../001_provider/001_AppStateProvider" import { useAppState } from "../../../001_provider/001_AppStateProvider";
import { useGuiState } from "../001_GuiStateProvider" import { useGuiState } from "../001_GuiStateProvider";
import { AUDIO_ELEMENT_FOR_SAMPLING_INPUT, AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT } from "../../../const" import { AUDIO_ELEMENT_FOR_SAMPLING_INPUT, AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT } from "../../../const";
export type RecorderAreaProps = { export type RecorderAreaProps = {};
}
export const RecorderArea = (_props: RecorderAreaProps) => { export const RecorderArea = (_props: RecorderAreaProps) => {
const { serverSetting } = useAppState() const { serverSetting } = useAppState();
const { audioOutputForAnalyzer, setAudioOutputForAnalyzer, outputAudioDeviceInfo } = useGuiState() const { audioOutputForAnalyzer, setAudioOutputForAnalyzer, outputAudioDeviceInfo } = useGuiState();
const [serverIORecording, setServerIORecording] = useState<boolean>(false)
const [serverIORecording, setServerIORecording] = useState<boolean>(false);
const serverIORecorderRow = useMemo(() => { const serverIORecorderRow = useMemo(() => {
const onServerIORecordStartClicked = async () => { const onServerIORecordStartClicked = async () => {
setServerIORecording(true) setServerIORecording(true);
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 1 }) await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 1 });
} };
const onServerIORecordStopClicked = async () => { const onServerIORecordStopClicked = async () => {
setServerIORecording(false) setServerIORecording(false);
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 0 }) await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 0 });
// set wav (input) // set wav (input)
const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement;
wavInput.src = "/tmp/in.wav?" + new Date().getTime() wavInput.src = "/tmp/in.wav?" + new Date().getTime();
wavInput.controls = true wavInput.controls = true;
// @ts-ignore try {
wavInput.setSinkId(audioOutputForAnalyzer) // @ts-ignore
wavInput.setSinkId(audioOutputForAnalyzer);
} catch (e) {
console.log(e);
}
// set wav (output) // set wav (output)
const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement;
wavOutput.src = "/tmp/out.wav?" + new Date().getTime() wavOutput.src = "/tmp/out.wav?" + new Date().getTime();
wavOutput.controls = true wavOutput.controls = true;
// @ts-ignore try {
wavOutput.setSinkId(audioOutputForAnalyzer) // @ts-ignore
} wavOutput.setSinkId(audioOutputForAnalyzer);
} catch (e) {
console.log(e);
}
};
const startClassName = serverIORecording ? "config-sub-area-button-active" : "config-sub-area-button" const startClassName = serverIORecording ? "config-sub-area-button-active" : "config-sub-area-button";
const stopClassName = serverIORecording ? "config-sub-area-button" : "config-sub-area-button-active" const stopClassName = serverIORecording ? "config-sub-area-button" : "config-sub-area-button-active";
return ( return (
<> <>
<div className="config-sub-area-control"> <div className="config-sub-area-control">
@ -49,34 +55,51 @@ export const RecorderArea = (_props: RecorderAreaProps) => {
<div className="config-sub-area-control-title">SIO rec.</div> <div className="config-sub-area-control-title">SIO rec.</div>
<div className="config-sub-area-control-field"> <div className="config-sub-area-control-field">
<div className="config-sub-area-buttons"> <div className="config-sub-area-buttons">
<div onClick={onServerIORecordStartClicked} className={startClassName}>start</div> <div onClick={onServerIORecordStartClicked} className={startClassName}>
<div onClick={onServerIORecordStopClicked} className={stopClassName}>stop</div> start
</div>
<div onClick={onServerIORecordStopClicked} className={stopClassName}>
stop
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="config-sub-area-control left-padding-1"> <div className="config-sub-area-control left-padding-1">
<div className="config-sub-area-control-title">output</div> <div className="config-sub-area-control-title">output</div>
<div className="config-sub-area-control-field"> <div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-auido-io"> <div className="config-sub-area-control-field-auido-io">
<select className="body-select" value={audioOutputForAnalyzer} onChange={(e) => { <select
setAudioOutputForAnalyzer(e.target.value) className="body-select"
const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement value={audioOutputForAnalyzer}
const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement onChange={(e) => {
//@ts-ignore setAudioOutputForAnalyzer(e.target.value);
wavInput.setSinkId(e.target.value) const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement;
//@ts-ignore const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement;
wavOutput.setSinkId(e.target.value) try {
}}> //@ts-ignore
{ wavInput.setSinkId(e.target.value);
outputAudioDeviceInfo.map(x => { //@ts-ignore
wavOutput.setSinkId(e.target.value);
} catch (e) {
console.log(e);
}
}}
>
{outputAudioDeviceInfo
.map((x) => {
if (x.deviceId == "none") { if (x.deviceId == "none") {
return null return null;
} }
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option> return (
}).filter(x => { return x != null }) <option key={x.deviceId} value={x.deviceId}>
} {x.label}
</option>
);
})
.filter((x) => {
return x != null;
})}
</select> </select>
</div> </div>
</div> </div>
@ -102,17 +125,9 @@ export const RecorderArea = (_props: RecorderAreaProps) => {
</div> </div>
</div> </div>
</div> </div>
</> </>
) );
}, [serverIORecording, audioOutputForAnalyzer, outputAudioDeviceInfo, serverSetting.updateServerSettings]);
}, [serverIORecording, audioOutputForAnalyzer, outputAudioDeviceInfo, serverSetting.updateServerSettings])
return (
<div className="config-sub-area">
{serverIORecorderRow}
</div>
)
}
return <div className="config-sub-area">{serverIORecorderRow}</div>;
};

View File

@ -1,191 +1,207 @@
export const RequestType = { export const RequestType = {
voice: "voice", voice: "voice",
config: "config", config: "config",
start: "start", start: "start",
stop: "stop", stop: "stop",
trancateBuffer: "trancateBuffer", trancateBuffer: "trancateBuffer",
} as const; } as const;
export type RequestType = (typeof RequestType)[keyof typeof RequestType]; export type RequestType = (typeof RequestType)[keyof typeof RequestType];
export const ResponseType = { export const ResponseType = {
volume: "volume", volume: "volume",
inputData: "inputData", inputData: "inputData",
start_ok: "start_ok", start_ok: "start_ok",
stop_ok: "stop_ok", stop_ok: "stop_ok",
} as const; } as const;
export type ResponseType = (typeof ResponseType)[keyof typeof ResponseType]; export type ResponseType = (typeof ResponseType)[keyof typeof ResponseType];
export type VoiceChangerWorkletProcessorRequest = { export type VoiceChangerWorkletProcessorRequest = {
requestType: RequestType; requestType: RequestType;
voice: Float32Array; voice: Float32Array;
numTrancateTreshold: number; numTrancateTreshold: number;
volTrancateThreshold: number; volTrancateThreshold: number;
volTrancateLength: number; volTrancateLength: number;
}; };
export type VoiceChangerWorkletProcessorResponse = { export type VoiceChangerWorkletProcessorResponse = {
responseType: ResponseType; responseType: ResponseType;
volume?: number; volume?: number;
recordData?: Float32Array[]; recordData?: Float32Array[];
inputData?: Float32Array; inputData?: Float32Array;
}; };
class VoiceChangerWorkletProcessor extends AudioWorkletProcessor { class VoiceChangerWorkletProcessor extends AudioWorkletProcessor {
private BLOCK_SIZE = 128; private BLOCK_SIZE = 128;
private initialized = false; private initialized = false;
private volume = 0; private volume = 0;
// private numTrancateTreshold = 100; // private numTrancateTreshold = 100;
// private volTrancateThreshold = 0.0005 // private volTrancateThreshold = 0.0005
// private volTrancateLength = 32 // private volTrancateLength = 32
// private volTrancateCount = 0 // private volTrancateCount = 0
private isRecording = false; private isRecording = false;
playBuffer: Float32Array[] = []; playBuffer: Float32Array[] = [];
unpushedF32Data: Float32Array = new Float32Array(0); unpushedF32Data: Float32Array = new Float32Array(0);
/** /**
* @constructor * @constructor
*/ */
constructor() { constructor() {
super(); super();
this.initialized = true; console.log("[AudioWorkletProcessor] created.");
this.port.onmessage = this.handleMessage.bind(this); this.initialized = true;
this.port.onmessage = this.handleMessage.bind(this);
}
calcVol = (data: Float32Array, prevVol: number) => {
const sum = data.reduce((prev, cur) => {
return prev + cur * cur;
}, 0);
const rms = Math.sqrt(sum / data.length);
return Math.max(rms, prevVol * 0.95);
};
trancateBuffer = () => {
console.log("[worklet] Buffer truncated");
while (this.playBuffer.length > 2) {
this.playBuffer.shift();
}
};
handleMessage(event: any) {
const request = event.data as VoiceChangerWorkletProcessorRequest;
if (request.requestType === "config") {
// this.numTrancateTreshold = request.numTrancateTreshold;
// this.volTrancateLength = request.volTrancateLength
// this.volTrancateThreshold = request.volTrancateThreshold
console.log("[worklet] worklet configured", request);
return;
} else if (request.requestType === "start") {
if (this.isRecording) {
console.warn("[worklet] recoring is already started");
return;
}
this.isRecording = true;
const startResponse: VoiceChangerWorkletProcessorResponse = {
responseType: "start_ok",
};
this.port.postMessage(startResponse);
return;
} else if (request.requestType === "stop") {
if (!this.isRecording) {
console.warn("[worklet] recoring is not started");
return;
}
this.isRecording = false;
const stopResponse: VoiceChangerWorkletProcessorResponse = {
responseType: "stop_ok",
};
this.port.postMessage(stopResponse);
return;
} else if (request.requestType === "trancateBuffer") {
this.trancateBuffer();
return;
} }
calcVol = (data: Float32Array, prevVol: number) => { const f32Data = request.voice;
const sum = data.reduce((prev, cur) => { // if (this.playBuffer.length > this.numTrancateTreshold) {
return prev + cur * cur; // console.log(`[worklet] Truncate ${this.playBuffer.length} > ${this.numTrancateTreshold}`);
}, 0); // this.trancateBuffer();
const rms = Math.sqrt(sum / data.length); // }
return Math.max(rms, prevVol * 0.95); if (this.playBuffer.length > (f32Data.length / this.BLOCK_SIZE) * 1.5) {
}; console.log(
`[worklet] Truncate ${this.playBuffer.length} > ${
trancateBuffer = () => { f32Data.length / this.BLOCK_SIZE
console.log("[worklet] Buffer truncated"); }`
while (this.playBuffer.length > 2) { );
this.playBuffer.shift(); this.trancateBuffer();
}
};
handleMessage(event: any) {
const request = event.data as VoiceChangerWorkletProcessorRequest;
if (request.requestType === "config") {
// this.numTrancateTreshold = request.numTrancateTreshold;
// this.volTrancateLength = request.volTrancateLength
// this.volTrancateThreshold = request.volTrancateThreshold
console.log("[worklet] worklet configured", request);
return;
} else if (request.requestType === "start") {
if (this.isRecording) {
console.warn("[worklet] recoring is already started");
return;
}
this.isRecording = true;
const startResponse: VoiceChangerWorkletProcessorResponse = {
responseType: "start_ok",
};
this.port.postMessage(startResponse);
return;
} else if (request.requestType === "stop") {
if (!this.isRecording) {
console.warn("[worklet] recoring is not started");
return;
}
this.isRecording = false;
const stopResponse: VoiceChangerWorkletProcessorResponse = {
responseType: "stop_ok",
};
this.port.postMessage(stopResponse);
return;
} else if (request.requestType === "trancateBuffer") {
this.trancateBuffer();
return;
}
const f32Data = request.voice;
// if (this.playBuffer.length > this.numTrancateTreshold) {
// console.log(`[worklet] Truncate ${this.playBuffer.length} > ${this.numTrancateTreshold}`);
// this.trancateBuffer();
// }
if (this.playBuffer.length > f32Data.length / this.BLOCK_SIZE) {
console.log(`[worklet] Truncate ${this.playBuffer.length} > ${f32Data.length / this.BLOCK_SIZE}`);
this.trancateBuffer();
}
const concatedF32Data = new Float32Array(this.unpushedF32Data.length + f32Data.length);
concatedF32Data.set(this.unpushedF32Data);
concatedF32Data.set(f32Data, this.unpushedF32Data.length);
const chunkNum = Math.floor(concatedF32Data.length / this.BLOCK_SIZE);
for (let i = 0; i < chunkNum; i++) {
const block = concatedF32Data.slice(i * this.BLOCK_SIZE, (i + 1) * this.BLOCK_SIZE);
this.playBuffer.push(block);
}
this.unpushedF32Data = concatedF32Data.slice(chunkNum * this.BLOCK_SIZE);
} }
pushData = (inputData: Float32Array) => { const concatedF32Data = new Float32Array(
const volumeResponse: VoiceChangerWorkletProcessorResponse = { this.unpushedF32Data.length + f32Data.length
responseType: ResponseType.inputData, );
inputData: inputData, concatedF32Data.set(this.unpushedF32Data);
}; concatedF32Data.set(f32Data, this.unpushedF32Data.length);
this.port.postMessage(volumeResponse);
};
process(_inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record<string, Float32Array>) { const chunkNum = Math.floor(concatedF32Data.length / this.BLOCK_SIZE);
if (!this.initialized) { for (let i = 0; i < chunkNum; i++) {
console.warn("[worklet] worklet_process not ready"); const block = concatedF32Data.slice(
return true; i * this.BLOCK_SIZE,
} (i + 1) * this.BLOCK_SIZE
);
if (this.isRecording) { this.playBuffer.push(block);
if (_inputs.length > 0 && _inputs[0].length > 0) {
this.pushData(_inputs[0][0]);
}
}
if (this.playBuffer.length === 0) {
// console.log("[worklet] no play buffer");
return true;
}
// console.log("[worklet] play buffer");
//// 一定期間無音状態が続いている場合はスキップ。
// let voice: Float32Array | undefined
// while (true) {
// voice = this.playBuffer.shift()
// if (!voice) {
// break
// }
// this.volume = this.calcVol(voice, this.volume)
// if (this.volume < this.volTrancateThreshold) {
// this.volTrancateCount += 1
// } else {
// this.volTrancateCount = 0
// }
// // V.1.5.0よりsilent skipで音飛びするようになったので無効化
// if (this.volTrancateCount < this.volTrancateLength || this.volTrancateLength < 0) {
// break
// } else {
// break
// // console.log("silent...skip")
// }
// }
let voice = this.playBuffer.shift();
if (voice) {
this.volume = this.calcVol(voice, this.volume);
const volumeResponse: VoiceChangerWorkletProcessorResponse = {
responseType: ResponseType.volume,
volume: this.volume,
};
this.port.postMessage(volumeResponse);
outputs[0][0].set(voice);
if (outputs[0].length == 2) {
outputs[0][1].set(voice);
}
}
return true;
} }
this.unpushedF32Data = concatedF32Data.slice(chunkNum * this.BLOCK_SIZE);
}
pushData = (inputData: Float32Array) => {
const volumeResponse: VoiceChangerWorkletProcessorResponse = {
responseType: ResponseType.inputData,
inputData: inputData,
};
this.port.postMessage(volumeResponse);
};
process(
_inputs: Float32Array[][],
outputs: Float32Array[][],
_parameters: Record<string, Float32Array>
) {
if (!this.initialized) {
console.warn("[worklet] worklet_process not ready");
return true;
}
if (this.isRecording) {
if (_inputs.length > 0 && _inputs[0].length > 0) {
this.pushData(_inputs[0][0]);
}
}
if (this.playBuffer.length === 0) {
// console.log("[worklet] no play buffer");
return true;
}
// console.log("[worklet] play buffer");
//// 一定期間無音状態が続いている場合はスキップ。
// let voice: Float32Array | undefined
// while (true) {
// voice = this.playBuffer.shift()
// if (!voice) {
// break
// }
// this.volume = this.calcVol(voice, this.volume)
// if (this.volume < this.volTrancateThreshold) {
// this.volTrancateCount += 1
// } else {
// this.volTrancateCount = 0
// }
// // V.1.5.0よりsilent skipで音飛びするようになったので無効化
// if (this.volTrancateCount < this.volTrancateLength || this.volTrancateLength < 0) {
// break
// } else {
// break
// // console.log("silent...skip")
// }
// }
let voice = this.playBuffer.shift();
if (voice) {
this.volume = this.calcVol(voice, this.volume);
const volumeResponse: VoiceChangerWorkletProcessorResponse = {
responseType: ResponseType.volume,
volume: this.volume,
};
this.port.postMessage(volumeResponse);
outputs[0][0].set(voice);
if (outputs[0].length == 2) {
outputs[0][1].set(voice);
}
}
return true;
}
} }
registerProcessor("voice-changer-worklet-processor", VoiceChangerWorkletProcessor); registerProcessor(
"voice-changer-worklet-processor",
VoiceChangerWorkletProcessor
);