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.4KB

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