1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226 | 1x
1x
1x
1x
21x
21x
1x
20x
20x
20x
20x
2x
10x
1x
1x
24x
24x
24x
24x
24x
3x
3x
21x
3x
18x
10x
10x
1x
9x
8x
7x
1x
1x
3x
3x
3x
1x
1x
2x
2x
2x
2x
3x
1x
17x
17x
17x
17x
17x
1x
1x
1x
1x
1x
17x
17x
17x
17x
17x
17x
6x
6x
6x
6x
6x
6x
6x
6x
6x
3x
3x
3x
3x
3x
17x
17x
1x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
1x
1x
1x
| var cp = require('child_process');
var Emitter = require('events').EventEmitter;
var priv = new WeakMap();
var Player = require('./player');
function Speaker(options) {
Emitter.call(this);
// Previously, Speaker was used for playing mp3s,
// but that functionality now lives in Player.
// This avoids breaking existing code.
if (options &&
(typeof options === 'string' && options.endsWith('.mp3'))) {
return new Player(options);
}
options = options || {
debug: false
};
var state = {
debug: options.debug,
queue: [],
isSpeaking: false,
interval: null,
process: null,
time: null,
currentTime: 0,
theLastWord: false,
};
priv.set(this, state);
Object.defineProperties(this, {
currentTime: {
get: () => Number(state.currentTime.toFixed(3)),
},
isSpeaking: {
get: () => state.isSpeaking,
},
});
}
Speaker.prototype = Object.create(Emitter.prototype, {
constructor: {
value: Speaker
}
});
Speaker.prototype.say = function(phrase) {
var state = priv.get(this);
var args = [];
var offset = null;
var time = 0;
if (state.isSpeaking) {
state.queue.push(phrase);
return this;
}
// If phrase was nothing at all, nothing to do
if (phrase == null) {
// speaker.say();
// speaker.say(null);
// speaker.say(undefined);
return this;
}
if (typeof phrase === 'string' ||
typeof phrase === 'number') {
// speaker.say('Hello!');
// speaker.say(1);
// Ensures: 0 -> "0"
phrase = String(phrase).trim();
// If the phrase is empty, nothing to do.
if (!phrase) {
return this;
}
args.push(phrase);
} else {
if (Array.isArray(phrase)) {
// speaker.say(['Hello!', '-a', 10, '-p', 50 ]);
args = phrase;
} else {
var hasPhrase;
// Don't need to check for null, since those are handled above
Eif (typeof phrase === 'object') {
// speaker.say({
// phrase: 'Hello!',
// a: 10,
// p: 50,
// });
//
args = Object.keys(phrase).reduce((accum, key) => {
var value = phrase[key];
var option = '';
// When the key is "phrase", we only want the value
if (key === 'phrase') {
hasPhrase = true;
accum.push(value.trim());
} else {
Iif (key.length === 1) {
option = '-';
}
option += key;
accum.push(option);
accum.push(value);
}
return accum;
}, []);
// If no "phrase" property was provided, nothing to do.
Iif (!hasPhrase) {
return this;
}
}
}
}
Eif (state.process === null) {
state.isSpeaking = true;
state.currentTime = 0;
state.startTime = Date.now();
state.interval = setInterval(() => {
var now = Date.now();
Eif (offset === null) {
offset = now - state.startTime;
}
state.currentTime = time + (now - state.startTime - offset) / 1000;
this.emit('timeupdate', this.currentTime);
}, 100);
// Apply some reasonable defaults...
Object.keys(espeak.options).forEach(key => {
Eif (!args.includes(key)) {
args.push(key, espeak.options[key]);
}
});
state.process = cp.spawn('espeak', args);
Iif (this.debug) {
state.process.stderr.on('data', (data) => {
var lines = data.toString().split('\n').filter(Boolean).map(line => line.trim());
lines.forEach(line => {
console.error(line);
});
});
}
state.process.on('exit', (code, signal) => {
Eif (code !== null && signal === null) {
Eif (state.interval) {
clearInterval(state.interval);
}
state.process = null;
state.currentTime = 0;
state.startTime = 0;
state.isSpeaking = false;
this.emit('ended');
if (state.queue.length) {
this.say(state.queue.shift());
} else {
Eif (!state.theLastWord) {
Iif (this._events.lastword) {
this.emit('lastword');
}
state.theLastWord = true;
}
this.emit('empty');
}
}
});
this.emit('say');
}
return this;
};
Speaker.prototype.stop = function() {
var state = priv.get(this);
Iif (!state.isSpeaking) {
return this;
}
state.isSpeaking = false;
Eif (state.interval) {
clearInterval(state.interval);
}
Eif (state.process) {
state.process.kill('SIGTERM');
state.process = null;
}
this.emit('stop');
return this;
};
var espeak = {
options: {
// Words per minute defaults to 160.
// Tweaking this to 130 makes the output speed
// much easier to hear and follow.
'-s': 130,
}
};
espeak.keys = Object.keys(espeak.options);
module.exports = Speaker;
|