-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit bc5c651
Showing
35 changed files
with
4,411 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>SpessaSynth</title> | ||
<link rel="stylesheet" href="style.css"> | ||
</head> | ||
<body> | ||
<div class="top_part"> | ||
<div id="title_wrapper"> | ||
<div id="progress_bar"></div> | ||
<h1 id="title">SpessaSynth: MIDI Soundfont2 Player</h1> | ||
</div> | ||
|
||
<label for="preset_selector">Presets: | ||
<select id="preset_selector"> | ||
<option value="-1" disabled selected>No preset selected</option> | ||
</select> | ||
</label> | ||
|
||
<label id="file_upload"> Upload a MIDI file | ||
<input type="file" accept="audio/parsedMidi" id="midi_file_input"><br/> | ||
</label> | ||
</div> | ||
|
||
<canvas id="note_canvas"></canvas> | ||
<table id="keyboard_table"> | ||
<tr id="keyboard"></tr> | ||
<tr> | ||
<td id="keyboard_text" colspan="128"></td> | ||
</tr> | ||
</table> | ||
|
||
<div class="bottom_part"> | ||
<input class="slider" type="range" min="0" max="1000"> | ||
<h2 id="text_event"></h2> | ||
<button id="note_killer">Kill all notes</button> | ||
</div> | ||
|
||
<script src="midi.js" type="module"></script> <!-- Here the magic happens ;) --> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import {MidiParser} from "./midi_parser/midi_parser.js"; | ||
import {MidiManager} from "./midi_visualizer/midi_manager.js"; | ||
|
||
import {SoundFont2Parser} from "./soundfont2_parser/soundfont_parser.js"; | ||
import {ShiftableUint8Array} from "./utils/shiftable_array.js"; | ||
|
||
/** | ||
* Parses the midi file (kinda) | ||
* | ||
* @param {File} midiFile | ||
*/ | ||
async function parseMidi(midiFile) | ||
{ | ||
let buffer = await midiFile.arrayBuffer(); | ||
let p = new MidiParser(); | ||
return await p.parse(Array.from(new Uint8Array(buffer)), t => titleMessage.innerText = t); | ||
} | ||
|
||
/** | ||
* @param fileName {"soundfont.sf2"|"gm.sf2"|"Touhou.sf2"|"FluidR3_GM.sf2"|"alex_gm.sf2"|"zunpet.sf2"|"pc98.sf2"|"zunfont.sf2"} | ||
* @param callback {function(number)} | ||
* @returns {Promise<ShiftableUint8Array>} | ||
*/ | ||
async function fetchFont(fileName, callback) | ||
{ | ||
let url = `http://localhost:80/other/soundfonts/${fileName}`; | ||
let response = await fetch(url); | ||
let size = response.headers.get("content-length"); | ||
let reader = await (await response.body).getReader(); | ||
let done = false; | ||
let dataArray = new ShiftableUint8Array(size); | ||
let offset = 0; | ||
do{ | ||
let readData = await reader.read(); | ||
if(readData.value) { | ||
dataArray.set(readData.value, offset); | ||
offset += readData.value.length; | ||
} | ||
done = readData.done; | ||
let percent = Math.round((offset / size) * 100); | ||
callback(percent); | ||
}while(!done); | ||
return dataArray; | ||
} | ||
|
||
/** | ||
* @param midiFile {File} | ||
*/ | ||
function startMidi(midiFile) | ||
{ | ||
|
||
parseMidi(midiFile).then(parsedMid => { | ||
manager.play(parsedMid, true, true); | ||
document.getElementById("file_upload").innerText = midiFile.name; | ||
}); | ||
} | ||
|
||
/** | ||
* @param url {string} | ||
* @param callback {function(string)} | ||
* @returns {Promise<ShiftableUint8Array>} | ||
*/ | ||
// async function fetchFontHeaderManipulation(url, callback) { | ||
// // 50MB | ||
// const chunkSize = 1024 * 1024 * 50; | ||
// const fileSize = (await fetch(url, {method: "HEAD"})).headers.get("content-length"); | ||
// const chunksAmount = Math.ceil(fileSize / chunkSize); | ||
// /** | ||
// * @type {Promise[]} | ||
// */ | ||
// let loaderWorkers = []; | ||
// let startIndex = 0; | ||
// let loadedWorkersAmount = 0; | ||
// for (let i = 0; i < chunksAmount; i++) | ||
// { | ||
// let thisChunkSize = | ||
// fileSize < startIndex + chunkSize ? | ||
// fileSize - startIndex | ||
// : | ||
// chunkSize; | ||
// | ||
// let bytesRange = [startIndex, startIndex + thisChunkSize - 1]; | ||
// let loaderWorker = new Promise(resolve => | ||
// { | ||
// let w = new Worker("soundfont2_parser/soundfont_loader_worker.js"); | ||
// | ||
// w.onmessage = d => { | ||
// callback(`Downloading Soundfont... (${++loadedWorkersAmount}/${chunksAmount})`); | ||
// resolve(d.data); | ||
// } | ||
// | ||
// w.postMessage({ | ||
// range: bytesRange, | ||
// url: window.location.href + url | ||
// }); | ||
// }); | ||
// loaderWorkers.push(loaderWorker); | ||
// startIndex += thisChunkSize | ||
// } | ||
// /** | ||
// * @type {Uint8Array[]} | ||
// */ | ||
// let data = await Promise.all(loaderWorkers); | ||
// let joinedData = new ShiftableUint8Array(fileSize); | ||
// let index = 0; | ||
// let totalDatalen = 0; | ||
// for(let arr of data) | ||
// { | ||
// totalDatalen += arr.length; | ||
// } | ||
// for(let arr of data) | ||
// { | ||
// joinedData.set(arr, index); | ||
// index += arr.length; | ||
// } | ||
// return joinedData; | ||
// } | ||
|
||
document.getElementById("midi_file_input").focus(); | ||
|
||
/** | ||
* @type {HTMLHeadingElement} | ||
*/ | ||
let titleMessage = document.getElementById("title"); | ||
/** | ||
* @type {HTMLDivElement} | ||
*/ | ||
let progressBar = document.getElementById("progress_bar"); | ||
/** | ||
* @type {HTMLInputElement} | ||
*/ | ||
let fileInput = document.getElementById("midi_file_input"); | ||
|
||
// remove the old files | ||
fileInput.value = ""; | ||
|
||
document.body.onclick = () => | ||
{ | ||
// user has clicked, we can create the ui | ||
if(!window.audioContextMain) { | ||
window.audioContextMain = new AudioContext(); | ||
if(window.soundFontParser) { | ||
titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player"; | ||
// prepare midi interface | ||
window.manager = new MidiManager(audioContextMain, soundFontParser); | ||
} | ||
} | ||
document.body.onclick = null; | ||
} | ||
|
||
titleMessage.innerText = "Downloading soundfont..."; | ||
|
||
// gm.sf2, soundfont.sf2, FluidR3_GM.sf2 | ||
fetchFont("soundfont.sf2", percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px`) | ||
.then(data => { | ||
titleMessage.innerText = "Parsing soundfont..."; | ||
setTimeout(() => { | ||
window.soundFontParser = new SoundFont2Parser(data, m => titleMessage.innerText = m); | ||
|
||
titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player"; | ||
progressBar.style.width = "0"; | ||
|
||
// prepare the preset selector | ||
let pNames = soundFontParser.presets.map(p => p.presetName); | ||
pNames.sort(); | ||
for(let pName of pNames) | ||
{ | ||
let option = document.createElement("option"); | ||
option.value = pName; | ||
option.innerText = pName; | ||
document.getElementById("preset_selector").appendChild(option); | ||
} | ||
|
||
if(!fileInput.files[0]) { | ||
fileInput.onchange = e => { | ||
if (!e.target.files[0]) { | ||
return; | ||
} | ||
startMidi(fileInput.files[0]); | ||
fileInput.onchange = null; | ||
}; | ||
} | ||
else | ||
{ | ||
startMidi(fileInput.files[0]); | ||
} | ||
|
||
// prompt the user to click if needed | ||
if(!window.audioContextMain) | ||
{ | ||
titleMessage.innerText = "Press anywhere to start the app"; | ||
return; | ||
} | ||
// prepare midi interface | ||
window.manager = new MidiManager(audioContextMain, soundFontParser); | ||
|
||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/** | ||
* @typedef {"Sequence Number"| | ||
* "Text Event"| | ||
* "Copyright"| | ||
* "Track Name"| | ||
* "Instrument Name"| | ||
* "Lyrics"| | ||
* "Marker"| | ||
* "Cue Point"| | ||
* "Device Port"| | ||
* "Channel Prefix"| | ||
* "Midi Port"| | ||
* "End Of Track"| | ||
* "Set Tempo"| | ||
* "SMPTE Offset"| | ||
* "Time Signature"| | ||
* "Key Signature"} MetaTypes | ||
*/ | ||
|
||
/** | ||
* | ||
* @type {Object<string, MetaTypes>} | ||
*/ | ||
const types = | ||
{ | ||
// type name | ||
0x00: "Sequence Number", | ||
0x01: "Text Event", | ||
0x02: "Copyright", | ||
0x03: "Track Name", | ||
0x04: "Instrument Name", | ||
0x05: "Lyrics", | ||
0x06: "Marker", | ||
0x07: "Cue Point", | ||
0x09: "Device Port", | ||
0x20: "Channel Prefix", // midi channel prefix | ||
0x21: "Midi Port", | ||
0x2F: "End Of Track", // end of track | ||
0x51: "Set Tempo", | ||
0x54: "SMPTE Offset", | ||
0x58: "Time Signature", | ||
0x59: "Key Signature" | ||
}; | ||
class MetaEvent | ||
{ | ||
/** | ||
* @param array {Array} | ||
* @param delta {number} | ||
*/ | ||
constructor(array, delta) { | ||
this.delta = delta; | ||
|
||
// skip the 0xFF | ||
array.shift(); | ||
|
||
let type = array.shift() | ||
|
||
// look up the type | ||
if(types[type]) | ||
{ | ||
/** | ||
* @type {MetaTypes} | ||
*/ | ||
this.type = types[type]; | ||
} | ||
else | ||
{ | ||
throw "Unknown Meta Event type!"; | ||
} | ||
|
||
// read the length and read all the bytes | ||
let metaLength = 0; | ||
while(array.length) | ||
{ | ||
let byte = array.shift(); | ||
// extract the first 7 bytes | ||
metaLength = (metaLength << 7) | (byte & 127); | ||
|
||
// if the last byte isn't 1, stop | ||
if((byte >> 7) !== 1) | ||
{ | ||
break; | ||
} | ||
} | ||
|
||
this.data = []; | ||
for (let byte = 0; byte < metaLength; byte++) { | ||
this.data.push(array.shift()); | ||
} | ||
} | ||
} |
Oops, something went wrong.