Last active
March 12, 2018 09:54
-
-
Save myu314/a3a374a7ad49d555adcb91b908c63661 to your computer and use it in GitHub Desktop.
SMF読み込み
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
const TinySMF = (() => { | |
const Group = { | |
ChannelVoiceMessage : 0, | |
ChannelModeMessage : 1, | |
SystemCommonMessage : 2, | |
SystemRealtimeMessage : 3, | |
MetaEventMessage : 4 | |
}; | |
const Type = { | |
// Channel Voice Message | |
NoteOff : 0, | |
NoteOn : 1, | |
PolyKeyPressure : 2, | |
ControlChange : 3, | |
ProgramChange : 4, | |
ChannelPressure : 5, | |
PitchBend : 6, | |
// System Common Message | |
SystemExclusive : 7, | |
MTCQuaterFrame : 8, | |
SongPositionPointer : 9, | |
SongSelect : 10, | |
TuneRequest : 11, | |
SystemExclusive2 : 12, | |
// System Realtime Message | |
TimingClock : 13, | |
Play : 14, | |
Continue : 15, | |
Stop : 16, | |
ActiveSensisng : 17, | |
SystemReset : 18, | |
// Meta Event Message | |
SequenceNumber : 19, | |
TextEvent : 20, | |
CopyrightNotice : 21, | |
SequenceTrackName : 22, | |
InstrumentName : 23, | |
Lyric : 24, | |
Marker : 25, | |
CuePoint : 26, | |
MIDIChannelPrefix : 27, | |
MIDIPortPrefix : 28, | |
EndOfTrack : 29, | |
SetTempo : 30, | |
SMPTEOffset : 31, | |
TimeSignature : 32, | |
KeySignature : 33, | |
SequencerSpecificMetaEvent : 34, | |
UnknownMetaEvent : 35, | |
}; | |
if (Object.freeze) { | |
Object.freeze(Group); | |
Object.freeze(Type); | |
} | |
function readSMF(source) { | |
"use strict"; | |
const reader = new Reader(source); | |
let header; | |
let tracks = []; | |
while (reader.remains() >= 8) { | |
switch (reader.char(4)) { | |
case 'MThd': | |
header = readHeader(reader); | |
break; | |
case 'MTrk': | |
tracks.push(readTrack(reader)); | |
break; | |
default: | |
reader.skip(reader.uint32()); | |
break; | |
} | |
} | |
if (header === undefined) { | |
throw "Can't find header chunk."; | |
} | |
return { header, tracks }; | |
} | |
function readHeader(reader) { | |
"use strict"; | |
const chunkSize = reader.uint32(); | |
if (chunkSize != 6) { | |
throw "Lenght of header chunk must be 6."; | |
} | |
const format = reader.uint16(); | |
const numTracks = reader.uint16(); | |
const division = reader.uint16(); | |
if (division & 0x8000) { | |
const framerate = 256 - (division >> 8); | |
const ticksPerFrame = division & 0xff; | |
return { format, numTracks, division, framerate, ticksPerFrame }; | |
} | |
return { format, numTracks, division }; | |
} | |
function readTrack(reader) { | |
"use strict"; | |
const chunkSize = reader.uint32(); | |
const last = reader.index() + chunkSize; | |
const events = []; | |
let currentTime = 0; | |
let runningStatus = 0; | |
while (reader.index() < last) { | |
currentTime += reader.varlen(); | |
const event = { time: currentTime }; | |
events.push(event); | |
let status = reader.uint8(); | |
if (status < 0x80) { | |
if (runningStatus < 0x80) { | |
throw "Can't find status byte."; | |
} | |
status = runningStatus; | |
reader.undo(); | |
} | |
// Channel Voice/Mode Message | |
if (status <= 0xef) { | |
runningStatus = status; | |
event.group = Group.ChannelVoiceMessage | |
event.channel = status & 0x0f; | |
if (status <= 0x8f) { | |
event.type = Type.NoteOff; | |
event.key = reader.uint8(); | |
event.velocity = reader.uint8(); | |
} else if (status <= 0x9f) { | |
event.key = reader.uint8(); | |
event.velocity = reader.uint8(); | |
event.type = (event.velocity > 0) ? Type.NoteOn : Type.NoteOff; | |
} else if (status <= 0xaf) { | |
event.type = Type.PolyKeyPressure; | |
event.key = reader.uint8(); | |
event.pressure = reader.uint8(); | |
} else if (status <= 0xbf) { | |
event.type = Type.ControlChange; | |
event.number = reader.uint8(); | |
event.value = reader.uint8(); | |
if (event.number >= 0x78) { | |
event.group = Group.ChannelModeMessage; | |
} | |
} else if (status <= 0xcf) { | |
event.type = Type.ProgramChange; | |
event.program = reader.uint8(); | |
} else if (status <= 0xdf) { | |
event.type = Type.ChannelPressure; | |
event.pressure = reader.uint8(); | |
} else { | |
event.type = Type.PitchBend; | |
event.value = reader.uint14() - 8192; | |
} | |
continue; | |
} | |
// System Common Message | |
if (status <= 0xf7) { | |
runningStatus = 0; | |
event.group = Group.SystemCommonMessage; | |
if (status == 0xf0) { | |
event.type = Type.SystemExclusive; | |
event.data = reader.bytes(reader.varlen()); | |
} else if (status == 0xf1) { | |
let v = reader.uint8(); | |
event.type = Type.MTCQuaterFrame; | |
event.msgtype = v >> 4; | |
event.value = v & 0x0f; | |
} else if (status == 0xf2) { | |
event.type = Type.SongPositionPointer; | |
event.position = reader.uint14(); | |
} else if (status == 0xf3) { | |
event.type = Type.SongSelect; | |
event.number = reader.uint8(); | |
} else if (status <= 0xf5) { | |
throw "Undfined status found: 0x" + status.toString(16); | |
} else if (status == 0xf6) { | |
event.type = Type.TuneRequest; | |
} else { | |
event.type = Type.SystemExclusive2; | |
event.data = reader.bytes(reader.varlen()); | |
} | |
continue; | |
} | |
// System Realtime Message | |
if (status <= 0xfe) { | |
event.group = Group.SystemRealtimeMessage; | |
switch (status) { | |
case 0xf8: | |
event.type = Type.TimingClock; | |
break; | |
case 0xfa: | |
event.type = Type.Play; | |
break; | |
case 0xfb: | |
event.type = Type.Continue; | |
break; | |
case 0xfc: | |
event.type = Type.Stop; | |
break; | |
case 0xfe: | |
event.type = Type.ActiveSensisng; | |
break; | |
case 0xff: | |
// never reached here. | |
event.type = Type.SystemReset; | |
break; | |
default: | |
throw "Undefined status found: 0x" + status.toString(16); | |
} | |
continue; | |
} | |
// Meta Event Message | |
const type = reader.uint8(); | |
const raw = new Uint8Array(reader.bytes(reader.varlen())); | |
event.group = Group.MetaEventMessage; | |
event.data = raw.buffer; | |
switch (type) { | |
case 0x00: | |
if (raw.byteLength != 2) { | |
throw "Length of 'Sequence Number' must be 2"; | |
} | |
event.type = Type.SequenceNumber; | |
event.number = raw[0]; | |
break; | |
case 0x01: | |
event.type = Type.TextEvent; | |
break; | |
case 0x02: | |
event.type = Type.CopyrightNotice; | |
break; | |
case 0x03: | |
event.type = Type.SequenceTrackName; | |
break; | |
case 0x04: | |
event.type = Type.InstrumentName; | |
break; | |
case 0x05: | |
event.type = Type.Lyric; | |
break; | |
case 0x06: | |
event.type = Type.Marker; | |
break; | |
case 0x07: | |
event.type = Type.CuePoint; | |
break; | |
case 0x20: | |
if (raw.byteLength != 1) { | |
throw "Length of 'MIDI Channel Prefix' must be 1"; | |
} | |
event.type = Type.MIDIChannelPrefix; | |
event.channel = raw[0]; | |
break; | |
case 0x21: | |
if (raw.byteLength != 1) { | |
throw "Length of 'MIDI Port Prefix' must be 1"; | |
} | |
event.type = Type.MIDIPortPrefix; | |
event.port = raw[0]; | |
break; | |
case 0x2f: | |
if (raw.byteLength != 0) { | |
throw "Length of 'End of Track' must be 0"; | |
} | |
event.type = Type.EndOfTrack; | |
break; | |
case 0x51: | |
if (raw.byteLength != 3) { | |
throw "Length of 'Set Tempo' must be 0"; | |
} | |
event.type = Type.SetTempo; | |
event.microsec = (raw[0] << 16) | (raw[1] << 8) | raw[2]; | |
event.bpm = 60000000 / event.microsec; | |
break; | |
case 0x54: | |
if (raw.byteLength != 5) { | |
throw "Length of 'SMPTE Offset' must be 0"; | |
} | |
event.type = Type.SMPTEOffset; | |
event.hours = raw[0]; | |
event.minutes = raw[1]; | |
event.seconds = raw[2]; | |
event.frames = raw[3]; | |
event.subframes = raw[4]; | |
break; | |
case 0x58: | |
if (raw.byteLength != 4) { | |
throw "Length of 'Time Signature' must be 4"; | |
} | |
event.type = Type.TimeSignature; | |
event.numerator = raw[0]; | |
event.denominator = 2 ** raw[1]; | |
event.metronome = raw[2]; | |
event.num32notes = raw[3]; | |
break; | |
case 0x59: | |
if (raw.byteLength != 2) { | |
throw "Length of 'Key Signature' must be 4"; | |
} | |
event.type = Type.KeySignature; | |
event.key = (raw[0] << 24) >> 24; | |
event.scale = raw[1]; | |
break; | |
case 0x7f: | |
event.type = Type.SequencerSpecificMetaEvent; | |
break; | |
default: | |
event.type = Type.UnknownMetaEvent; | |
break; | |
} | |
} | |
return events; | |
} | |
class Reader { | |
constructor(buffer) { | |
this._buffer = buffer; | |
this._dataview = new DataView(buffer); | |
this._index = 0; | |
this._prev_index = null; | |
} | |
index() { | |
return this._index; | |
} | |
remains() { | |
return this._buffer.byteLength - this._index; | |
} | |
uint8() { | |
this._prev_index = this._index; | |
return this._dataview.getUint8(this._index++); | |
} | |
uint16() { | |
const v = this._dataview.getUint16(this._index); | |
this._prev_index = this._index; | |
this._index += 2; | |
return v; | |
} | |
uint32() { | |
const v = this._dataview.getUint32(this._index); | |
this._prev_index = this._index; | |
this._index += 4; | |
return v; | |
} | |
uint14() { | |
const v = this._dataview.getUint16(this._index, true); | |
this._prev_index = this._index; | |
this._index += 2; | |
return ((v & 0x7f00) >> 1) | (v & 0x7f); | |
} | |
varlen() { | |
this._prev = this._index; | |
let value = 0; | |
let t; | |
do { | |
t = this._dataview.getUint8(this._index++); | |
value = (value << 7) | (t & 0x7f); | |
} while (t > 0x7f); | |
return value; | |
} | |
bytes(len) { | |
const v = this._buffer.slice(this._index, this._index + len); | |
this._prev_index = this._index; | |
this._index += len; | |
return v; | |
} | |
char(len) { | |
const b = new Uint8Array(this._buffer, this._index, len) | |
this._prev_index = this._index; | |
this._index += len; | |
return b.reduce((t, c) => t + String.fromCharCode(c), ''); | |
} | |
skip(len) { | |
this._prev_index = this._index; | |
this._index += len; | |
} | |
undo() { | |
if (this._prev_index === null) { | |
return; | |
} | |
this._index = this._prev_index; | |
this._prev = null; | |
} | |
}; | |
return { | |
Group, | |
Type, | |
read: readSMF | |
} | |
})(); | |
function eventToStr(evt) { | |
"use strict"; | |
function fmtKey(k) { | |
const kt = [ | |
'C ','C#','D ','D#','E ','F ','F#','G ','G#','A ','A#','B ' | |
]; | |
return kt[k % 12] + (' ' + ((k / 12 |0) -1)).slice(-2); | |
} | |
function fmtV3(v) { | |
return (' ' + v).slice(-3); | |
} | |
function fmtData(x) { | |
const u8 = new Uint8Array(x); | |
let s = ''; | |
for (let i = 0, e = Math.min(u8.byteLength, 8); i < e; i++) { | |
s += ' ' + ('0' + u8[i]).slice(-2); | |
} | |
if (u8.byteLength > 8) | |
s += ' ...'; | |
return s.slice(1); | |
} | |
function fmtStr(x) { | |
const u8 = new Uint8Array(x); | |
let s = u8.reduce((s, c) => s + String.fromCharCode(c), ''); | |
if (s.length > 24) | |
s = s.slice(0, 24) + ' ...'; | |
return s; | |
} | |
const Group = TinySMF.Group; | |
const Type = TinySMF.Type; | |
let tm = '' + evt.time; | |
if (tm.length < 6) tm = (' ' + tm).slice(-6); | |
let gr; | |
switch (evt.group) { | |
case Group.ChannelVoiceMessage: | |
gr = 'Chan.Voice'; break; | |
case Group.ChannelModeMessage: | |
gr = 'Chan.Mode '; break; | |
case Group.SystemCommonMessage: | |
gr = 'Sys.Common'; break; | |
case Group.SystemRealtimeMessage: | |
gr = 'Sys.RTime '; break; | |
case Group.MetaEventMessage: | |
gr = 'Meta Event'; break; | |
default: | |
gr = 'UNKNOWN '; break; | |
} | |
let ch = (evt.channel !== undefined) ? | |
(`Ch:${(' ' + evt.channel).slice(-2)}`) : ''; | |
let tp, vl = ''; | |
switch (evt.type) { | |
case Type.NoteOff: | |
tp = 'Note Off'; | |
vl = `${ch}, Key:${fmtKey(evt.key)}, Vel:${fmtV3(evt.velocity)}`; | |
break; | |
case Type.NoteOn: | |
tp = 'Note On '; | |
vl = `${ch}, Key:${fmtKey(evt.key)}, Vel:${fmtV3(evt.velocity)}`; | |
break; | |
case Type.PolyKeyPressure: | |
tp = 'PolyPres'; | |
vl = `${ch}, Key:${fmtKey(evt.key)}, Prs:${fmtV3(evt.pressure)}`; | |
break; | |
case Type.ControlChange: | |
tp = 'Control '; | |
vl = `${ch}, Num:${fmtV3(evt.number)}, Val:${fmtV3(evt.value)}`; | |
break; | |
case Type.ProgramChange: | |
tp = 'Program '; | |
vl = `${ch}, Prg:${fmtV3(evt.program)}`; | |
break; | |
case Type.ChannelPressure: | |
tp = 'ChanPres'; | |
vl = `${ch}, Prs:${fmtV3(evt.pressure)}`; | |
break; | |
case Type.PitchBend: | |
tp = 'P.Bend '; | |
vl = `${ch}, Val:${evt.value}`; | |
break; | |
case Type.SystemExclusive: | |
tp = 'SysEx '; | |
vl = `${fmtData(evt.data)}`; | |
break; | |
case Type.MTCQuaterFrame: | |
tp = 'MTCQFrm '; | |
vl = `Type:${evt.msgtype}, val:${evt.value}` | |
break | |
case Type.SongPositionPointer: | |
tp = 'SongPos '; | |
vl = `Pos:${evt.position}`; | |
break | |
case Type.SongSelect: | |
tp = 'SongSel '; | |
vl = `Num:${evt.number}`; | |
break | |
case Type.TuneRequest: | |
tp = 'TuneReq '; | |
break | |
case Type.SystemExclusive2: | |
tp = 'SysEx2 '; | |
vl = `${fmtData(evt.data)}`; | |
break; | |
case Type.TimingClock: | |
tp = 'Timing.C'; | |
break; | |
case Type.Play: | |
tp = 'Play '; | |
break; | |
case Type.Continue: | |
tp = 'Continue'; | |
break; | |
case Type.Stop: | |
tp = 'Stop'; | |
break; | |
case Type.ActiveSensisng: | |
tp = 'Active.S'; | |
break; | |
case Type.SystemReset: | |
tp = 'SysReset'; | |
break; | |
case Type.SequenceNumber: | |
tp = 'SeqNum '; | |
vl = `Num:${evt.number}`; | |
break; | |
case Type.TextEvent: | |
tp = 'TxtEvent'; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.CopyrightNotice: | |
tp = 'Copyrite'; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.SequenceTrackName: | |
tp = 'Trk.Name'; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.InstrumentName: | |
tp = 'InstName'; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.Lyric: | |
tp = 'Lyric '; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.Marker: | |
tp = 'Marker '; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.CuePoint: | |
tp = 'CuePoint'; | |
vl = `${fmtStr(evt.data)}`; | |
break; | |
case Type.MIDIChannelPrefix: | |
tp = 'Chan.Pre'; | |
vl = `Ch:${evt.channel}`; | |
break; | |
case Type.MIDIPortPrefix: | |
tp = 'Port.Pre'; | |
vl = `Port:${evt.channel}`; | |
break; | |
case Type.EndOfTrack: | |
tp = 'EndOfTrk'; | |
break; | |
case Type.SetTempo: | |
tp = 'SetTempo'; | |
vl = `BPM:${evt.bpm |0}, ${evt.microsec} usec/QNote`; | |
break; | |
case Type.SMPTEOffset: | |
tp = 'SMPTEOfs'; | |
vl = `${fmtData(evt.data)}`; | |
break; | |
case Type.TimeSignature: | |
tp = 'TimeSig.'; | |
vl = `${evt.numerator} / ${evt.denominator}, Met:${evt.metronome}, n32:${evt.num32notes}`; | |
break; | |
case Type.KeySignature: | |
tp = 'KeySig. '; | |
vl = `${(evt.key < 0) ? 'b' : '#'}${Math.abs(evt.key)} ${evt.scale ? 'minor' : 'major'}`; | |
break; | |
case Type.SequencerSpecificMetaEvent: | |
tp = 'Seq.Meta'; | |
vl = `${fmtData(evt.data)}`; | |
break; | |
case Type.UnknownMetaEvent: | |
tp = '?MetaEvt'; | |
vl = `${fmtData(evt.data)}`; | |
break; | |
default: | |
tp = ' '; | |
} | |
return `time:${tm} / ${gr} / [${tp}] ${vl}`; | |
} | |
function disp(header, tracks) { | |
let s = `*** ${filename} *** | |
format : ${header.format} | |
tracks : ${header.numTracks} | |
division : ${header.division}\n`; | |
if (header.division & 0x8000) { | |
s += `framerate : ${header.framerate} | |
ticks/frm : ${header.ticksPerFrame}\n`; | |
} | |
for (let [i, tr] of tracks.entries()) { | |
s += `\n[ channel: ${i} ]\n`; | |
for (let e of tr) s += eventToStr(e) + '\n'; | |
s += '\n'; | |
} | |
console.log(s); | |
} | |
const fs = require('fs'); | |
const filename = process.argv[2]; | |
const bufU8 = new Uint8Array(fs.readFileSync(filename)); | |
const { header, tracks } = TinySMF.read(bufU8.buffer); | |
disp(header, tracks); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment