Skip to content

Commit bc5c651

Browse files
committed
Initial Commit
0 parents  commit bc5c651

35 files changed

+4411
-0
lines changed

.idea/.gitignore

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/SpessaSynth.iml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.html

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>SpessaSynth</title>
8+
<link rel="stylesheet" href="style.css">
9+
</head>
10+
<body>
11+
<div class="top_part">
12+
<div id="title_wrapper">
13+
<div id="progress_bar"></div>
14+
<h1 id="title">SpessaSynth: MIDI Soundfont2 Player</h1>
15+
</div>
16+
17+
<label for="preset_selector">Presets:
18+
<select id="preset_selector">
19+
<option value="-1" disabled selected>No preset selected</option>
20+
</select>
21+
</label>
22+
23+
<label id="file_upload"> Upload a MIDI file
24+
<input type="file" accept="audio/parsedMidi" id="midi_file_input"><br/>
25+
</label>
26+
</div>
27+
28+
<canvas id="note_canvas"></canvas>
29+
<table id="keyboard_table">
30+
<tr id="keyboard"></tr>
31+
<tr>
32+
<td id="keyboard_text" colspan="128"></td>
33+
</tr>
34+
</table>
35+
36+
<div class="bottom_part">
37+
<input class="slider" type="range" min="0" max="1000">
38+
<h2 id="text_event"></h2>
39+
<button id="note_killer">Kill all notes</button>
40+
</div>
41+
42+
<script src="midi.js" type="module"></script> <!-- Here the magic happens ;) -->
43+
</body>
44+
</html>

midi.js

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import {MidiParser} from "./midi_parser/midi_parser.js";
2+
import {MidiManager} from "./midi_visualizer/midi_manager.js";
3+
4+
import {SoundFont2Parser} from "./soundfont2_parser/soundfont_parser.js";
5+
import {ShiftableUint8Array} from "./utils/shiftable_array.js";
6+
7+
/**
8+
* Parses the midi file (kinda)
9+
*
10+
* @param {File} midiFile
11+
*/
12+
async function parseMidi(midiFile)
13+
{
14+
let buffer = await midiFile.arrayBuffer();
15+
let p = new MidiParser();
16+
return await p.parse(Array.from(new Uint8Array(buffer)), t => titleMessage.innerText = t);
17+
}
18+
19+
/**
20+
* @param fileName {"soundfont.sf2"|"gm.sf2"|"Touhou.sf2"|"FluidR3_GM.sf2"|"alex_gm.sf2"|"zunpet.sf2"|"pc98.sf2"|"zunfont.sf2"}
21+
* @param callback {function(number)}
22+
* @returns {Promise<ShiftableUint8Array>}
23+
*/
24+
async function fetchFont(fileName, callback)
25+
{
26+
let url = `http://localhost:80/other/soundfonts/${fileName}`;
27+
let response = await fetch(url);
28+
let size = response.headers.get("content-length");
29+
let reader = await (await response.body).getReader();
30+
let done = false;
31+
let dataArray = new ShiftableUint8Array(size);
32+
let offset = 0;
33+
do{
34+
let readData = await reader.read();
35+
if(readData.value) {
36+
dataArray.set(readData.value, offset);
37+
offset += readData.value.length;
38+
}
39+
done = readData.done;
40+
let percent = Math.round((offset / size) * 100);
41+
callback(percent);
42+
}while(!done);
43+
return dataArray;
44+
}
45+
46+
/**
47+
* @param midiFile {File}
48+
*/
49+
function startMidi(midiFile)
50+
{
51+
52+
parseMidi(midiFile).then(parsedMid => {
53+
manager.play(parsedMid, true, true);
54+
document.getElementById("file_upload").innerText = midiFile.name;
55+
});
56+
}
57+
58+
/**
59+
* @param url {string}
60+
* @param callback {function(string)}
61+
* @returns {Promise<ShiftableUint8Array>}
62+
*/
63+
// async function fetchFontHeaderManipulation(url, callback) {
64+
// // 50MB
65+
// const chunkSize = 1024 * 1024 * 50;
66+
// const fileSize = (await fetch(url, {method: "HEAD"})).headers.get("content-length");
67+
// const chunksAmount = Math.ceil(fileSize / chunkSize);
68+
// /**
69+
// * @type {Promise[]}
70+
// */
71+
// let loaderWorkers = [];
72+
// let startIndex = 0;
73+
// let loadedWorkersAmount = 0;
74+
// for (let i = 0; i < chunksAmount; i++)
75+
// {
76+
// let thisChunkSize =
77+
// fileSize < startIndex + chunkSize ?
78+
// fileSize - startIndex
79+
// :
80+
// chunkSize;
81+
//
82+
// let bytesRange = [startIndex, startIndex + thisChunkSize - 1];
83+
// let loaderWorker = new Promise(resolve =>
84+
// {
85+
// let w = new Worker("soundfont2_parser/soundfont_loader_worker.js");
86+
//
87+
// w.onmessage = d => {
88+
// callback(`Downloading Soundfont... (${++loadedWorkersAmount}/${chunksAmount})`);
89+
// resolve(d.data);
90+
// }
91+
//
92+
// w.postMessage({
93+
// range: bytesRange,
94+
// url: window.location.href + url
95+
// });
96+
// });
97+
// loaderWorkers.push(loaderWorker);
98+
// startIndex += thisChunkSize
99+
// }
100+
// /**
101+
// * @type {Uint8Array[]}
102+
// */
103+
// let data = await Promise.all(loaderWorkers);
104+
// let joinedData = new ShiftableUint8Array(fileSize);
105+
// let index = 0;
106+
// let totalDatalen = 0;
107+
// for(let arr of data)
108+
// {
109+
// totalDatalen += arr.length;
110+
// }
111+
// for(let arr of data)
112+
// {
113+
// joinedData.set(arr, index);
114+
// index += arr.length;
115+
// }
116+
// return joinedData;
117+
// }
118+
119+
document.getElementById("midi_file_input").focus();
120+
121+
/**
122+
* @type {HTMLHeadingElement}
123+
*/
124+
let titleMessage = document.getElementById("title");
125+
/**
126+
* @type {HTMLDivElement}
127+
*/
128+
let progressBar = document.getElementById("progress_bar");
129+
/**
130+
* @type {HTMLInputElement}
131+
*/
132+
let fileInput = document.getElementById("midi_file_input");
133+
134+
// remove the old files
135+
fileInput.value = "";
136+
137+
document.body.onclick = () =>
138+
{
139+
// user has clicked, we can create the ui
140+
if(!window.audioContextMain) {
141+
window.audioContextMain = new AudioContext();
142+
if(window.soundFontParser) {
143+
titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player";
144+
// prepare midi interface
145+
window.manager = new MidiManager(audioContextMain, soundFontParser);
146+
}
147+
}
148+
document.body.onclick = null;
149+
}
150+
151+
titleMessage.innerText = "Downloading soundfont...";
152+
153+
// gm.sf2, soundfont.sf2, FluidR3_GM.sf2
154+
fetchFont("soundfont.sf2", percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px`)
155+
.then(data => {
156+
titleMessage.innerText = "Parsing soundfont...";
157+
setTimeout(() => {
158+
window.soundFontParser = new SoundFont2Parser(data, m => titleMessage.innerText = m);
159+
160+
titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player";
161+
progressBar.style.width = "0";
162+
163+
// prepare the preset selector
164+
let pNames = soundFontParser.presets.map(p => p.presetName);
165+
pNames.sort();
166+
for(let pName of pNames)
167+
{
168+
let option = document.createElement("option");
169+
option.value = pName;
170+
option.innerText = pName;
171+
document.getElementById("preset_selector").appendChild(option);
172+
}
173+
174+
if(!fileInput.files[0]) {
175+
fileInput.onchange = e => {
176+
if (!e.target.files[0]) {
177+
return;
178+
}
179+
startMidi(fileInput.files[0]);
180+
fileInput.onchange = null;
181+
};
182+
}
183+
else
184+
{
185+
startMidi(fileInput.files[0]);
186+
}
187+
188+
// prompt the user to click if needed
189+
if(!window.audioContextMain)
190+
{
191+
titleMessage.innerText = "Press anywhere to start the app";
192+
return;
193+
}
194+
// prepare midi interface
195+
window.manager = new MidiManager(audioContextMain, soundFontParser);
196+
197+
});
198+
});

midi_parser/events/meta_event.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @typedef {"Sequence Number"|
3+
* "Text Event"|
4+
* "Copyright"|
5+
* "Track Name"|
6+
* "Instrument Name"|
7+
* "Lyrics"|
8+
* "Marker"|
9+
* "Cue Point"|
10+
* "Device Port"|
11+
* "Channel Prefix"|
12+
* "Midi Port"|
13+
* "End Of Track"|
14+
* "Set Tempo"|
15+
* "SMPTE Offset"|
16+
* "Time Signature"|
17+
* "Key Signature"} MetaTypes
18+
*/
19+
20+
/**
21+
*
22+
* @type {Object<string, MetaTypes>}
23+
*/
24+
const types =
25+
{
26+
// type name
27+
0x00: "Sequence Number",
28+
0x01: "Text Event",
29+
0x02: "Copyright",
30+
0x03: "Track Name",
31+
0x04: "Instrument Name",
32+
0x05: "Lyrics",
33+
0x06: "Marker",
34+
0x07: "Cue Point",
35+
0x09: "Device Port",
36+
0x20: "Channel Prefix", // midi channel prefix
37+
0x21: "Midi Port",
38+
0x2F: "End Of Track", // end of track
39+
0x51: "Set Tempo",
40+
0x54: "SMPTE Offset",
41+
0x58: "Time Signature",
42+
0x59: "Key Signature"
43+
};
44+
class MetaEvent
45+
{
46+
/**
47+
* @param array {Array}
48+
* @param delta {number}
49+
*/
50+
constructor(array, delta) {
51+
this.delta = delta;
52+
53+
// skip the 0xFF
54+
array.shift();
55+
56+
let type = array.shift()
57+
58+
// look up the type
59+
if(types[type])
60+
{
61+
/**
62+
* @type {MetaTypes}
63+
*/
64+
this.type = types[type];
65+
}
66+
else
67+
{
68+
throw "Unknown Meta Event type!";
69+
}
70+
71+
// read the length and read all the bytes
72+
let metaLength = 0;
73+
while(array.length)
74+
{
75+
let byte = array.shift();
76+
// extract the first 7 bytes
77+
metaLength = (metaLength << 7) | (byte & 127);
78+
79+
// if the last byte isn't 1, stop
80+
if((byte >> 7) !== 1)
81+
{
82+
break;
83+
}
84+
}
85+
86+
this.data = [];
87+
for (let byte = 0; byte < metaLength; byte++) {
88+
this.data.push(array.shift());
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)