index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. 'use strict';
  2. var child_process = require('child_process');
  3. var path = require('path');
  4. var say = exports;
  5. var childD;
  6. // use the correct library per platform
  7. if (process.platform === 'darwin') {
  8. say.speaker = 'say';
  9. say.base_speed = 175;
  10. } else if (process.platform === 'linux') {
  11. say.speaker = 'festival';
  12. say.base_speed = 100;
  13. } else if (process.platform === 'win32') {
  14. say.speaker = 'powershell';
  15. }
  16. /**
  17. * Uses system libraries to speak text via the speakers.
  18. *
  19. * @param {string} text Text to be spoken
  20. * @param {string|null} voice Name of voice to be spoken with
  21. * @param {number|null} speed Speed of text (e.g. 1.0 for normal, 0.5 half, 2.0 double)
  22. * @param {Function|null} callback A callback of type function(err) to return.
  23. */
  24. say.speak = function(text, voice, speed, callback) {
  25. var commands, pipedData;
  26. if (typeof callback !== 'function') {
  27. callback = function() {};
  28. }
  29. if (!text) {
  30. // throw TypeError because API was used incorrectly
  31. throw new TypeError('Must provide text parameter');
  32. }
  33. // tailor command arguments to specific platforms
  34. if (process.platform === 'darwin') {
  35. if (!voice) {
  36. commands = [ text ];
  37. } else {
  38. commands = [ '-v', voice, text];
  39. }
  40. if (speed) {
  41. commands.push('-r', convertSpeed(speed));
  42. }
  43. } else if (process.platform === 'linux') {
  44. commands = ['--pipe'];
  45. if (speed) {
  46. pipedData = '(Parameter.set \'Audio_Command "aplay -q -c 1 -t raw -f s16 -r $(($SR*' + convertSpeed(speed) + '/100)) $FILE") ';
  47. }
  48. if (voice) {
  49. pipedData += '(' + voice + ') ';
  50. }
  51. pipedData += '(SayText \"' + text + '\")';
  52. } else if (process.platform === 'win32') {
  53. pipedData = text;
  54. commands = [ 'Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; [Console]::InputEncoding = [System.Text.Encoding]::UTF8; $speak.Speak([Console]::In.ReadToEnd())' ];
  55. } else {
  56. // if we don't support the platform, callback with an error (next tick) - don't continue
  57. return process.nextTick(function() {
  58. callback(new Error('say.js speak does not support platform ' + process.platform));
  59. });
  60. }
  61. var options = (process.platform === 'win32') ? { shell: true } : undefined;
  62. childD = child_process.spawn(say.speaker, commands, options);
  63. childD.stdin.setEncoding('ascii');
  64. childD.stderr.setEncoding('ascii');
  65. if (pipedData) {
  66. childD.stdin.end(pipedData);
  67. }
  68. childD.stderr.once('data', function(data) {
  69. // we can't stop execution from this function
  70. callback(new Error(data));
  71. });
  72. childD.addListener('exit', function (code, signal) {
  73. if (code === null || signal !== null) {
  74. return callback(new Error('say.js: could not talk, had an error [code: ' + code + '] [signal: ' + signal + ']'));
  75. }
  76. childD = null;
  77. callback(null);
  78. });
  79. };
  80. say.export = function(text, voice, speed, filename, callback) {
  81. var commands, pipedData;
  82. if (!text) {
  83. // throw TypeError because API was used incorrectly
  84. throw new TypeError('Must provide text parameter');
  85. }
  86. if (!filename) {
  87. // throw TypeError because API was used incorrectly
  88. throw new TypeError('Must provide a filename');
  89. }
  90. if (typeof callback !== 'function') {
  91. callback = function() {};
  92. }
  93. // tailor command arguments to specific platforms
  94. if (process.platform === 'darwin') {
  95. if (!voice) {
  96. commands = [ text ];
  97. } else {
  98. commands = [ '-v', voice, text];
  99. }
  100. if (speed) {
  101. commands.push('-r', convertSpeed(speed));
  102. }
  103. if (filename){
  104. commands.push('-o', filename, '--data-format=LEF32@32000');
  105. }
  106. } else {
  107. // if we don't support the platform, callback with an error (next tick) - don't continue
  108. return process.nextTick(function() {
  109. callback(new Error('say.js export does not support platform ' + process.platform));
  110. });
  111. }
  112. childD = child_process.spawn(say.speaker, commands);
  113. childD.stdin.setEncoding('ascii');
  114. childD.stderr.setEncoding('ascii');
  115. if (pipedData) {
  116. childD.stdin.end(pipedData);
  117. }
  118. childD.stderr.once('data', function(data) {
  119. // we can't stop execution from this function
  120. callback(new Error(data));
  121. });
  122. childD.addListener('exit', function (code, signal) {
  123. if (code === null || signal !== null) {
  124. return callback(new Error('say.js: could not talk, had an error [code: ' + code + '] [signal: ' + signal + ']'));
  125. }
  126. childD = null;
  127. callback(null);
  128. });
  129. };
  130. /**
  131. * Stops currently playing audio. There will be unexpected results if multiple audios are being played at once
  132. *
  133. * TODO: If two messages are being spoken simultaneously, childD points to new instance, no way to kill previous
  134. */
  135. exports.stop = function(callback) {
  136. if (typeof callback !== 'function') {
  137. callback = function() {};
  138. }
  139. if (!childD) {
  140. return callback(new Error('No speech to kill'));
  141. }
  142. if (process.platform === 'linux') {
  143. // TODO: Need to ensure the following is true for all users, not just me. Danger Zone!
  144. // On my machine, original childD.pid process is completely gone. Instead there is now a
  145. // childD.pid + 1 sh process. Kill it and nothing happens. There's also a childD.pid + 2
  146. // aplay process. Kill that and the audio actually stops.
  147. process.kill(childD.pid + 2);
  148. } else if (process.platform === 'win32') {
  149. childD.stdin.pause();
  150. child_process.exec('taskkill /pid ' + childD.pid + ' /T /F')
  151. } else {
  152. childD.stdin.pause();
  153. childD.kill();
  154. }
  155. childD = null;
  156. callback(null);
  157. };
  158. function convertSpeed(speed) {
  159. return Math.ceil(say.base_speed * speed);
  160. }