2023-01-05 05:45:42 +03:00
import { VoiceChangerWorkletNode , VolumeListener } 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-01-08 10:18:20 +03:00
import { BufferSize , DefaultVoiceChangerOptions , Protocol , ServerSettingKey , VoiceChangerMode , VOICE_CHANGER_CLIENT_EXCEPTION } from "./const" ;
2023-01-04 20:28:36 +03:00
import MicrophoneStream from "microphone-stream" ;
import { AudioStreamer , Callbacks , AudioStreamerListeners } from "./AudioStreamer" ;
2023-01-08 10:18:20 +03:00
import { ServerConfigurator } from "./ServerConfigurator" ;
2023-01-11 21:49:22 +03:00
import { VoiceChangerWorkletProcessorRequest } from "./@types/voice-changer-worklet-processor" ;
2023-01-04 20:28:36 +03:00
// オーディオデータの流れ
// input node(mic or MediaStream) -> [vf node] -> microphne stream -> audio streamer ->
// sio/rest server -> audio streamer-> vc node -> output node
export class VoiceChnagerClient {
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
private outputNodeFromVF : MediaStreamAudioDestinationNode | null = null
private micStream : MicrophoneStream | null = null
private audioStreamer ! : AudioStreamer
private vcNode ! : VoiceChangerWorkletNode
private currentMediaStreamAudioDestinationNode ! : MediaStreamAudioDestinationNode
private promiseForInitialize : Promise < void >
2023-01-05 05:45:42 +03:00
private _isVoiceChanging = false
2023-01-04 20:28:36 +03:00
2023-01-08 11:58:27 +03:00
private sslCertified : string [ ] = [ ]
2023-01-04 20:28:36 +03:00
private callbacks : Callbacks = {
onVoiceReceived : ( voiceChangerMode : VoiceChangerMode , data : ArrayBuffer ) : void = > {
2023-01-05 05:45:42 +03:00
// console.log(voiceChangerMode, data)
2023-01-04 20:28:36 +03:00
if ( voiceChangerMode === "realtime" ) {
2023-01-11 21:49:22 +03:00
const req : VoiceChangerWorkletProcessorRequest = {
requestType : "voice" ,
voice : data ,
numTrancateTreshold : 0 ,
volTrancateThreshold : 0 ,
volTrancateLength : 0
}
this . vcNode . postReceivedVoice ( req )
2023-01-04 20:28:36 +03:00
return
}
// For Near Realtime Mode
console . log ( "near realtime mode" )
const i16Data = new Int16Array ( data )
const f32Data = new Float32Array ( i16Data . length )
// https://stackoverflow.com/questions/35234551/javascript-converting-from-int16-to-float32
i16Data . forEach ( ( x , i ) = > {
const float = ( x >= 0x8000 ) ? - ( 0x10000 - x ) / 0x8000 : x / 0x7FFF ;
f32Data [ i ] = float
} )
const source = this . ctx . createBufferSource ( ) ;
const buffer = this . ctx . createBuffer ( 1 , f32Data . length , 24000 ) ;
buffer . getChannelData ( 0 ) . set ( f32Data ) ;
source . buffer = buffer ;
source . start ( ) ;
source . connect ( this . currentMediaStreamAudioDestinationNode )
}
}
2023-01-05 05:45:42 +03:00
constructor ( ctx : AudioContext , vfEnable : boolean , audioStreamerListeners : AudioStreamerListeners , volumeListener : VolumeListener ) {
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-01-05 05:45:42 +03:00
this . vcNode = new VoiceChangerWorkletNode ( this . ctx , volumeListener ) ; // vc node
2023-01-04 20:28:36 +03:00
this . currentMediaStreamAudioDestinationNode = this . ctx . createMediaStreamDestination ( ) // output node
this . vcNode . connect ( this . currentMediaStreamAudioDestinationNode ) // vc node -> output node
// (vc nodeにはaudio streamerのcallbackでデータが投げ込まれる)
2023-01-05 05:45:42 +03:00
this . audioStreamer = new AudioStreamer ( this . callbacks , audioStreamerListeners , { objectMode : true , } )
this . audioStreamer . setInputChunkNum ( DefaultVoiceChangerOptions . inputChunkNum )
this . audioStreamer . setVoiceChangerMode ( DefaultVoiceChangerOptions . voiceChangerMode )
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 ;
this . outputNodeFromVF = this . ctx . createMediaStreamDestination ( ) ;
}
resolve ( )
} )
}
isInitialized = async ( ) = > {
if ( this . promiseForInitialize ) {
await this . promiseForInitialize
}
return true
}
// forceVfDisable is for the condition that vf is enabled in constructor.
setup = async ( input : string | MediaStream , bufferSize : BufferSize , forceVfDisable : boolean = false ) = > {
// condition check
if ( ! this . vcNode ) {
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
}
if ( typeof input == "string" ) {
this . currentMediaStream = await navigator . mediaDevices . getUserMedia ( {
audio : { deviceId : input }
} )
} else {
this . currentMediaStream = input
}
// create mic stream
2023-01-05 05:45:42 +03:00
if ( this . micStream ) {
2023-01-05 12:35:56 +03:00
this . micStream . unpipe ( )
2023-01-05 05:45:42 +03:00
this . micStream . destroy ( )
this . micStream = null
}
2023-01-04 20:28:36 +03:00
this . micStream = new MicrophoneStream ( {
objectMode : true ,
bufferSize : bufferSize ,
context : this.ctx
} )
// connect nodes.
if ( this . currentDevice && forceVfDisable == false ) {
this . currentMediaStreamAudioSourceNode = this . ctx . createMediaStreamSource ( this . currentMediaStream ) // input node
this . currentDevice . chooseNewInnerDevice ( this . currentMediaStream )
const voiceFocusNode = await this . currentDevice . createAudioNode ( this . ctx ) ; // vf node
this . currentMediaStreamAudioSourceNode . connect ( voiceFocusNode . start ) // input node -> vf node
voiceFocusNode . end . connect ( this . outputNodeFromVF ! )
this . micStream . setStream ( this . outputNodeFromVF ! . stream ) // vf node -> mic stream
} else {
2023-01-05 05:45:42 +03:00
console . log ( "VF disabled" )
2023-01-04 20:28:36 +03:00
this . micStream . setStream ( this . currentMediaStream ) // input device -> mic stream
}
2023-01-05 12:35:56 +03:00
this . micStream . pipe ( this . audioStreamer ) // mic stream -> audio streamer
if ( ! this . _isVoiceChanging ) {
this . micStream . pauseRecording ( )
} else {
this . micStream . playRecording ( )
}
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-01-07 14:07:39 +03:00
if ( ! this . micStream ) {
throw ` Exception: ${ VOICE_CHANGER_CLIENT_EXCEPTION . ERR_MIC_STREAM_NOT_INITIALIZED } `
return
}
2023-01-05 05:45:42 +03:00
this . micStream . playRecording ( )
this . _isVoiceChanging = true
}
stop = ( ) = > {
if ( ! this . micStream ) { return }
this . micStream . pauseRecording ( )
this . _isVoiceChanging = false
}
get isVoiceChanging ( ) : boolean {
return this . _isVoiceChanging
}
2023-01-04 20:28:36 +03:00
// Audio Streamer Settingg
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-01-08 10:18:20 +03:00
this . audioStreamer . setServerUrl ( url )
this . configurator . setServerUrl ( url )
2023-01-04 20:28:36 +03:00
}
2023-01-08 10:18:20 +03:00
setProtocol = ( mode : Protocol ) = > {
this . audioStreamer . setProtocol ( mode )
2023-01-04 20:28:36 +03:00
}
2023-01-05 05:45:42 +03:00
setInputChunkNum = ( num : number ) = > {
this . audioStreamer . setInputChunkNum ( num )
2023-01-04 20:28:36 +03:00
}
setVoiceChangerMode = ( val : VoiceChangerMode ) = > {
this . audioStreamer . setVoiceChangerMode ( val )
}
2023-01-05 05:45:42 +03:00
2023-01-08 10:18:20 +03:00
// Configurator Method
uploadFile = ( file : File , onprogress : ( progress : number , end : boolean ) = > void ) = > {
return this . configurator . uploadFile ( file , onprogress )
}
concatUploadedFile = ( file : File , chunkNum : number ) = > {
return this . configurator . concatUploadedFile ( file , chunkNum )
}
loadModel = ( configFile : File , pyTorchModelFile : File | null , onnxModelFile : File | null ) = > {
return this . configurator . loadModel ( configFile , pyTorchModelFile , onnxModelFile )
}
updateServerSettings = ( key : ServerSettingKey , val : string ) = > {
return this . configurator . updateSettings ( key , val )
}
// Information
getClientSettings = ( ) = > {
return this . audioStreamer . getSettings ( )
}
getServerSettings = ( ) = > {
return this . configurator . getSettings ( )
}
2023-01-05 05:45:42 +03:00
2023-01-04 20:28:36 +03:00
}