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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. try {
  77. await this.disconnect(true);
  78. } catch (err) {
  79. console.warn(err.message);
  80. }
  81. console.log(
  82. `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
  83. `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`,
  84. `inputController.desiredSize:${this.inputController.desiredSize}, ac.state:${this.ac.state}`
  85. );
  86. if (
  87. this.transferableWindow ||
  88. document.body.querySelector(`iframe[src="${this.src}"]`)
  89. ) {
  90. document.body.removeChild(this.transferableWindow);
  91. }
  92. this.resolve('Stream aborted.');
  93. };
  94. this.signal.onabort = this.abortHandler;
  95. }
  96. async disconnect(abort = false) {
  97. if (abort) {
  98. this.audioReadableAbortable.abort();
  99. }
  100. this.msd.disconnect();
  101. this.osc.disconnect();
  102. this.outputSource.disconnect();
  103. this.track.stop();
  104. try {
  105. await this.audioWriter.close();
  106. await this.audioWriter.closed;
  107. await this.inputReader.cancel();
  108. } catch (err) {
  109. throw err;
  110. }
  111. this.generator.stop();
  112. if (this.recorder && this.recorder.state === 'recording') {
  113. this.recorder.stop();
  114. }
  115. return this.ac.close();
  116. }
  117. async start() {
  118. return this.nativeTransferableStream();
  119. }
  120. async abort() {
  121. this.abortable.abort();
  122. if (this.source) {
  123. this.source.postMessage('Abort.', '*');
  124. }
  125. return this.promise;
  126. }
  127. async nativeTransferableStream() {
  128. return new Promise((resolve) => {
  129. onmessage = (e) => {
  130. this.source = e.source;
  131. if (typeof e.data === 'string') {
  132. console.log(e.data);
  133. if (e.data === 'Ready.') {
  134. this.source.postMessage(this.stdin, '*', [this.stdin]);
  135. }
  136. if (e.data === 'Local server off.') {
  137. document.body.removeChild(this.transferableWindow);
  138. this.transferableWindow = onmessage = null;
  139. }
  140. }
  141. if (e.data instanceof ReadableStream) {
  142. this.stdout = e.data;
  143. resolve(this.audioStream());
  144. }
  145. };
  146. this.transferableWindow = document.createElement('iframe');
  147. this.transferableWindow.style.display = 'none';
  148. this.transferableWindow.name = location.href;
  149. this.transferableWindow.src = this.src;
  150. document.body.appendChild(this.transferableWindow);
  151. }).catch((err) => {
  152. throw err;
  153. });
  154. }
  155. async audioStream() {
  156. let channelData = [];
  157. try {
  158. await this.ac.resume();
  159. await this.audioWriter.ready;
  160. await Promise.allSettled([
  161. this.stdout.pipeTo(
  162. new WritableStream({
  163. write: async (value, c) => {
  164. let i = 0;
  165. if (!this.init) {
  166. this.init = true;
  167. i = 44;
  168. }
  169. for (; i < value.buffer.byteLength; i++, this.readOffset++) {
  170. if (channelData.length === this.channelDataLength) {
  171. this.inputController.enqueue(
  172. new Uint8Array(
  173. channelData.splice(0, this.channelDataLength)
  174. )
  175. );
  176. }
  177. channelData.push(value[i]);
  178. }
  179. },
  180. abort(e) {
  181. console.error(e.message);
  182. },
  183. close: async () => {
  184. console.log('Done writing input stream.');
  185. if (channelData.length) {
  186. this.inputController.enqueue(new Uint8Array(channelData.splice(0, channelData.length)));
  187. }
  188. this.inputController.close();
  189. this.source.postMessage('Done writing input stream.', '*');
  190. },
  191. }),
  192. { signal: this.signal }
  193. ),
  194. this.audioReadable.pipeTo(
  195. new WritableStream({
  196. write: async ({ timestamp }) => {
  197. const { value, done } = await this.inputReader.read();
  198. if (done) {
  199. await this.inputReader.closed;
  200. try {
  201. await this.disconnect();
  202. } catch (err) {
  203. console.warn(err.message);
  204. }
  205. console.log(
  206. `readOffset:${this.readOffset}, duration:${this.duration}, ac.currentTime:${this.ac.currentTime}`,
  207. `generator.readyState:${this.generator.readyState}, audioWriter.desiredSize:${this.audioWriter.desiredSize}`
  208. );
  209. return await Promise.all([
  210. new Promise((resolve) => (this.stream.oninactive = resolve)),
  211. new Promise((resolve) => (this.ac.onstatechange = resolve)),
  212. ]);
  213. }
  214. const frame = new AudioData({
  215. format: 's16',
  216. sampleRate: 22050,
  217. numberOfChannels: 1,
  218. numberOfFrames: value.length / 2,
  219. timestamp,
  220. data: value,
  221. });
  222. this.duration += (frame.duration / 10**6);
  223. if (this.recorder && this.recorder.state === 'inactive') {
  224. this.recorder.start();
  225. }
  226. await this.audioWriter.write(frame);
  227. },
  228. abort(e) {
  229. console.error(e.message);
  230. },
  231. close() {
  232. console.log('Done reading input stream.');
  233. },
  234. }),
  235. { signal: this.audioReadableSignal }
  236. ),
  237. ]);
  238. this.resolve(
  239. this.recorder
  240. ? this.data && (await this.data.arrayBuffer())
  241. : 'Done streaming.'
  242. );
  243. return this.promise;
  244. } catch (err) {
  245. console.error(err);
  246. throw err;
  247. }
  248. }
  249. }