@logue/smfplayer
Version:
smfplayer.js is JavaScript based Standard Midi Player for WebMidiLink based synthesizer.
245 lines (231 loc) • 7.72 kB
JavaScript
import PSGConverter from './PSGConverter';
import Ini from 'ini';
import { MetaEvent, ChannelEvent, SystemExclusiveEvent } from './midi_event';
/**
* @classdesc MakiMabi Sequence File Parser
*
* @author Logue <logue@hotmail.co.jp>
* @copyright 2019 Masashi Yoshikawa <https://logue.dev/> All rights reserved.
* @license MIT
*/
export default class MakiMabiSequence {
/**
* @param {ByteArray} input
* @param {Object=} optParams
*/
constructor(input, optParams = {}) {
/** @type {string} */
const string = String.fromCharCode.apply('', new Uint16Array(input));
/** @type {Ini} MMSファイルをパースしたもの */
this.input = Ini.parse(string) || {};
/** @type {Array.<Array.<Object>>} 全トラックの演奏情報 */
this.tracks = [];
/** @type {Array.<Array.<Uint8Array>>} WMLに送る生のMIDIイベント */
this.plainTracks = [];
/** @param {number} トラック数 */
this.numberOfTracks = 1;
/** @type {number} 分解能 */
this.timeDivision = optParams.timeDivision || 96;
}
/**
* パース処理
*/
parse() {
this.parseHeader();
this.parseTracks();
this.toPlainTrack();
}
/**
* ヘッダーメタ情報をパース
*/
parseHeader() {
/** @type {TextEncoder} */
this.encoder = new TextEncoder('shift_jis');
/** @type {object} インフォメーション情報 */
const header = this.input.infomation; // informationじゃない
/** @type {string} タイトル */
this.title = header.title;
/** @type {string} 著者情報 */
this.author = header.auther; // authorじゃない。
/** @param {number} 解像度 */
this.timeDivision = header.timeBase | 0 || 96;
/** @type {array} まきまびしーくの楽器番号変換テーブル(MabiIccoのMMSFile.javaのテーブルを流用) */
this.mmsInstTable = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 65, 66, 67,
68, 69, 70, 71, 72, 73, 74, 75, 76, 18,
];
// 曲名と著者情報を付加
/** @type {array} */
const headerTrack = [];
// GM Reset
headerTrack.push(
new SystemExclusiveEvent(
'SystemExclusive',
0,
0,
[0x7e, 0x7f, 0x09, 0x01]
)
);
headerTrack.push(new MetaEvent('SequenceTrackName', 0, 0, [this.title]));
headerTrack.push(new MetaEvent('CopyrightNotice', 0, 0, [this.author]));
headerTrack.push(
new MetaEvent('TimeSignature', 0, 0, [
header.rythmNum | 0 || 4,
header.rythmBase | 0 || 4,
0,
0,
])
);
headerTrack.push(new MetaEvent('EndOfTrack', 0, 0));
this.tracks.push(headerTrack);
// infomationおよびmms-fileを取り除く
delete this.input.infomation;
delete this.input['mms-file'];
}
/**
* MML parse
*/
parseTracks() {
/** @type {array} MIDIイベント */
let track = [];
/** @type {array} 終了時間比較用 */
const endTimes = [];
/** @type {number} チャンネル */
let ch = 0;
for (const part in this.input) {
if (!Object.prototype.hasOwnProperty.call(this.input, part)) {
continue;
}
const currentPart = this.input[part];
/** @param {array} MMLの配列 */
const mmls = [
currentPart.ch0_mml,
currentPart.ch1_mml,
currentPart.ch2_mml,
];
/** @param {number} パンポット */
const panpot = Number(currentPart.panpot) + 64;
// 楽器名
track.push(new MetaEvent('InsturumentName', 0, 48, [currentPart.name]));
// プログラムチェンジ
track.push(
new ChannelEvent(
'ProgramChange',
0,
96,
ch,
this.mmsInstTable[currentPart.instrument] | 0
)
);
// パン
track.push(new ChannelEvent('ControlChange', 0, 154, ch, 10, panpot));
// MMLの各チャンネルの処理
for (const chord in mmls) {
if (!Object.prototype.hasOwnProperty.call(mmls, chord)) {
continue;
}
/** @param {PSGConverter} */
const mml2Midi = new PSGConverter({
timeDivision: this.timeDivision,
channel: ch,
timeOffset: 386,
mml: mmls[chord],
});
// トラックにマージ
track = track.concat(mml2Midi.events);
endTimes.push(mml2Midi.endTime);
}
ch++;
// トラック終了
track.concat(new MetaEvent('EndOfTrack', 0, Math.max(endTimes)));
this.tracks.push(track);
}
this.numberOfTracks = this.tracks.length;
}
/**
* WebMidiLink信号に変換
*/
toPlainTrack() {
for (let i = 0; i < this.tracks.length; i++) {
/** @type {array} トラックのイベント */
let rawTrackEvents = [];
/** @type {array} 全イベント */
let rawEvents = [];
/** @type {array} */
const events = this.tracks[i];
for (const event of events) {
/** @var {Uint8Array} WebMidiLink信号 */
let raw;
if (event instanceof ChannelEvent) {
switch (event.subtype) {
case 'NoteOn':
// console.log(event);
if (event.parameter2 === 0) {
raw = new Uint8Array([
0x80 | event.channel,
event.parameter1,
event.parameter2,
]);
} else {
raw = new Uint8Array([
0x90 | event.channel,
event.parameter1,
event.parameter2,
]);
}
break;
case 'NoteOff':
raw = new Uint8Array([
0x80 | event.channel,
event.parameter1,
event.parameter2,
]);
break;
case 'ControlChange':
raw = new Uint8Array([
0xb0 | event.channel,
event.parameter1,
event.parameter2,
]);
break;
case 'ProgramChange':
raw = new Uint8Array([0xc0 | event.channel, event.parameter1]);
break;
}
} else if (event instanceof MetaEvent) {
// Metaイベントの内容は実際使われない。単なる配列の数合わせのためのプレースホルダ(音を鳴らすことには関係ない処理だから)
/** @type {Uint8Array} */
const data = this.encoder.encode(event.data);
switch (event.subtype) {
case 'TextEvent':
raw = new Uint8Array([0xff, 0x01].concat(data));
break;
case 'SequenceTrackName':
raw = new Uint8Array([0xff, 0x03].concat(data));
break;
case 'CopyrightNotice':
raw = new Uint8Array([0xff, 0x02].concat(data));
break;
case 'InsturumentName':
raw = new Uint8Array([0xff, 0x04].concat(data));
break;
case 'SetTempo':
raw = new Uint8Array([0xff, 0x51].concat(data));
break;
case 'TimeSignature':
raw = new Uint8Array([0xff, 0x58].concat(data));
break;
case 'EndOfTrack':
raw = new Uint8Array([0xff, 0x2f]);
break;
}
} else if (event instanceof SystemExclusiveEvent) {
raw = new Uint8Array([0xf0, 0x05].concat(event.data));
}
rawEvents = rawEvents.concat(raw);
}
rawTrackEvents = rawTrackEvents.concat(rawEvents);
this.plainTracks[i] = rawTrackEvents;
}
}
}