File size: 10,349 Bytes
d6943da
f84329f
d6943da
9440d54
 
 
 
173b7fd
d6943da
9440d54
 
 
 
 
d6943da
9440d54
173b7fd
9440d54
d6943da
 
9440d54
173b7fd
 
 
 
 
 
9440d54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6943da
 
9440d54
 
 
173b7fd
d6943da
 
9440d54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173b7fd
 
d6943da
9440d54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6943da
 
9440d54
 
 
 
 
173b7fd
9440d54
 
d6943da
9440d54
173b7fd
9440d54
 
 
d6943da
9440d54
 
 
 
 
 
 
d6943da
 
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
<!DOCTYPE html>
<html>
<head>
    <title>AI Fugue - Audio Playback</title>
    <!-- Use Tone.js for high-quality audio and scheduling -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.min.js"></script>
    <!-- Use midi-parser-js to process MIDI data -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/src/midi-parser.js"></script>
    <style>
        body {
            font-family: sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        #player-controls { margin-bottom: 20px; }
        button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>AI Fugue - Audio Playback</h1>
    <div id="player-controls">
        <button id="playButton">Play</button>
        <button id="pauseButton">Pause</button>
        <button id="stopButton">Stop</button>
    </div>

    <script>
        // --- Helper functions ---
        const noteNameToMidi = (noteName) => {
            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 };
            const [note, octave] = noteName.split('/');
            return noteMap[note.toLowerCase()] + (parseInt(octave, 10) + 1) * 12;
        };
        const deltaEncode = (value) => {
            let bytes = []; let buffer = value & 0x7F;
            while ((value >>= 7)) { buffer <<= 8; buffer |= ((value & 0x7F) | 0x80); }
            while (true) { bytes.push(buffer & 0xFF); if (buffer & 0x80) { buffer >>= 8; } else { break; } }
            return bytes;
        };
        const intToBytes = (value, numBytes) => Array.from({ length: numBytes }, (_, i) => (value >> ((numBytes - 1 - i) * 8)) & 0xFF);
        const ticksToSeconds = (ticks) => (60 / 120) * (ticks / 96); // 120 BPM, 96 ticks/quarter

        // --- Fugue Data ---
        const subject = [
            "a/4/16", "c/5/16", "b/4/16", "a/4/16", "g/4/8", "f/4/16", "e/4/16",
            "f/4/8", "g/4/16", "a/4/16", "b/4/8", "c/5/8", "a/4/8"
        ];
        const countersubject = [
            "e/4/8", "f/4/16", "g/4/16",  "a/4/8", "b/4/16","c/5/16",
            "d/5/8", "c/5/16", "b/4/16",  "a/4/8","g/4/8", "f/4/8","e/4/8",
        ];

        const allNotes = [
            ...subject, ...Array(12).fill("b/4/8r"), ...countersubject.map(n => n.replace('4', '5')), ...Array(16).fill("b/4/8r"),  // Voice 1 - Page 1
            ...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
            ...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
            ...countersubject, ...Array(16).fill("b/4/8r"), "e/4/4.", ...Array(16).fill("b/4/8r"), //Voice 2 continues
            ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 3 - Page 1
            ...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
            ...Array(16).fill("b/4/8r"), ...Array(8).fill("b/4/8r"), // Voice 4 - Page 1
            ...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
            ...countersubject.map(n => n.replace('4', '5')), ...Array(8).fill("b/4/8r"),
            ...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
            ...countersubject, ...Array(12).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
            ...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
            ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '3').replace('5', '4')),
              ...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
            ...Array(16).fill("b/4/8r"), ...Array(16).fill("b/4/8r"),  ...Array(8).fill("b/4/8r"),
              ...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
                ...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')),
              ...Array(8).fill("b/4/8r"), ...subject.map(n => n.replace('4', '5')),  ...Array(16).fill("b/4/4r"), "a/5/2.", //V1 P3
                ...Array(16).fill("b/4/8r"),  ...countersubject, ...Array(16).fill("b/4/8r"), ...subject.map(n => n.replace('4', '4')),
              ...Array(8).fill("b/4/8r"), "e/4/2.",//V2 P3
           ...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')),
              ...Array(16).fill("b/4/8r"), "c/4/2.", //V3 P3
               ...Array(8).fill("b/4/8r"),
              ...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
        ];

       // --- MIDI Data Generation ---
        function generateMidiData() {
            const headerChunk = [0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x60]; // Single track
            let trackData = [];
            let currentTime = 0;

            for (const noteStr of allNotes) {
                const [note, duration] = noteStr.split('/');
                const isRest = noteStr.includes('r');
                const durationTicks = {
                    '2.': 96 * 3, '2': 96 * 2, '4': 96, '4.': 96 * 1.5, '8': 48, '16': 24,
                    '4r': 96, '8r': 48, '16r': 24
                }[duration] || 0;

                if (!isRest) {
                    const midiNote = noteNameToMidi(note);
                    trackData.push(...deltaEncode(currentTime), 0x90, midiNote, 0x64); // Note On
                    trackData.push(...deltaEncode(durationTicks), 0x80, midiNote, 0x00); // Note Off
                    currentTime = 0;
                } else {
                    currentTime += durationTicks;
                }
            }
            trackData.push(0x00, 0xFF, 0x2F, 0x00);
            const trackHeader = [0x4d, 0x54, 0x72, 0x6b, ...intToBytes(trackData.length, 4)];
            return new Uint8Array([...headerChunk, ...trackHeader, ...trackData]);
        }


        const midiData = generateMidiData();
        const midiBase64 = btoa(String.fromCharCode.apply(null, midiData));
        const midiDataUri = "data:audio/midi;base64," + midiBase64;

        // --- Tone.js Setup ---
        const sampler = new Tone.Sampler({
            urls: {
                "A3": "A3.mp3",
                "A4": "A4.mp3",
                "C3": "C3.mp3",
                "C4": "C4.mp3",
                "C5": "C5.mp3",
                "D#3": "Ds3.mp3",
                "D#4": "Ds4.mp3",
                "F#3": "Fs3.mp3",
                "F#4": "Fs4.mp3",
            },
            baseUrl: "https://tonejs.github.io/audio/salamander/", // Sample files
            release: 1,  // Adjust release time as needed
        }).toDestination();

        // --- Playback State ---
        let playbackStartTime = 0;
        let isPlaying = false;
        let midiEvents;  // Store parsed MIDI events

        // --- Scheduling Function ---
        function scheduleNotes() {
            if (!midiEvents) return;

            let currentTime = Tone.now() - playbackStartTime;

            for (const event of midiEvents) {
                const eventTime = ticksToSeconds(event.time);
                if (eventTime >= currentTime) {
                    if (event.type === 9) { // Note On
                        sampler.triggerAttack(Tone.Midi(event.data[0]).toNote(), eventTime, event.data[1] / 127); // Schedule attack
                    } else if (event.type === 8) { // Note Off
                       sampler.triggerRelease(Tone.Midi(event.data[0]).toNote(), eventTime); // Schedule release
                    }
                }
            }
        }

        // --- Event Listeners ---
        document.getElementById('playButton').addEventListener('click', async () => {
            if (!isPlaying) {
                await Tone.start(); // Ensure audio context is started

                // Parse MIDI data *once* and store events
                const buffer = new Uint8Array(midiData).buffer;
                const midiFile = MidiParser.parse(buffer);
                midiEvents = [];
                let accumulatedTime = 0;
                for (const track of midiFile.track) {
                    for(const event of track.event) {
                      if (event.deltaTime > 0) {
                        accumulatedTime += event.deltaTime;
                      }
                      event.time = accumulatedTime
                      midiEvents.push(event); //Add time information to be scheduled
                    }
                }
                playbackStartTime = Tone.now(); // Reset start time
                isPlaying = true;
                scheduleNotes(); // Initial scheduling
                Tone.Transport.scheduleRepeat(scheduleNotes, 0.1); // Reschedule every 100ms
                Tone.Transport.start(playbackStartTime);

            } else {
                Tone.Transport.start(); // Restart if already playing (but paused)
            }
        });

        document.getElementById('pauseButton').addEventListener('click', () => {
            Tone.Transport.pause();
        });

        document.getElementById('stopButton').addEventListener('click', () => {
            Tone.Transport.stop();
            isPlaying = false;
            playbackStartTime = 0;
            Tone.Transport.cancel();  // Clear all scheduled events
        });
    </script>
</body>
</html>