| import {TYPING_ANIMATION_DELAY_MS} from './StreamingInterface'; |
| import {getURLParams} from './URLParams'; |
| import audioBuffertoWav from 'audiobuffer-to-wav'; |
| import './StreamingInterface.css'; |
|
|
| type StartEndTime = { |
| start: number; |
| end: number; |
| }; |
|
|
| type StartEndTimeWithAudio = StartEndTime & { |
| float32Audio: Float32Array; |
| }; |
|
|
| type Text = { |
| time: number; |
| chars: number; |
| }; |
|
|
| type DebugTimings = { |
| receivedAudio: StartEndTime[]; |
| playedAudio: StartEndTimeWithAudio[]; |
| receivedText: Text[]; |
| renderedText: StartEndTime[]; |
| sentAudio: StartEndTimeWithAudio[]; |
| startRenderTextTime: number | null; |
| startRecordingTime: number | null; |
| receivedAudioSampleRate: number | null; |
| }; |
|
|
| function getInitialTimings(): DebugTimings { |
| return { |
| receivedAudio: [], |
| playedAudio: [], |
| receivedText: [], |
| renderedText: [], |
| sentAudio: [], |
| startRenderTextTime: null, |
| startRecordingTime: null, |
| receivedAudioSampleRate: null, |
| }; |
| } |
|
|
| function downloadAudioBuffer(audioBuffer: AudioBuffer, fileName: string): void { |
| const wav = audioBuffertoWav(audioBuffer); |
| const wavBlob = new Blob([new DataView(wav)], { |
| type: 'audio/wav', |
| }); |
| const url = URL.createObjectURL(wavBlob); |
| const anchor = document.createElement('a'); |
| anchor.href = url; |
| anchor.target = '_blank'; |
| anchor.download = fileName; |
| anchor.click(); |
| } |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| class DebugTimingsManager { |
| timings: DebugTimings = getInitialTimings(); |
|
|
| start(): void { |
| this.timings = getInitialTimings(); |
| this.timings.startRecordingTime = new Date().getTime(); |
| } |
|
|
| sentAudio(event: AudioProcessingEvent): void { |
| const end = new Date().getTime(); |
| const start = end - event.inputBuffer.duration * 1000; |
| |
| const float32Audio = new Float32Array(event.inputBuffer.getChannelData(0)); |
| this.timings.sentAudio.push({ |
| start, |
| end, |
| float32Audio, |
| }); |
| } |
|
|
| receivedText(text: string): void { |
| this.timings.receivedText.push({ |
| time: new Date().getTime(), |
| chars: text.length, |
| }); |
| } |
|
|
| startRenderText(): void { |
| if (this.timings.startRenderTextTime == null) { |
| this.timings.startRenderTextTime = new Date().getTime(); |
| } |
| } |
|
|
| endRenderText(): void { |
| if (this.timings.startRenderTextTime == null) { |
| console.warn( |
| 'Wrong timings of start / end rendering text. startRenderText is null', |
| ); |
| return; |
| } |
|
|
| this.timings.renderedText.push({ |
| start: this.timings.startRenderTextTime as number, |
| end: new Date().getTime(), |
| }); |
| this.timings.startRenderTextTime = null; |
| } |
|
|
| receivedAudio(duration: number): void { |
| const start = new Date().getTime(); |
| this.timings.receivedAudio.push({ |
| start, |
| end: start + duration * 1000, |
| }); |
| } |
|
|
| playedAudio(start: number, end: number, buffer: AudioBuffer | null): void { |
| if (buffer != null) { |
| if (this.timings.receivedAudioSampleRate == null) { |
| this.timings.receivedAudioSampleRate = buffer.sampleRate; |
| } |
| if (this.timings.receivedAudioSampleRate != buffer.sampleRate) { |
| console.error( |
| 'Sample rates of received audio are unequal, will fail to reconstruct debug audio', |
| this.timings.receivedAudioSampleRate, |
| buffer.sampleRate, |
| ); |
| } |
| } |
| this.timings.playedAudio.push({ |
| start, |
| end, |
| float32Audio: |
| buffer == null |
| ? new Float32Array() |
| : new Float32Array(buffer.getChannelData(0)), |
| }); |
| } |
|
|
| getChartData() { |
| const columns = [ |
| {type: 'string', id: 'Series'}, |
| {type: 'date', id: 'Start'}, |
| {type: 'date', id: 'End'}, |
| ]; |
| return [ |
| columns, |
| ...this.timings.sentAudio.map((sentAudio) => [ |
| 'Sent Audio', |
| new Date(sentAudio.start), |
| new Date(sentAudio.end), |
| ]), |
| ...this.timings.receivedAudio.map((receivedAudio) => [ |
| 'Received Audio', |
| new Date(receivedAudio.start), |
| new Date(receivedAudio.end), |
| ]), |
| ...this.timings.playedAudio.map((playedAudio) => [ |
| 'Played Audio', |
| new Date(playedAudio.start), |
| new Date(playedAudio.end), |
| ]), |
| |
| ...this.timings.receivedText.map((receivedText) => [ |
| 'Received Text', |
| new Date(receivedText.time), |
| new Date( |
| receivedText.time + receivedText.chars * TYPING_ANIMATION_DELAY_MS, |
| ), |
| ]), |
| ...this.timings.renderedText.map((renderedText) => [ |
| 'Rendered Text', |
| new Date(renderedText.start), |
| new Date(renderedText.end), |
| ]), |
| ]; |
| } |
|
|
| downloadInputAudio() { |
| const audioContext = new AudioContext(); |
| const totalLength = this.timings.sentAudio.reduce((acc, cur) => { |
| return acc + cur?.float32Audio?.length ?? 0; |
| }, 0); |
| if (totalLength === 0) { |
| return; |
| } |
|
|
| const incomingArrayBuffer = audioContext.createBuffer( |
| 1, |
| totalLength, |
| audioContext.sampleRate, |
| ); |
|
|
| const buffer = incomingArrayBuffer.getChannelData(0); |
| let i = 0; |
| this.timings.sentAudio.forEach((sentAudio) => { |
| sentAudio.float32Audio.forEach((bytes) => { |
| buffer[i++] = bytes; |
| }); |
| }); |
|
|
| |
| |
| downloadAudioBuffer(incomingArrayBuffer, `input_audio.wav`); |
| } |
|
|
| downloadOutputAudio() { |
| const playedAudio = this.timings.playedAudio; |
| const sampleRate = this.timings.receivedAudioSampleRate; |
| if ( |
| playedAudio.length === 0 || |
| this.timings.startRecordingTime == null || |
| sampleRate == null |
| ) { |
| return null; |
| } |
|
|
| let previousEndTime = this.timings.startRecordingTime; |
| const audioArray: number[] = []; |
| playedAudio.forEach((audio) => { |
| const delta = (audio.start - previousEndTime) / 1000; |
| for (let i = 0; i < delta * sampleRate; i++) { |
| audioArray.push(0.0); |
| } |
| audio.float32Audio.forEach((bytes) => audioArray.push(bytes)); |
| previousEndTime = audio.end; |
| }); |
| const audioContext = new AudioContext(); |
| const incomingArrayBuffer = audioContext.createBuffer( |
| 1, |
| audioArray.length, |
| sampleRate, |
| ); |
|
|
| incomingArrayBuffer.copyToChannel( |
| new Float32Array(audioArray), |
| 0, |
| ); |
|
|
| |
| |
| downloadAudioBuffer(incomingArrayBuffer, 'output_audio.wav'); |
| } |
| } |
|
|
| const debugSingleton = new DebugTimingsManager(); |
| export default function debug(): DebugTimingsManager | null { |
| const debugParam = getURLParams().debug; |
| return debugParam ? debugSingleton : null; |
| } |
|
|