Automusic / index.html
luigi12345's picture
Update index.html
9440d54 verified
raw
history blame
10.3 kB
<!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>