123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- class AudioStream {
- constructor({ stdin, recorder = false }) {
- if (!/^espeak-ng/.test(stdin)) {
- throw new Error(`stdin should begin with "espeak-ng" command`);
- }
- this.command = stdin;
- this.stdin = new ReadableStream({
- start(c) {
- c.enqueue(
- new File([stdin], 'espeakng', {
- type: 'application/octet-stream',
- })
- );
- c.close();
- },
- });
- this.readOffset = 0;
- this.duration = 0;
- this.channelDataLength = 440;
- this.sampleRate = 22050;
- this.numberOfChannels = 1;
- this.src =
- 'chrome-extension://<id>/nativeTransferableStream.html';
- this.ac = new AudioContext({
- latencyHint: 0,
- });
- this.ac.suspend();
- this.msd = new MediaStreamAudioDestinationNode(this.ac, {
- channelCount: this.numberOfChannels,
- });
- this.inputController = void 0;
- this.inputStream = new ReadableStream({
- start: (_) => {
- return (this.inputController = _);
- },
- });
- this.inputReader = this.inputStream.getReader();
- const { stream } = this.msd;
- this.stream = stream;
- const [track] = stream.getAudioTracks();
- this.track = track;
- this.osc = new OscillatorNode(this.ac, { frequency: 0 });
- this.processor = new MediaStreamTrackProcessor({ track });
- this.generator = new MediaStreamTrackGenerator({ kind: 'audio' });
- const { writable } = this.generator;
- this.writable = writable;
- const { readable: audioReadable } = this.processor;
- this.audioReadable = audioReadable;
- this.audioWriter = this.writable.getWriter();
- this.mediaStream = new MediaStream([this.generator]);
- if (recorder) {
- this.recorder = new MediaRecorder(this.mediaStream);
- this.recorder.ondataavailable = ({ data }) => {
- this.data = data;
- };
- }
- this.outputSource = new MediaStreamAudioSourceNode(this.ac, {
- mediaStream: this.mediaStream,
- });
- this.outputSource.connect(this.ac.destination);
- this.resolve = void 0;
- this.promise = new Promise((_) => (this.resolve = _));
- this.osc.connect(this.msd);
- this.osc.start();
- this.track.onmute = this.track.onunmute = this.track.onended = (e) =>
- console.log(e);
- this.abortable = new AbortController();
- const { signal } = this.abortable;
- this.signal = signal;
- this.audioReadableAbortable = new AbortController();
- const { signal: audioReadableSignal } = this.audioReadableAbortable;
- this.audioReadableSignal = audioReadableSignal;
- this.audioReadableSignal.onabort = (e) => console.log(e.type);
- this.abortHandler = async (e) => {
- await this.disconnect(true);
- console.log(
- `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
- `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`,
- `inputController.desiredSize:${this.inputController.desiredSize}, ac.state:${this.ac.state}`
- );
- if (
- this.transferableWindow ||
- document.body.querySelector(`iframe[src="${this.src}"]`)
- ) {
- document.body.removeChild(this.transferableWindow);
- }
- this.resolve('Stream aborted.');
- };
- this.signal.onabort = this.abortHandler;
- }
- async disconnect(abort = false) {
- if (abort) {
- this.audioReadableAbortable.abort();
- }
- this.msd.disconnect();
- this.osc.disconnect();
- this.outputSource.disconnect();
- this.track.stop();
- await this.audioWriter.close();
- await this.audioWriter.closed;
- await this.inputReader.cancel();
- this.generator.stop();
- if (this.recorder && this.recorder.state === 'recording') {
- this.recorder.stop();
- }
- return this.ac.close();
- }
- async start() {
- return this.nativeTransferableStream();
- }
- async abort() {
- this.abortable.abort();
- if (this.source) {
- this.source.postMessage('Abort.', '*');
- }
- return this.promise;
- }
- async nativeTransferableStream() {
- return new Promise((resolve) => {
- onmessage = (e) => {
- this.source = e.source;
- if (typeof e.data === 'string') {
- console.log(e.data);
- if (e.data === 'Ready.') {
- this.source.postMessage(this.stdin, '*', [this.stdin]);
- }
- if (e.data === 'Local server off.') {
- document.body.removeChild(this.transferableWindow);
- this.transferableWindow = onmessage = null;
- }
- }
- if (e.data instanceof ReadableStream) {
- this.stdout = e.data;
- resolve(this.audioStream());
- }
- };
- this.transferableWindow = document.createElement('iframe');
- this.transferableWindow.style.display = 'none';
- this.transferableWindow.name = location.href;
- this.transferableWindow.src = this.src;
- document.body.appendChild(this.transferableWindow);
- }).catch((err) => {
- throw err;
- });
- }
- async audioStream() {
- let channelData = [];
- try {
- await this.ac.resume();
- await this.audioWriter.ready;
- await Promise.allSettled([
- this.stdout.pipeTo(
- new WritableStream({
- write: async (value, c) => {
- for (
- let i = 0;
- i < value.buffer.byteLength;
- i++, this.readOffset++
- ) {
- if (channelData.length === this.channelDataLength) {
- this.inputController.enqueue(new Uint8Array(channelData));
- channelData.length = 0;
- }
- channelData.push(value[i]);
- }
- },
- abort(e) {
- console.error(e.message);
- },
- close: async () => {
- console.log('Done writing input stream.');
- if (channelData.length) {
- this.inputController.enqueue(channelData);
- }
- this.inputController.close();
- this.source.postMessage('Done writing input stream.', '*');
- },
- }),
- { signal: this.signal }
- ),
- this.audioReadable.pipeTo(
- new WritableStream({
- write: async ({ timestamp }) => {
- if (this.inputController.desiredSize === 0) {
- await this.disconnect();
- console.log(
- `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
- `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`
- );
- return await Promise.all([
- new Promise((resolve) => (this.stream.oninactive = resolve)),
- new Promise((resolve) => (this.ac.onstatechange = resolve)),
- ]);
- }
- const { value, done } = await this.inputReader.read();
- if (done) {
- console.log({ done });
- }
- const uint16 = new Uint16Array(value.buffer);
- // https://stackoverflow.com/a/35248852
- const floats = new Float32Array(this.channelDataLength / 2);
- for (let i = 0; i < uint16.length; i++) {
- const int = uint16[i];
- // If the high bit is on, then it is a negative number, and actually counts backwards.
- const float =
- int >= 0x8000 ? -(0x10000 - int) / 0x8000 : int / 0x7fff;
- floats[i] = float;
- }
- const buffer = new AudioBuffer({
- numberOfChannels: this.numberOfChannels,
- length: floats.length,
- sampleRate: this.sampleRate,
- });
- buffer.getChannelData(0).set(floats);
- this.duration += buffer.duration;
- const frame = new AudioData({ timestamp, buffer });
- await this.audioWriter.write(frame);
- if (this.recorder && this.recorder.state === 'inactive') {
- this.recorder.start();
- }
- },
- abort(e) {
- console.error(e.message);
- },
- close() {
- console.log('Done reading input stream.');
- },
- }),
- { signal: this.audioReadableSignal }
- ),
- ]);
- this.resolve(
- this.recorder ? await this.data.arrayBuffer() : 'Done streaming.'
- );
- return this.promise;
- } catch (err) {
- console.error(err);
- throw err;
- }
- }
- }
|