277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
import { io, Socket } from "socket.io-client";
|
||
import { DefaultEventsMap } from "@socket.io/component-emitter";
|
||
import { Duplex, DuplexOptions } from "readable-stream";
|
||
import { DefaultVoiceChangerRequestParamas, MajarModeTypes, VoiceChangerMode, VoiceChangerRequestParamas } from "./const";
|
||
|
||
export type Callbacks = {
|
||
onVoiceReceived: (voiceChangerMode: VoiceChangerMode, data: ArrayBuffer) => void
|
||
}
|
||
export type AudioStreamerListeners = {
|
||
notifySendBufferingTime: (time: number) => void
|
||
notifyResponseTime: (time: number) => void
|
||
notifyException: (message: string) => void
|
||
}
|
||
export class AudioStreamer extends Duplex {
|
||
private callbacks: Callbacks
|
||
private audioStreamerListeners: AudioStreamerListeners
|
||
private majarMode: MajarModeTypes
|
||
private serverUrl = ""
|
||
private socket: Socket<DefaultEventsMap, DefaultEventsMap> | null = null
|
||
private voiceChangerMode: VoiceChangerMode = "realtime"
|
||
private requestParamas: VoiceChangerRequestParamas = DefaultVoiceChangerRequestParamas
|
||
private chunkNum = 8
|
||
private requestChunks: ArrayBuffer[] = []
|
||
private recordChunks: ArrayBuffer[] = []
|
||
private isRecording = false
|
||
|
||
// performance monitor
|
||
private bufferStart = 0;
|
||
|
||
constructor(majarMode: MajarModeTypes, callbacks: Callbacks, audioStreamerListeners: AudioStreamerListeners, options?: DuplexOptions) {
|
||
super(options);
|
||
this.majarMode = majarMode
|
||
this.callbacks = callbacks
|
||
this.audioStreamerListeners = audioStreamerListeners
|
||
}
|
||
|
||
private createSocketIO = () => {
|
||
if (this.socket) {
|
||
this.socket.close()
|
||
}
|
||
if (this.majarMode === "sio") {
|
||
this.socket = io(this.serverUrl);
|
||
this.socket.on('connect', () => console.log(`[SIO] sonnect to ${this.serverUrl}`));
|
||
this.socket.on('response', (response: any[]) => {
|
||
const cur = Date.now()
|
||
const responseTime = cur - response[0]
|
||
const result = response[1] as ArrayBuffer
|
||
if (result.byteLength < 128 * 2) {
|
||
this.audioStreamerListeners.notifyException(`[SIO] recevied data is too short ${result.byteLength}`)
|
||
} else {
|
||
this.audioStreamerListeners.notifyException(``)
|
||
this.callbacks.onVoiceReceived(this.voiceChangerMode, response[1])
|
||
this.audioStreamerListeners.notifyResponseTime(responseTime)
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Option Change
|
||
setServerUrl = (serverUrl: string, mode: MajarModeTypes) => {
|
||
this.serverUrl = serverUrl
|
||
this.majarMode = mode
|
||
window.open(serverUrl, '_blank')
|
||
console.log(`[AudioStreamer] Server Setting:${this.serverUrl} ${this.majarMode}`)
|
||
|
||
this.createSocketIO()// mode check is done in the method.
|
||
}
|
||
|
||
setRequestParams = (val: VoiceChangerRequestParamas) => {
|
||
this.requestParamas = val
|
||
}
|
||
|
||
setChunkNum = (num: number) => {
|
||
this.chunkNum = num
|
||
}
|
||
|
||
setVoiceChangerMode = (val: VoiceChangerMode) => {
|
||
this.voiceChangerMode = val
|
||
}
|
||
|
||
|
||
// Main Process
|
||
//// Pipe from mic stream
|
||
_write = (chunk: AudioBuffer, _encoding: any, callback: any) => {
|
||
const buffer = chunk.getChannelData(0);
|
||
// console.log("SAMPLERATE:", chunk.sampleRate, chunk.numberOfChannels, chunk.length, buffer)
|
||
if (this.voiceChangerMode === "realtime") {
|
||
this._write_realtime(buffer)
|
||
} else {
|
||
this._write_record(buffer)
|
||
}
|
||
callback();
|
||
}
|
||
|
||
private _write_realtime = (buffer: Float32Array) => {
|
||
// bufferSize個のデータ(48Khz)が入ってくる。
|
||
//// 48000Hz で入ってくるので間引いて24000Hzに変換する。
|
||
//// バイトサイズは周波数変換で(x1/2), 16bit(2byte)で(x2)
|
||
const arrayBuffer = new ArrayBuffer((buffer.length / 2) * 2)
|
||
const dataView = new DataView(arrayBuffer);
|
||
|
||
for (let i = 0; i < buffer.length; i++) {
|
||
if (i % 2 == 0) {
|
||
let s = Math.max(-1, Math.min(1, buffer[i]));
|
||
s = s < 0 ? s * 0x8000 : s * 0x7FFF
|
||
// 2分の1個目で2バイトずつ進むので((i/2)*2)
|
||
dataView.setInt16((i / 2) * 2, s, true);
|
||
}
|
||
}
|
||
// 256byte(最低バッファサイズ256から間引いた個数x2byte)をchunkとして管理
|
||
const chunkByteSize = 256 // (const.ts ★1)
|
||
for (let i = 0; i < arrayBuffer.byteLength / chunkByteSize; i++) {
|
||
const ab = arrayBuffer.slice(i * chunkByteSize, (i + 1) * chunkByteSize)
|
||
this.requestChunks.push(ab)
|
||
}
|
||
|
||
//// リクエストバッファの中身が、リクエスト送信数と違う場合は処理終了。
|
||
if (this.requestChunks.length < this.chunkNum) {
|
||
return
|
||
}
|
||
|
||
// リクエスト用の入れ物を作成
|
||
const windowByteLength = this.requestChunks.reduce((prev, cur) => {
|
||
return prev + cur.byteLength
|
||
}, 0)
|
||
const newBuffer = new Uint8Array(windowByteLength);
|
||
|
||
// リクエストのデータをセット
|
||
this.requestChunks.reduce((prev, cur) => {
|
||
newBuffer.set(new Uint8Array(cur), prev)
|
||
return prev + cur.byteLength
|
||
}, 0)
|
||
|
||
console.log("send buff length", newBuffer.length)
|
||
|
||
this.sendBuffer(newBuffer)
|
||
this.requestChunks = []
|
||
|
||
this.audioStreamerListeners.notifySendBufferingTime(Date.now() - this.bufferStart)
|
||
this.bufferStart = Date.now()
|
||
}
|
||
|
||
|
||
private _write_record = (buffer: Float32Array) => {
|
||
if (!this.isRecording) { return }
|
||
// buffer(for48Khz)x16bit * chunksize / 2(for24Khz)
|
||
const sendBuffer = new ArrayBuffer(buffer.length * 2 / 2);
|
||
const sendDataView = new DataView(sendBuffer);
|
||
for (var i = 0; i < buffer.length; i++) {
|
||
if (i % 2 == 0) {
|
||
let s = Math.max(-1, Math.min(1, buffer[i]));
|
||
s = s < 0 ? s * 0x8000 : s * 0x7FFF
|
||
sendDataView.setInt16(i, s, true);
|
||
// if (i % 3000 === 0) {
|
||
// console.log("buffer_converting", s, buffer[i])
|
||
// }
|
||
}
|
||
}
|
||
this.recordChunks.push(sendBuffer)
|
||
}
|
||
|
||
// Near Realtime用のトリガ
|
||
sendRecordedData = () => {
|
||
const length = this.recordChunks.reduce((prev, cur) => {
|
||
return prev + cur.byteLength
|
||
}, 0)
|
||
const newBuffer = new Uint8Array(length);
|
||
this.recordChunks.reduce((prev, cur) => {
|
||
newBuffer.set(new Uint8Array(cur), prev)
|
||
return prev + cur.byteLength
|
||
}, 0)
|
||
|
||
this.sendBuffer(newBuffer)
|
||
}
|
||
|
||
startRecord = () => {
|
||
this.recordChunks = []
|
||
this.isRecording = true
|
||
}
|
||
|
||
stopRecord = () => {
|
||
this.isRecording = false
|
||
}
|
||
|
||
private sendBuffer = async (newBuffer: Uint8Array) => {
|
||
if (this.serverUrl.length == 0) {
|
||
console.error("no server url")
|
||
throw "no server url"
|
||
}
|
||
const timestamp = Date.now()
|
||
// console.log("REQUEST_MESSAGE:", [this.gpu, this.srcId, this.dstId, timestamp, newBuffer.buffer])
|
||
console.log("SERVER_URL", this.serverUrl, this.majarMode)
|
||
const convertChunkNum = this.voiceChangerMode === "realtime" ? this.requestParamas.convertChunkNum : 0
|
||
if (this.majarMode === "sio") {
|
||
if (!this.socket) {
|
||
console.warn(`sio is not initialized`)
|
||
return
|
||
}
|
||
console.log("emit!")
|
||
this.socket.emit('request_message', [
|
||
this.requestParamas.gpu,
|
||
this.requestParamas.srcId,
|
||
this.requestParamas.dstId,
|
||
timestamp,
|
||
convertChunkNum,
|
||
this.requestParamas.crossFadeLowerValue,
|
||
this.requestParamas.crossFadeOffsetRate,
|
||
this.requestParamas.crossFadeEndRate,
|
||
newBuffer.buffer]);
|
||
} else {
|
||
const res = await postVoice(
|
||
this.serverUrl,
|
||
this.requestParamas.gpu,
|
||
this.requestParamas.srcId,
|
||
this.requestParamas.dstId,
|
||
timestamp,
|
||
convertChunkNum,
|
||
this.requestParamas.crossFadeLowerValue,
|
||
this.requestParamas.crossFadeOffsetRate,
|
||
this.requestParamas.crossFadeEndRate,
|
||
newBuffer.buffer)
|
||
|
||
if (res.byteLength < 128 * 2) {
|
||
this.audioStreamerListeners.notifyException(`[REST] recevied data is too short ${res.byteLength}`)
|
||
} else {
|
||
this.audioStreamerListeners.notifyException(``)
|
||
this.callbacks.onVoiceReceived(this.voiceChangerMode, res)
|
||
this.audioStreamerListeners.notifyResponseTime(Date.now() - timestamp)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export const postVoice = async (
|
||
url: string,
|
||
gpu: number,
|
||
srcId: number,
|
||
dstId: number,
|
||
timestamp: number,
|
||
convertSize: number,
|
||
crossFadeLowerValue: number,
|
||
crossFadeOffsetRate: number,
|
||
crossFadeEndRate: number,
|
||
buffer: ArrayBuffer) => {
|
||
const obj = {
|
||
gpu,
|
||
srcId,
|
||
dstId,
|
||
timestamp,
|
||
convertSize,
|
||
crossFadeLowerValue,
|
||
crossFadeOffsetRate,
|
||
crossFadeEndRate,
|
||
buffer: Buffer.from(buffer).toString('base64')
|
||
};
|
||
const body = JSON.stringify(obj);
|
||
|
||
const res = await fetch(`${url}`, {
|
||
method: "POST",
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: body
|
||
})
|
||
|
||
const receivedJson = await res.json()
|
||
const changedVoiceBase64 = receivedJson["changedVoiceBase64"]
|
||
const buf = Buffer.from(changedVoiceBase64, "base64")
|
||
const ab = new ArrayBuffer(buf.length);
|
||
// console.log("RECIV", buf.length)
|
||
const view = new Uint8Array(ab);
|
||
for (let i = 0; i < buf.length; ++i) {
|
||
view[i] = buf[i];
|
||
}
|
||
return ab
|
||
} |