-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplayer.js
More file actions
418 lines (361 loc) · 14.3 KB
/
player.js
File metadata and controls
418 lines (361 loc) · 14.3 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/*
Structural language learning and tandem method.
A JavaScript computer program for coaching.
Version of 15 Lug 2012.
Copyright (c) 2012 Antonio Bonifati http://ninuzzo.github.com/about.html
This work is licensed under the Creative Commons
Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a copy of
this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a
letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View,
California, 94041, USA.
*/
// Convention: lk = language known; l1 = language 1; l2 = language 2 (tandems only)
/* This is the only function that needs to be global. It has been prefixed
with pl_ hoping that will avoid any conflict with other JavaScript code */
function pl_$(id) {
// Change CSS prefix pl_ here and in pl_check_ and pl_definitions_ below.
return document.getElementById('pl_' + id);
}
/* This is the only global variable, prefixed with pl_. */
var pl_sampa = {};
/* DOMContentLoaded fires when the document's DOM content is finished loading,
but unlike "load", does not wait until all images are loaded. */
window.addEventListener('DOMContentLoaded', function() {
// http://stackoverflow.com/questions/950087/include-javascript-file-inside-javascript-file
function load_script(url, callback) {
// Add a script tag to the head.
var head = document.getElementsByTagName('head')[0],
script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
script.onreadystatechange = callback;
script.onload = callback;
// Fire the loading.
head.appendChild(script);
}
function main() {
var step, steps = lesson.length, lname = {
// TODO: add other languages here.
de: 'German', es: 'Spanish', it: 'Italian', la: 'Latin'
};
/* Basic lesson syntax check. In particular, warn if there are further
unused arguments in a slide, probably additional square brackets are
missing! If we */
function check_syntax() {
var error = [], errors;
for (var step = 1; step <= steps; step++) {
var slide = lesson[step - 1], type = slide[0], size;
switch (type) {
case 'tra':
case 'def':
size = tandem ? 5 : 4;
break;
case 'com':
size = 2;
break;
default:
error.push('Unknown slide type ' + type);
continue;
}
if (slide[size]) {
error.push('Unused argument on ' + type + ' slide #' + step);
}
}
if (errors = error.join("\n")) {
// It's better to open a warning box than logging on console.
// The latter could be easily ignored if there is no open console!
alert(errors);
}
}
function format_iso_date(date) {
var month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May',
'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
date = new Date(date);
return date.getDate() + ' ' + month_names[date.getMonth()] + ' '
+ date.getFullYear();
}
function go_to(newstep) {
function s2a(value) {
return value instanceof Array ? value : [value];
}
/* This plays a file, and calls a callback once completed
(if a callback is set). See:
http://stackoverflow.com/questions/9456238/i-want-to-play-a-multiple-sound-files-base-on-queue */
function play(audio, callback) {
audio.play();
if (callback) {
// When the audio object completes it's playback,
// call the callback provided
audio.onended = callback;
}
}
// Plays multiple sound files based on a queue
function play_sound_queue(sounds) {
var index = 0;
function recursive_play() {
// If the index is the last of the table, play the sound
// without running a callback after
if (index + 1 === sounds.length) {
play(sounds[index], null);
} else {
/* Else, play the sound, and when the playing is complete
increment index by one and play the sound in the
indexth position of the array */
play(sounds[index], function() { index++; recursive_play(); });
}
}
// Call the recursive_play for the first time
if (sounds) { recursive_play(); };
}
function play_definitions() {
function get_definitions(lang) {
// http://stackoverflow.com/questions/2430121/javascript-concatenate-multiple-nodelists-together
return [].slice.call(pl_$('definitions_' + lang)
.getElementsByTagName('audio'));
}
var definitions;
if (pl_$('l1').checked) {
definitions = get_definitions('l1');
}
if (tandem && pl_$('l2').checked) {
if (definitions) {
definitions.concat(get_definitions('l2'));
} else {
definitions = get_definitions('l2');
}
}
// Play the definition(s).
play_sound_queue(definitions);
}
// Return a list of sentences in the given language,
// optionally asking the user to guess before.
function define(lang, sentences, guess_mode) {
// Until Microsoft and Apple decide to support OGG,
// I have to provide MP3 as well. I hate 'em!
function audio_source(sentence) {
// I would like to use ['ogg','mp3'].forEach(function (format) { ... });
// but Explorer does not still support it ATTOW
var html = '', format = ['ogg','mp3'];
for (var i = 0; i < 2; i++) {
html += '<source src="' + audio_path + '/' + lang + '/'
+ (typeof audio_dir != 'undefined' ? audio_dir + '/' : '')
/* Distribute files amongst subdirs for scalability. */
+ sentence[0] + '/' + (sentence[1] ? sentence[1] + '/' : '')
/* Windows NTFS forbids the following characters: " * : < > ? \ / |
Trim or replace the ones we happen to use,
for now only replace ? with Q */
+ encodeURIComponent(sentence.replace(/\?/g, 'Q'))
+ '.' + format[i] + '" type="audio/' + format[i] + '" />';
}
return html;
}
function reveal_definitions(lang_name) {
var definitions = pl_$('definitions_' + lang_name);
// Show the solution(s).
definitions.style.display = 'block';
// Play the solution(s).
play_sound_queue(definitions.getElementsByTagName('audio'));
}
// Returns the SAMPA pronunciation of a sentence as a string
function pronunciation(lang, sentence) {
// Trims punctuation marks from the beginning and end of a string
// (not the middle)
function trim_punctuation(string) {
// TODO: add other marks here as they are used
return string.replace(/^[?!,.]+|[?!,.]+$/g, '');
}
var sentence_sampa = '/', word = sentence.split(/\s+/);
for (var i = 0; i < word.length; i++) {
var word_sampa;
if (typeof (word_sampa
= pl_sampa[lang][trim_punctuation(word[i]).toLowerCase()])
== 'undefined') {
word_sampa = '?'; // Denotes a missing word pronunciation.
}
if (sentence_sampa != '/') {
sentence_sampa += ' ';
}
sentence_sampa += word_sampa;
}
return sentence_sampa + '/';
}
// function define begins.
var html,
lang_name, // Which of the three languages lang is?
bridge = lang == lk, // Tells whether lang is the bridge language.
learning; // Tells whether lang is a language the user is learning.
switch (lang) {
case lk:
lang_name = 'lk';
break;
case l1:
lang_name = 'l1';
break;
case l2:
lang_name = 'l2';
break;
default:
console.warn('Undefined lesson language ' + lang);
return;
}
if (bridge) {
html = '<dt lang="' + lang + '"><ul>';
} else {
html = '<dd lang="' + lang + '">';
if ((learning = pl_$(lang_name).checked) && guess_mode) {
html += '<textarea rows="2" cols="40" autofocus="" '
+ 'placeholder="speak your answer and optionally write it here in '
+ lname[lang] + ' and then compare"></textarea>'
+ '<button id="pl_check_' + lang_name + '">check</button>'
+ '<ul id="pl_definitions_' + lang_name + '" style="display: none">';
/* This trick allows me to use to attach an event handler without
polluting the global namespace or having to generatore HTML code
using boring DOM calls. */
setTimeout(function () {
pl_$('check_' + lang_name).onclick = function () {
reveal_definitions(lang_name);
};
}, 0);
} else {
html += '<ul id="pl_definitions_' + lang_name + '">';
}
}
sentences = s2a(sentences);
for (var i = 0; i < sentences.length; i++) {
var sentence = sentences[i];
html += '<li>';
if (bridge) {
html += sentence;
} else {
var id = lang + i; // It just have to be unique in the page.
html += '<audio id="pl_' + id + '"'
+ (learning ? ' preload="auto"' : ' preload="none"')
+ '>' + audio_source(sentence) + '</audio><button onclick="pl_$(\''
+ id + "').play()\" title='" + pronunciation(lang, sentences[i])
+ "'>" + sentences[i] + '</button>';
}
html += '</li>';
}
html += '</ul>' + (bridge ? '</dt>' : '</dd>');
// DEBUG
//alert(html);
return html;
}
// Returns a comment in one or more paragraphs.
function comments(sentences) {
var html = '';
if (sentences) {
sentences = s2a(sentences);
for (var i = 0; i < sentences.length; i++) {
html += '<p>' + sentences[i] + '</p>';
}
}
return html;
}
// function go_to begins.
// Update controls.
// + needed to make sure step remains a number!
pl_$('step').value = step = +newstep;
if (step == 1) {
pl_$('first').disabled = true;
pl_$('previous').disabled = true;
} else {
pl_$('first').disabled = false;
pl_$('previous').disabled = false;
}
if (step == steps) {
pl_$('next').disabled = true;
pl_$('last').disabled = true;
} else {
pl_$('next').disabled = false;
pl_$('last').disabled = false;
}
// Update content. If you change slides format or add new slides,
// be sure to update check_syntax above to reflect your changes too!
var slide = lesson[step-1], guess_mode;
switch (slide[0]) {
case 'tra':
guess_mode = true;
case 'def':
pl_$('translations').innerHTML = '<dl>' + define(lk, slide[1])
+ define(l1, slide[2], guess_mode)
+ (tandem ? define(l2, slide[3], guess_mode) : '') + '</dl>';
if (! guess_mode) {
// Don't play the definitions right away, give the user
// a bit of time to read and concentrate before playing starts.
setTimeout(function () { play_definitions(); }, 1 * 1000);
}
pl_$('comments').innerHTML = comments(slide[tandem ? 4 : 3]);
break;
case 'com':
pl_$('translations').innerHTML = '';
pl_$('comments').innerHTML = comments(slide[1]);
break;
// TODO: implement other slide types here, if needed!
}
}
check_syntax();
// Attach events.
// Navigation bar.
pl_$('first').onclick = function () {
go_to(1);
return false; // This avoids a page reload.
};
pl_$('previous').onclick = function () { go_to(step - 1); return false; };
pl_$('step_form').onsubmit = function () { go_to(pl_$('step').value); return false; };
pl_$('step').onclick = function () { this.select(); };
pl_$('step').onblur = function () {
if (this.value < 1 || this.value > steps) {
this.value = step;
}
};
pl_$('next').onclick = function () { go_to(step + 1); return false; };
pl_$('last').onclick = function () { go_to(steps); return false; };
// Other.
pl_$('l1').onchange = function() { go_to(step); };
if (tandem) pl_$('l2').onchange = function() { go_to(step); };
// Initialize HTML.
pl_$('step').min = 1;
pl_$('step').max = pl_$('steps').textContent = steps;
pl_$('ll1').textContent = lname[l1];
if (tandem) {
var idx = pl_$('idx');
if (idx) {
idx.setAttribute('href', l1 + '.html');
idx.textContent = lname[l1] + '-' + lname[l2] + ' chat index';
}
}
if (tandem) pl_$('ll2').textContent = lname[l2];
pl_$('title').textContent = title;
pl_$('date').setAttribute('datetime', date);
pl_$('date').textContent = format_iso_date(date);
// Display first slide.
go_to(1);
} // main
// Configuration begins:
// Example of configuring an external site for serving audio files:
//var audio_path = 'http://host/path/audio',
var audio_path = 'audio',
sampa_path = 'mini/sampa';
// end of configuration.
// Make sure the data file is loaded before the main player code.
var path = location.pathname, search = location.search.substring(1),
pos = path.lastIndexOf('/') + 1, tandem;
if (search != '') {
load_script(path.substr(pos, path.lastIndexOf('.') - pos) + '/'
+ location.search.substring(1) + '.js', function () {
// Make sure the SAMPA files are loaded before the main player code.
load_script(sampa_path + '/' + l1 + '.js', function () {
tandem = typeof l2 != 'undefined';
if (tandem) {
load_script(sampa_path + '/' + l2 + '.js', main);
} else {
main();
}
});
}); // load_script
}
}, false); // window.addEventListener
/* vim: set ft=javascript et ts=2 sw=2 ai si: */