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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +165 -370
index.html CHANGED
@@ -1,401 +1,196 @@
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/vexflow@4.2.2/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>
401
  </html>
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
+ <title>AI Fugue - Audio Playback</title>
5
+ <!-- Use Tone.js for high-quality audio and scheduling -->
6
+ <script src="https://cdn.jsdelivr.net/npm/tone@14.7.77/build/Tone.min.js"></script>
7
+ <!-- Use midi-parser-js to process MIDI data -->
 
 
8
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/src/midi-parser.js"></script>
9
  <style>
10
+ body {
11
+ font-family: sans-serif;
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
 
15
  }
16
+ #player-controls { margin-bottom: 20px; }
17
  button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
18
+ </style>
 
 
 
 
 
 
 
 
19
  </head>
20
  <body>
21
+ <h1>AI Fugue - Audio Playback</h1>
 
22
  <div id="player-controls">
23
  <button id="playButton">Play</button>
24
  <button id="pauseButton">Pause</button>
25
  <button id="stopButton">Stop</button>
26
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ <script>
29
+ // --- Helper functions ---
30
+ const noteNameToMidi = (noteName) => {
31
+ const noteMap = { 'c': 0, 'c#': 1, 'db': 1, 'd': 2, 'd#': 3, 'eb': 3, 'e': 4, 'f': 5, 'f#': 6, 'gb': 6, 'g': 7, 'g#': 8, 'ab': 8, 'a': 9, 'a#': 10, 'bb': 10, 'b': 11 };
32
+ const [note, octave] = noteName.split('/');
33
+ return noteMap[note.toLowerCase()] + (parseInt(octave, 10) + 1) * 12;
34
+ };
35
+ const deltaEncode = (value) => {
36
+ let bytes = []; let buffer = value & 0x7F;
37
+ while ((value >>= 7)) { buffer <<= 8; buffer |= ((value & 0x7F) | 0x80); }
38
+ while (true) { bytes.push(buffer & 0xFF); if (buffer & 0x80) { buffer >>= 8; } else { break; } }
39
+ return bytes;
40
+ };
41
+ const intToBytes = (value, numBytes) => Array.from({ length: numBytes }, (_, i) => (value >> ((numBytes - 1 - i) * 8)) & 0xFF);
42
+ const ticksToSeconds = (ticks) => (60 / 120) * (ticks / 96); // 120 BPM, 96 ticks/quarter
43
+
44
+ // --- Fugue Data ---
45
+ const subject = [
46
+ "a/4/16", "c/5/16", "b/4/16", "a/4/16", "g/4/8", "f/4/16", "e/4/16",
47
+ "f/4/8", "g/4/16", "a/4/16", "b/4/8", "c/5/8", "a/4/8"
48
+ ];
49
+ const countersubject = [
50
+ "e/4/8", "f/4/16", "g/4/16", "a/4/8", "b/4/16","c/5/16",
51
+ "d/5/8", "c/5/16", "b/4/16", "a/4/8","g/4/8", "f/4/8","e/4/8",
52
+ ];
53
+
54
+ const allNotes = [
55
+ ...subject, ...Array(12).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/8r"), // Voice 1 - Page 1
56
+ ...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
57
+ ...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
58
+ ...countersubject, ...Array(16).fill("b/4/8r"), "e/4/4.", ...Array(16).fill("b/4/8r"), //Voice 2 continues
59
+ ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 3 - Page 1
60
+ ...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
61
+ ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 4 - Page 1
62
+ ...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
63
+ ...countersubject.map(n => n.replace('4', '5')), ...Array(8).fill("b/4/8r"),
64
+ ...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
65
+ ...countersubject, ...Array(12).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
66
+ ...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
67
+ ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '3').replace('5', '4')),
68
+ ...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
69
+ ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"),
70
+ ...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
71
+ ...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')),
72
+ ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/4r"), "a/5/2.", //V1 P3
73
+ ...Array(16).fill("b/4/8r"), ...countersubject, ...Array(16).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
74
+ ...Array(8).fill("b/4/8r"), "e/4/2.",//V2 P3
75
+ ...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')),
76
+ ...Array(16).fill("b/4/8r"), "c/4/2.", //V3 P3
77
+ ...Array(8).fill("b/4/8r"),
78
+ ...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
79
+ ];
80
+
81
+ // --- MIDI Data Generation ---
82
+ function generateMidiData() {
83
+ const headerChunk = [0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x60]; // Single track
84
+ let trackData = [];
85
+ let currentTime = 0;
86
+
87
+ for (const noteStr of allNotes) {
88
+ const [note, duration] = noteStr.split('/');
89
+ const isRest = noteStr.includes('r');
90
+ const durationTicks = {
91
+ '2.': 96 * 3, '2': 96 * 2, '4': 96, '4.': 96 * 1.5, '8': 48, '16': 24,
92
+ '4r': 96, '8r': 48, '16r': 24
93
+ }[duration] || 0;
94
+
95
+ if (!isRest) {
96
+ const midiNote = noteNameToMidi(note);
97
+ trackData.push(...deltaEncode(currentTime), 0x90, midiNote, 0x64); // Note On
98
+ trackData.push(...deltaEncode(durationTicks), 0x80, midiNote, 0x00); // Note Off
99
+ currentTime = 0;
100
+ } else {
101
+ currentTime += durationTicks;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
 
103
  }
104
+ trackData.push(0x00, 0xFF, 0x2F, 0x00);
105
+ const trackHeader = [0x4d, 0x54, 0x72, 0x6b, ...intToBytes(trackData.length, 4)];
106
+ return new Uint8Array([...headerChunk, ...trackHeader, ...trackData]);
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
 
 
 
109
 
110
+ const midiData = generateMidiData();
111
+ const midiBase64 = btoa(String.fromCharCode.apply(null, midiData));
112
+ const midiDataUri = "data:audio/midi;base64," + midiBase64;
113
+
114
+ // --- Tone.js Setup ---
115
+ const sampler = new Tone.Sampler({
116
+ urls: {
117
+ "A3": "A3.mp3",
118
+ "A4": "A4.mp3",
119
+ "C3": "C3.mp3",
120
+ "C4": "C4.mp3",
121
+ "C5": "C5.mp3",
122
+ "D#3": "Ds3.mp3",
123
+ "D#4": "Ds4.mp3",
124
+ "F#3": "Fs3.mp3",
125
+ "F#4": "Fs4.mp3",
126
+ },
127
+ baseUrl: "https://tonejs.github.io/audio/salamander/", // Sample files
128
+ release: 1, // Adjust release time as needed
129
+ }).toDestination();
130
+
131
+ // --- Playback State ---
132
+ let playbackStartTime = 0;
133
+ let isPlaying = false;
134
+ let midiEvents; // Store parsed MIDI events
135
+
136
+ // --- Scheduling Function ---
137
+ function scheduleNotes() {
138
+ if (!midiEvents) return;
139
+
140
+ let currentTime = Tone.now() - playbackStartTime;
141
+
142
+ for (const event of midiEvents) {
143
+ const eventTime = ticksToSeconds(event.time);
144
+ if (eventTime >= currentTime) {
145
+ if (event.type === 9) { // Note On
146
+ sampler.triggerAttack(Tone.Midi(event.data[0]).toNote(), eventTime, event.data[1] / 127); // Schedule attack
147
+ } else if (event.type === 8) { // Note Off
148
+ sampler.triggerRelease(Tone.Midi(event.data[0]).toNote(), eventTime); // Schedule release
149
+ }
150
+ }
151
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ // --- Event Listeners ---
155
+ document.getElementById('playButton').addEventListener('click', async () => {
156
+ if (!isPlaying) {
157
+ await Tone.start(); // Ensure audio context is started
158
+
159
+ // Parse MIDI data *once* and store events
160
+ const buffer = new Uint8Array(midiData).buffer;
161
+ const midiFile = MidiParser.parse(buffer);
162
+ midiEvents = [];
163
+ let accumulatedTime = 0;
164
+ for (const track of midiFile.track) {
165
+ for(const event of track.event) {
166
+ if (event.deltaTime > 0) {
167
+ accumulatedTime += event.deltaTime;
168
+ }
169
+ event.time = accumulatedTime
170
+ midiEvents.push(event); //Add time information to be scheduled
171
  }
 
 
 
172
  }
173
+ playbackStartTime = Tone.now(); // Reset start time
174
+ isPlaying = true;
175
+ scheduleNotes(); // Initial scheduling
176
+ Tone.Transport.scheduleRepeat(scheduleNotes, 0.1); // Reschedule every 100ms
177
+ Tone.Transport.start(playbackStartTime);
178
 
179
+ } else {
180
+ Tone.Transport.start(); // Restart if already playing (but paused)
181
  }
182
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ document.getElementById('pauseButton').addEventListener('click', () => {
185
+ Tone.Transport.pause();
186
+ });
187
 
188
+ document.getElementById('stopButton').addEventListener('click', () => {
189
+ Tone.Transport.stop();
190
+ isPlaying = false;
191
+ playbackStartTime = 0;
192
+ Tone.Transport.cancel(); // Clear all scheduled events
193
+ });
194
+ </script>
195
  </body>
196
  </html>