Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
spessasus committed May 21, 2023
0 parents commit bc5c651
Show file tree
Hide file tree
Showing 35 changed files with 4,411 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/SpessaSynth.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions index.html
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>
198 changes: 198 additions & 0 deletions midi.js
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);

});
});
91 changes: 91 additions & 0 deletions midi_parser/events/meta_event.js
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());
}
}
}
Loading

0 comments on commit bc5c651

Please sign in to comment.