|
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; |
|
} |
|
|