Comparing version 1.2.1 to 2.0.0
{ | ||
"name": "yjmidi", | ||
"version": "1.2.1", | ||
"version": "2.0.0", | ||
"main": "index.js", | ||
@@ -28,6 +28,6 @@ "description": "midi/yjk file parser/player", | ||
"dependencies": { | ||
"byte-data-stream": "^1.1.1", | ||
"commander": "^9.1.0", | ||
"midifile": "^2.0.0", | ||
"yj-binaryxml": "^1.0.1-arisa0" | ||
} | ||
} |
@@ -48,5 +48,2 @@ # yjmidi | ||
player.play(); | ||
``` | ||
## Others | ||
This library is using [midifile](https://github.com/nfroidure/midifile) to parse midi files. | ||
``` |
@@ -23,3 +23,3 @@ module.exports = { | ||
SET_TEMPO:0x51, | ||
SMTPE_OFFSET:0x54, | ||
SMPTE_OFFSET:0x54, | ||
TIME_SIGNATURE:0x58, | ||
@@ -26,0 +26,0 @@ KEY_SIGNATURE:0x59, |
const MidiFileData = require('midifile'); | ||
const MidiTrack = require('./MidiTrack'); | ||
const Consts = require('./Consts'); | ||
const { ByteStream } = require('byte-data-stream'); | ||
@@ -11,26 +12,127 @@ const DURATION_TAIL_MS = 3000; | ||
constructor(data,unsafe = true){ | ||
this.d = new MidiFileData(data); | ||
let chunks = []; | ||
let d = new ByteStream(data); | ||
while(d.i < d.buffer.byteLength){ | ||
let id = d.readBytes(4).reduce((a,b) => a+String.fromCharCode(b),''); | ||
if(d.i <= 4 && id != 'MThd') throw new Error('Invalid header signature(MThd)'); | ||
let length = d.readUint32(); | ||
let data = d.readBytes(length); | ||
let chunk = { id,data }; | ||
chunks.push(chunk); | ||
} | ||
let headerChunk = []; | ||
this.unknownChunks = []; | ||
let trackChunks = chunks.filter(chunk => { | ||
if(chunk.id == 'MThd'){ | ||
headerChunk.push(chunk); | ||
return false; | ||
}else if(chunk.id == 'MTrk'){ | ||
return true; | ||
}else{ | ||
unknownChunks.push(chunk); | ||
return false; | ||
} | ||
}); | ||
if(headerChunk.length > 1){ | ||
throw new Error('Too many header chunks'); | ||
}else if(headerChunk.length < 1){ | ||
throw new Error('No header chunk'); | ||
} | ||
let headerChunkData = new ByteStream(headerChunk[0].data); | ||
if(headerChunkData.buffer.byteLength != 6){ | ||
throw new Error('Invalid header length'); | ||
} | ||
this.header = { | ||
format:this.d.header.getFormat(), | ||
format:headerChunkData.readUint16(), | ||
tracksCount:headerChunkData.readUint16(), | ||
ticksPerBeat:null, | ||
framesPerSecond:null, | ||
ticksPerFrame:null, | ||
tickResolution:this.d.header.getTickResolution(), | ||
tracksCount:this.d.header.getTracksCount(), | ||
tickResolution:null | ||
}; | ||
if(this.d.header.getTimeDivision() == MidiFileData.Header.TICKS_PER_BEAT){ | ||
this.header.ticksPerBeat = this.d.header.getTicksPerBeat(); | ||
let division = headerChunkData.readBytes(2); | ||
if(division[0] & 128){ | ||
this.header.framesPerSecond = division[0] & 127; | ||
this.header.ticksPerFrame = division[1]; | ||
// 마이크로초 단위 | ||
this.header.tickResolution = 1000000 / (this.header.framesPerSecond*this.header.ticksPerFrame); | ||
}else{ | ||
this.header.framesPerSecond = this.d.header.getSMTPEFrames(); | ||
this.header.ticksPerFrame = this.d.header.getTicksPerFrame(); | ||
this.header.ticksPerBeat = (division[0] << 8) + division[1]; | ||
} | ||
let tracks = []; | ||
trackChunks.forEach(chunk => { | ||
let dd = new ByteStream(chunk.data); | ||
let events = []; | ||
let lastMidiEventType; | ||
let lastMidiEventChannel; | ||
while(dd.i < dd.buffer.byteLength){ | ||
let delta = dd.readVarUint(); | ||
let type = dd.readUint8(); | ||
let length; | ||
let subtype; | ||
let channel; | ||
let data,params; | ||
if((type & 0xf0) == 0xf0){ | ||
if(type == Consts.events.types.META){ | ||
subtype = dd.readUint8(); | ||
length = dd.readVarUint(); | ||
data = dd.readBytes(length); | ||
}else if( | ||
type == Consts.events.types.SYSEX | ||
|| type == Consts.events.types.ESCAPE | ||
){ | ||
length = dd.readVarUint(); | ||
data = dd.readBytes(length); | ||
} | ||
}else{ // midi 이벤트 | ||
params = []; | ||
if((type & 128) == 0){ | ||
// 마지막 midi 이벤트와 같은 type과 channel을 따름 | ||
subtype = lastMidiEventType; | ||
channel = lastMidiEventChannel; | ||
params.push(type); | ||
}else{ | ||
subtype = lastMidiEventType = type >> 4; | ||
channel = lastMidiEventChannel = type & 15; | ||
params.push(dd.readUint8()); | ||
} | ||
type = Consts.events.types.MIDI; | ||
if( | ||
subtype == Consts.events.subtypes.midi.PROGRAM_CHANGE | ||
|| subtype == Consts.events.subtypes.midi.CHANNEL_AFTERTOUCH | ||
){ | ||
// 파라미터가 1개인 이벤트 | ||
}else{ | ||
params.push(dd.readUint8()); | ||
} | ||
// velocity 0인 note on은 note off로 처리 | ||
if(subtype == Consts.events.subtypes.midi.NOTE_ON && params[1] == 0){ | ||
subtype = Consts.events.subtypes.midi.NOTE_OFF; | ||
params[1] = 127; | ||
} | ||
} | ||
let event = { delta,type }; | ||
if(typeof subtype != 'undefined') event.subtype = subtype; | ||
if(typeof length != 'undefined') event.length = length; | ||
if(typeof channel != 'undefined') event.channel = channel; | ||
if(typeof data != 'undefined') event.data = [...data]; | ||
if(typeof params != 'undefined') event.params = params; | ||
events.push(event); | ||
} | ||
tracks.push(events); | ||
}); | ||
this.ports = []; | ||
let endtimes = []; | ||
let endtimesMs = []; | ||
let events = this.d.getEvents(); | ||
this.tempoEvents = new MidiTrack(); | ||
for(let i = 0;i < this.header.tracksCount;i++){ | ||
for(let i in tracks){ | ||
let playtick = 0; | ||
@@ -42,28 +144,48 @@ let playms = 0; | ||
let track = new MidiTrack(i); | ||
events.forEach((event) => { | ||
let textDecoder = new TextDecoder(); | ||
tracks[i].forEach((event) => { | ||
playtick += event.delta; | ||
event.playms = event.playTime; | ||
// 일부 미디파일에서 트랙 번호가 undefined로 되어있는 문제 수정 | ||
// 트랙번호가 없으면 0번트랙으로 처리 | ||
if(typeof event.track == 'undefined') event.track = 0; | ||
if(event.track != i) return; | ||
// meta 이벤트 처리 | ||
if(event.type == Consts.events.types.META){ | ||
// 어차피 event.data에 다 있음 | ||
delete event.param1; | ||
delete event.param2; | ||
delete event.param3; | ||
delete event.param4; | ||
// 포트번호 분류 | ||
if(event.subtype == Consts.events.subtypes.meta.PORT_PREFIX){ | ||
if(event.subtype == Consts.events.subtypes.meta.SEQUENCE_NUMBER){ | ||
// 아무것도 안함 | ||
}else if( | ||
event.subtype == Consts.events.subtypes.meta.TEXT | ||
|| event.subtype == Consts.events.subtypes.meta.COPYRIGHT_NOTICE | ||
|| event.subtype == Consts.events.subtypes.meta.TRACK_NAME | ||
|| event.subtype == Consts.events.subtypes.meta.INSTRUMENT_NAME | ||
|| event.subtype == Consts.events.subtypes.meta.LYRICS | ||
|| event.subtype == Consts.events.subtypes.meta.MARKER | ||
|| event.subtype == Consts.events.subtypes.meta.CUE_POINT | ||
){ | ||
// 텍스트 생성 | ||
event.content = textDecoder.decode(new Uint8Array(event.data)); | ||
}else if(event.subtype == Consts.events.subtypes.meta.CHANNEL_PREFIX){ | ||
event.prefix = event.data[0]; | ||
}else if(event.subtype == Consts.events.subtypes.meta.PORT_PREFIX){ | ||
port = event.port = event.data[0]; | ||
}else if(event.subtype == Consts.events.subtypes.meta.END_OF_TRACK){ | ||
// 아무것도 안함 | ||
}else if(event.subtype == Consts.events.subtypes.meta.SET_TEMPO){ | ||
event.tempo = 0; // 마이크로초 단위(1박자의 길이) | ||
event.tempo += event.data[0] << 16; | ||
event.tempo += event.data[1] << 8; | ||
event.tempo += event.data[2]; | ||
event.tempoBPM = 60000000 / event.tempo; | ||
}else if(event.subtype == Consts.events.subtypes.meta.SMPTE_OFFSET){ | ||
event.hour = event.data[0]; | ||
event.minutes = event.data[1]; | ||
event.seconds = event.data[2]; | ||
event.frames = event.data[3]; | ||
event.subframes = event.data[4]; | ||
}else if(event.subtype == Consts.events.subtypes.meta.TIME_SIGNATURE){ | ||
// 박자표. 이해를 못하겠음 | ||
}else if(event.subtype == Consts.events.subtypes.meta.KEY_SIGNATURE){ | ||
// 조표의 갯수(양수 = ♯,음수 = ♭) | ||
event.key = event.data[0] > 127 ? event.data[0] - 256 : event.data[0]; | ||
event.scale = event.data[1]; // 0 = major,1 = minor | ||
}else if(event.subtype == Consts.events.subtypes.meta.SEQUENCER_SPECIFIC){ | ||
// 아무것도 안함 | ||
} | ||
}else if(event.type == Consts.events.types.MIDI){ | ||
let p = []; | ||
p.push(event.param1 || 0); | ||
p.push(event.param2 || 0); | ||
event.params = p; | ||
delete event.param1; | ||
delete event.param2; | ||
} | ||
@@ -73,3 +195,2 @@ track.addEvent(playtick,event); | ||
lastMidiEvent = playtick; | ||
lastMidiEventMs = event.playms; | ||
} | ||
@@ -82,3 +203,2 @@ if(event.type == Consts.events.types.META && event.subtype == Consts.events.subtypes.meta.SET_TEMPO){ | ||
lastMidiEvent = playtick; | ||
lastMidiEventMs = event.playms; | ||
} | ||
@@ -89,11 +209,31 @@ }); | ||
endtimes.push(unsafe ? lastMidiEvent : playtick); | ||
endtimesMs.push(unsafe ? lastMidiEventMs : playms); | ||
//endtimesMs.push(unsafe ? lastMidiEventMs : playms); | ||
} | ||
this.header.durationTick = Math.max(...endtimes); | ||
this.header.durationMs = Math.round(Math.max(...endtimesMs)); | ||
// durationMs 계산 | ||
if(this.header.ticksPerBeat){ | ||
this.header.durationMs = 0; | ||
let tevents = this.tempoEvents.getEvents(); | ||
let currentTempo = 500000; // 마이크로초 단위 | ||
let currentTick = 0; | ||
for(let i of Object.keys(tevents)){ | ||
this.header.durationMs += currentTempo*((i - currentTick)/this.header.ticksPerBeat); | ||
// 같은 시간에 템포 이벤트가 여러 개 뜨면 마지막에 있는것만 반영됨 | ||
currentTempo = tevents[i][tevents[i].length-1].tempo; | ||
for(let j in tevents[i]){ | ||
tevents[i][j].playms = this.header.durationMs/1000; | ||
} | ||
currentTick = i; | ||
} | ||
this.header.durationMs += currentTempo*((this.header.durationTick - currentTick)/this.header.ticksPerBeat); | ||
this.header.durationMs /= 1000; // 마이크로초 단위이므로 밀리초로 바꾼다 | ||
}else{ | ||
this.header.durationMs = (this.header.durationTick*this.header.tickResolution) / 1000; | ||
} | ||
this.header.durationMs = Math.round(this.header.durationMs); | ||
// durationTick에도 정확히 3초를 추가 | ||
if(unsafe){ | ||
this.header.durationMs += 3000; | ||
this.header.durationMs += DURATION_TAIL_MS; | ||
if(this.header.ticksPerBeat){ | ||
@@ -100,0 +240,0 @@ let tevents = this.tempoEvents.getEvents(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
64942
1282
48
+ Addedbyte-data-stream@^1.1.1
+ Addedbyte-data-stream@1.3.0(transitive)
+ Addedsigned-varint@2.0.1(transitive)
+ Addedvarint@5.0.26.0.0(transitive)
- Removedmidifile@^2.0.0
- Removedmidievents@2.0.0(transitive)
- Removedmidifile@2.0.0(transitive)
- Removedutf-8@2.0.0(transitive)