eSpeak NG is an open source speech synthesizer that supports more than hundred languages and accents.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AudioStream.js 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. class AudioStream {
  2. constructor({ stdin, recorder = false }) {
  3. if (!/^espeak-ng/.test(stdin)) {
  4. throw new Error(`stdin should begin with "espeak-ng" command`);
  5. }
  6. this.command = stdin;
  7. this.stdin = new ReadableStream({
  8. start(c) {
  9. c.enqueue(
  10. new File([stdin], 'espeakng', {
  11. type: 'application/octet-stream',
  12. })
  13. );
  14. c.close();
  15. },
  16. });
  17. this.readOffset = 0;
  18. this.duration = 0;
  19. this.channelDataLength = 440;
  20. this.sampleRate = 22050;
  21. this.numberOfChannels = 1;
  22. this.init = false;
  23. this.src =
  24. 'chrome-extension://<id>/nativeTransferableStream.html';
  25. this.ac = new AudioContext({
  26. latencyHint: 0,
  27. });
  28. this.ac.suspend();
  29. this.msd = new MediaStreamAudioDestinationNode(this.ac, {
  30. channelCount: this.numberOfChannels,
  31. });
  32. this.inputController = void 0;
  33. this.inputStream = new ReadableStream({
  34. start: (_) => {
  35. return (this.inputController = _);
  36. },
  37. });
  38. this.inputReader = this.inputStream.getReader();
  39. const { stream } = this.msd;
  40. this.stream = stream;
  41. const [track] = stream.getAudioTracks();
  42. this.track = track;
  43. this.osc = new OscillatorNode(this.ac, { frequency: 0 });
  44. this.processor = new MediaStreamTrackProcessor({ track });
  45. this.generator = new MediaStreamTrackGenerator({ kind: 'audio' });
  46. const { writable } = this.generator;
  47. this.writable = writable;
  48. const { readable: audioReadable } = this.processor;
  49. this.audioReadable = audioReadable;
  50. this.audioWriter = this.writable.getWriter();
  51. this.mediaStream = new MediaStream([this.generator]);
  52. if (recorder) {
  53. this.recorder = new MediaRecorder(this.mediaStream);
  54. this.recorder.ondataavailable = ({ data }) => {
  55. this.data = data;
  56. };
  57. }
  58. this.outputSource = new MediaStreamAudioSourceNode(this.ac, {
  59. mediaStream: this.mediaStream,
  60. });
  61. this.outputSource.connect(this.ac.destination);
  62. this.resolve = void 0;
  63. this.promise = new Promise((_) => (this.resolve = _));
  64. this.osc.connect(this.msd);
  65. this.osc.start();
  66. this.track.onmute = this.track.onunmute = this.track.onended = (e) =>
  67. console.log(e);
  68. this.abortable = new AbortController();
  69. const { signal } = this.abortable;
  70. this.signal = signal;
  71. this.audioReadableAbortable = new AbortController();
  72. const { signal: audioReadableSignal } = this.audioReadableAbortable;
  73. this.audioReadableSignal = audioReadableSignal;
  74. this.audioReadableSignal.onabort = (e) => console.log(e.type);
  75. this.abortHandler = async (e) => {
  76. await this.disconnect(true);
  77. console.log(
  78. `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
  79. `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`,
  80. `inputController.desiredSize:${this.inputController.desiredSize}, ac.state:${this.ac.state}`
  81. );
  82. if (
  83. this.transferableWindow ||
  84. document.body.querySelector(`iframe[src="${this.src}"]`)
  85. ) {
  86. document.body.removeChild(this.transferableWindow);
  87. }
  88. this.resolve('Stream aborted.');
  89. };
  90. this.signal.onabort = this.abortHandler;
  91. }
  92. async disconnect(abort = false) {
  93. if (abort) {
  94. this.audioReadableAbortable.abort();
  95. }
  96. this.msd.disconnect();
  97. this.osc.disconnect();
  98. this.outputSource.disconnect();
  99. this.track.stop();
  100. await this.audioWriter.close();
  101. await this.audioWriter.closed;
  102. await this.inputReader.cancel();
  103. this.generator.stop();
  104. if (this.recorder && this.recorder.state === 'recording') {
  105. this.recorder.stop();
  106. }
  107. return this.ac.close();
  108. }
  109. async start() {
  110. return this.nativeTransferableStream();
  111. }
  112. async abort() {
  113. this.abortable.abort();
  114. if (this.source) {
  115. this.source.postMessage('Abort.', '*');
  116. }
  117. return this.promise;
  118. }
  119. async nativeTransferableStream() {
  120. return new Promise((resolve) => {
  121. onmessage = (e) => {
  122. this.source = e.source;
  123. if (typeof e.data === 'string') {
  124. console.log(e.data);
  125. if (e.data === 'Ready.') {
  126. this.source.postMessage(this.stdin, '*', [this.stdin]);
  127. }
  128. if (e.data === 'Local server off.') {
  129. document.body.removeChild(this.transferableWindow);
  130. this.transferableWindow = onmessage = null;
  131. }
  132. }
  133. if (e.data instanceof ReadableStream) {
  134. this.stdout = e.data;
  135. resolve(this.audioStream());
  136. }
  137. };
  138. this.transferableWindow = document.createElement('iframe');
  139. this.transferableWindow.style.display = 'none';
  140. this.transferableWindow.name = location.href;
  141. this.transferableWindow.src = this.src;
  142. document.body.appendChild(this.transferableWindow);
  143. }).catch((err) => {
  144. throw err;
  145. });
  146. }
  147. async audioStream() {
  148. let channelData = [];
  149. try {
  150. await this.ac.resume();
  151. await this.audioWriter.ready;
  152. await Promise.allSettled([
  153. this.stdout.pipeTo(
  154. new WritableStream({
  155. write: async (value, c) => {
  156. let i = 0;
  157. if (!this.init) {
  158. this.init = true;
  159. i = 44;
  160. }
  161. for (;
  162. i < value.buffer.byteLength;
  163. i++, this.readOffset++
  164. ) {
  165. if (channelData.length === this.channelDataLength) {
  166. this.inputController.enqueue(new Uint8Array(channelData));
  167. channelData.length = 0;
  168. }
  169. channelData.push(value[i]);
  170. }
  171. },
  172. abort(e) {
  173. console.error(e.message);
  174. },
  175. close: async () => {
  176. console.log('Done writing input stream.');
  177. if (channelData.length) {
  178. this.inputController.enqueue(channelData);
  179. }
  180. this.inputController.close();
  181. this.source.postMessage('Done writing input stream.', '*');
  182. },
  183. }),
  184. { signal: this.signal }
  185. ),
  186. this.audioReadable.pipeTo(
  187. new WritableStream({
  188. write: async ({ timestamp }) => {
  189. if (this.inputController.desiredSize === 0) {
  190. await this.disconnect();
  191. console.log(
  192. `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
  193. `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`
  194. );
  195. return await Promise.all([
  196. new Promise((resolve) => (this.stream.oninactive = resolve)),
  197. new Promise((resolve) => (this.ac.onstatechange = resolve)),
  198. ]);
  199. }
  200. const { value, done } = await this.inputReader.read();
  201. if (done) {
  202. console.log({ done });
  203. }
  204. const uint16 = new Uint16Array(value.buffer);
  205. // https://stackoverflow.com/a/35248852
  206. const floats = new Float32Array(this.channelDataLength / 2);
  207. for (let i = 0; i < uint16.length; i++) {
  208. const int = uint16[i];
  209. // If the high bit is on, then it is a negative number, and actually counts backwards.
  210. const float =
  211. int >= 0x8000 ? -(0x10000 - int) / 0x8000 : int / 0x7fff;
  212. floats[i] = float;
  213. }
  214. const buffer = new AudioBuffer({
  215. numberOfChannels: this.numberOfChannels,
  216. length: floats.length,
  217. sampleRate: this.sampleRate,
  218. });
  219. buffer.getChannelData(0).set(floats);
  220. this.duration += buffer.duration;
  221. const frame = new AudioData({ timestamp, buffer });
  222. await this.audioWriter.write(frame);
  223. if (this.recorder && this.recorder.state === 'inactive') {
  224. this.recorder.start();
  225. }
  226. },
  227. abort(e) {
  228. console.error(e.message);
  229. },
  230. close() {
  231. console.log('Done reading input stream.');
  232. },
  233. }),
  234. { signal: this.audioReadableSignal }
  235. ),
  236. ]);
  237. this.resolve(
  238. this.recorder ? await this.data.arrayBuffer() : 'Done streaming.'
  239. );
  240. return this.promise;
  241. } catch (err) {
  242. console.error(err);
  243. throw err;
  244. }
  245. }
  246. }