-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Highly experimental SMF disassembler.
- Loading branch information
1 parent
486b482
commit 6fec085
Showing
2 changed files
with
390 additions
and
0 deletions.
There are no files selected for viewing
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,375 @@ | ||
"use strict"; | ||
|
||
let u8Enc = new TextEncoder(); | ||
|
||
let readVLV = function (buffer) { | ||
// Expects Uint8Array or the likes | ||
let result = 0, ptr = 0, resume = true; | ||
while (ptr < 4 && resume) { | ||
if (ptr) { | ||
result = result << 7; | ||
}; | ||
result |= buffer[ptr] & 127; | ||
if (buffer[ptr] < 128) { | ||
resume = false; | ||
}; | ||
ptr ++; | ||
}; | ||
return [result, ptr]; | ||
}; | ||
let readUintBE = function (buffer, byteLength) { | ||
let result = 0, ptr = 0; | ||
if (byteLength > 6) { | ||
throw(new RangeError(`Cannot read more than 48 bits`)); | ||
}; | ||
if (byteLength > buffer.length) { | ||
throw(new RangeError(`Trying to read out of bounds`)); | ||
}; | ||
while (ptr < byteLength) { | ||
if (ptr < 3) { | ||
if (ptr) { | ||
result = result << 8; | ||
}; | ||
result |= buffer[ptr]; | ||
} else { | ||
result *= 256; | ||
result += buffer[ptr]; | ||
}; | ||
ptr ++; | ||
}; | ||
return result; | ||
}; | ||
|
||
ReadableStreamDefaultController.prototype.send = function (data) { | ||
this.unsent = false; | ||
this.enqueue(data); | ||
}; | ||
|
||
let streamDisassemble = function (source) { | ||
let reader = source.getReader(); | ||
let totalPtr = 0, ptr = 0, ptrStart = 0, chunk; | ||
let sectStart = 0, sectLength = 0; | ||
let state = 0, strBuf = "", | ||
nextState = 0, // transient state | ||
finalState = 0, // final state to jump to | ||
intValue = 0; | ||
let extLen = 0, startNextSect = false; | ||
let trackIndex = 0; | ||
let sink = new ReadableStream({ | ||
"pull": async (controller) => { | ||
controller.unsent = true; | ||
while (controller.unsent) { | ||
// Chunk read handling | ||
if (!chunk || ptr >= chunk.length) { | ||
console.error(`[Reader] Reading a new chunk.`); | ||
let {value, done} = await reader.read(); | ||
if (chunk) { | ||
totalPtr += chunk.length; | ||
console.error(`[Reader] Submitted the last chunk.`); | ||
}; | ||
ptr = 0; | ||
chunk = value; | ||
if (done) { | ||
console.error(`[Reader] All chunks have been read.`); | ||
controller.close(); | ||
return; | ||
}; | ||
}; | ||
let e = chunk[ptr]; | ||
console.error(`${`${e}`.padStart(3, "0")}-${e.toString(16).padStart(2, "0")}(${String.fromCharCode(Math.max(32, e))}) L: ${ptr - ptrStart}, S: ${ptrStart}, I: ${ptr}, TS: ${totalPtr + ptrStart}, TI: ${totalPtr + ptr}, TL: ${chunk.length} [${state}]`); | ||
switch (state) { | ||
case 0: { // Chunk marker search | ||
console.error("Searching for chunk markers..."); | ||
strBuf += String.fromCharCode(e); | ||
switch (strBuf) { | ||
case "MThd": { | ||
console.error("MIDI Header chunk!"); | ||
controller.send(u8Enc.encode(`:hd\n`)); | ||
state = 1; | ||
nextState = 3; | ||
finalState = 4; | ||
ptrStart = ptr + 1; | ||
strBuf = ""; | ||
break; | ||
}; | ||
case "MTrk": { | ||
console.error("MIDI Track chunk!"); | ||
trackIndex ++; | ||
controller.send(u8Enc.encode(`:tr#${trackIndex}\n`)); | ||
state = 1; | ||
nextState = 3; | ||
finalState = 5; | ||
ptrStart = ptr + 1; | ||
strBuf = ""; | ||
break; | ||
}; | ||
default: { | ||
if (strBuf.length >= 4) { | ||
strBuf = strBuf.substring(1, 4); | ||
}; | ||
} | ||
}; | ||
break; | ||
}; | ||
case 1: { | ||
// 32-bit length int read | ||
console.error(`Reading uint32...`); | ||
let i = ptr - ptrStart; | ||
if (!i) { | ||
intValue = 0; | ||
}; | ||
intValue = intValue << 8; | ||
intValue |= e; | ||
if (i >= 3) { | ||
if (intValue < 0) { | ||
intValue += 4294967296; | ||
}; | ||
state = nextState; | ||
ptr --; | ||
}; | ||
break; | ||
}; | ||
case 2: { | ||
// VLV int read | ||
console.error(`Reading VLV...`); | ||
let i = ptr - ptrStart; | ||
if (!i) { | ||
intValue = 0; | ||
}; | ||
intValue = intValue << 7; | ||
intValue |= e & 127; | ||
if (e < 128 || i >= 3) { | ||
state = nextState; | ||
if (startNextSect) { | ||
ptrStart = ptr + 1; | ||
startNextSect = false; | ||
}; | ||
ptr --; | ||
}; | ||
break; | ||
}; | ||
case 3: { | ||
// Write length | ||
console.error(`Writing length...`); | ||
controller.send(u8Enc.encode(`\tln ${intValue}\n`)); | ||
state = finalState; | ||
ptrStart = ptr + 1; | ||
break; | ||
}; | ||
case 4: { | ||
// MIDI head read | ||
console.error(`Reading MThd...`); | ||
let i = ptr - ptrStart; | ||
if (!(i & 1)) { | ||
intValue = 0; | ||
}; | ||
intValue = intValue << 8; | ||
intValue |= e; | ||
if (i & 1) { | ||
switch (i >> 1) { | ||
case 0: { | ||
controller.send(u8Enc.encode(`\tft ${intValue}\n`)); | ||
break; | ||
}; | ||
case 1: { | ||
controller.send(u8Enc.encode(`\ttc ${intValue}\n`)); | ||
break; | ||
}; | ||
case 2: { | ||
if (intValue < 32768) { | ||
controller.send(u8Enc.encode(`\tdv ${intValue}\n`)); | ||
} else { | ||
controller.send(u8Enc.encode(`\ttd ${intValue}\n`)); | ||
}; | ||
state = 0; | ||
ptrStart = ptr + 1; | ||
intValue = 0; | ||
break; | ||
}; | ||
}; | ||
}; | ||
break; | ||
}; | ||
case 5: { | ||
// Read delta time | ||
console.error(`Reading delta time...`); | ||
state = 2; | ||
nextState = 6; | ||
ptr --; | ||
break; | ||
}; | ||
case 6: { | ||
// Write delta time and jump to different event types | ||
let i = ptr - ptrStart; | ||
if (i) { | ||
console.error(`Reading event type...`); | ||
switch (e) { | ||
case 240: { | ||
state = 15; | ||
break; | ||
}; | ||
case 247: { | ||
state = 16; | ||
break; | ||
}; | ||
case 255: { | ||
state = 7; | ||
break; | ||
}; | ||
default: { | ||
state = e >> 4; | ||
ptrStart = ptr + 1; | ||
}; | ||
}; | ||
} else { | ||
console.error(`Delta time: ${intValue}`); | ||
if (intValue) { | ||
controller.send(u8Enc.encode(`\tdt ${intValue}\n`)); | ||
}; | ||
}; | ||
break; | ||
}; | ||
case 7: { | ||
// Meta event init, jump to 17 for full reads | ||
console.error(`Reading meta...`); | ||
controller.send(u8Enc.encode(`\tmt ${e}`)); | ||
state = 2; | ||
nextState = 17; | ||
startNextSect = true; | ||
break; | ||
}; | ||
case 15: { | ||
// SysEx event init, jump to 17 for full reads | ||
console.error(`Reading SysEx...`); | ||
controller.send(u8Enc.encode(`\tse`)); | ||
state = 2; | ||
nextState = 17; | ||
ptr --; | ||
startNextSect = true; | ||
break; | ||
}; | ||
case 16: { | ||
// SysEx multi-segment event init, jump to 17 for full reads | ||
console.error(`Reading SysEx multi-segment...`); | ||
controller.send(u8Enc.encode(`\tsc`)); | ||
state = 2; | ||
nextState = 17; | ||
ptr --; | ||
startNextSect = true; | ||
break; | ||
}; | ||
case 17: { | ||
// Multi-byte direct dump | ||
let i = ptr - ptrStart; | ||
if (i == 0) { | ||
extLen = intValue; | ||
console.error(`Extension length: ${extLen}`); | ||
controller.send(u8Enc.encode(` ${extLen}`)); | ||
}; | ||
if (i < 0) {} else if (i < extLen) { | ||
controller.send(u8Enc.encode(` ${e.toString(16).padStart(2, "0")}`)); | ||
} else { | ||
controller.send(u8Enc.encode(`\n`)); | ||
ptr --; | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
}; | ||
break; | ||
}; | ||
case 8: { | ||
// Note off | ||
console.error(`Note off!`); | ||
let i = ptr - ptrStart; | ||
if (i) { | ||
controller.send(u8Enc.encode(` ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
} else { | ||
controller.send(u8Enc.encode(`\tnd ${e}`)); | ||
}; | ||
break; | ||
}; | ||
case 9: { | ||
// Note on | ||
console.error(`Note on!`); | ||
let i = ptr - ptrStart; | ||
if (i) { | ||
controller.send(u8Enc.encode(` ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
} else { | ||
controller.send(u8Enc.encode(`\tna ${e}`)); | ||
}; | ||
break; | ||
}; | ||
case 10: { | ||
// Note aftertouch (PAT) | ||
console.error(`Note AT!`); | ||
let i = ptr - ptrStart; | ||
if (i) { | ||
controller.send(u8Enc.encode(` ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
} else { | ||
controller.send(u8Enc.encode(`\tpa ${e}`)); | ||
}; | ||
break; | ||
}; | ||
case 11: { | ||
// Control change | ||
console.error(`Control change!`); | ||
let i = ptr - ptrStart; | ||
if (i) { | ||
controller.send(u8Enc.encode(` ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
} else { | ||
controller.send(u8Enc.encode(`\tcc ${e}`)); | ||
}; | ||
break; | ||
}; | ||
case 12: { | ||
// Program change | ||
console.error(`Program change!`); | ||
controller.send(u8Enc.encode(`\tpc ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
break; | ||
}; | ||
case 13: { | ||
// Channel aftertouch (CAT) | ||
console.error(`Channel AT!`); | ||
controller.send(u8Enc.encode(`\tca ${e}\n`)); | ||
state = 5; | ||
ptrStart = ptr + 1; | ||
break; | ||
}; | ||
case 14: { | ||
// Pitch bend | ||
console.error(`Pitch bend!`); | ||
let i = ptr - ptrStart; | ||
if (!i) { | ||
intValue = 0; | ||
}; | ||
intValue |= e << (i << 3); | ||
if (i) { | ||
controller.send(u8Enc.encode(`\tpb ${intValue - 8192}\n`)); | ||
}; | ||
break; | ||
}; | ||
default: { | ||
console.error("Unhandled state."); | ||
controller.close(); | ||
}; | ||
}; | ||
ptr ++; | ||
}; | ||
console.error("Data pulled."); | ||
} | ||
}, new ByteLengthQueuingStrategy({"highWaterMark": 256})); | ||
return sink; | ||
}; | ||
|
||
export { | ||
streamDisassemble | ||
}; |
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,15 @@ | ||
"use strict"; | ||
|
||
import {streamDisassemble} from "../../src/micc/index.mjs"; | ||
|
||
WritableStream.prototype.from = async function (asyncIt) { | ||
let writer = this.getWriter(); | ||
for await (const e of asyncIt) { | ||
await writer.ready; | ||
await writer.write(e); | ||
}; | ||
writer.releaseLock(); // Manual closure is required | ||
}; | ||
|
||
let stream = streamDisassemble((await Deno.open(Deno.args[0])).readable); | ||
await Deno.stdout.writable.from(stream); |