Spaces:
Running
Running
<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> |