luigi12345 commited on
Commit
173b7fd
·
verified ·
1 Parent(s): d6943da

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +342 -378
index.html CHANGED
@@ -1,436 +1,400 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>Bach-like Fugue in A Minor</title>
5
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/build/cjs/vexflow.css">
6
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/cjs/vexflow-debug.js"></script>
7
- <!-- Use WebAudioFont for piano sound -->
8
- <script src="https://surikov.github.io/webaudiofont/npm/dist/WebAudioFontPlayer.js"></script>
9
- <script src="https://surikov.github.io/webaudiofontdata/sound/0000_JCLive_sf2_file.js"></script>
 
10
  <style>
11
- body {
12
- font-family: sans-serif;
13
- }
14
- #score {
15
- /* Styling to improve print layout */
16
- width: 750px; /* A4 width approximation at 96 DPI */
17
- margin: 20px auto; /* Center on the page */
18
- background-color: white;
19
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Optional: Add a subtle shadow */
20
- padding: 25px; /* Add padding */
21
- page-break-after: always; /* Force page break after each score */
22
- overflow: visible; /*Ensure nothing is clipped */
23
- }
24
-
25
- #player-controls {
26
- text-align: center;
27
- margin-bottom: 20px;
28
- }
29
-
30
- button {
31
- padding: 10px 20px;
32
- font-size: 16px;
33
- cursor: pointer;
34
  }
 
 
35
  @media print {
36
- body {
37
- margin: 0; /* Remove default margins */
38
- padding: 0;
39
- width: 100%; /* Use 100% width for printing */
40
-
41
- }
42
- #score {
43
- box-shadow: none; /* Remove shadow for printing*/
44
- width: 100%; /* Adjust width */
45
- margin: 0;
46
- padding:0;
47
- page-break-inside: avoid;
48
- break-inside: avoid;
49
- }
50
- #player-controls{
51
- display:none;
52
- }
53
-
54
- /* Hide controls on print */
55
- #player-controls, button {
56
- display: none;
57
- }
58
- .vf-stavenote {
59
- transform: scale(1) !important; /* Ensure notes aren't scaled down */
60
- }
61
-
62
  }
63
-
64
  </style>
65
  </head>
66
  <body>
67
-
68
- <h1>Fugue in A Minor (Bach-like)</h1>
69
-
70
- <div id="player-controls">
71
- <button id="playButton">Play</button>
72
- <button id="stopButton">Stop</button>
 
 
 
 
73
  </div>
74
 
75
- <div id="score"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
 
 
 
 
77
 
78
- <script>
79
- const {
80
- Accidental,
81
- Annotation,
82
- Beam,
83
- Dot,
84
- EasyScore,
85
- Factory,
86
- Formatter,
87
- Fraction,
88
- Renderer,
89
- Stave,
90
- StaveNote,
91
- Stem,
92
- Tuplet,
93
- Voice,
94
- VexFlow,
95
- parseSVG,
96
- renderVexFlow,
97
- } = Vex; // Destructuring for easier use
98
-
99
- // --- WebAudioFont Setup for Piano Sound ---
100
- const AudioContextFunc = window.AudioContext || window.webkitAudioContext;
101
- const audioContext = new AudioContextFunc();
102
- const player = new WebAudioFontPlayer();
103
- player.loader.decodeAfterLoading(audioContext, '_tone_0000_JCLive_sf2_file'); // Load piano sound
104
-
105
-
106
- // ------- VexFlow Rendering --------
107
- const factory = new Factory({
108
- renderer: { elementId: 'score', width: 750, height: 450 } // Adjusted for A4 and multiple staves
109
  });
110
- const score = factory.EasyScore();
111
 
112
- // Function to create a dotted note
113
- function dottedNote(note, duration) {
114
- return score.notes(note + duration)[0].addDotToAll();
115
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- // Helper function to create rests
118
- function createRest(duration) {
119
- return new StaveNote({ keys: ["b/4"], duration: duration + "r" }); // b/4 as placeholder
120
  }
121
 
122
- // --- Fugue Subject and Countersubject (Modified for more "Little Fugue" feel) ---
123
- const subject = [
124
- "a/4/16", "c/5/16", "b/4/16", "a/4/16", "g/4/8", "f/4/16", "e/4/16",
125
- "f/4/8", "g/4/16", "a/4/16", "b/4/8", "c/5/8", "a/4/8"
126
- ];
127
-
128
- const countersubject = [
129
- "e/4/8", "f/4/16", "g/4/16", "a/4/8", "b/4/16","c/5/16",
130
- "d/5/8", "c/5/16", "b/4/16", "a/4/8","g/4/8", "f/4/8","e/4/8",
131
- ];
132
-
133
- // --- Voices (Expanded for more musical interest and A4 layout) ---
134
- // Voice 1 (Soprano) - Page 1
135
- const voice1NotesPage1 = [
136
- ...subject.map(n => n),
137
- ...Array(12).fill("b/4/8r"), // Rests
138
- ...countersubject.map(n=>n.replace('4', '5')), //Adjust Octave
139
- ...Array(16).fill("b/4/8r"), // Rests
140
- ...subject.map(n => n.replace('4', '5')),//octave up
141
- ...Array(16).fill("b/4/8r"), // Rests
142
- "a/5/4.",
143
-
144
- ];
145
-
146
-
147
- // Voice 2 (Alto) - Page 1
148
- const voice2NotesPage1 = [
149
- ...Array(16).fill("b/4/8r"), // Rests
150
- ...subject.map(n=>n.replace('a', 'e').replace('4','4')),
151
- ...Array(8).fill("b/4/8r"), // Rests
152
- ...countersubject.map(n=>n), // Countersubject
153
- ...Array(16).fill("b/4/8r"), // Rests
154
- "e/4/4."
155
- ];
156
-
157
-
158
- // Voice 3 (Tenor) - Page 1
159
- const voice3NotesPage1 = [
160
- ...Array(16).fill("b/4/8r"),
161
- ...Array(16).fill("b/4/8r"),
162
- ...Array(8).fill("b/4/8r"),
163
- ...subject.map(n => n.replace('4', '4').replace('5', '4')),//octave up
164
- ...Array(16).fill("b/4/8r"),
165
- "c/4/4", ...Array(2).fill("b/4/8r")
166
- ];
167
- // Voice 4 (Bass) - Page 1
168
- const voice4NotesPage1 = [
169
- ...Array(16).fill("b/4/8r"),
170
- ...Array(8).fill("b/4/8r"),
171
- ...subject.map(n => n.replace('4', '3').replace('5','4').replace('a/','e/')),
172
- ...Array(16).fill("b/4/8r"),
173
- "a/2/4.", ...Array(10).fill("b/4/8r")
174
- ];
175
-
176
- const allNotes = [voice1NotesPage1, voice2NotesPage1, voice3NotesPage1, voice4NotesPage1];
177
- let beamedNotes = [];
178
-
179
- for (const voiceNotes of allNotes) {
180
- let currentBeamGroup = [];
181
- let beamedVoiceNotes = [];
182
-
183
- for (const note of voiceNotes) {
184
- const duration = note.split('/').pop(); // '8', '16', etc.
185
- if (duration === '8' || duration === '16') {
186
- currentBeamGroup.push(score.notes(note)[0]);
187
- } else {
188
- if (currentBeamGroup.length > 0) {
189
- beamedVoiceNotes.push(new Beam(currentBeamGroup));
190
- currentBeamGroup = [];
191
- }
192
- beamedVoiceNotes.push(score.notes(note)[0]); // Add the non-beamed note
193
- }
194
- }
195
- // Handle any remaining notes in the beam group
196
- if (currentBeamGroup.length > 0) {
197
- beamedVoiceNotes.push(new Beam(currentBeamGroup));
198
- }
199
- beamedNotes.push(beamedVoiceNotes);
200
- }
201
-
202
- // Create Voices
203
- const voicesPage1 = [
204
- new Voice({ num_beats: 16, beat_value: 4 }).addTickables(beamedNotes[0]), //Soprano
205
- new Voice({ num_beats: 16, beat_value: 4 }).addTickables(beamedNotes[1]), //Alto
206
- new Voice({ num_beats: 16, beat_value: 4 }).addTickables(beamedNotes[2]), // Tenor
207
- new Voice({ num_beats: 16, beat_value: 4 }).addTickables(beamedNotes[3]), // Bass
208
- ];
209
-
210
-
211
- // -------- Staves - Page 1 ----------
212
- const stave1Page1 = new Stave(10, 0, 700).addClef('treble').addTimeSignature('4/4').addKeySignature('Am'); //Soprano
213
- const stave2Page1 = new Stave(10, 110, 700).addClef('treble').addTimeSignature('4/4').addKeySignature('Am');//Alto
214
- const stave3Page1 = new Stave(10, 220, 700).addClef('bass').addTimeSignature('4/4').addKeySignature('Am'); //Tenor
215
- const stave4Page1 = new Stave(10, 330, 700).addClef('bass').addTimeSignature('4/4').addKeySignature('Am'); // Bass
216
-
217
- // Draw Page 1
218
- factory.getContext().scale(1,1); // Adjust scaling if needed
219
- factory.getContext().setFont("Arial", 10);
220
-
221
- const formatterPage1 = new Formatter().joinVoices(voicesPage1).format(voicesPage1, 650);
222
- voicesPage1.forEach((v, i) => v.draw(factory.getContext(), [stave1Page1, stave2Page1, stave3Page1, stave4Page1][i]));
223
-
224
- stave1Page1.setContext(factory.getContext()).draw();
225
- stave2Page1.setContext(factory.getContext()).draw();
226
- stave3Page1.setContext(factory.getContext()).draw();
227
- stave4Page1.setContext(factory.getContext()).draw();
228
-
229
- // -------- MIDI Data Generation --------
230
- function generateMidiData() {
231
- const headerChunk = [
232
- 0x4d, 0x54, 0x68, 0x64, // "MThd"
233
- 0x00, 0x00, 0x00, 0x06, // Header length (6 bytes)
234
- 0x00, 0x01, // Format 1 (multiple tracks)
235
- 0x00, 0x04, // Number of tracks (4 voices)
236
- 0x00, 0x60 // Ticks per quarter note (96) - Adjust if needed
237
- ];
238
- function createTrackChunk(notes) {
239
- let trackData = [];
240
- let currentTime = 0;
241
-
242
- for (const noteStr of notes) {
243
- const [note, duration] = noteStr.split('/');
244
- const isRest = noteStr.includes('r');
245
-
246
- // Calculate duration in ticks
247
- let durationTicks;
248
- switch (duration) {
249
- case '2.':
250
- durationTicks = 96 * 3; // Dotted Half note (3 beats)
251
- break;
252
- case '2':
253
- durationTicks = 96 * 2; // Half
254
- break;
255
- case '4':
256
- durationTicks = 96; // Quarter note
257
- break;
258
- case '4.':
259
- durationTicks = 96*1.5;
260
- break;
261
- case '8':
262
- durationTicks = 48; // Eighth note
263
- break;
264
- case '16':
265
- durationTicks = 24; // Sixteenth note
266
- break;
267
- case '4r':
268
- durationTicks = 96;
269
- break;
270
- case '8r':
271
- durationTicks = 48;
272
- break;
273
- case '16r':
274
- durationTicks = 24;
275
- break;
276
- default:
277
- durationTicks = 0; // Should not happen, but handle for safety
278
- }
279
-
280
- if (!isRest) {
281
- const midiNote = noteNameToMidi(note);
282
-
283
- // Note On event
284
- trackData.push(...deltaEncode(currentTime));
285
- trackData.push(0x90); // Note On, channel 1
286
- trackData.push(midiNote);
287
- trackData.push(0x64); // Medium Velocity (100) - Adjust as needed
288
-
289
- // Note Off event
290
- trackData.push(...deltaEncode(durationTicks));
291
- trackData.push(0x80); // Note Off, channel 1
292
- trackData.push(midiNote);
293
- trackData.push(0x00); // Release velocity
294
-
295
- currentTime = 0; // Reset current time for next delta
296
- } else {
297
- currentTime += durationTicks; // Increment time for rests
298
  }
 
299
  }
 
 
 
 
 
 
300
 
301
- // End of track
302
- trackData.push(0x00, 0xFF, 0x2F, 0x00);
 
303
 
304
- const trackHeader = [
305
- 0x4d, 0x54, 0x72, 0x6b,
306
- ...intToBytes(trackData.length, 4)
307
- ];
 
 
308
 
309
- return [...trackHeader, ...trackData];
310
- }
 
311
 
312
 
313
- const trackChunks = [voice1NotesPage1, voice2NotesPage1, voice3NotesPage1, voice4NotesPage1].map(createTrackChunk);
314
 
315
- const midiData = [...headerChunk, ...trackChunks.flat()];
316
- return new Uint8Array(midiData);
317
- }
 
 
 
 
318
 
319
- // Helper functions (same as before, but consolidated)
320
- function noteNameToMidi(noteName) {
321
- const noteMap = {
322
- 'c': 0, 'c#': 1, 'db': 1, 'd': 2, 'd#': 3, 'eb': 3, 'e': 4, 'f': 5,
323
- 'f#': 6, 'gb': 6, 'g': 7, 'g#': 8, 'ab': 8, 'a': 9, 'a#': 10, 'bb': 10, 'b': 11
324
- };
325
-
326
- const [note, octave] = noteName.split('/');
327
- const noteValue = noteMap[note.toLowerCase()];
328
- const octaveValue = parseInt(octave, 10);
329
-
330
- if (noteValue === undefined || isNaN(octaveValue)) {
331
- console.error("Invalid note name:", noteName);
332
- return 0; // Return a default value (or throw an error)
333
- }
334
-
335
- return (octaveValue + 1) * 12 + noteValue;
336
- }
337
-
338
- function deltaEncode(value) {
339
- let bytes = [];
340
- let buffer = value & 0x7F;
341
- while ((value >>= 7)) {
342
- buffer <<= 8;
343
- buffer |= ((value & 0x7F) | 0x80);
344
- }
345
- while (true) {
346
- bytes.push(buffer & 0xFF);
347
- if (buffer & 0x80) {
348
- buffer >>= 8;
349
- } else {
350
- break;
351
- }
352
- }
353
- return bytes;
354
- }
355
 
356
- function intToBytes(value, numBytes) {
357
- const bytes = [];
358
- for (let i = 0; i < numBytes; i++) {
359
- bytes.unshift((value >> (i * 8)) & 0xFF);
360
- }
361
- return bytes;
362
- }
363
 
364
- // --- MIDI Playback with WebAudioFont ---
365
- const midiData = generateMidiData();
366
- const midiBase64 = btoa(String.fromCharCode.apply(null, midiData));
367
- const midiDataUri = "data:audio/midi;base64," + midiBase64;
368
 
369
- let scheduleAheadTime = 0.1; // How far ahead to schedule notes (in seconds)
370
- let currentMidiTime = 0; // Keep track of the current time in the MIDI sequence
371
- let midiPlaybackInterval;
372
- let startTime = 0;
373
 
 
 
 
 
 
 
 
 
374
 
375
- document.getElementById('playButton').addEventListener('click', function() {
376
- if (midiPlaybackInterval) {
377
- return; // Prevent starting multiple playbacks
378
- }
379
 
380
- // Reset playback variables
381
- currentMidiTime = 0;
382
- startTime = audioContext.currentTime;
383
 
384
- // Create a buffer from the MIDI data
385
- const buffer = new Uint8Array(midiData).buffer;
386
- const midiFile = MidiParser.parse(buffer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
- playMidiFile(midiFile);
389
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- document.getElementById('stopButton').addEventListener('click', function() {
392
- clearInterval(midiPlaybackInterval);
393
- midiPlaybackInterval = null;
394
- player.cancelQueue(audioContext);
395
 
396
- });
397
  function playMidiFile(midiFile) {
 
398
  midiPlaybackInterval = setInterval(() => {
 
 
399
  const lookaheadEnd = audioContext.currentTime - startTime + scheduleAheadTime;
 
400
 
401
- // Iterate through tracks and schedule notes within the lookahead window
402
- for (const track of midiFile.track) {
403
  for (const event of track.event) {
404
  if (event.deltaTime > 0) {
405
- currentMidiTime += ticksToSeconds(event.deltaTime, midiFile.timeDivision); //update first!
406
  }
407
 
408
  if (currentMidiTime <= lookaheadEnd) {
409
- if (event.type === 9) { // Note On
410
- player.queueWaveTable(audioContext, audioContext.destination
411
- , _tone_0000_JCLive_sf2_file, startTime + currentMidiTime, event.data[0], event.data[1]/127, 0.5 );
412
- }
413
- if(event.type === 8) { //Note off, we won't use it.
414
- //player.noteOff(audioContext, event.data[0]);
 
 
 
 
 
 
 
 
415
  }
 
416
  } else {
417
- // Notes beyond lookahead are handled in the next interval
418
- break;
419
  }
 
420
  }
421
  }
422
- }, scheduleAheadTime * 1000 / 2); // Run slightly more frequently than scheduleAheadTime
423
- }
424
 
425
 
426
- function ticksToSeconds(ticks, timeDivision) {
427
- // Assuming tempo is 120 bpm (beats per minute)
428
- const beatsPerMinute = 120;
429
- const ticksPerBeat = timeDivision;
430
- const secondsPerBeat = 60 / beatsPerMinute;
431
- const secondsPerTick = secondsPerBeat / ticksPerBeat;
432
- return ticks * secondsPerTick;
433
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
  </script>
436
  </body>
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
+ <title>AI FUGUE by Sami Halawa</title>
5
+ <!-- Optimized CDN Links -->
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/build/cjs/vexflow.css">
7
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/cjs/vexflow-debug.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/webAudioFontPlayer.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/data/0000_JCLive_sf2_file.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/src/midi-parser.js"></script>
11
  <style>
12
+ body { font-family: sans-serif; }
13
+ #container { display: flex; flex-direction: column; align-items: center; }
14
+ #score, #score2, #score3 {
15
+ width: 750px; margin: 20px auto; background-color: white;
16
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); padding: 25px;
17
+ page-break-after: always; overflow: visible;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  }
19
+ #player-controls { text-align: center; margin-bottom: 20px; }
20
+ button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
21
  @media print {
22
+ body { margin: 0; padding: 0; width: 100%; }
23
+ #score, #score2, #score3 { box-shadow: none; width: 100%; margin: 0; padding: 0; page-break-inside: avoid; }
24
+ #player-controls, button { display: none; }
25
+ .vf-stavenote { transform: scale(1) !important; }
26
+ .highlight { fill: black !important; stroke: black !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
+ .highlight { fill: blue !important; stroke: blue !important; }
29
  </style>
30
  </head>
31
  <body>
32
+ <div id="container">
33
+ <h1>AI FUGUE by Sami Halawa</h1>
34
+ <div id="player-controls">
35
+ <button id="playButton">Play</button>
36
+ <button id="pauseButton">Pause</button>
37
+ <button id="stopButton">Stop</button>
38
+ </div>
39
+ <div id="score"></div>
40
+ <div id="score2"></div>
41
+ <div id="score3"></div>
42
  </div>
43
 
44
+ <script>
45
+ const {
46
+ Accidental, Beam, Dot, EasyScore, Factory, Formatter,
47
+ Renderer, Stave, StaveNote, Voice
48
+ } = Vex.Flow;
49
+
50
+ // --- WebAudioFont Setup ---
51
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
52
+ const player = new WebAudioFontPlayer();
53
+ player.loader.decodeAfterLoading(audioContext, '_tone_0000_JCLive_sf2_file');
54
+
55
+ // --- Helper functions ---
56
+ const noteNameToMidi = (noteName) => {
57
+ const noteMap = {
58
+ 'c': 0, 'c#': 1, 'db': 1, 'd': 2, 'd#': 3, 'eb': 3, 'e': 4, 'f': 5,
59
+ 'f#': 6, 'gb': 6, 'g': 7, 'g#': 8, 'ab': 8, 'a': 9, 'a#': 10, 'bb': 10, 'b': 11
60
+ };
61
+ const [note, octave] = noteName.split('/');
62
+ const noteValue = noteMap[note.toLowerCase()];
63
+ const octaveValue = parseInt(octave, 10);
64
+ return (isNaN(noteValue) || isNaN(octaveValue)) ? NaN : (octaveValue + 1) * 12 + noteValue;
65
+ };
66
+ const deltaEncode = (value) => {
67
+ let bytes = []; let buffer = value & 0x7F;
68
+ while ((value >>= 7)) { buffer <<= 8; buffer |= ((value & 0x7F) | 0x80); }
69
+ while (true) { bytes.push(buffer & 0xFF); if (buffer & 0x80) { buffer >>= 8; } else { break; } }
70
+ return bytes;
71
+ };
72
+ const intToBytes = (value, numBytes) => {
73
+ const bytes = []; for (let i = 0; i < numBytes; i++) { bytes.unshift((value >> (i * 8)) & 0xFF); }
74
+ return bytes;
75
+ };
76
+ const ticksToSeconds = (ticks, timeDivision) => (60 / 120) * (ticks / timeDivision); // Assumes 120 BPM
77
+ const createRest = (duration) => new StaveNote({ keys: ["b/4"], duration: duration + "r" });
78
+
79
+ // --- Fugue Data ---
80
+ const subject = [
81
+ "a/4/16", "c/5/16", "b/4/16", "a/4/16", "g/4/8", "f/4/16", "e/4/16",
82
+ "f/4/8", "g/4/16", "a/4/16", "b/4/8", "c/5/8", "a/4/8"
83
+ ];
84
+ const countersubject = [
85
+ "e/4/8", "f/4/16", "g/4/16", "a/4/8", "b/4/16","c/5/16",
86
+ "d/5/8", "c/5/16", "b/4/16", "a/4/8","g/4/8", "f/4/8","e/4/8",
87
+ ];
88
+
89
+ // --- All Notes (Combined, continuous) ---
90
+ const allNotes = [
91
+ ...subject, ...Array(12).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/8r"), // Voice 1 - Page 1
92
+ ...subject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/8r"), "a/5/4.", ...Array(16).fill("b/4/8r"), //Voice 1 continues
93
+ ...Array(16).fill("b/4/8r"), ...subject.map(n => n.replace('a', 'e').replace('4', '4')), ...Array(8).fill("b/4/8r"), // Voice 2 - Page 1
94
+ ...countersubject, ...Array(16).fill("b/4/8r"), "e/4/4.", ...Array(16).fill("b/4/8r"), //Voice 2 continues
95
+ ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 3 - Page 1
96
+ ...subject.map(n => n.replace('4', '4').replace('5', '4')), ...Array(16).fill("b/4/8r"), "c/4/4", ...Array(2).fill("b/4/8r"), ...Array(16).fill("b/4/8r"),//Voice 3
97
+ ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 4 - Page 1
98
+ ...subject.map(n => n.replace('4', '3').replace('5', '4').replace('a/', 'e/')), ...Array(16).fill("b/4/8r"), "a/2/4.", ...Array(10).fill("b/4/8r"), //Voice 4
99
+ ...countersubject.map(n => n.replace('4', '5')), ...Array(8).fill("b/4/8r"),
100
+ ...Array(16).fill("b/4/8r"), ...subject.map(n => n.replace('4', '5')), ...Array(8).fill("b/4/4r"), "b/4/4", ...Array(8).fill("b/4/8r"),//V1 P2
101
+ ...countersubject, ...Array(12).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
102
+ ...Array(8).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...countersubject.map(n=>n.replace('a','f#').replace('4','4')), ...Array(8).fill("b/4/8r"), //V2 P2
103
+ ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '3').replace('5', '4')),
104
+ ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '3').replace('5','4')), ...Array(16).fill("b/4/8r"), "e/3/4.",//V3 P2
105
+ ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"),
106
+ ...subject.map(n => n.replace('4', '2').replace('5','3').replace('a/','e/')), ...Array(16).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '2').replace('5','3')),//V4 P2
107
+ ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '5')), ...Array(8).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '5')),
108
+ ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/4r"), "a/5/2.", //V1 P3
109
+ ...Array(16).fill("b/4/8r"), ...countersubject, ...Array(16).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
110
+ ...Array(8).fill("b/4/8r"), "e/4/2.",//V2 P3
111
+ ...countersubject.map(n => n.replace('4', '3').replace('5','4')), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '3').replace('5', '4')),
112
+ ...Array(16).fill("b/4/8r"), "c/4/2.", //V3 P3
113
+ ...Array(8).fill("b/4/8r"),
114
+ ...subject.map(n => n.replace('4', '2').replace('5','3').replace('a/','e/')), ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '2').replace('5','3')), "a/2/2." // V4 P3
115
+ ];
116
+
117
+ // --- MIDI Data Generation (Continuous) ---
118
+ function generateMidiData() {
119
+ const headerChunk = [0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x60]; // Single track
120
+ let trackData = [];
121
+ let currentTime = 0;
122
+
123
+ for (const noteStr of allNotes) {
124
+ const [note, duration] = noteStr.split('/');
125
+ const isRest = noteStr.includes('r');
126
+ const durationTicks = {
127
+ '2.': 96 * 3, '2': 96 * 2, '4': 96, '4.': 96 * 1.5, '8': 48, '16': 24,
128
+ '4r': 96, '8r': 48, '16r': 24
129
+ }[duration] || 0;
130
+
131
+ if (!isRest) {
132
+ const midiNote = noteNameToMidi(note);
133
+ trackData.push(...deltaEncode(currentTime), 0x90, midiNote, 0x64); // Note On
134
+ trackData.push(...deltaEncode(durationTicks), 0x80, midiNote, 0x00); // Note Off
135
+ currentTime = 0;
136
+ } else {
137
+ currentTime += durationTicks;
138
+ }
139
+ }
140
+ trackData.push(0x00, 0xFF, 0x2F, 0x00); // End of track
141
+ const trackHeader = [0x4d, 0x54, 0x72, 0x6b, ...intToBytes(trackData.length, 4)];
142
+ return new Uint8Array([...headerChunk, ...trackHeader, ...trackData]);
143
+ }
144
+
145
+ const midiData = generateMidiData();
146
+ const midiBase64 = btoa(String.fromCharCode.apply(null, midiData));
147
+ const midiDataUri = "data:audio/midi;base64," + midiBase64;
148
 
149
+ // --- Rendering (Continuous Staves) ---
150
+ function renderPage(startIndex, numMeasures, pageDivId) {
151
+ const factory = new Factory({ renderer: { elementId: pageDivId, width: 750, height: 450 } });
152
+ const score = factory.EasyScore();
153
 
154
+ // Create staves
155
+ const staves = Array(4).fill(null).map((_, i) => {
156
+ const stave = new Stave(10, i * 110, 700);
157
+ if (i < 2) stave.addClef('treble'); else stave.addClef('bass');
158
+ stave.addTimeSignature('4/4').addKeySignature('Am');
159
+ return stave;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  });
 
161
 
162
+ // Create voices
163
+ const voices = staves.map(() => []);
164
+ let currentTime = [0, 0, 0, 0]; // Keep track of time for each voice
165
+
166
+ // Process notes and distribute to voices
167
+ for (let i = startIndex; i < allNotes.length && currentTime[0] < numMeasures * 96 *4 ; i++) {
168
+ const noteStr = allNotes[i];
169
+ const [note, duration] = noteStr.split("/");
170
+ const durationTicks = {
171
+ '2.': 96 * 3, '2': 96 * 2, '4': 96, '4.': 96 * 1.5, '8': 48, '16': 24,
172
+ '4r': 96, '8r': 48, '16r': 24
173
+ }[duration] || 0;
174
+
175
+ // Determine voice based on current time (simplified logic)
176
+ let voiceIndex = 0;
177
+ if(currentTime[0] <= currentTime[1] && currentTime[0] <= currentTime[2] && currentTime[0] <= currentTime[3] ){
178
+ voiceIndex = 0;
179
+ } else if (currentTime[1] <= currentTime[2] && currentTime[1] <= currentTime[3]){
180
+ voiceIndex = 1;
181
+ }
182
+ else if(currentTime[2] <= currentTime[3]){
183
+ voiceIndex = 2;
184
+ } else {
185
+ voiceIndex = 3;
186
+ }
187
 
188
+ voices[voiceIndex].push(noteStr.includes('r') ? createRest(duration) : score.notes(noteStr)[0]);
189
+ currentTime[voiceIndex] += durationTicks;
 
190
  }
191
 
192
+ // Beam notes
193
+ const beamedVoices = voices.map(voiceNotes => {
194
+ let currentBeamGroup = [];
195
+ let beamedVoiceNotes = [];
196
+ voiceNotes.forEach(note => {
197
+ if (['8', '16'].includes(note.duration)) {
198
+ currentBeamGroup.push(note);
199
+ } else {
200
+ if (currentBeamGroup.length > 0) {
201
+ beamedVoiceNotes.push(new Beam(currentBeamGroup));
202
+ currentBeamGroup = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
+ beamedVoiceNotes.push(note);
205
  }
206
+ });
207
+ if (currentBeamGroup.length > 0) {
208
+ beamedVoiceNotes.push(new Beam(currentBeamGroup));
209
+ }
210
+ return beamedVoiceNotes
211
+ });
212
 
213
+ // Create VexFlow Voices and format
214
+ const vfVoices = beamedVoices.map((notes, i) => new Voice({ num_beats: numMeasures*4, beat_value: 4 }).addTickables(notes));
215
+ const formatter = new Formatter().joinVoices(vfVoices).format(vfVoices, 650);
216
 
217
+ // Draw
218
+ factory.getContext().scale(1, 1).setFont("Arial", 10);
219
+ vfVoices.forEach((voice, i) => voice.draw(factory.getContext(), staves[i]));
220
+ staves.forEach(stave => stave.setContext(factory.getContext()).draw());
221
+ return voices; // Return created voices for highlighting
222
+ }
223
 
224
+ const voicesPage1 = renderPage(0, 16, 'score'); // First 16 measures
225
+ const voicesPage2 = renderPage(240, 16, 'score2'); // Next 16
226
+ const voicesPage3 = renderPage(480, 16, 'score3'); // Last ones
227
 
228
 
 
229
 
230
+ // --- Note Highlighting and Playback ---
231
+ let scheduleAheadTime = 0.1;
232
+ let currentMidiTime = 0;
233
+ let midiPlaybackInterval;
234
+ let startTime = 0;
235
+ let paused = false;
236
+ let highlightedNotes = [];
237
 
238
+ function clearHighlightedNotes() {
239
+ highlightedNotes.forEach(note => note.setStyle({ fill: 'black', stroke: 'black' }));
240
+ highlightedNotes = [];
241
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ function highlightNote(note) {
244
+ if (note && note.attrs) {
245
+ note.setStyle({ fill: 'blue', stroke: 'blue' });
246
+ highlightedNotes.push(note);
247
+ }
248
+ }
 
249
 
 
 
 
 
250
 
251
+ function getVexflowNote(midiNote, currentMidiTime) {
252
+ let accumulatedTime = 0;
253
+ const timeDivision = 96;
254
+ let currentTime = [0,0,0,0];
255
 
256
+ for (let i = 0; i < allNotes.length; i++) {
257
+ const noteStr = allNotes[i];
258
+ const [note, duration] = noteStr.split('/');
259
+ const isRest = noteStr.includes('r');
260
+ const durationTicks = {
261
+ '2.': 96 * 3, '2': 96 * 2, '4': 96, '4.': 96 * 1.5, '8': 48, '16': 24,
262
+ '4r': 96, '8r': 48, '16r': 24
263
+ }[duration] || 0;
264
 
265
+ if (!isRest) {
266
+ const currentNoteMidi = noteNameToMidi(note);
267
+ if (currentNoteMidi === midiNote && Math.floor(accumulatedTime) === Math.floor(currentMidiTime * timeDivision)) {
 
268
 
269
+ // Determine the voice and page.
270
+ let voicesToCheck = null;
 
271
 
272
+ if(i < 240){ // First page
273
+ voicesToCheck = voicesPage1;
274
+ }
275
+ else if (i < 480 ){ //Second page
276
+ voicesToCheck = voicesPage2;
277
+ } else { //Last Page
278
+ voicesToCheck = voicesPage3
279
+ }
280
+ let voiceIndex = 0;
281
+ if(currentTime[0] <= currentTime[1] && currentTime[0] <= currentTime[2] && currentTime[0] <= currentTime[3] ){
282
+ voiceIndex = 0;
283
+ } else if (currentTime[1] <= currentTime[2] && currentTime[1] <= currentTime[3]){
284
+ voiceIndex = 1;
285
+ }
286
+ else if(currentTime[2] <= currentTime[3]){
287
+ voiceIndex = 2;
288
+ } else {
289
+ voiceIndex = 3;
290
+ }
291
 
292
+ // Find the VexFlow note within the correct voice
293
+ if (voicesToCheck && voicesToCheck[voiceIndex]) {
294
+ let noteIdx = 0;
295
+ for(let j = 0; j <= i; j++){ //Iterate notes until current note
296
+ if(!allNotes[j].includes("r")) noteIdx++; //Increase counter if it is nota rest
297
+ }
298
+ noteIdx--; //Compensate the index to start at 0
299
+ const vexNote = voicesToCheck[voiceIndex][noteIdx]
300
+
301
+ if(vexNote){
302
+ return vexNote
303
+ }
304
+ }
305
+ return null; // Note not found in expected voice
306
+ }
307
+ }
308
+ accumulatedTime += durationTicks;
309
+ if(currentTime[0] <= currentTime[1] && currentTime[0] <= currentTime[2] && currentTime[0] <= currentTime[3] ){
310
+ voiceIndex = 0;
311
+ } else if (currentTime[1] <= currentTime[2] && currentTime[1] <= currentTime[3]){
312
+ voiceIndex = 1;
313
+ }
314
+ else if(currentTime[2] <= currentTime[3]){
315
+ voiceIndex = 2;
316
+ } else {
317
+ voiceIndex = 3;
318
+ }
319
+ currentTime[voiceIndex] += durationTicks
320
+ }
321
+ return null; // Note not found
322
+ }
323
 
 
 
 
 
324
 
 
325
  function playMidiFile(midiFile) {
326
+
327
  midiPlaybackInterval = setInterval(() => {
328
+ if (paused) return;
329
+
330
  const lookaheadEnd = audioContext.currentTime - startTime + scheduleAheadTime;
331
+ let anyNotesPlayed = false;
332
 
333
+ for (const track of midiFile.track) { //Should be only 1 track
 
334
  for (const event of track.event) {
335
  if (event.deltaTime > 0) {
336
+ currentMidiTime += ticksToSeconds(event.deltaTime, midiFile.timeDivision);
337
  }
338
 
339
  if (currentMidiTime <= lookaheadEnd) {
340
+
341
+ if (event.type === 9) { //Note on
342
+ anyNotesPlayed = true; //To avoid clearing notes when there are only rests at the start
343
+ player.queueWaveTable(audioContext, audioContext.destination, _tone_0000_JCLive_sf2_file, startTime + currentMidiTime, event.data[0], event.data[1] / 127, 0.5);
344
+
345
+ const vexNote = getVexflowNote(event.data[0], currentMidiTime);
346
+ if (vexNote) {
347
+ highlightNote(vexNote);
348
+ }
349
+
350
+ } else if (event.type === 8){
351
+ //For getVexFlowNote purpose, we are going to simulate it like a note on event
352
+ const vexNote = getVexflowNote(event.data[0], currentMidiTime);
353
+ if(vexNote) clearHighlightedNotes();
354
  }
355
+
356
  } else {
357
+ break; // Exit inner loop, move to the next interval
 
358
  }
359
+
360
  }
361
  }
 
 
362
 
363
 
364
+ }, scheduleAheadTime * 1000 /2);
 
 
 
 
 
 
365
  }
366
+ // --- Event Listeners ---
367
+ document.getElementById('playButton').addEventListener('click', () => {
368
+ if (!midiPlaybackInterval) { // Start if not already playing
369
+ currentMidiTime = 0;
370
+ startTime = audioContext.currentTime;
371
+ paused = false;
372
+ const buffer = new Uint8Array(midiData).buffer;
373
+ const midiFile = MidiParser.parse(buffer);
374
+ playMidiFile(midiFile);
375
+
376
+ } else if (paused) { // Resume if paused
377
+ paused = false;
378
+ startTime = audioContext.currentTime - currentMidiTime; // Adjust start time
379
+ }
380
+ });
381
+
382
+ document.getElementById('pauseButton').addEventListener('click', () => {
383
+ paused = true;
384
+ player.cancelQueue(audioContext); // Important: Cancel scheduled notes
385
+ clearHighlightedNotes()
386
+
387
+ });
388
+
389
+ document.getElementById('stopButton').addEventListener('click', () => {
390
+ clearInterval(midiPlaybackInterval);
391
+ midiPlaybackInterval = null;
392
+ currentMidiTime = 0;
393
+ paused = false;
394
+ player.cancelQueue(audioContext);
395
+ clearHighlightedNotes();
396
+
397
+ });
398
 
399
  </script>
400
  </body>