export const RequestType = { voice: "voice", config: "config", start: "start", stop: "stop", trancateBuffer: "trancateBuffer", } as const; export type RequestType = (typeof RequestType)[keyof typeof RequestType]; export const ResponseType = { volume: "volume", inputData: "inputData", start_ok: "start_ok", stop_ok: "stop_ok", } as const; export type ResponseType = (typeof ResponseType)[keyof typeof ResponseType]; export type VoiceChangerWorkletProcessorRequest = { requestType: RequestType; voice: Float32Array; numTrancateTreshold: number; volTrancateThreshold: number; volTrancateLength: number; }; export type VoiceChangerWorkletProcessorResponse = { responseType: ResponseType; volume?: number; recordData?: Float32Array[]; inputData?: Float32Array; }; class VoiceChangerWorkletProcessor extends AudioWorkletProcessor { private BLOCK_SIZE = 128; private initialized = false; private volume = 0; // private numTrancateTreshold = 100; // private volTrancateThreshold = 0.0005 // private volTrancateLength = 32 // private volTrancateCount = 0 private isRecording = false; playBuffer: Float32Array[] = []; unpushedF32Data: Float32Array = new Float32Array(0); /** * @constructor */ constructor() { super(); console.log("[AudioWorkletProcessor] created."); 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; } 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) * 1.5) { 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 volumeResponse: VoiceChangerWorkletProcessorResponse = { responseType: ResponseType.inputData, inputData: inputData, }; this.port.postMessage(volumeResponse); }; process( _inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record ) { 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 );