├── package.json
├── LICENSE
├── .gitignore
├── ctrl2smf.js
├── ctrl2syx.js
├── nec932_decoder.js
├── .eslintrc.json
├── rcm2smf.js
├── rcm_ctrl_converter.js
├── index.html
└── rcm_converter.js
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rcm2smf",
3 | "version": "0.1.0",
4 | "description": "Recomposer file (.RCP, .R36, .G36, and .MCP) to Standard MIDI File (.mid) converter",
5 | "engines": {
6 | "node": ">=14"
7 | },
8 | "type": "module",
9 | "scripts": {
10 | "lint": "eslint {*.js,*.html}"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+ssh://git@github.com/shingo45endo/rcm2smf.git"
15 | },
16 | "author": "shingo45endo",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/shingo45endo/rcm2smf/issues"
20 | },
21 | "homepage": "https://github.com/shingo45endo/rcm2smf#readme",
22 | "dependencies": {
23 | "yargs": "^13.2.2"
24 | },
25 | "devDependencies": {
26 | "eslint": "^7.32.0",
27 | "eslint-plugin-html": "^6.2.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2024 shingo45endo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 |
--------------------------------------------------------------------------------
/ctrl2smf.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import util from 'util';
4 | import assert from 'assert';
5 |
6 | import yargs from 'yargs';
7 | import {ctrl2smf} from './rcm_converter.js';
8 |
9 | // Parses argv by yargs.
10 | const argv = yargs.
11 | strict().
12 | options({
13 | 'reset-before-ctrl': {
14 | type: 'boolean',
15 | default: true,
16 | describe: 'Send reset SysEx before sending control files',
17 | },
18 | 'optimize-ctrl': {
19 | type: 'boolean',
20 | default: true,
21 | describe: 'Optimize redundant SysEx generated from control files',
22 | },
23 | 'debug': {
24 | type: 'boolean',
25 | default: false,
26 | describe: 'Debug mode (Enable assertion)',
27 | },
28 | }).
29 | demandCommand(1, "Argument 'ctrl-file' is not specified.").
30 | help().
31 | alias('h', 'help').
32 | alias('v', 'version').
33 | usage('$0 [options] ctrl-file [syx-file]').
34 | wrap(Math.max(yargs.terminalWidth() - 2, 80)).
35 | argv;
36 |
37 | // Gets the file names from argv.
38 | const ctrlFile = argv._[0];
39 | let smfFile = argv._[1];
40 | if (!smfFile) {
41 | // If the destination file name is not specify, makes it from the source file name.
42 | const p = path.parse(ctrlFile);
43 | p.name = p.base;
44 | p.base = '';
45 | p.ext = '.mid';
46 | smfFile = path.format(p);
47 | }
48 |
49 | console.assert = (argv.debug) ? assert : () => {/* EMPTY */};
50 |
51 | const readFileAsync = util.promisify(fs.readFile);
52 | const writeFileAsync = util.promisify(fs.writeFile);
53 |
54 | // Converts an control file to a syx File.
55 | (async () => {
56 | try {
57 | const ctrlData = await readFileAsync(ctrlFile);
58 | const smfData = await ctrl2smf(new Uint8Array(ctrlData), path.basename(ctrlFile), argv);
59 | await writeFileAsync(smfFile, smfData);
60 | } catch (e) {
61 | console.error((argv.debug) ? e : `${e}`);
62 | }
63 | })();
64 |
--------------------------------------------------------------------------------
/ctrl2syx.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import util from 'util';
4 | import assert from 'assert';
5 |
6 | import yargs from 'yargs';
7 | import {convertMtdToSysEx, convertCm6ToSysEx, convertGsdToSysEx, isSysExRedundant} from './rcm_ctrl_converter.js';
8 |
9 | // Parses argv by yargs.
10 | const argv = yargs.
11 | strict().
12 | options({
13 | 'optimize-ctrl': {
14 | type: 'boolean',
15 | default: false,
16 | describe: 'Optimize redundant SysEx generated from control files',
17 | },
18 | 'debug': {
19 | type: 'boolean',
20 | default: false,
21 | describe: 'Debug mode (Enable assertion)',
22 | },
23 | }).
24 | demandCommand(1, "Argument 'ctrl-file' is not specified.").
25 | help().
26 | alias('h', 'help').
27 | alias('v', 'version').
28 | usage('$0 [options] ctrl-file [syx-file]').
29 | wrap(Math.max(yargs.terminalWidth() - 2, 80)).
30 | argv;
31 |
32 | // Gets the file names from argv.
33 | const ctrlFile = argv._[0];
34 | let syxFile = argv._[1];
35 | if (!syxFile) {
36 | // If the destination file name is not specify, makes it from the source file name.
37 | const p = path.parse(ctrlFile);
38 | p.name = p.base;
39 | p.base = '';
40 | p.ext = '.syx';
41 | syxFile = path.format(p);
42 | }
43 |
44 | console.assert = (argv.debug) ? assert : () => {/* EMPTY */};
45 |
46 | const readFileAsync = util.promisify(fs.readFile);
47 | const writeFileAsync = util.promisify(fs.writeFile);
48 |
49 | // Converts a control file to a syx File.
50 | (async () => {
51 | try {
52 | const bytes = new Uint8Array(await readFileAsync(ctrlFile));
53 | const allSysExs = convertCm6ToSysEx(bytes) || convertMtdToSysEx(bytes) || convertGsdToSysEx(bytes);
54 | const sysExs = (argv.optimizeCtrl) ? allSysExs.filter((e) => !isSysExRedundant(e)) : allSysExs;
55 |
56 | await writeFileAsync(syxFile, new Uint8Array([].concat(...sysExs)));
57 | } catch (e) {
58 | console.error((argv.debug) ? e : `${e}`);
59 | }
60 | })();
61 |
--------------------------------------------------------------------------------
/nec932_decoder.js:
--------------------------------------------------------------------------------
1 | const decoder = new TextDecoder('Shift_JIS');
2 |
3 | const tablePc98DependentChars = Object.freeze({
4 | 0x857b: '¥', // YEN SIGN
5 | 0x859e: '‾', // OVERLINE
6 |
7 | 0x85de: 'ヰ',
8 | 0x85df: 'ヱ',
9 | 0x85e0: 'ヮ',
10 | 0x85e1: 'ヵ',
11 | 0x85e2: 'ヶ',
12 | 0x85e3: 'ヴ',
13 | 0x85e4: 'ガ',
14 | 0x85e5: 'ギ',
15 | 0x85e6: 'グ',
16 | 0x85e7: 'ゲ',
17 | 0x85e8: 'ゴ',
18 | 0x85e9: 'ザ',
19 | 0x85ea: 'ジ',
20 | 0x85eb: 'ズ',
21 | 0x85ec: 'ゼ',
22 | 0x85ed: 'ゾ',
23 | 0x85ee: 'ダ',
24 | 0x85ef: 'ヂ',
25 | 0x85f0: 'ヅ',
26 | 0x85f1: 'デ',
27 | 0x85f2: 'ド',
28 | 0x85f3: 'バ',
29 | 0x85f4: 'パ',
30 | 0x85f5: 'ビ',
31 | 0x85f6: 'ピ',
32 | 0x85f7: 'ブ',
33 | 0x85f8: 'プ',
34 | 0x85f9: 'ベ',
35 | 0x85fa: 'ペ',
36 | 0x85fb: 'ボ',
37 | 0x85fc: 'ポ',
38 | });
39 |
40 | function isPc98DependentChars(cp932code) {
41 | return (0x8540 <= cp932code && cp932code <= 0x86ff);
42 | }
43 |
44 | export function decodeNec932(bytes) {
45 | // Checks whether the bytes seem to contain PC-98 dependent characters.
46 | if ([...bytes].some((e, i, a) => (i > 0) && isPc98DependentChars((a[i - 1] << 8) | e))) { // It allows false positive.
47 | // Separates from the bytes to each character code according to Shift_JIS encoding.
48 | let leadingByte = -1;
49 | const charArrays = [...bytes].reduce((p, c) => {
50 | if (leadingByte >= 0) {
51 | p.push([leadingByte, c]);
52 | leadingByte = -1;
53 | } else if ((0x81 <= c && c <= 0x9f) || (0xe0 <= c && c <= 0xfc)) {
54 | leadingByte = c;
55 | } else {
56 | p.push([c]);
57 | }
58 | return p;
59 | }, []);
60 |
61 | // Replaces PC-98 dependent characters into Unicode's ones.
62 | return charArrays.map((e) => {
63 | const cp932code = e.reduce((p, c) => (p << 8) | c);
64 | if (tablePc98DependentChars[cp932code]) {
65 | return tablePc98DependentChars[cp932code];
66 | } else if ((0x86a2 <= cp932code && cp932code <= 0x86ee)) { // PC-98 dependent full-width box drawing
67 | return String.fromCodePoint(cp932code - 0x61a2);
68 | } else if (0x8643 <= cp932code && cp932code <= 0x868f) { // PC-98 dependent multi-byte half-width box drawing
69 | return String.fromCodePoint(cp932code - 0x6143);
70 | } else if (0x859f <= cp932code && cp932code <= 0x85dd) { // PC-98 dependent multi-byte half-width katakana
71 | return String.fromCodePoint(cp932code + 0x79c2);
72 | } else if (0x8540 <= cp932code && cp932code <= 0x859e) { // PC-98 dependent multi-byte half-width JIS X 0201
73 | return String.fromCodePoint(cp932code - 0x851f);
74 | } else {
75 | return decoder.decode(new Uint8Array(e));
76 | }
77 | }).join('');
78 |
79 | } else {
80 | return decoder.decode(bytes);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2017": true,
5 | "node": true
6 | },
7 | "extends": "eslint:all",
8 | "parserOptions": {
9 | "ecmaVersion": 2018,
10 | "sourceType": "module"
11 | },
12 | "plugins": [
13 | "html"
14 | ],
15 | "rules": {
16 | "indent": ["error", "tab", {"ignoreComments": true}],
17 | "linebreak-style": ["error", "unix"],
18 | "quotes": ["error", "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
19 | "semi": ["error", "always"],
20 |
21 | "array-bracket-newline": ["error", "consistent"],
22 | "block-spacing": ["error", "never"],
23 | "comma-dangle": ["error", "always-multiline"],
24 | "key-spacing": ["error",
25 | {
26 | "beforeColon": false,
27 | "afterColon": true,
28 | "mode": "minimum"
29 | }
30 | ],
31 | "no-fallthrough": ["error", {"commentPattern": "FALLTHRU"}],
32 | "no-mixed-operators": ["error",
33 | {
34 | "groups": [
35 | ["&", "|", "^", "~", "<<", ">>", ">>>"],
36 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
37 | ["&&", "||"],
38 | ["in", "instanceof"]
39 | ],
40 | "allowSamePrecedence": true
41 | }
42 | ],
43 | "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
44 | "no-use-before-define": ["error", "nofunc"],
45 | "no-underscore-dangle": ["error", {"allowAfterThis": true}],
46 | "no-unused-vars": ["error", {"argsIgnorePattern": "^_$", "varsIgnorePattern": "^_$"}],
47 | "quote-props": ["error", "consistent-as-needed"],
48 | "space-before-function-paren": ["error", {
49 | "anonymous": "never",
50 | "named": "never",
51 | "asyncArrow": "always"
52 | }],
53 | "spaced-comment": ["error", "always", {
54 | "exceptions": ["EMPTY", "FALLTHRU", "FALLTHROUGH", "NOTREACHED"]
55 | }],
56 | "wrap-iife": ["error", "inside"],
57 | "yoda": ["error", "never", {"exceptRange": true}],
58 |
59 | "no-warning-comments": "warn",
60 |
61 | "array-element-newline": "off",
62 | "capitalized-comments": "off",
63 | "complexity": "off",
64 | "func-names": "off",
65 | "func-style": "off",
66 | "function-call-argument-newline": "off",
67 | "id-length": "off",
68 | "init-declarations": "off",
69 | "line-comment-position": "off",
70 | "lines-around-comment": "off",
71 | "lines-between-class-members": "off",
72 | "max-classes-per-file": "off",
73 | "max-depth": "off",
74 | "max-len": "off",
75 | "max-lines": "off",
76 | "max-lines-per-function": "off",
77 | "max-params": "off",
78 | "max-statements": "off",
79 | "multiline-comment-style": "off",
80 | "multiline-ternary": "off",
81 | "newline-per-chained-call": "off",
82 | "no-await-in-loop": "off",
83 | "no-bitwise": "off",
84 | "no-confusing-arrow": "off",
85 | "no-console": "off",
86 | "no-continue": "off",
87 | "no-else-return": "off",
88 | "no-extra-parens": "off",
89 | "no-inline-comments": "off",
90 | "no-labels": "off",
91 | "no-magic-numbers": "off",
92 | "no-multi-spaces": "off",
93 | "no-negated-condition": "off",
94 | "no-nested-ternary": "off",
95 | "no-param-reassign": "off",
96 | "no-plusplus": "off",
97 | "no-shadow": "off",
98 | "no-sync": "off",
99 | "no-tabs": "off",
100 | "no-ternary": "off",
101 | "no-undefined": "off",
102 | "object-curly-newline": "off",
103 | "object-property-newline": "off",
104 | "one-var": "off",
105 | "padded-blocks": "off",
106 | "prefer-destructuring": "off",
107 | "prefer-named-capture-group": "off",
108 | "sort-imports": "off",
109 | "sort-keys": "off",
110 | "sort-vars": "off",
111 | "wrap-regex": "off"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/rcm2smf.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import util from 'node:util';
4 | import assert from 'node:assert';
5 |
6 | import yargs from 'yargs';
7 |
8 | import {rcm2smf, defaultSettings} from './rcm_converter.js';
9 | import {decodeNec932} from './nec932_decoder.js';
10 |
11 | // Options for yargs.
12 | const options = {
13 | // Common
14 | 'debug': {
15 | type: 'boolean',
16 | default: false,
17 | describe: 'Debug mode (Enable assertion)',
18 | },
19 |
20 | // About SMF generation
21 | 'meta-text-memo': {
22 | describe: 'Generate SMF Text events for RCM memo area',
23 | },
24 | 'meta-text-comment': {
25 | describe: 'Generate SMF Text events for RCM Comment events',
26 | },
27 | 'meta-text-usr-exc': {
28 | describe: 'Generate SMF Text events for each RCM UsrExc events',
29 | },
30 | 'meta-cue': {
31 | describe: 'Generate SMF Cue Point events for RCM KeyScan and External Command event',
32 | },
33 | 'meta-time-signature': {
34 | describe: 'Generate SMF Time Signature events from each measure\'s step time',
35 | },
36 | 'trim-track-name': {
37 | describe: 'Remove whitespace from SMF Sequence/Track Name events',
38 | choices: ['none', 'left', 'right', 'both'],
39 | },
40 | 'trim-text-memo': {
41 | describe: 'Remove whitespace from SMF Text events for RCM memo area',
42 | choices: ['none', 'left', 'right', 'both'],
43 | },
44 | 'trim-text-comment': {
45 | describe: 'Remove whitespace from SMF Text events for RCM Comment events',
46 | choices: ['none', 'left', 'right', 'both'],
47 | },
48 | 'trim-text-usr-exc': {
49 | describe: 'Remove whitespace from SMF Text events for RCM UsrExc events',
50 | choices: ['none', 'left', 'right', 'both'],
51 | },
52 | 'note-off': {
53 | describe: 'Use note-off ("8nH kk uu") events instead of "9nH kk 00H"',
54 | },
55 | 'note-off-vel': {
56 | describe: 'Velocity of note-off ("8nH kk uu") events',
57 | },
58 |
59 | // About RCM parsing
60 | 'st-plus': {
61 | describe: 'Assume ST+ as signed (>= RCM 2.5) or unsigned (<= RCM 2.3a)',
62 | choices: ['auto', 'signed', 'unsigned'],
63 | },
64 | 'reset-before-ctrl': {
65 | describe: 'Send reset SysEx before sending control files',
66 | },
67 | 'optimize-ctrl': {
68 | describe: 'Optimize redundant SysEx generated from control files',
69 | },
70 | 'extra-sysex-wait': {
71 | describe: 'Wait for extra time after each SysEx in control files (for MT-32 Ver.1.xx)',
72 | },
73 | 'ignore-ctrl-file': {
74 | describe: 'Ignore control files',
75 | },
76 | 'ignore-out-of-range': {
77 | describe: 'Ignore out-of-range values in events',
78 | },
79 | 'ignore-wrong-event': {
80 | describe: 'Ignore unexpected events',
81 | },
82 | 'max-loop-nest': {
83 | describe: 'Maximum loop nest level',
84 | },
85 | 'infinity-loop-count': {
86 | describe: 'Loop count to revise to avoid loop-bomb',
87 | },
88 | 'loop-bomb-threshold': {
89 | describe: 'Number of extracted beats considered as loop-bomb',
90 | },
91 | 'roland-dev-id': {
92 | describe: 'Initial value of device ID for RolDev#',
93 | },
94 | 'roland-model-id': {
95 | describe: 'Initial value of model ID for RolDev#',
96 | },
97 | 'roland-base-addr-h': {
98 | describe: 'Initial value of Initial value of the base address (H) for RolBase',
99 | },
100 | 'roland-base-addr-m': {
101 | describe: 'Initial value of Initial value of the base address (M) for RolBase',
102 | },
103 | 'yamaha-dev-id': {
104 | describe: 'Initial value of device ID for YamDev#',
105 | },
106 | 'yamaha-model-id': {
107 | describe: 'Initial value of model ID for YamDev#',
108 | },
109 | 'yamaha-base-addr-h': {
110 | describe: 'Initial value of Initial value of the base address (H) for YamBase',
111 | },
112 | 'yamaha-base-addr-m': {
113 | describe: 'Initial value of Initial value of the base address (M) for YamBase',
114 | },
115 | };
116 |
117 | // Adds type and default value from the default settings defined in rcm_converter.
118 | for (const key of Object.keys(defaultSettings)) {
119 | const optName = key.replace(/([A-Z])/ug, (s) => `-${s.charAt(0).toLowerCase()}`);
120 | console.assert(optName in options);
121 | Object.assign(options[optName], {
122 | type: typeof defaultSettings[key],
123 | default: defaultSettings[key],
124 | });
125 | }
126 |
127 | // Parses argv by yargs.
128 | const argv = yargs.
129 | strict().
130 | options(options).
131 | config(defaultSettings).
132 | config('settings').
133 | check((argv) => {
134 | // Checks whether the specified numbers are valid.
135 | for (const key of Object.keys(argv).filter((e) => typeof argv[e] === 'number')) {
136 | if (Number.isNaN(argv[key]) || !Number.isInteger(argv[key])) {
137 | throw new Error(`${key} must be a positive integer number.`);
138 | }
139 | }
140 |
141 | // Checks whether the specified numbers are in range.
142 | const ranges = {
143 | noteOffVel: [0, 127],
144 | maxLoopNest: [0, 100],
145 | infinityLoopCount: [1, 255],
146 | loopBombThreshold: [100, Infinity],
147 | rolandDevId: [0, 127],
148 | rolandModelId: [0, 127],
149 | rolandBaseAddrH: [0, 127],
150 | rolandBaseAddrM: [0, 127],
151 | yamahaDevId: [0, 127],
152 | yamahaModelId: [0, 127],
153 | yamahaBaseAddrH: [0, 127],
154 | yamahaBaseAddrM: [0, 127],
155 | };
156 | for (const key of Object.keys(ranges)) {
157 | const [min, max] = ranges[key];
158 | if (argv[key] < min || max < argv[key]) {
159 | throw new Error(`${key} must be in a range of (${min} - ${max}).`);
160 | }
161 | }
162 |
163 | // Initial reset SysEx cannot be omitted when optimizing SysEx generated from control files.
164 | if (argv.optimizeCtrl && !argv.resetBeforeCtrl) {
165 | throw new Error(`In case of optimizing SysEx for control files, adding an initial reset SysEx is needed.\n(Use "--reset-before-ctrl" or "--no-optimize-ctrl")`);
166 | }
167 |
168 | return true;
169 | }).
170 | demandCommand(1, "Argument 'rcm-file' is not specified.").
171 | help().
172 | alias('h', 'help').
173 | alias('v', 'version').
174 | group([
175 | 'meta-text-memo', 'meta-text-comment', 'meta-text-usr-exc', 'meta-cue', 'meta-time-signature',
176 | 'trim-track-name', 'trim-text-memo', 'trim-text-comment', 'trim-text-usr-exc',
177 | 'note-off', 'note-off-vel',
178 | ], 'SMF Generation:').
179 | group([
180 | 'st-plus',
181 | 'reset-before-ctrl', 'optimize-ctrl', 'extra-sysex-wait', 'ignore-ctrl-file',
182 | 'ignore-out-of-range', 'ignore-wrong-event',
183 | 'max-loop-nest', 'infinity-loop-count', 'loop-bomb-threshold',
184 | 'roland-dev-id', 'roland-model-id', 'roland-base-addr-h', 'roland-base-addr-m',
185 | 'yamaha-dev-id', 'yamaha-model-id', 'yamaha-base-addr-h', 'yamaha-base-addr-m',
186 | ], 'RCM Parsing:').
187 | usage('$0 [options] rcm-file [smf-file]').
188 | wrap(Math.max(yargs.terminalWidth() - 2, 80)).
189 | argv;
190 |
191 | // Gets the file names from argv.
192 | const rcmFile = argv._[0];
193 | let smfFile = argv._[1];
194 | if (!smfFile) {
195 | // If the destination file name is not specify, makes it from the source file name.
196 | const p = path.parse(rcmFile);
197 | p.name = p.base;
198 | p.base = '';
199 | p.ext = '.mid';
200 | smfFile = path.format(p);
201 | }
202 |
203 | // Extracts properties which have camel-case name as a settings.
204 | const settings = Object.keys(argv).filter((e) => /^[a-zA-Z0-9]+$/u.test(e)).reduce((p, c) => {
205 | p[c] = argv[c];
206 | return p;
207 | }, {});
208 |
209 | console.assert = (argv.debug) ? assert : () => {/* EMPTY */};
210 |
211 | const readFileAsync = util.promisify(fs.readFile);
212 | const writeFileAsync = util.promisify(fs.writeFile);
213 |
214 | // Converts an RCM file to a Standard MIDI File.
215 | (async () => {
216 | try {
217 | const rcmData = await readFileAsync(rcmFile);
218 | const smfData = await rcm2smf(new Uint8Array(rcmData), fileReader, settings);
219 | await writeFileAsync(smfFile, smfData);
220 | } catch (e) {
221 | console.error((settings.debug) ? e : `${e}`);
222 | }
223 |
224 | function fileReader(fileName, fileNameRaw) {
225 | console.assert(fileName, 'Invalid argument');
226 |
227 | const baseDir = path.parse(rcmFile).dir;
228 | if (/^[\x20-\x7E]*$/u.test(fileName)) {
229 | return readFileAsync(path.join(baseDir, fileName));
230 |
231 | } else if (fileNameRaw) {
232 | const fileNameCP932 = decodeNec932(fileNameRaw);
233 | return readFileAsync(path.join(baseDir, fileNameCP932));
234 |
235 | } else {
236 | return Promise.reject(new Error('File not found'));
237 | }
238 | };
239 | })();
240 |
--------------------------------------------------------------------------------
/rcm_ctrl_converter.js:
--------------------------------------------------------------------------------
1 | export const [convertMtdToSysEx, convertCm6ToSysEx] = ['MTD', 'CM6'].map((kind) => {
2 | const pos = {
3 | MTD: {
4 | la: {
5 | SystemArea: 0x0080,
6 | PatchTempArea: 0x00a0,
7 | RhythmSetupTemp: 0x0130,
8 | TimbreTempArea: 0x0230,
9 | PatchMemory: 0x09e0,
10 | UserPatch: 0x0de0, // Only for MTD
11 | TimbreMemory: 0x0fe0,
12 | },
13 | totalSize: 0x4fe0,
14 | },
15 | CM6: {
16 | la: {
17 | SystemArea: 0x0080,
18 | PatchTempArea: 0x00a0,
19 | RhythmSetupTemp: 0x0130,
20 | RhythmSetupTemp2: 0x0230, // Only for CM6
21 | TimbreTempArea: 0x0284,
22 | PatchMemory: 0x0a34,
23 | TimbreMemory: 0x0e34,
24 | },
25 | pcm: {
26 | PatchTempArea: 0x4e34,
27 | PatchMemory: 0x4eb2,
28 | SystemArea: 0x5832,
29 | },
30 | totalSize: 0x5843,
31 | },
32 | }[kind];
33 |
34 | const makeSysExMTCM = (bytes, addrH, addrM, addrL) => makeSysEx(bytes, 0x16, addrH, addrM, addrL);
35 |
36 | return (buf) => {
37 | // Checks the file header.
38 | console.assert(pos.totalSize);
39 | if (!buf || !buf.length || buf.length < pos.totalSize ||
40 | !String.fromCharCode(...buf.slice(0x0000, 0x000d)).startsWith('COME ON MUSIC') ||
41 | !String.fromCharCode(...buf.slice(0x0010, 0x0012)).startsWith('R ')) {
42 | return null;
43 | }
44 | const idStr = String.fromCharCode(...buf.slice(0x0012, 0x001a));
45 | if (!idStr.startsWith('MT-32') && !idStr.startsWith('CM-64')) {
46 | return null;
47 | }
48 |
49 | const sysExs = [];
50 |
51 | // [LA SOUND PART]
52 | console.assert(pos.la);
53 |
54 | // System Area
55 | sysExs.push(makeSysExMTCM(buf.slice(pos.la.SystemArea, pos.la.SystemArea + 0x17), 0x10, 0x00, 0x00));
56 |
57 | // Timbre Memory (#1 - #64)
58 | for (let i = 0; i < 64; i++) {
59 | const index = pos.la.TimbreMemory + i * 0x100;
60 | sysExs.push(makeSysExMTCM(buf.slice(index, index + 0xf6), 0x08, i * 2, 0x00));
61 | }
62 |
63 | // Rhythm Setup Temporary Area
64 | sysExs.push(makeSysExMTCM(buf.slice(pos.la.RhythmSetupTemp, pos.la.RhythmSetupTemp + 0x4 * 64), 0x03, 0x01, 0x10)); // #24 - #87
65 | if (pos.la.RhythmSetupTemp2) {
66 | sysExs.push(makeSysExMTCM(buf.slice(pos.la.RhythmSetupTemp2, pos.la.RhythmSetupTemp2 + 0x4 * 21), 0x03, 0x03, 0x10)); // #88 - #108
67 | }
68 |
69 | // Patch Temporary Area
70 | sysExs.push(makeSysExMTCM(buf.slice(pos.la.PatchTempArea, pos.la.PatchTempArea + 0x10 * 9), 0x03, 0x00, 0x00));
71 |
72 | // Timbre Temporary Area
73 | for (let i = 0; i < 8; i++) {
74 | const addr = i * 0xf6; // 0xf6: 0x0e + 0x3a * 4
75 | const index = pos.la.TimbreTempArea + addr;
76 | sysExs.push(makeSysExMTCM(buf.slice(index, index + 0xf6), 0x04, addr >> 7, addr & 0x7f));
77 | }
78 |
79 | // Patch Memory (#1 - #128)
80 | for (let i = 0; i < 8; i++) {
81 | const index = pos.la.PatchMemory + i * 0x8 * 16;
82 | sysExs.push(makeSysExMTCM(buf.slice(index, index + 0x8 * 16), 0x05, i, 0x00));
83 | }
84 |
85 | // User Patch (Only for MTD)
86 | if (pos.la.UserPatch) {
87 | for (let i = 0; i < 4; i++) {
88 | const index = pos.la.UserPatch + i * 0x8 * 16;
89 | sysExs.push(makeSysExMTCM(buf.slice(index, index + 0x8 * 16), 0x05, i, 0x00));
90 | }
91 | }
92 |
93 | // [PCM SOUND PART]
94 | if (pos.pcm) {
95 | // Patch Temporary Area
96 | sysExs.push(makeSysExMTCM(buf.slice(pos.pcm.PatchTempArea, pos.pcm.PatchTempArea + 0x15 * 6), 0x50, 0x00, 0x00));
97 |
98 | // Patch Memory (#1 - #128)
99 | for (let i = 0; i < 16; i++) {
100 | const addr = i * 0x13 * 8;
101 | const index = pos.pcm.PatchMemory + addr;
102 | sysExs.push(makeSysExMTCM(buf.slice(index, index + 0x13 * 8), 0x51, addr >> 7, addr & 0x7f));
103 | }
104 |
105 | // System Area
106 | sysExs.push(makeSysExMTCM(buf.slice(pos.pcm.SystemArea, pos.pcm.SystemArea + 0x11), 0x52, 0x00, 0x00));
107 | }
108 |
109 | console.assert(sysExs.every((e) => e.length <= 256 + 10), 'Too long SysEx', {sysExs});
110 | return sysExs;
111 | };
112 | });
113 |
114 | export function convertGsdToSysEx(buf) {
115 | // Checks the file header.
116 | if (!buf || !buf.length || buf.length < 0x0a71 ||
117 | !String.fromCharCode(...buf.slice(0x0000, 0x000d)).startsWith('COME ON MUSIC') ||
118 | !String.fromCharCode(...buf.slice(0x000e, 0x001c)).startsWith('GS CONTROL 1.0')) {
119 | return null;
120 | }
121 |
122 | const makeSysExGS = (bytes, addrH, addrM, addrL) => makeSysEx(bytes, 0x42, addrH, addrM, addrL);
123 | const sysExs = [];
124 |
125 | // Master Tune
126 | sysExs.push(makeSysExGS(buf.slice(0x0020, 0x0024), 0x40, 0x00, 0x00));
127 |
128 | // Master Volume, Master Key Shift, and Master Panpot
129 | for (let i = 0; i < 3; i++) {
130 | sysExs.push(makeSysExGS([buf[0x0024 + i]], 0x40, 0x00, 0x04 + i));
131 | }
132 |
133 | // Reverb
134 | for (let i = 0; i < 7; i++) {
135 | sysExs.push(makeSysExGS([buf[0x0027 + i]], 0x40, 0x01, 0x30 + i));
136 | }
137 |
138 | // Chorus
139 | for (let i = 0; i < 8; i++) {
140 | sysExs.push(makeSysExGS([buf[0x002e + i]], 0x40, 0x01, 0x38 + i));
141 | }
142 |
143 | // Voice Reserve
144 | sysExs.push(makeSysExGS([0x04f9, 0x00af, 0x0129, 0x01a3, 0x021d, 0x0297, 0x0311, 0x038b, 0x0405, 0x047f, 0x0573, 0x05ed, 0x0667, 0x06e1, 0x075b, 0x07d5].map((e) => buf[e]), 0x40, 0x01, 0x10));
145 |
146 | // Patch Parameter
147 | for (let i = 0; i < 16; i++) {
148 | const index = 0x0036 + i * 0x7a;
149 | const bytes = buf.slice(index, index + 0x7a);
150 | const addr = 0x90 + 0xe0 * [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 10, 11, 12, 13, 14, 15][i];
151 | sysExs.push(...makeSysExsForPatch(bytes, 0x48, addr >> 7, addr & 0x7f));
152 | }
153 |
154 | // Drum Setup Parameter
155 | for (let i = 0; i < 2; i++) {
156 | const index = 0x07d6 + i * 0x14c;
157 | const bytes = buf.slice(index, index + 0x148);
158 | const zeroes = new Array(128).fill(0);
159 | const [level, panpot, reverb, chorus] = bytes.reduce((p, c, i) => {
160 | p[i % 4][27 + Math.trunc(i / 4)] = c;
161 | return p;
162 | }, [[...zeroes], [...zeroes], [...zeroes], [...zeroes]]);
163 |
164 | sysExs.push(makeSysExGS(nibblize(...level.slice(0, 64)), 0x49, 0x02 + i * 0x10, 0x00));
165 | sysExs.push(makeSysExGS(nibblize(...level.slice(64)), 0x49, 0x03 + i * 0x10, 0x00));
166 | sysExs.push(makeSysExGS(nibblize(...panpot.slice(0, 64)), 0x49, 0x06 + i * 0x10, 0x00));
167 | sysExs.push(makeSysExGS(nibblize(...panpot.slice(64)), 0x49, 0x07 + i * 0x10, 0x00));
168 | sysExs.push(makeSysExGS(nibblize(...reverb.slice(0, 64)), 0x49, 0x08 + i * 0x10, 0x00));
169 | sysExs.push(makeSysExGS(nibblize(...reverb.slice(64)), 0x49, 0x09 + i * 0x10, 0x00));
170 | sysExs.push(makeSysExGS(nibblize(...chorus.slice(0, 64)), 0x49, 0x0a + i * 0x10, 0x00));
171 | sysExs.push(makeSysExGS(nibblize(...chorus.slice(64)), 0x49, 0x0b + i * 0x10, 0x00));
172 | }
173 |
174 | // Master Fine Tune and Master Course Tuning
175 | // (Needed to add universal SysEx?)
176 |
177 | console.assert(sysExs.every((e) => e.length <= 256 + 10), 'Too long SysEx', {sysExs});
178 | return sysExs;
179 |
180 | function nibblize(...values) {
181 | return values.reduce((p, c) => {
182 | p.push((c >> 4) & 0x0f, c & 0x0f);
183 | return p;
184 | }, []);
185 | }
186 |
187 | function makeSysExsForPatch(bytes, addrH, addrM, addrL) {
188 | console.assert([addrH, addrM, addrL].every((e) => (0x00 <= e && e < 0x80)), 'Invalid address', {addrH, addrM, addrL});
189 |
190 | const nibbles = [];
191 |
192 | // [0-3] Tone Number (Bank MSB & Program Change)
193 | nibbles.push(...nibblize(bytes[0x00], bytes[0x01]));
194 | console.assert(nibbles.length === 4, {nibbles});
195 |
196 | // [4-7] Rx. parameters
197 | nibbles.push(...bytes.slice(0x03, 0x13).reduce((p, c, i) => {
198 | const bit = c & 0x01;
199 | if (i % 4 === 0) {
200 | p.push(bit << 3);
201 | } else {
202 | p[p.length - 1] |= bit << (3 - i % 4);
203 | }
204 | return p;
205 | }, []));
206 | console.assert(nibbles.length === 8, {nibbles});
207 |
208 | // [8-9] MIDI Ch.
209 | nibbles.push(...nibblize(bytes[0x02]));
210 | console.assert(nibbles.length === 10, {nibbles});
211 |
212 | // [10-11] MONO/POLY Mode, Assign Mode, and Use For Rhythm Part
213 | nibbles.push(((bytes[0x13] & 0x01) << 3) | ((bytes[0x15] & 0x03) << 1) | ((bytes[0x15] > 0) ? 0x01 : 0x00), bytes[0x14] & 0x03);
214 | console.assert(nibbles.length === 12, {nibbles});
215 |
216 | // [12-15] Pitch Key Shift and Pitch Offset Fine
217 | nibbles.push(...nibblize(bytes[0x16]), bytes[0x17] & 0x0f, bytes[0x18] & 0x0f);
218 | console.assert(nibbles.length === 16, {nibbles});
219 |
220 | // [16-27] Part Level, Part Panpot, Velocity Sense Offset, Velocity Sense Depth, Key Range Low, and Key Range High
221 | nibbles.push(...nibblize(bytes[0x19]), ...nibblize(bytes[0x1c]), ...nibblize(bytes[0x1b]), ...nibblize(bytes[0x1a]), ...nibblize(bytes[0x1d]), ...nibblize(bytes[0x1e]));
222 | console.assert(nibbles.length === 28, {nibbles});
223 |
224 | // [28-47] Chorus Send Depth, Reverb Send Depth, and Tone Modify 1-8
225 | nibbles.push(...nibblize(...bytes.slice(0x21, 0x2b)));
226 | console.assert(nibbles.length === 48, {nibbles});
227 |
228 | // [48-51] Zero
229 | nibbles.push(0, 0, 0, 0);
230 | console.assert(nibbles.length === 52, {nibbles});
231 |
232 | // [52-75] Scale Tuning C to B
233 | nibbles.push(...nibblize(...bytes.slice(0x2b, 0x37)));
234 | console.assert(nibbles.length === 76, {nibbles});
235 |
236 | // [76-79] CC1/CC2 Controller Number
237 | nibbles.push(...nibblize(...bytes.slice(0x1f, 0x21)));
238 | console.assert(nibbles.length === 80, {nibbles});
239 |
240 | // [80-223] Destination Controllers
241 | for (let i = 0; i < 6; i++) {
242 | const index = 0x37 + i * 11;
243 | nibbles.push(...nibblize(...bytes.slice(index, index + 3), (i === 2 || i === 3) ? 0x40 : 0x00, ...bytes.slice(index + 3, index + 11)));
244 | }
245 | console.assert(nibbles.length === 224, {nibbles});
246 | console.assert(nibbles.every((e) => (0x0 <= e && e < 0x10)), 'Invalid SysEx nibble', {nibbles});
247 |
248 | // Divides the whole data by 2 packets.
249 | return [0, 1].map((i) => makeSysExGS(nibbles.slice(i * 128, (i + 1) * 128), addrH, addrM + i, addrL));
250 | }
251 | }
252 |
253 | function makeSysEx(bytes, modelId, addrH, addrM, addrL) {
254 | console.assert([modelId, addrH, addrM, addrL].every((e) => (0x00 <= e && e < 0x80)), 'Invalid address', {addrH, addrM, addrL});
255 | const sysEx = [0xf0, 0x41, 0x10, modelId, 0x12, addrH, addrM, addrL, ...bytes, -1, 0xf7];
256 | sysEx[sysEx.length - 2] = checkSum(sysEx.slice(5, -2));
257 | return sysEx;
258 | }
259 |
260 | function checkSum(bytes) {
261 | console.assert(bytes && bytes.length, 'Invalid argument', {bytes});
262 | const sum = bytes.reduce((p, c) => p + c, 0);
263 | return (0x80 - (sum & 0x7f)) & 0x7f;
264 | }
265 |
266 | const initialValues = Object.freeze({
267 | // MT-32/CM-64
268 | 0x16: {
269 | // [LA SOUND PART]
270 | 0x03: [
271 | // Patch Temporary Area
272 | 0x01, 0x04, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x07, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 1
273 | 0x00, 0x30, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x08, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 2
274 | 0x01, 0x1f, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x07, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 3
275 | 0x01, 0x0e, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x08, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 4
276 | 0x00, 0x29, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x04, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 5
277 | 0x00, 0x03, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x0a, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 6
278 | 0x01, 0x2e, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 7
279 | 0x01, 0x3a, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x0e, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Part 8
280 | 0x00, 0x00, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00, 0x50, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, // Rhythm Part
281 |
282 | // Rhythm Setup Temporary Area
283 | 0x7f, 0x00, 0x07, 0x00, // Note #24
284 | 0x7f, 0x00, 0x07, 0x00, // Note #25
285 | 0x7f, 0x00, 0x07, 0x00, // Note #26
286 | 0x7f, 0x00, 0x07, 0x00, // Note #27
287 | 0x7f, 0x00, 0x07, 0x00, // Note #28
288 | 0x7f, 0x00, 0x07, 0x00, // Note #29
289 | 0x7f, 0x00, 0x07, 0x00, // Note #30
290 | 0x7f, 0x00, 0x07, 0x00, // Note #31
291 | 0x7f, 0x00, 0x07, 0x00, // Note #32
292 | 0x7f, 0x00, 0x07, 0x00, // Note #33
293 | 0x7f, 0x00, 0x07, 0x00, // Note #34
294 | 0x40, 0x64, 0x07, 0x01, // Note #35
295 | 0x40, 0x64, 0x07, 0x01, // Note #36
296 | 0x4a, 0x64, 0x06, 0x01, // Note #37
297 | 0x41, 0x64, 0x07, 0x01, // Note #38
298 | 0x4b, 0x64, 0x08, 0x01, // Note #39
299 | 0x45, 0x64, 0x06, 0x01, // Note #40
300 | 0x44, 0x64, 0x0b, 0x01, // Note #41
301 | 0x46, 0x64, 0x06, 0x01, // Note #42
302 | 0x44, 0x64, 0x0b, 0x01, // Note #43
303 | 0x5d, 0x64, 0x06, 0x01, // Note #44
304 | 0x43, 0x64, 0x08, 0x01, // Note #45
305 | 0x47, 0x64, 0x06, 0x01, // Note #46
306 | 0x43, 0x64, 0x08, 0x01, // Note #47
307 | 0x42, 0x64, 0x03, 0x01, // Note #48
308 | 0x48, 0x64, 0x06, 0x01, // Note #49
309 | 0x42, 0x64, 0x03, 0x01, // Note #50
310 | 0x49, 0x64, 0x08, 0x01, // Note #51
311 | 0x7f, 0x00, 0x07, 0x00, // Note #52
312 | 0x7f, 0x00, 0x07, 0x00, // Note #53
313 | 0x56, 0x64, 0x09, 0x01, // Note #54
314 | 0x7f, 0x00, 0x07, 0x00, // Note #55
315 | 0x4c, 0x64, 0x07, 0x01, // Note #56
316 | 0x7f, 0x00, 0x07, 0x00, // Note #57
317 | 0x7f, 0x00, 0x07, 0x00, // Note #58
318 | 0x7f, 0x00, 0x07, 0x00, // Note #59
319 | 0x52, 0x64, 0x02, 0x01, // Note #60
320 | 0x53, 0x64, 0x04, 0x01, // Note #61
321 | 0x4d, 0x64, 0x08, 0x01, // Note #62
322 | 0x4e, 0x64, 0x09, 0x01, // Note #63
323 | 0x4f, 0x64, 0x0a, 0x01, // Note #64
324 | 0x50, 0x64, 0x07, 0x01, // Note #65
325 | 0x51, 0x64, 0x05, 0x01, // Note #66
326 | 0x54, 0x64, 0x02, 0x01, // Note #67
327 | 0x55, 0x64, 0x02, 0x01, // Note #68
328 | 0x5b, 0x64, 0x09, 0x01, // Note #69
329 | 0x58, 0x64, 0x04, 0x01, // Note #70
330 | 0x5a, 0x64, 0x09, 0x01, // Note #71
331 | 0x59, 0x64, 0x09, 0x01, // Note #72
332 | 0x5c, 0x64, 0x0a, 0x01, // Note #73
333 | 0x7f, 0x00, 0x07, 0x00, // Note #74
334 | 0x57, 0x64, 0x0c, 0x01, // Note #75
335 | 0x5e, 0x64, 0x07, 0x01, // Note #76
336 | 0x5f, 0x64, 0x07, 0x01, // Note #77
337 | 0x60, 0x64, 0x07, 0x01, // Note #78
338 | 0x61, 0x64, 0x07, 0x01, // Note #79
339 | 0x62, 0x64, 0x07, 0x01, // Note #80
340 | 0x63, 0x64, 0x07, 0x01, // Note #81
341 | 0x64, 0x64, 0x07, 0x01, // Note #82
342 | 0x65, 0x64, 0x07, 0x01, // Note #83
343 | 0x66, 0x64, 0x07, 0x01, // Note #84
344 | 0x67, 0x64, 0x07, 0x01, // Note #85
345 | 0x68, 0x64, 0x07, 0x01, // Note #86
346 | 0x69, 0x64, 0x07, 0x01, // Note #87
347 | 0x6a, 0x64, 0x07, 0x01, // Note #88
348 | 0x6b, 0x64, 0x07, 0x01, // Note #89
349 | 0x6c, 0x64, 0x07, 0x01, // Note #90
350 | 0x6d, 0x64, 0x07, 0x01, // Note #91
351 | 0x6e, 0x64, 0x07, 0x01, // Note #92
352 | 0x6f, 0x64, 0x07, 0x01, // Note #93
353 | 0x70, 0x64, 0x07, 0x01, // Note #94
354 | 0x71, 0x64, 0x07, 0x01, // Note #95
355 | 0x72, 0x64, 0x07, 0x01, // Note #96
356 | 0x73, 0x64, 0x07, 0x01, // Note #97
357 | 0x74, 0x64, 0x07, 0x01, // Note #98
358 | 0x75, 0x64, 0x07, 0x01, // Note #99
359 | 0x76, 0x64, 0x07, 0x01, // Note #100
360 | 0x77, 0x64, 0x07, 0x01, // Note #101
361 | 0x78, 0x64, 0x07, 0x01, // Note #102
362 | 0x79, 0x64, 0x07, 0x01, // Note #103
363 | 0x7a, 0x64, 0x07, 0x01, // Note #104
364 | 0x7b, 0x64, 0x07, 0x01, // Note #105
365 | 0x7c, 0x64, 0x07, 0x01, // Note #106
366 | 0x7d, 0x64, 0x07, 0x01, // Note #107
367 | 0x7e, 0x64, 0x07, 0x01, // Note #108
368 | ],
369 | 0x04: [
370 | // [Timbre Temporary Area (Part 1)]
371 | // Common Parameter
372 | ...'Slap Bass1'.split('').map((ch) => ch.charCodeAt()), 0x05, 0x00, 0x07, 0x00,
373 | // Partial Parameter (for Partial #1)
374 | 0x24, 0x32, 0x10, 0x01, 0x00, 0x24, 0x00, 0x07,
375 | 0x00, 0x00, 0x00, 0x0f, 0x0c, 0x12, 0x00, 0x15, 0x23, 0x2c, 0x32, 0x32,
376 | 0x00, 0x00, 0x00,
377 | 0x00, 0x00, 0x03, 0x00, 0x07,
378 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
379 | 0x64, 0x4b, 0x50, 0x06, 0x0f, 0x0c,
380 | 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x1e, 0x64, 0x00, 0x00, 0x00,
381 | // Partial Parameter (for Partial #2)
382 | 0x24, 0x26, 0x10, 0x01, 0x00, 0x25, 0x00, 0x07,
383 | 0x00, 0x00, 0x00, 0x0d, 0x0c, 0x12, 0x00, 0x15, 0x25, 0x2c, 0x32, 0x32,
384 | 0x40, 0x00, 0x38,
385 | 0x00, 0x00, 0x03, 0x00, 0x07,
386 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
387 | 0x64, 0x4b, 0x43, 0x04, 0x0f, 0x0c,
388 | 0x00, 0x00, 0x00, 0x3b, 0x4e, 0x64, 0x1e, 0x64, 0x50, 0x37, 0x00,
389 | // Partial Parameter (for Partial #3)
390 | 0x24, 0x37, 0x10, 0x01, 0x01, 0x00, 0x26, 0x07,
391 | 0x04, 0x00, 0x00, 0x0c, 0x0c, 0x0c, 0x00, 0x1b, 0x27, 0x2f, 0x32, 0x32,
392 | 0x37, 0x19, 0x26,
393 | 0x28, 0x14, 0x07, 0x2e, 0x07,
394 | 0x64, 0x19, 0x00, 0x00, 0x00, 0x2d, 0x50, 0x5e, 0x1c, 0x64, 0x55, 0x30, 0x1a,
395 | 0x50, 0x4b, 0x5b, 0x0c, 0x0f, 0x0c,
396 | 0x00, 0x00, 0x00, 0x3b, 0x3b, 0x64, 0x1e, 0x64, 0x50, 0x37, 0x00,
397 | // Partial Parameter (for Partial #4)
398 | 0x24, 0x37, 0x10, 0x01, 0x01, 0x00, 0x26, 0x07,
399 | 0x04, 0x00, 0x00, 0x0c, 0x0c, 0x0c, 0x00, 0x1b, 0x27, 0x2f, 0x32, 0x32,
400 | 0x37, 0x19, 0x26,
401 | 0x28, 0x14, 0x07, 0x2e, 0x07,
402 | 0x64, 0x19, 0x00, 0x00, 0x00, 0x2d, 0x50, 0x5e, 0x1c, 0x64, 0x55, 0x30, 0x1a,
403 | 0x50, 0x4b, 0x5b, 0x0c, 0x0f, 0x0c,
404 | 0x00, 0x00, 0x00, 0x3b, 0x3b, 0x64, 0x1e, 0x64, 0x50, 0x37, 0x00,
405 |
406 | // [Timbre Temporary Area (Part 2)]
407 | // Common Parameter
408 | ...'Str Sect 1'.split('').map((ch) => ch.charCodeAt()), 0x07, 0x05, 0x0f, 0x00,
409 | // Partial Parameter (for Partial #1)
410 | 0x24, 0x2b, 0x10, 0x01, 0x00, 0x00, 0x59, 0x07,
411 | 0x07, 0x01, 0x03, 0x07, 0x0f, 0x18, 0x00, 0x30, 0x63, 0x29, 0x32, 0x32,
412 | 0x43, 0x1c, 0x2e,
413 | 0x4b, 0x00, 0x09, 0x1b, 0x0a,
414 | 0x1e, 0x14, 0x00, 0x00, 0x10, 0x18, 0x18, 0x50, 0x58, 0x64, 0x60, 0x5a, 0x12,
415 | 0x46, 0x50, 0x5b, 0x0c, 0x1b, 0x0c,
416 | 0x04, 0x01, 0x10, 0x0a, 0x12, 0x18, 0x3c, 0x38, 0x50, 0x60, 0x64,
417 | // Partial Parameter (for Partial #2)
418 | 0x24, 0x39, 0x10, 0x01, 0x00, 0x00, 0x50, 0x07,
419 | 0x07, 0x01, 0x04, 0x06, 0x11, 0x09, 0x00, 0x31, 0x64, 0x2b, 0x32, 0x32,
420 | 0x3f, 0x1c, 0x29,
421 | 0x55, 0x00, 0x09, 0x27, 0x0a,
422 | 0x0f, 0x14, 0x00, 0x00, 0x12, 0x20, 0x1c, 0x59, 0x58, 0x55, 0x44, 0x1a, 0x12,
423 | 0x46, 0x50, 0x70, 0x09, 0x5b, 0x0c,
424 | 0x04, 0x01, 0x0c, 0x0e, 0x12, 0x1c, 0x3c, 0x30, 0x4a, 0x5a, 0x64,
425 | // Partial Parameter (for Partial #3)
426 | 0x24, 0x32, 0x10, 0x01, 0x00, 0x2c, 0x00, 0x07,
427 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
428 | 0x00, 0x00, 0x00,
429 | 0x00, 0x00, 0x03, 0x00, 0x07,
430 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
431 | 0x5a, 0x46, 0x4f, 0x04, 0x41, 0x0a,
432 | 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x3d, 0x64, 0x00, 0x00, 0x00,
433 | // Partial Parameter (for Partial #4)
434 | 0x24, 0x54, 0x10, 0x01, 0x00, 0x2d, 0x00, 0x07,
435 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
436 | 0x00, 0x00, 0x00,
437 | 0x00, 0x00, 0x03, 0x00, 0x07,
438 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
439 | 0x5c, 0x5a, 0x2b, 0x08, 0x5b, 0x0c,
440 | 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x3c, 0x64, 0x00, 0x00, 0x00,
441 |
442 | // [Timbre Temporary Area (Part 3)]
443 | // Common Parameter
444 | ...'Brs Sect 1'.split('').map((ch) => ch.charCodeAt()), 0x05, 0x07, 0x0f, 0x00,
445 | // Partial Parameter (for Partial #1)
446 | 0x30, 0x32, 0x10, 0x01, 0x00, 0x17, 0x00, 0x07,
447 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
448 | 0x00, 0x00, 0x00,
449 | 0x00, 0x00, 0x03, 0x00, 0x07,
450 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
451 | 0x5f, 0x48, 0x7f, 0x06, 0x24, 0x05,
452 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x14, 0x64, 0x64, 0x64, 0x64,
453 | // Partial Parameter (for Partial #2)
454 | 0x24, 0x32, 0x10, 0x01, 0x00, 0x1d, 0x00, 0x07,
455 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
456 | 0x00, 0x00, 0x00,
457 | 0x00, 0x00, 0x03, 0x00, 0x07,
458 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
459 | 0x60, 0x48, 0x7f, 0x06, 0x24, 0x0c,
460 | 0x00, 0x01, 0x00, 0x00, 0x1d, 0x2e, 0x28, 0x64, 0x64, 0x64, 0x00,
461 | // Partial Parameter (for Partial #3)
462 | 0x30, 0x32, 0x10, 0x01, 0x01, 0x00, 0x00, 0x07,
463 | 0x06, 0x03, 0x00, 0x0b, 0x21, 0x23, 0x00, 0x13, 0x22, 0x32, 0x32, 0x32,
464 | 0x3f, 0x0f, 0x28,
465 | 0x46, 0x08, 0x09, 0x24, 0x09,
466 | 0x37, 0x3a, 0x00, 0x02, 0x13, 0x01, 0x08, 0x1f, 0x64, 0x56, 0x3c, 0x57, 0x48,
467 | 0x4e, 0x44, 0x7f, 0x00, 0x1b, 0x0c,
468 | 0x01, 0x02, 0x06, 0x08, 0x04, 0x17, 0x14, 0x4b, 0x2c, 0x64, 0x5d,
469 | // Partial Parameter (for Partial #4)
470 | 0x24, 0x2f, 0x10, 0x01, 0x00, 0x00, 0x48, 0x07,
471 | 0x06, 0x03, 0x00, 0x0b, 0x0f, 0x18, 0x00, 0x13, 0x22, 0x2b, 0x32, 0x32,
472 | 0x3d, 0x0f, 0x28,
473 | 0x46, 0x08, 0x09, 0x24, 0x09,
474 | 0x37, 0x3a, 0x00, 0x02, 0x18, 0x01, 0x08, 0x1f, 0x64, 0x56, 0x3c, 0x57, 0x48,
475 | 0x4e, 0x44, 0x7f, 0x00, 0x1b, 0x0c,
476 | 0x01, 0x02, 0x0b, 0x08, 0x0d, 0x17, 0x14, 0x4b, 0x2c, 0x64, 0x5d,
477 |
478 | // [Timbre Temporary Area (Part 4)]
479 | // Common Parameter
480 | ...'Sax 1 '.split('').map((ch) => ch.charCodeAt()), 0x01, 0x05, 0x0f, 0x00,
481 | // Partial Parameter (for Partial #1)
482 | 0x24, 0x32, 0x10, 0x01, 0x01, 0x00, 0x00, 0x07,
483 | 0x07, 0x03, 0x04, 0x15, 0x15, 0x27, 0x00, 0x32, 0x27, 0x32, 0x32, 0x32,
484 | 0x40, 0x1a, 0x30,
485 | 0x58, 0x11, 0x07, 0x21, 0x09,
486 | 0x30, 0x47, 0x00, 0x00, 0x03, 0x11, 0x19, 0x3e, 0x2d, 0x4c, 0x37, 0x29, 0x1d,
487 | 0x64, 0x4b, 0x5b, 0x0c, 0x1b, 0x0c,
488 | 0x03, 0x00, 0x0b, 0x0e, 0x0c, 0x16, 0x1e, 0x2f, 0x42, 0x60, 0x64,
489 | // Partial Parameter (for Partial #2)
490 | 0x37, 0x31, 0x10, 0x01, 0x01, 0x00, 0x00, 0x07,
491 | 0x00, 0x00, 0x00, 0x0a, 0x14, 0x32, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
492 | 0x40, 0x21, 0x2e,
493 | 0x64, 0x00, 0x06, 0x26, 0x0a,
494 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
495 | 0x64, 0x64, 0x73, 0x00, 0x24, 0x08,
496 | 0x02, 0x00, 0x0a, 0x1a, 0x14, 0x3c, 0x11, 0x3e, 0x64, 0x59, 0x45,
497 | // Partial Parameter (for Partial #3)
498 | 0x24, 0x00, 0x10, 0x01, 0x00, 0x1e, 0x00, 0x07,
499 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
500 | 0x00, 0x00, 0x00,
501 | 0x00, 0x00, 0x0b, 0x00, 0x07,
502 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
503 | 0x64, 0x50, 0x50, 0x00, 0x1b, 0x0c,
504 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x64, 0x64, 0x64, 0x64,
505 | // Partial Parameter (for Partial #4)
506 | 0x24, 0x38, 0x10, 0x01, 0x00, 0x1d, 0x00, 0x07,
507 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
508 | 0x00, 0x00, 0x00,
509 | 0x00, 0x00, 0x0b, 0x00, 0x07,
510 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
511 | 0x64, 0x50, 0x6f, 0x05, 0x1b, 0x06,
512 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x64, 0x64, 0x64, 0x64,
513 |
514 | // [Timbre Temporary Area (Part 5)]
515 | // Common Parameter
516 | ...'Ice Rain '.split('').map((ch) => ch.charCodeAt()), 0x07, 0x02, 0x07, 0x00,
517 | 0x24, 0x37, 0x10, 0x01, 0x01, 0x00, 0x00, 0x06,
518 | // Partial Parameter (for Partial #1)
519 | 0x08, 0x01, 0x00, 0x01, 0x08, 0x06, 0x64, 0x32, 0x62, 0x29, 0x32, 0x32,
520 | 0x40, 0x1e, 0x4c,
521 | 0x19, 0x00, 0x07, 0x29, 0x0a,
522 | 0x5c, 0x58, 0x00, 0x01, 0x01, 0x0f, 0x29, 0x3d, 0x59, 0x4f, 0x1f, 0x40, 0x28,
523 | 0x5f, 0x4b, 0x5b, 0x0c, 0x1b, 0x0c,
524 | 0x01, 0x01, 0x00, 0x07, 0x0e, 0x54, 0x55, 0x64, 0x47, 0x53, 0x00,
525 | // Partial Parameter (for Partial #2)
526 | 0x24, 0x2d, 0x10, 0x01, 0x00, 0x00, 0x55, 0x06,
527 | 0x05, 0x01, 0x00, 0x01, 0x08, 0x06, 0x64, 0x32, 0x62, 0x29, 0x32, 0x32,
528 | 0x3d, 0x1e, 0x46,
529 | 0x19, 0x00, 0x07, 0x29, 0x0a,
530 | 0x5c, 0x58, 0x00, 0x01, 0x01, 0x0f, 0x29, 0x3d, 0x59, 0x4f, 0x1f, 0x40, 0x28,
531 | 0x5f, 0x4b, 0x5b, 0x0c, 0x1b, 0x0c,
532 | 0x01, 0x01, 0x00, 0x07, 0x0e, 0x54, 0x55, 0x64, 0x47, 0x53, 0x00,
533 | // Partial Parameter (for Partial #3)
534 | 0x43, 0x32, 0x05, 0x01, 0x00, 0x69, 0x00, 0x07,
535 | 0x08, 0x03, 0x00, 0x36, 0x36, 0x44, 0x4c, 0x5d, 0x57, 0x4d, 0x2d, 0x00,
536 | 0x00, 0x00, 0x00,
537 | 0x00, 0x00, 0x0b, 0x00, 0x07,
538 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
539 | 0x5f, 0x46, 0x7f, 0x00, 0x1b, 0x0c,
540 | 0x00, 0x00, 0x00, 0x3f, 0x3d, 0x48, 0x4c, 0x64, 0x5e, 0x41, 0x00,
541 | // Partial Parameter (for Partial #4)
542 | 0x43, 0x32, 0x05, 0x01, 0x00, 0x69, 0x00, 0x07,
543 | 0x08, 0x03, 0x00, 0x36, 0x36, 0x44, 0x4c, 0x5d, 0x57, 0x4d, 0x2d, 0x00,
544 | 0x00, 0x00, 0x00,
545 | 0x00, 0x00, 0x0b, 0x00, 0x07,
546 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
547 | 0x5f, 0x46, 0x7f, 0x00, 0x1b, 0x0c,
548 | 0x00, 0x00, 0x00, 0x3f, 0x3d, 0x48, 0x4c, 0x64, 0x5e, 0x41, 0x00,
549 |
550 | // [Timbre Temporary Area (Part 6)]
551 | // Common Parameter
552 | ...'ElecPiano1'.split('').map((ch) => ch.charCodeAt()), 0x01, 0x00, 0x07, 0x00,
553 | // Partial Parameter (for Partial #1)
554 | 0x24, 0x37, 0x0f, 0x01, 0x00, 0x00, 0x3c, 0x07,
555 | 0x03, 0x00, 0x01, 0x0a, 0x07, 0x16, 0x00, 0x32, 0x42, 0x33, 0x32, 0x32,
556 | 0x00, 0x00, 0x00,
557 | 0x35, 0x00, 0x07, 0x16, 0x08,
558 | 0x29, 0x28, 0x00, 0x02, 0x00, 0x20, 0x3b, 0x64, 0x52, 0x64, 0x47, 0x29, 0x00,
559 | 0x5a, 0x55, 0x5c, 0x0c, 0x59, 0x0c,
560 | 0x02, 0x00, 0x00, 0x32, 0x47, 0x64, 0x38, 0x64, 0x52, 0x28, 0x00,
561 | // Partial Parameter (for Partial #2)
562 | 0x4e, 0x47, 0x07, 0x01, 0x00, 0x00, 0x21, 0x07,
563 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
564 | 0x00, 0x00, 0x00,
565 | 0x1d, 0x01, 0x09, 0x61, 0x0a,
566 | 0x64, 0x00, 0x00, 0x03, 0x00, 0x1a, 0x40, 0x4b, 0x1d, 0x64, 0x38, 0x14, 0x00,
567 | 0x47, 0x3c, 0x33, 0x08, 0x5b, 0x0c,
568 | 0x01, 0x01, 0x00, 0x28, 0x3d, 0x4a, 0x64, 0x64, 0x36, 0x23, 0x06,
569 | // Partial Parameter (for Partial #3)
570 | 0x30, 0x2d, 0x0f, 0x01, 0x01, 0x00, 0x3c, 0x07,
571 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x32, 0x34, 0x30, 0x30, 0x31,
572 | 0x3e, 0x08, 0x42,
573 | 0x37, 0x00, 0x07, 0x16, 0x09,
574 | 0x28, 0x28, 0x00, 0x02, 0x00, 0x18, 0x3b, 0x64, 0x52, 0x64, 0x47, 0x29, 0x00,
575 | 0x5a, 0x55, 0x5c, 0x0c, 0x59, 0x0c,
576 | 0x02, 0x00, 0x00, 0x32, 0x47, 0x64, 0x33, 0x64, 0x52, 0x28, 0x00,
577 | // Partial Parameter (for Partial #4)
578 | 0x30, 0x2d, 0x0f, 0x01, 0x01, 0x00, 0x3c, 0x07,
579 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x32, 0x34, 0x30, 0x30, 0x31,
580 | 0x3e, 0x08, 0x42,
581 | 0x37, 0x00, 0x07, 0x16, 0x09,
582 | 0x28, 0x28, 0x00, 0x02, 0x00, 0x18, 0x3b, 0x64, 0x52, 0x64, 0x47, 0x29, 0x00,
583 | 0x5a, 0x55, 0x5c, 0x0c, 0x59, 0x0c,
584 | 0x02, 0x00, 0x00, 0x32, 0x47, 0x64, 0x33, 0x64, 0x52, 0x28, 0x00,
585 |
586 | // [Timbre Temporary Area (Part 7)]
587 | // Common Parameter
588 | ...'BottleBlow'.split('').map((ch) => ch.charCodeAt()), 0x05, 0x01, 0x0f, 0x00,
589 | // Partial Parameter (for Partial #1)
590 | 0x21, 0x32, 0x10, 0x01, 0x00, 0x1c, 0x00, 0x07,
591 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
592 | 0x00, 0x00, 0x00,
593 | 0x00, 0x00, 0x03, 0x00, 0x07,
594 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
595 | 0x64, 0x55, 0x7b, 0x08, 0x20, 0x05,
596 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x64, 0x64, 0x64, 0x64,
597 | // Partial Parameter (for Partial #2)
598 | 0x3c, 0x49, 0x07, 0x01, 0x00, 0x35, 0x00, 0x07,
599 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
600 | 0x3f, 0x25, 0x00,
601 | 0x00, 0x00, 0x03, 0x00, 0x07,
602 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
603 | 0x20, 0x46, 0x7d, 0x04, 0x1b, 0x0c,
604 | 0x00, 0x00, 0x08, 0x18, 0x23, 0x28, 0x1a, 0x54, 0x5f, 0x56, 0x46,
605 | // Partial Parameter (for Partial #3)
606 | 0x24, 0x32, 0x10, 0x01, 0x00, 0x00, 0x00, 0x07,
607 | 0x06, 0x03, 0x01, 0x0e, 0x11, 0x30, 0x00, 0x42, 0x38, 0x32, 0x32, 0x32,
608 | 0x3f, 0x19, 0x44,
609 | 0x32, 0x18, 0x0c, 0x1d, 0x0b,
610 | 0x2a, 0x43, 0x00, 0x00, 0x05, 0x15, 0x2c, 0x2d, 0x44, 0x11, 0x1d, 0x17, 0x0e,
611 | 0x64, 0x44, 0x7f, 0x00, 0x1b, 0x0c,
612 | 0x00, 0x00, 0x04, 0x08, 0x17, 0x22, 0x36, 0x57, 0x64, 0x5d, 0x64,
613 | // Partial Parameter (for Partial #4)
614 | 0x43, 0x2d, 0x10, 0x01, 0x00, 0x00, 0x00, 0x07,
615 | 0x05, 0x03, 0x01, 0x0d, 0x12, 0x22, 0x00, 0x3f, 0x37, 0x33, 0x32, 0x32,
616 | 0x3f, 0x28, 0x00,
617 | 0x2a, 0x16, 0x08, 0x64, 0x04,
618 | 0x24, 0x37, 0x00, 0x00, 0x09, 0x25, 0x2b, 0x4e, 0x64, 0x42, 0x23, 0x17, 0x11,
619 | 0x44, 0x3c, 0x67, 0x07, 0x1b, 0x0c,
620 | 0x00, 0x00, 0x19, 0x0f, 0x19, 0x22, 0x26, 0x4f, 0x61, 0x5e, 0x64,
621 |
622 | // [Timbre Temporary Area (Part 8)]
623 | // Common Parameter
624 | ...'Orche Hit '.split('').map((ch) => ch.charCodeAt()), 0x02, 0x08, 0x0f, 0x00,
625 | // Partial Parameter (for Partial #1)
626 | 0x24, 0x32, 0x10, 0x01, 0x00, 0x2f, 0x00, 0x07,
627 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
628 | 0x00, 0x00, 0x00,
629 | 0x00, 0x00, 0x03, 0x00, 0x07,
630 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
631 | 0x64, 0x46, 0x1b, 0x0c, 0x67, 0x02,
632 | 0x02, 0x00, 0x01, 0x2e, 0x2d, 0x1f, 0x4c, 0x64, 0x5d, 0x32, 0x00,
633 | // Partial Parameter (for Partial #2)
634 | 0x18, 0x32, 0x10, 0x01, 0x01, 0x00, 0x64, 0x07,
635 | 0x04, 0x00, 0x00, 0x0c, 0x13, 0x25, 0x00, 0x3d, 0x36, 0x32, 0x32, 0x32,
636 | 0x39, 0x21, 0x43,
637 | 0x3a, 0x00, 0x0a, 0x1e, 0x09,
638 | 0x3e, 0x00, 0x00, 0x00, 0x08, 0x2f, 0x23, 0x2d, 0x29, 0x1e, 0x2a, 0x22, 0x05,
639 | 0x3d, 0x49, 0x1b, 0x0c, 0x67, 0x02,
640 | 0x01, 0x00, 0x14, 0x24, 0x2c, 0x22, 0x3e, 0x64, 0x61, 0x3f, 0x00,
641 | // Partial Parameter (for Partial #3)
642 | 0x30, 0x32, 0x10, 0x01, 0x00, 0x17, 0x00, 0x07,
643 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
644 | 0x00, 0x00, 0x00,
645 | 0x00, 0x00, 0x03, 0x00, 0x07,
646 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
647 | 0x64, 0x46, 0x1b, 0x0c, 0x67, 0x02,
648 | 0x02, 0x00, 0x00, 0x33, 0x2d, 0x19, 0x4c, 0x64, 0x5e, 0x32, 0x00,
649 | // Partial Parameter (for Partial #4)
650 | 0x31, 0x48, 0x10, 0x01, 0x00, 0x2d, 0x00, 0x07,
651 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32,
652 | 0x00, 0x00, 0x00,
653 | 0x00, 0x00, 0x03, 0x00, 0x07,
654 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
655 | 0x64, 0x48, 0x1b, 0x0c, 0x67, 0x02,
656 | 0x02, 0x00, 0x00, 0x33, 0x2d, 0x19, 0x4c, 0x64, 0x5e, 0x32, 0x00,
657 | ],
658 | 0x05: [
659 | // Patch Memory
660 | ...[...new Array(128)].reduce((p, _, i) => {
661 | p.push(...[Math.trunc(i / 64), i % 64, 0x18, 0x32, 0x0c, 0x00, 0x01, 0x00]);
662 | return p;
663 | }, []),
664 | ],
665 | 0x08: [
666 | // Timbre Memory
667 | ...[...new Array(64)].reduce((p, _) => {
668 | p.push(...new Array(10).fill(-1), ...new Array(4 + 58 * 4 + 10).fill(0x00));
669 | return p;
670 | }, []),
671 | ],
672 | 0x10: [
673 | // System Area
674 | 0x4a,
675 | 0x00, 0x05, 0x03,
676 | 0x03, 0x0a, 0x06, 0x04, 0x03, 0x00, 0x00, 0x00, 0x06,
677 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
678 | 0x64, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00,
679 | ],
680 | // [PCM SOUND PART]
681 | 0x50: [
682 | // Patch Temporary Area
683 | 0x00, 0x28, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x40, 0x64, // Part 1
684 | 0x00, 0x2b, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x51, 0x64, // Part 2
685 | 0x00, 0x00, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x40, 0x64, // Part 3
686 | 0x00, 0x33, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x63, 0x64, // Part 4
687 | 0x00, 0x14, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x1b, 0x64, // Part 5
688 | 0x00, 0x40, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c, 0x2d, 0x64, // Part 6
689 | ],
690 | 0x51: [
691 | // Patch Memory
692 | ...[
693 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x0f, 0x11, 0x12, 0x14, 0x15, 0x1a,
694 | 0x1b, 0x1c, 0x1d, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x2a, 0x2b, 0x2c, 0x2d,
695 | 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x42,
696 | 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53,
697 | ...(new Array(64)).fill(0),
698 | ].reduce((p, c, i) => {
699 | const tones = (i < 64) ? [0x00, c] : [0x01, i % 64];
700 | p.push(...[...tones, 0x0c, 0x32, 0x0c, 0x00, 0x7f, 0x00, 0x01, 0x0f, 0x40, 0x40, 0x32, 0x00, 0x00, 0x00, 0x02, 0x04, 0x0c]);
701 | return p;
702 | }, []),
703 | ],
704 | 0x52: [
705 | // System Area
706 | 0x40,
707 | 0x00, 0x05, 0x03,
708 | 0x02, 0x08, 0x15, 0x00, 0x00, 0x00,
709 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
710 | 0x64,
711 | ],
712 | },
713 |
714 | // GS
715 | 0x42: {
716 | 0x48: [],
717 | 0x49: [],
718 | },
719 | });
720 |
721 | export function isSysExRedundant(bytes) {
722 | // Checks the argument.
723 | if (!bytes || !bytes.length || bytes[0] !== 0xf0 || bytes[bytes.length - 1] !== 0xf7) {
724 | throw new Error('Invalid argument');
725 | }
726 |
727 | // Checks whether the length of the SysEx is enough.
728 | if (bytes.length <= 10) {
729 | return false;
730 | }
731 |
732 | // Parses the SysEx.
733 | const [tmpF0, mfrId, deviceId, modelId, command, addrH, addrM, addrL, ...payload] = bytes;
734 | const tmpF7 = payload.pop();
735 | const sum = payload.pop();
736 | console.assert(tmpF0 === 0xf0 && tmpF7 === 0xf7);
737 |
738 | // Checks whether the SysEx is DT1 for Roland.
739 | if (mfrId !== 0x41 || deviceId !== 0x10 || command !== 0x12) {
740 | return false;
741 | }
742 | if ([addrH, addrM, addrL].some((e) => (e < 0x00 || 0x80 <= e))) {
743 | return false;
744 | }
745 |
746 | // If the check sum is invalid, it can be ignored.
747 | if (sum !== checkSum(bytes.slice(5, -2))) {
748 | return true;
749 | }
750 |
751 | // Gets the table corresponding to the model ID and the addresses.
752 | const table = initialValues[modelId];
753 | if (!table) {
754 | return false;
755 | }
756 | const page = table[addrH];
757 | if (!page) {
758 | return false;
759 | }
760 |
761 | // Gets the initial values.
762 | const index = (addrM << 7) | addrL;
763 | const values = page.slice(index, index + payload.length);
764 | if (payload.length !== values.length) {
765 | return false;
766 | }
767 |
768 | // Checks all the payload data is same to the initial value or it's "don't care".
769 | if (payload.some((e, i) => (e !== values[i] && values[i] >= 0))) {
770 | return false;
771 | }
772 |
773 | return true;
774 | }
775 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | rcm2smf
7 |
8 |
9 |
10 |
11 |
32 |
880 |
912 |
913 |
914 |
915 |
916 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
1182 |
1183 |
1184 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 |
1203 |
1204 |
1205 |
1216 |
1217 |
1218 |
1219 |
1220 |
1221 |
1222 |
1223 |
1224 |
1225 |
1226 |
1227 | | Tempo: | |
1228 | | Play Bias: | |
1229 | | Key: | |
1230 | | Beat: | / |
1231 | | Time Base: | |
1232 |
1233 |
1234 |
1235 |
Control File(s):
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
1243 |
1244 |
1245 |
1246 |
1254 |
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 |
1263 |
1264 | | Tr. |
1265 | Mode |
1266 | Ch. |
1267 | ST+ |
1268 | K#+ |
1269 | Memo |
1270 |
1271 |
1272 |
1273 |
1274 |
1275 |
1276 |
1277 |
1278 |
1279 |
1280 |
1281 |
1282 |
1283 |
1284 |
1285 |
1286 |
1287 |
1288 | | No. |
1289 | Memo & Content |
1290 |
1291 |
1292 |
1293 |
1294 |
1295 |
1296 |
1297 |
1298 |
1299 |
1300 |
1319 |
1320 |
1321 |
1322 |
1323 |
1324 |
1325 |
1326 |
1327 |
1328 |
1329 |
1330 |
1331 |
1332 |
Original:
1333 |
1334 |
1335 |
1336 |
Extracted:
1337 |
1338 |
1339 |
1340 |
1341 |
1342 |
1343 |
1344 |
1345 |
1346 |
Control File
1347 |
1348 |
1359 |
1370 |
1371 |
1384 |
1385 |
1386 |
1387 |
1388 |
1393 |
1394 |
1395 |
1396 |
1397 | What is this?
1398 | -------------
1399 |
1400 | This is a Recomposer file (.RCP, .R36, .G36, and .MCP) to Standard MIDI File (.mid) converter.
1401 |
1402 | For more detail, refer to https://github.com/shingo45endo/rcm2smf
1403 |
1404 | How to use
1405 | ----------
1406 |
1407 | 1. Drag & drop RCP/R36/G36/MCP file(s).
1408 | * If the input files need control files (such as MTD/CM6/GSD), drag & drop them too.
1409 | 2. Push the "Convert" button on the top-right of the pane area.
1410 | * If necessary, you can change the file name of generated SMF with the adjacent text field.
1411 |
1412 | Supported Formats
1413 | -----------------
1414 |
1415 | ### Sequence Files
1416 |
1417 | * MCP
1418 | * RCM-PC98 Ver.1.0
1419 | * Contains at most 8 tracks and special tracks for rhythm part.
1420 | * RCP
1421 | * Recomposer (aka RCM-PC98) Ver.2.0 ~ Ver.2.5F
1422 | * Contains at most 18 tracks.
1423 | * R36
1424 | * Recomposer Ver.2.5F
1425 | * Contains at most 36 tracks.
1426 | * G36
1427 | * Recomposer Ver.2.5G, Ver.3.0, and for Windows
1428 | * Time base can be 480 ticks per beat at a maximum.
1429 | * Contains at most 36 tracks (or 99 tracks only for Windows 95 Version)
1430 |
1431 | ### Control Files
1432 |
1433 | Some Recomposer sequence files contain "control files" to set up the initial settings of sound modules by bulk dump. This converter converts them into SysExs and prepends the generated them to the beginning of SMF.
1434 |
1435 | * MTD
1436 | * For MCP files
1437 | * Intended to Roland MT-32.
1438 | * CM6
1439 | * For RCP files
1440 | * Intended to Roland CM-64 (and MT-32).
1441 | * GSD
1442 | * For RCP/R36/G36 files
1443 | * Intended to Roland SC-55.
1444 | * As for G36, two GSD files can be used for each port.
1445 |
1446 |
1447 |
1448 |
1449 |
--------------------------------------------------------------------------------
/rcm_converter.js:
--------------------------------------------------------------------------------
1 | import {convertMtdToSysEx, convertCm6ToSysEx, convertGsdToSysEx, isSysExRedundant} from './rcm_ctrl_converter.js';
2 |
3 | // Default settings of this converter.
4 | export const defaultSettings = Object.freeze({
5 | metaTextMemo: true,
6 | metaTextComment: true,
7 | metaTextUsrExc: true,
8 | metaCue: true,
9 | metaTimeSignature: true,
10 | trimTrackName: 'both',
11 | trimTextMemo: 'none',
12 | trimTextComment: 'both',
13 | trimTextUsrExc: 'both',
14 | noteOff: false,
15 | noteOffVel: 64,
16 |
17 | stPlus: 'auto',
18 | resetBeforeCtrl: true,
19 | optimizeCtrl: true,
20 | extraSysexWait: true,
21 | ignoreCtrlFile: false,
22 | ignoreOutOfRange: true,
23 | ignoreWrongEvent: true,
24 | maxLoopNest: 5,
25 | infinityLoopCount: 2,
26 | loopBombThreshold: 4000,
27 | rolandDevId: 0x10,
28 | rolandModelId: 0x16,
29 | rolandBaseAddrH: 0x00,
30 | rolandBaseAddrM: 0x10,
31 | yamahaDevId: 0x10,
32 | yamahaModelId: 0x4c,
33 | yamahaBaseAddrH: 0x00,
34 | yamahaBaseAddrM: 0x00,
35 | });
36 |
37 | const EVENT_MCP = Object.freeze({
38 | UsrExc0: -1,
39 | UsrExc1: -1,
40 | UsrExc2: -1,
41 | UsrExc3: -1,
42 | UsrExc4: -1,
43 | UsrExc5: -1,
44 | UsrExc6: -1,
45 | UsrExc7: -1,
46 | TrExcl: -1,
47 | ExtCmd: -1,
48 | DX7FUNC: 0xc0, // DX7 Function
49 | DX_PARA: 0xc1, // DX Voice Parameter
50 | DX_PERF: 0xc2, // DX Performance
51 | TX_FUNC: 0xc3, // TX Function
52 | FB_01_P: 0xc5, // FB-01 Parameter
53 | FB_01_S: 0xc6, // FB-01 System Parameter
54 | TX81Z_V: 0xc7, // TX81Z VCED
55 | TX81Z_A: 0xc8, // TX81Z ACED
56 | TX81Z_P: 0xc9, // TX81Z PCED
57 | TX81Z_S: 0xca, // TX81Z System
58 | TX81Z_E: 0xcb, // TX81Z Effect
59 | DX7_2_R: 0xcc, // DX7-2 Remote SW
60 | DX7_2_A: 0xcd, // DX7-2 ACED
61 | DX7_2_P: 0xce, // DX7-2 PCED
62 | TX802_P: 0xcf, // TX802 PCED
63 | YamBase: -1,
64 | YamDev: -1,
65 | YamPara: -1,
66 | XGPara: -1,
67 | MKS_7: -1,
68 | RolBase: 0xe7, // MT32BASE
69 | RolPara: 0xe8, // MT32PARA
70 | RolDev: 0xe6, // R.EXCLU
71 | BankPrgL: -1,
72 | BankPrg: -1,
73 | KeyScan: -1,
74 | MIDI_CH: 0xd0, // MIDI Channel Change
75 | TEMPO: 0xfa, // Relative Tempo Change
76 | AFTER_C: 0xe3, // After Touch (Ch.)
77 | CONTROL: 0xe2, // Control Change
78 | PROGRAM: -1,
79 | AFTER_K: 0xe1, // After Touch (Poly)
80 | PITCH: 0xe4, // Pitch Bend Change
81 | MusicKey: -1, // TODO: Investigate whether F2 event is for MusicKey or not.
82 | Comment: -1,
83 | SecondEvt: -1,
84 | LoopEnd: 0xfb, // Loop End
85 | LoopStart: 0xfc, // Loop Start
86 | SameMeas: -1,
87 | MeasEnd: 0xfd, // Measure End
88 | TrackEnd: 0xfe, // End of Track
89 | CMU_800: 0xf9, // CMU-800
90 | UserPrg: 0xe0, // Program Change (User Program)
91 | });
92 |
93 | const EVENT_RCP = Object.freeze({
94 | UsrExc0: 0x90, // User Exclusive 0
95 | UsrExc1: 0x91, // User Exclusive 1
96 | UsrExc2: 0x92, // User Exclusive 2
97 | UsrExc3: 0x93, // User Exclusive 3
98 | UsrExc4: 0x94, // User Exclusive 4
99 | UsrExc5: 0x95, // User Exclusive 5
100 | UsrExc6: 0x96, // User Exclusive 6
101 | UsrExc7: 0x97, // User Exclusive 7
102 | TrExcl: 0x98, // Track Exclusive
103 | ExtCmd: 0x99, // External Command
104 | DX7FUNC: 0xc0, // DX7 Function
105 | DX_PARA: 0xc1, // DX Voice Parameter
106 | DX_PERF: 0xc2, // DX Performance
107 | TX_FUNC: 0xc3, // TX Function
108 | FB_01_P: 0xc5, // FB-01 Parameter
109 | FB_01_S: 0xc6, // FB-01 System Parameter
110 | TX81Z_V: 0xc7, // TX81Z VCED
111 | TX81Z_A: 0xc8, // TX81Z ACED
112 | TX81Z_P: 0xc9, // TX81Z PCED
113 | TX81Z_S: 0xca, // TX81Z System
114 | TX81Z_E: 0xcb, // TX81Z Effect
115 | DX7_2_R: 0xcc, // DX7-2 Remote SW
116 | DX7_2_A: 0xcd, // DX7-2 ACED
117 | DX7_2_P: 0xce, // DX7-2 PCED
118 | TX802_P: 0xcf, // TX802 PCED
119 | YamBase: 0xd0, // Yamaha Base Address
120 | YamDev: 0xd1, // Yamaha Dev# & Model ID
121 | YamPara: 0xd2, // Yamaha Address & Parameter
122 | XGPara: 0xd3, // Yamaha XG Address & Parameter
123 | MKS_7: 0xdc, // Roland MKS-7
124 | RolBase: 0xdd, // Roland Base Address
125 | RolPara: 0xde, // Roland Address & Parameter
126 | RolDev: 0xdf, // Roland Dev# & Model ID
127 | BankPrgL: 0xe1, // Program Bank Change (LSB)
128 | BankPrg: 0xe2, // Program Bank Change (MSB)
129 | KeyScan: 0xe5, // Key Scan
130 | MIDI_CH: 0xe6, // MIDI Channel Change
131 | TEMPO: 0xe7, // Relative Tempo Change
132 | AFTER_C: 0xea, // After Touch (Ch.)
133 | CONTROL: 0xeb, // Control Change
134 | PROGRAM: 0xec, // Program Change
135 | AFTER_K: 0xed, // After Touch (Poly)
136 | PITCH: 0xee, // Pitch Bend Change
137 | MusicKey: 0xf5, // Music Key
138 | Comment: 0xf6, // Comment
139 | SecondEvt: 0xf7, // 2nd Event for Comment, TrExcl, and ExtCmd
140 | LoopEnd: 0xf8, // Loop End
141 | LoopStart: 0xf9, // Loop Start
142 | SameMeas: 0xfc, // Same Measure
143 | MeasEnd: 0xfd, // Measure End
144 | TrackEnd: 0xfe, // End of Track
145 | CMU_800: -1,
146 | UserPrg: -1,
147 | });
148 |
149 | export async function rcm2smf(buf, controlFileReader, options) {
150 | // Checks the arguments.
151 | if (!buf || !buf.length) {
152 | throw new Error(`Invalid argument: ${buf}`);
153 | }
154 |
155 | // Converts from RCP/G36 to SMF.
156 | const rcm = await parseRcm(buf, controlFileReader, options);
157 | const seq = convertRcmToSeq(rcm, options);
158 | const smf = convertSeqToSmf(seq, rcm.header.timeBase, options);
159 |
160 | return smf;
161 | }
162 |
163 | export function ctrl2smf(buf, title, options) {
164 | // Checks the arguments.
165 | if (!buf || !buf.length) {
166 | throw new Error(`Invalid argument: ${buf}`);
167 | }
168 |
169 | // Makes a dummy (empty) rcm object.
170 | const rcm = {
171 | header: {
172 | title: new Uint8Array(String(title).split('').map((e) => e.charCodeAt())),
173 | timeBase: 48,
174 | tempo: 120,
175 | beatN: 4,
176 | beatD: 4,
177 | maxTracks: 0,
178 | },
179 | tracks: [],
180 | };
181 |
182 | // Converts from MCP/CM6/GSD to SysExs.
183 | let sysExs = convertCm6ToSysEx(buf) || convertMtdToSysEx(buf);
184 | if (sysExs) {
185 | rcm.header.sysExsCM6 = sysExs; // Handles MTD as CM6 to avoid particularities of MCP.
186 | } else {
187 | sysExs = convertGsdToSysEx(buf);
188 | if (sysExs) {
189 | rcm.header.sysExsGSD = sysExs;
190 | } else {
191 | throw new Error('Invalid control file');
192 | }
193 | }
194 |
195 | // Converts from the generated rcm object to SMF.
196 | const seq = convertRcmToSeq(rcm, options);
197 | const smf = convertSeqToSmf(seq, rcm.header.timeBase, options);
198 |
199 | return smf;
200 | }
201 |
202 | export async function parseRcm(buf, controlFileReader, options) {
203 | // Checks the arguments.
204 | if (!buf || !buf.length) {
205 | throw new Error(`Invalid argument: ${buf}`);
206 | }
207 |
208 | // Makes a settings object from the default settings and the specified ones.
209 | const settings = {...defaultSettings, ...options};
210 |
211 | // Parses the data as RCP format. If it failed, parses it again as G36 format. If it failed again, tries MCP parser.
212 | const rcm = parseRcp(buf) || parseG36(buf) || parseMcp(buf);
213 | if (!rcm) {
214 | throw new Error('Not Recomposer file');
215 | }
216 |
217 | // Reads and parses control files.
218 | for (const kind of ['MTD', 'CM6', 'GSD', 'GSD2']) {
219 | if (settings.ignoreCtrlFile) {
220 | break;
221 | }
222 |
223 | const keyName = `fileName${kind}`;
224 | const keyData = `fileData${kind}`;
225 | const keySysEx = `sysExs${kind}`;
226 |
227 | if (!rcm.header[keyName] || rcm.header[keyName].length === 0) {
228 | continue;
229 | }
230 |
231 | // Checks whether the control file reader is provided.
232 | if (!controlFileReader) {
233 | throw new Error('Control file reader is not specified');
234 | }
235 |
236 | // Reads the control file.
237 | const fileName = String.fromCharCode(...rcm.header[keyName]);
238 | const buf = await controlFileReader(fileName, (/^[\x20-\x7E]*$/u.test(fileName)) ? undefined : rcm.header[keyName]).catch((e) => {
239 | throw new Error(`Control file not found: ${fileName}${(settings.debug) ? `\n${e}` : ''}`);
240 | });
241 |
242 | // Parses the control file.
243 | console.assert(buf);
244 | const sysExs = {
245 | MTD: convertMtdToSysEx,
246 | CM6: convertCm6ToSysEx,
247 | GSD: convertGsdToSysEx,
248 | GSD2: convertGsdToSysEx,
249 | }[kind](buf);
250 | if (!sysExs) {
251 | throw new Error(`Not ${kind.slice(0, 3)} file: ${fileName}`);
252 | }
253 |
254 | rcm.header[keyData] = buf;
255 | rcm.header[keySysEx] = sysExs;
256 |
257 | // Gets Patch Memory information for user patches.
258 | if (kind === 'MTD') {
259 | const patches = sysExs.filter((e) => {
260 | // Extracts SysExs regarding Patch Memory. (#1-#128)
261 | console.assert(e[0] === 0xf0 && e[1] === 0x41 && e[2] === 0x10 && e[3] === 0x16 && e[4] === 0x12);
262 | return (e[5] === 0x05); // Address '05 xx xx' is for Patch Memory
263 | }).reduce((p, c) => {
264 | // Splits payloads of SysExs by 8-byte.
265 | console.assert(c.length > 5 + 3 + 2);
266 | for (let i = 5 + 3; i < c.length; i += 8) {
267 | const e = c.slice(i, i + 8);
268 | if (e.length === 8) {
269 | p.push(e);
270 | }
271 | }
272 | return p;
273 | }, []);
274 | console.assert(patches.length === 192);
275 | rcm.header.patches = patches;
276 | }
277 | }
278 |
279 | // Executes post-processing for each track.
280 | if (!rcm.header.isMCP) {
281 | // For RCP/G36
282 | for (const track of rcm.tracks) {
283 | // Sets MIDI channel No. and port No.
284 | track.chNo = (track.midiCh >= 0) ? track.midiCh % 16 : -1;
285 | track.portNo = (track.midiCh >= 0) ? Math.trunc(track.midiCh / 16) : 0;
286 |
287 | // Reinterprets ST+ if necessary.
288 | if (!rcm.header.isF && !rcm.header.isG) {
289 | console.assert('stShiftS' in track && 'stShiftU' in track);
290 | if (settings.stPlus === 'signed') {
291 | if (track.stShiftS !== track.stShift && (track.stShiftS < -99 || 99 < track.stShiftS)) {
292 | console.warn(`ST+ has been converted to signed as specified. (${track.stShift} -> ${track.stShiftS}) But, it seems to be unsigned.`);
293 | }
294 | track.stShift = track.stShiftS;
295 | } else if (settings.stPlus === 'unsigned') {
296 | track.stShift = track.stShiftU;
297 | }
298 | }
299 |
300 | // Extracts same measures and loops.
301 | track.extractedEvents = extractEvents(track.events, rcm.header.timeBase, false, settings);
302 | }
303 | } else {
304 | // For MCP
305 | for (const track of rcm.tracks.slice(1, -1)) {
306 | // Sets MIDI channel No.
307 | track.chNo = (track.midiCh >= 0) ? track.midiCh : -1;
308 |
309 | // Extracts loops.
310 | track.extractedEvents = extractEvents(track.events, rcm.header.timeBase, true, settings);
311 | }
312 |
313 | // Extracts rhythm track.
314 | console.assert(rcm.tracks.length >= 10);
315 | const seqTrack = rcm.tracks[9];
316 | const patternTrack = rcm.tracks[0];
317 |
318 | seqTrack.chNo = seqTrack.midiCh;
319 | seqTrack.extractedEvents = extractRhythm(seqTrack.events, patternTrack.events, settings);
320 | }
321 |
322 | return rcm;
323 | }
324 |
325 | export function parseMcp(buf) {
326 | // Checks the arguments.
327 | if (!buf || !buf.length) {
328 | throw new Error(`Invalid argument: ${buf}`);
329 | }
330 |
331 | // Checks the file header.
332 | // Note: Confirmed 3 types of header: 'M1', 'MC', and [0x00, 0x00]
333 | if (buf.length < 256) {
334 | return null;
335 | }
336 | const id = buf.slice(0x00, 0x02);
337 | if (!/^(?:M1|MC)$/u.test(String.fromCharCode(...id)) && !(id[0] === 0x00 && id[1] === 0x00)) {
338 | return null;
339 | }
340 |
341 | const view = new DataView(buf.buffer, buf.byteOffset);
342 | const rcm = {header: {isMCP: true, maxTracks: 1 + 8 + 1}, tracks: []};
343 |
344 | // Header
345 | rcm.header.title = buf.slice(0x02, 0x20);
346 |
347 | rcm.header.timeBase = view.getUint8(0x20);
348 | rcm.header.tempo = view.getUint8(0x21);
349 | rcm.header.beatN = view.getUint8(0x22);
350 | rcm.header.beatD = view.getUint8(0x23);
351 | rcm.header.key = view.getUint8(0x24);
352 |
353 | if (buf[0x60] !== 0x00 && buf[0x60] !== 0x20) {
354 | rcm.header.fileNameMTD = new Uint8Array([...rawTrim(rawTrimNul(buf.slice(0x60, 0x66))), '.'.codePointAt(), ...rawTrim(rawTrimNul(buf.slice(0x66, 0x69)))]);
355 | }
356 |
357 | // Tracks
358 | rcm.tracks = [...new Array(rcm.header.maxTracks)].map((_, i) => {
359 | const track = {events: []};
360 | if (i > 0) {
361 | track.midiCh = view.getInt8(0x40 + i - 1);
362 | track.isCMU = (buf[0x50 + i - 1] !== 0);
363 | track.memo = buf.slice(0x70 + (i - 1) * 16, 0x70 + i * 16);
364 | }
365 | return track;
366 | });
367 |
368 | // All events
369 | const events = buf.slice(0x0100).reduce((p, _, i, a) => {
370 | if (i % 4 === 0) {
371 | p.push(a.slice(i, i + 4));
372 | }
373 | return p;
374 | }, []);
375 | if (events[events.length - 1].length !== 4) {
376 | events.pop();
377 | }
378 |
379 | // Separates all the events into each track.
380 | let trackNo = 0;
381 | for (const event of events) {
382 | console.assert(Array.isArray(rcm.tracks[trackNo].events));
383 | rcm.tracks[trackNo].events.push(event);
384 |
385 | if (event[0] === EVENT_MCP.TrackEnd) {
386 | trackNo++;
387 | if (trackNo >= rcm.header.maxTracks) {
388 | break;
389 | }
390 | }
391 | }
392 |
393 | return rcm;
394 | }
395 |
396 | export function parseRcp(buf) {
397 | // Checks the arguments.
398 | if (!buf || !buf.length) {
399 | throw new Error(`Invalid argument: ${buf}`);
400 | }
401 |
402 | // Checks the file header.
403 | if (buf.length < 518 || !String.fromCharCode(...buf.slice(0x0000, 0x0020)).startsWith('RCM-PC98V2.0(C)COME ON MUSIC')) {
404 | return null;
405 | }
406 |
407 | const view = new DataView(buf.buffer, buf.byteOffset);
408 | const rcm = {header: {}, tracks: []};
409 |
410 | // Header
411 | rcm.header.title = buf.slice(0x0020, 0x0060);
412 | rcm.header.memoLines = [...new Array(12)].map((_, i) => buf.slice(0x0060 + 28 * i, 0x0060 + 28 * (i + 1)));
413 |
414 | rcm.header.timeBase = (view.getUint8(0x01e7) << 8) | view.getUint8(0x01c0);
415 | rcm.header.tempo = view.getUint8(0x01c1);
416 | rcm.header.beatN = view.getUint8(0x01c2);
417 | rcm.header.beatD = view.getUint8(0x01c3);
418 | rcm.header.key = view.getUint8(0x01c4);
419 | rcm.header.playBias = view.getInt8(0x01c5);
420 |
421 | rcm.header.fileNameCM6 = rawTrim(rawTrimNul(buf.slice(0x01c6, 0x01d2)));
422 | rcm.header.fileNameGSD = rawTrim(rawTrimNul(buf.slice(0x01d6, 0x01e2)));
423 |
424 | const trackNum = view.getUint8(0x01e6);
425 | rcm.header.maxTracks = (trackNum === 0) ? 18 : trackNum;
426 | rcm.header.isF = (trackNum !== 0);
427 |
428 | rcm.header.userSysExs = [...new Array(8)].map((_, i) => {
429 | const index = 0x0406 + 48 * i;
430 | return {
431 | memo: buf.slice(index, index + 24),
432 | bytes: buf.slice(index + 24, index + 48),
433 | };
434 | });
435 |
436 | // Tracks
437 | const HEADER_LENGTH = 44;
438 | const EVENT_LENGTH = 4;
439 | let index = 0x0586;
440 | for (let i = 0; i < rcm.header.maxTracks && index + HEADER_LENGTH < buf.length; i++) {
441 | // If the footer data found, terminates the loop.
442 | if (String.fromCharCode(...buf.slice(index, index + 4)).startsWith('RCFW')) {
443 | break;
444 | }
445 |
446 | const track = {};
447 |
448 | // Track header
449 | let size = view.getUint16(index, true);
450 | if (size < HEADER_LENGTH || index + size > buf.length) {
451 | console.warn(`Invalid track size: ${size}`);
452 | break;
453 | }
454 |
455 | track.trackNo = view.getUint8(index + 2);
456 | track.midiCh = view.getInt8(index + 4);
457 | track.keyShift = view.getUint8(index + 5);
458 | track.stShiftS = view.getInt8(index + 6);
459 | track.stShiftU = view.getUint8(index + 6);
460 | track.mode = view.getUint8(index + 7);
461 | track.memo = buf.slice(index + 8, index + 44);
462 |
463 | // Track events
464 | let events = buf.slice(index + HEADER_LENGTH, index + size).reduce((p, _, i, a) => {
465 | if (i % EVENT_LENGTH === 0) {
466 | const event = a.slice(i, i + EVENT_LENGTH);
467 | p.push(event);
468 | }
469 | return p;
470 | }, []);
471 |
472 | // Checks whether the last event is End of Track to judge the track size information is reliable or not.
473 | // If the track size information seems to be wrong, gets actual size by End of Track event.
474 | // Note 1: A very few RCP files contain unknown "FF xx xx xx" event as if it is End of Track.
475 | // Note 2: STed2 (a Recomposer clone) seems to treat 16-bit track size information as 18-bit
476 | // by using unused lower 2-bit. But, this program doesn't follow to such unofficial extension.
477 | const lastEvent = events[events.length - 1];
478 | if ((lastEvent[0] !== 0xfe && lastEvent[0] !== 0xff) || lastEvent.length !== EVENT_LENGTH) {
479 | // Track events
480 | let isEot = false;
481 | events = buf.slice(index + HEADER_LENGTH, buf.length).reduce((p, _, i, a) => {
482 | if (i % EVENT_LENGTH === 0 && !isEot) {
483 | const event = a.slice(i, i + EVENT_LENGTH);
484 | if (event.length === EVENT_LENGTH) {
485 | p.push(event);
486 | if (event[0] === 0xfe || event[0] === 0xff) {
487 | isEot = true;
488 | }
489 | }
490 | }
491 | return p;
492 | }, []);
493 |
494 | // Track size
495 | const actualSize = HEADER_LENGTH + EVENT_LENGTH * events.length;
496 | if (size !== actualSize) {
497 | console.warn(`Track size information doesn't match the actual size: (${size} -> ${actualSize})`);
498 | }
499 | size = actualSize;
500 | }
501 |
502 | track.events = events;
503 |
504 | rcm.tracks.push(track);
505 |
506 | index += size;
507 | }
508 |
509 | // Sets ST+ for each track.
510 | const isStSigned = rcm.header.isF || rcm.tracks.every((track) => (-99 <= track.stShiftS && track.stShiftS <= 99));
511 | rcm.tracks.forEach((track) => {
512 | track.stShift = (isStSigned) ? track.stShiftS : track.stShiftU;
513 | });
514 |
515 | return rcm;
516 | }
517 |
518 | export function parseG36(buf) {
519 | // Checks the arguments.
520 | if (!buf || !buf.length) {
521 | throw new Error(`Invalid argument: ${buf}`);
522 | }
523 |
524 | // Checks the file header.
525 | if (buf.length < 518 || !String.fromCharCode(...buf.slice(0x0000, 0x0020)).startsWith('COME ON MUSIC RECOMPOSER RCP3.0')) {
526 | return null;
527 | }
528 |
529 | const view = new DataView(buf.buffer, buf.byteOffset);
530 | const rcm = {header: {isG: true}, tracks: []};
531 |
532 | // Header
533 | rcm.header.title = buf.slice(0x0020, 0x0060);
534 | rcm.header.memoLines = [...new Array(12)].map((_, i) => buf.slice(0x00a0 + 30 * i, 0x00a0 + 30 * (i + 1)));
535 |
536 | rcm.header.maxTracks = view.getUint16(0x0208, true);
537 | rcm.header.timeBase = view.getUint16(0x020a, true);
538 | rcm.header.tempo = view.getUint16(0x020c, true);
539 | rcm.header.beatN = view.getUint8(0x020e);
540 | rcm.header.beatD = view.getUint8(0x020f);
541 | rcm.header.key = view.getUint8(0x0210);
542 | rcm.header.playBias = view.getInt8(0x0211);
543 |
544 | rcm.header.fileNameGSD = rawTrim(rawTrimNul(buf.slice(0x0298, 0x02a8)));
545 | rcm.header.fileNameGSD2 = rawTrim(rawTrimNul(buf.slice(0x02a8, 0x02b8)));
546 | rcm.header.fileNameCM6 = rawTrim(rawTrimNul(buf.slice(0x02b8, 0x02c8)));
547 |
548 | rcm.header.userSysExs = [...new Array(8)].map((_, i) => {
549 | const index = 0x0b18 + 48 * i;
550 | return {
551 | memo: buf.slice(index, index + 23),
552 | bytes: buf.slice(index + 23, index + 48),
553 | };
554 | });
555 |
556 | // Tracks
557 | const HEADER_LENGTH = 46;
558 | const EVENT_LENGTH = 6;
559 | let index = 0x0c98;
560 | for (let i = 0; i < rcm.header.maxTracks && index + HEADER_LENGTH < buf.length; i++) {
561 | // If the footer data found, terminates the loop.
562 | if (String.fromCharCode(...buf.slice(index, index + 4)).startsWith('RCFW')) {
563 | break;
564 | }
565 |
566 | const track = {};
567 |
568 | // Track header
569 | const size = view.getUint32(index, true);
570 | if (size < HEADER_LENGTH || index + size > buf.length) {
571 | console.warn(`Invalid track size: ${size}`);
572 | break;
573 | }
574 |
575 | track.trackNo = view.getUint8(index + 4);
576 | track.midiCh = view.getInt8(index + 6);
577 | track.keyShift = view.getUint8(index + 7);
578 | track.stShift = view.getInt8(index + 8);
579 | track.mode = view.getUint8(index + 9);
580 | track.memo = buf.slice(index + 10, index + 46);
581 |
582 | // Track events
583 | track.events = buf.slice(index + HEADER_LENGTH, index + size).reduce((p, _, i, a) => {
584 | if (i % EVENT_LENGTH === 0) {
585 | p.push(a.slice(i, i + EVENT_LENGTH));
586 | }
587 | return p;
588 | }, []);
589 |
590 | rcm.tracks.push(track);
591 |
592 | index += size;
593 | }
594 |
595 | return rcm;
596 | }
597 |
598 | function extractEvents(events, timeBase, isMCP, settings) {
599 | console.assert(Array.isArray(events), 'Invalid argument', {events});
600 | console.assert(timeBase > 0, 'Invalid argument', {timeBase});
601 | console.assert(settings, 'Invalid argument', {settings});
602 |
603 | if (events.length === 0) {
604 | return [];
605 | }
606 |
607 | // Sets constants and chooses event parser.
608 | const EVENT = (isMCP) ? EVENT_MCP : EVENT_RCP;
609 | const EVENT_LENGTH = events[0].length;
610 | console.assert(EVENT_LENGTH === 4 || EVENT_LENGTH === 6, 'Event length must be 4 or 6', {EVENT_LENGTH});
611 | console.assert(events.every((e) => e.length === EVENT_LENGTH), 'All of events must be same length', {events});
612 | const HEADER_LENGTH = (isMCP) ? NaN : (EVENT_LENGTH === 4) ? 44 : 46;
613 | const convertEvent = (isMCP) ? (e) => e : (EVENT_LENGTH === 4) ? convertEvent4byte : convertEvent6byte;
614 |
615 | // Extracts same measures and loops.
616 | const extractedEvents = [];
617 | const stacks = [];
618 | let lastIndex = -1;
619 | for (let index = 0; index < events.length;) {
620 | const event = convertEvent(events[index]);
621 |
622 | // Resolves Same Measure event.
623 | if (event[0] === EVENT.SameMeas) {
624 | // If it is already in Same Measure mode, quits Same Measure.
625 | if (lastIndex >= 0) {
626 | // Leaves Same Measure mode and goes backs to the previous position.
627 | index = lastIndex + 1;
628 | lastIndex = -1;
629 |
630 | // Adds a dummy End Measure event.
631 | extractedEvents.push([EVENT.MeasEnd, 0x00, 0xfc, 0x01]);
632 |
633 | } else {
634 | // Enters Same Measure mode.
635 | lastIndex = index;
636 |
637 | // If the previous event isn't an End Measure event, adds a dummy End Measure event.
638 | if (index > 0 && events[index - 1][0] !== EVENT.MeasEnd && events[index - 1][0] !== EVENT.SameMeas) {
639 | extractedEvents.push([EVENT.MeasEnd, 0x00, 0xfc, 0x02]);
640 | }
641 |
642 | // Moves the current index to the measure. // TODO: Avoid infinity loop
643 | let counter = 0;
644 | while (events[index][0] === EVENT.SameMeas) {
645 | const [cmd, measure, offset] = convertEvent(events[index]);
646 | console.assert(cmd === EVENT.SameMeas, {cmd, measure, offset});
647 |
648 | index = (offset - HEADER_LENGTH) / EVENT_LENGTH;
649 | validateAndThrow(Number.isInteger(index) && (0 <= index && index < events.length), `Invalid Same Measure event: ${{cmd, measure, offset}}`);
650 |
651 | counter++;
652 | validateAndThrow(counter < 100, `Detected infinity Same Measure reference.`);
653 | }
654 | }
655 | continue;
656 | }
657 |
658 | // Handles a special event or just adds a normal event to the event array.
659 | switch (event[0]) {
660 | case EVENT.SameMeas:
661 | console.assert(false, 'Same Measure event must be resolved', {event});
662 | break;
663 |
664 | case EVENT.LoopStart:
665 | if (stacks.length < settings.maxLoopNest) {
666 | stacks.push({index, lastIndex,
667 | count: -1,
668 | extractedIndex: (extractedEvents.length > 0) ? extractedEvents.length - 1 : 0,
669 | });
670 | } else {
671 | console.warn(`Detected more than ${settings.maxLoopNest}-level of nested loops. Skipped.`);
672 | }
673 | index++;
674 | break;
675 |
676 | case EVENT.LoopEnd:
677 | if (stacks.length > 0) {
678 | const lastStack = stacks[stacks.length - 1];
679 |
680 | // If it is a first Loop End event, sets a counter value to it.
681 | if (lastStack.count < 0) {
682 | // Checks whether the loop is infinite and revises the number of loops if necessary.
683 | if (event[1] > 0) {
684 | lastStack.count = event[1];
685 | } else {
686 | console.warn(`Detected an infinite loop. Set number of loops to ${settings.infinityLoopCount}.`);
687 | lastStack.count = settings.infinityLoopCount;
688 | }
689 |
690 | // Checks whether it would be a "loop bomb" and revises the number of loops if necessary.
691 | // Note: "0xf5" means Musickey of RCP. As for RCP, >=0xf5 events don't have ST. But, as for MCP, TEMPO event (0xfa) also has ST. Won't fix.
692 | const beatNum = extractedEvents.slice(lastStack.extractedIndex).reduce((p, c) => ((c[0] < 0xf5) ? p + c[1] : p), 0) / timeBase;
693 | if (beatNum * lastStack.count >= settings.loopBombThreshold && lastStack.count > settings.infinityLoopCount) {
694 | console.warn(`Detected a loop bomb. Set number of loops to ${settings.infinityLoopCount}.`);
695 | lastStack.count = settings.infinityLoopCount;
696 | }
697 | }
698 |
699 | // Decrements the loop counter and moves the index up to the counter.
700 | lastStack.count--;
701 | if (lastStack.count > 0) {
702 | index = lastStack.index + 1;
703 | lastIndex = lastStack.lastIndex;
704 | } else {
705 | const _ = stacks.pop();
706 | console.assert(_.count === 0, {stacks});
707 | console.assert(_.lastIndex === lastStack.lastIndex, {stacks});
708 | index++;
709 | }
710 |
711 | } else {
712 | console.warn(`Detected a dangling Loop End event. Skipped.`);
713 | index++;
714 | }
715 | break;
716 |
717 | case EVENT.TrackEnd:
718 | if (stacks.length > 0) {
719 | console.warn(`Detected ${stacks.length}-level of unclosed loop. Skipped.`);
720 | }
721 | /* FALLTHRU */
722 | case EVENT.MeasEnd:
723 | if (lastIndex >= 0) {
724 | index = lastIndex + 1;
725 | lastIndex = -1;
726 | } else {
727 | index++;
728 | }
729 | extractedEvents.push([...event]);
730 | break;
731 |
732 | case EVENT.TrExcl:
733 | case EVENT.ExtCmd:
734 | case EVENT.Comment:
735 | {
736 | // Concatenates trailing F7 events.
737 | const longEvent = [...event];
738 | index++;
739 |
740 | if (events[index][0] !== EVENT.SecondEvt && event[0] === EVENT.TrExcl) {
741 | console.warn(`Detected an empty Tr.Excl event: [${hexStr(events[index - 1])}], [${hexStr(events[index])}], ...`);
742 | }
743 |
744 | while (events[index][0] === EVENT.SecondEvt) {
745 | longEvent.push(...convertEvent(events[index]).slice(1));
746 | index++;
747 | }
748 |
749 | // Trims trailing 0xf7.
750 | const end = String.fromCharCode(...longEvent).replace(/\xf7+$/u, '').length;
751 | extractedEvents.push(longEvent.slice(0, end));
752 | }
753 | break;
754 |
755 | case EVENT.SecondEvt:
756 | ((settings.ignoreWrongEvent) ? validateAndIgnore : validateAndThrow)(false, `Detected an unexpected F7 event: [${hexStr(events[index])}]`);
757 | index++;
758 | break;
759 |
760 | default:
761 | extractedEvents.push([...event]);
762 | index++;
763 | break;
764 | }
765 | }
766 |
767 | return extractedEvents;
768 |
769 | function convertEvent4byte(bytes) {
770 | console.assert(bytes && bytes.length && bytes.length === 4, 'Invalid argument', {bytes});
771 |
772 | if (bytes[0] === EVENT_RCP.Comment || bytes[0] === EVENT_RCP.SecondEvt) {
773 | return [bytes[0], bytes[2], bytes[3]];
774 | } else if (bytes[0] === EVENT_RCP.SameMeas) {
775 | const measure = bytes[1] | ((bytes[2] & 0x03) << 8);
776 | const offset = (bytes[2] & 0xfc) | (bytes[3] << 8);
777 | return [bytes[0], measure, offset];
778 | } else {
779 | return [...bytes];
780 | }
781 | }
782 | function convertEvent6byte(bytes) {
783 | console.assert(bytes && bytes.length && bytes.length === 6, 'Invalid argument', {bytes});
784 |
785 | if (bytes[0] === EVENT_RCP.Comment || bytes[0] === EVENT_RCP.SecondEvt) {
786 | return [...bytes];
787 | } else if (bytes[0] === EVENT_RCP.SameMeas) {
788 | const measure = bytes[2] | (bytes[3] << 8);
789 | const offset = (bytes[4] | (bytes[5] << 8)) * 6 - 0xf2;
790 | return [bytes[0], measure, offset];
791 | } else {
792 | return [bytes[0], bytes[2] | (bytes[3] << 8), bytes[4] | (bytes[5] << 8), bytes[1]];
793 | }
794 | }
795 | }
796 |
797 | function extractRhythm(seqEvents, patternEvents, settings) {
798 | console.assert(Array.isArray(seqEvents), 'Invalid argument', {seqEvents});
799 | console.assert(Array.isArray(patternEvents), 'Invalid argument', {patternEvents});
800 | console.assert(settings, 'Invalid argument', {settings});
801 |
802 | const validate = (settings.ignoreWrongEvent) ? (isValid, message) => validateAndIgnore(isValid, message) : (isValid, message) => validateAndThrow(isValid, message);
803 |
804 | // Rhythm pattern track
805 | const patterns = patternEvents.reduce((p, c, i, a) => {
806 | if (i % (16 + 1) === 0 && c[0] !== EVENT_MCP.TrackEnd) {
807 | const pattern = a.slice(i, i + 16);
808 | if (validate(pattern.length === 16 && a[i + 16][0] === EVENT_MCP.MeasEnd, `Invalid rhythm pattern.`)) {
809 | p.push(pattern);
810 | } else {
811 | // Adds a dummy data.
812 | p.push([...[...new Array(16)].map((_) => [0x00, 0x00, 0x00, 0x00])]);
813 | }
814 | }
815 | return p;
816 | }, []);
817 |
818 | // Sequence track
819 | const extractedEvents = [];
820 | for (const seq of seqEvents) {
821 | if (seq[0] === EVENT_MCP.TrackEnd) {
822 | break;
823 | }
824 |
825 | // Chooses a rhythm pattern.
826 | const [patternNo, ...velValues] = seq;
827 | const pattern = patterns[patternNo - 1];
828 |
829 | // Extracts the rhythm pattern with velocity data from sequence track.
830 | if (validate(pattern, `Invalid rhythm pattern No.${patternNo}: [${hexStr(seq)}]`)) {
831 | for (const shot of pattern) {
832 | const st = shot[3];
833 | const velBits = shot.slice(0, 3).reduce((p, c) => {
834 | p.push(...[(c >> 6) & 0x03, (c >> 4) & 0x03, (c >> 2) & 0x03, c & 0x03]);
835 | return p;
836 | }, []);
837 |
838 | const events = velBits.reduce((p, c, i) => {
839 | if (c > 0) {
840 | const event = [
841 | // BD, SD, LT, MT, HT, RS, HC, CH, OH, CC, RC
842 | [0, 36, 38, 41, 45, 48, 37, 39, 42, 46, 49, 51][i], // Note No.
843 | 0, // Step time
844 | 1, // Gate time
845 | velValues[c - 1], // Velocity
846 | ];
847 | p.push(event);
848 | }
849 | return p;
850 | }, []);
851 | events.push([0, st, 0, 0]); // For step time
852 |
853 | extractedEvents.push(...events);
854 | }
855 |
856 | // Adds a dummy End Measure event.
857 | extractedEvents.push([EVENT_MCP.MeasEnd, 0x00, 0xfd, 0x01]);
858 | }
859 | }
860 |
861 | return extractedEvents;
862 | }
863 |
864 | function calcSetupMeasureTick(beatN, beatD, timeBase, minTick) {
865 | console.assert(Number.isInteger(Math.log2(beatD)), 'Invalid argument', {beatD});
866 |
867 | const requiredTick = ((beatN === 3 && beatD === 4) || (beatN === 6 && beatD === 8)) ? timeBase * 3 : timeBase * 4;
868 | const unit = beatN * timeBase * 4 / beatD;
869 |
870 | let setupTick = unit * Math.trunc(requiredTick / unit);
871 | while (setupTick < requiredTick || setupTick < minTick) {
872 | setupTick += unit;
873 | }
874 |
875 | console.assert(Number.isInteger(setupTick));
876 | return setupTick;
877 | }
878 |
879 | function spaceEachSysEx(sysExs, maxTick, timeBase, isOldMt32) {
880 | console.assert(sysExs && sysExs.length, 'Invalid argument', {sysExs});
881 | console.assert(sysExs.length <= maxTick, 'Too many SysEx', {sysExs});
882 | console.assert(maxTick >= timeBase, 'Too small tick time', {maxTick, timeBase});
883 |
884 | // Calculates the time required for sending and executing each of SysEx.
885 | const timings = sysExs.map((sysEx) => {
886 | // Transmit time of SysEx
887 | let usec = sysEx.length * (8 + 1 + 1) * 1000 * 1000 / 31250.0;
888 |
889 | // Additional wait time
890 | const [tmpF0, mfrId, deviceId, modelId, command, addrH, addrM, addrL, ...rest] = sysEx;
891 | console.assert(tmpF0 === 0xf0);
892 | console.assert(rest[rest.length - 1] === 0xf7);
893 | let isReset = false;
894 | if (mfrId === 0x41 && deviceId === 0x10 && command === 0x12) {
895 | switch (modelId) {
896 | case 0x16: // MT-32/CM-64
897 | if (addrH === 0x7f) {
898 | // Waits for reset.
899 | if (isOldMt32) {
900 | // MT-32 Ver.1.xx requires 420 msec of delay after All Parameters Reset.
901 | // Note: If the wait time is too short, "Exc. Buffer overflow" error occurs when receiving next SysEx. (confirmed on MT-32 Ver.1.07)
902 | usec += 420 * 1000;
903 | } else {
904 | // No basis for the wait time. Makes it same as GS reset.
905 | usec += 50 * 1000;
906 | }
907 | isReset = true;
908 | } else if ((0x00 < addrH && addrH <= 0x20) && isOldMt32) {
909 | // MT-32 Ver.1.xx requires 40 msec of delay between SysExs.
910 | usec += 40 * 1000;
911 | } else {
912 | // DT1 needs more than 20 msec time interval.
913 | usec += 20 * 1000;
914 | }
915 | break;
916 | case 0x42: // GS
917 | if (addrH === 0x40 && addrM === 0x00 && addrL === 0x7f) {
918 | // Waits for GS reset.
919 | usec += 50 * 1000;
920 | isReset = true;
921 | } else {
922 | // DT1 needs more than 20 msec time interval.
923 | usec += 20 * 1000;
924 | }
925 | break;
926 | default:
927 | console.assert(false);
928 | break;
929 | }
930 | }
931 |
932 | return {sysEx, usec, isReset};
933 | });
934 |
935 | // Calculates each tick time from the ratio of the time of each SysEx to the total time of SysEx.
936 | const totalUsec = timings.reduce((p, c) => p + c.usec, 0);
937 | timings.forEach((e) => {
938 | e.tick = Math.max(Math.trunc(e.usec * maxTick / totalUsec), 1);
939 | e.usecPerBeat = e.usec * timeBase / e.tick;
940 | });
941 |
942 | // Decreases each tick time to set all SysEx with in given time frame.
943 | while (getTotalTick(timings) > maxTick) {
944 | const minUsecPerBeat = Math.min(...timings.filter((e) => e.tick > 1).map((e) => e.usecPerBeat));
945 | timings.filter((e) => e.usecPerBeat === minUsecPerBeat).forEach((e) => {
946 | e.tick--;
947 | console.assert(e.tick > 0);
948 | e.usecPerBeat = e.usec * timeBase / e.tick;
949 | });
950 | }
951 |
952 | // Increases each tick time to make tempo faster as much as possible.
953 | while (getTotalTick(timings) < maxTick) {
954 | const maxUsecPerBeat = Math.max(...timings.map((e) => e.usecPerBeat));
955 | const elems = timings.filter((e) => e.usecPerBeat === maxUsecPerBeat);
956 | if (getTotalTick(timings) + elems.length > maxTick) {
957 | break;
958 | }
959 | elems.forEach((e) => {
960 | e.tick++;
961 | e.usecPerBeat = e.usec * timeBase / e.tick;
962 | });
963 | }
964 |
965 | return timings;
966 |
967 | function getTotalTick(timings) {
968 | return timings.reduce((p, c) => p + c.tick, 0);
969 | }
970 | }
971 |
972 | function getMeasureSt(rcm) {
973 | console.assert(rcm);
974 | console.assert(EVENT_RCP.MeasEnd === EVENT_MCP.MeasEnd);
975 | console.assert(EVENT_RCP.TrackEnd === EVENT_MCP.TrackEnd);
976 |
977 | // Extracts all events and calculates step time of every measure.
978 | const allStMeasures = rcm.tracks.filter((track) => track.extractedEvents).map((track) => {
979 | const stMeasures = [];
980 | let st = 0;
981 | for (const event of track.extractedEvents) {
982 | if (event[0] < 0xf5) {
983 | st += event[1];
984 | } else if (event[0] === EVENT_RCP.MeasEnd || event[0] === EVENT_RCP.TrackEnd) {
985 | if (st > 0) {
986 | stMeasures.push(st);
987 | st = 0;
988 | }
989 | }
990 | }
991 | return stMeasures;
992 | });
993 |
994 | // Chooses the most "common" step times of each measure from all the tracks.
995 | const maxMeasureNo = Math.max(...allStMeasures.map((e) => e.length));
996 | const wholeStMeasures = [];
997 | let survivors = new Set([...new Array(allStMeasures.length)].map((_, i) => i));
998 | for (let measureNo = 0; measureNo < maxMeasureNo; measureNo++) {
999 | // Gets each track's step time in the current measure.
1000 | const map = new Map();
1001 | for (let trackNo = 0; trackNo < allStMeasures.length; trackNo++) {
1002 | if (!survivors.has(trackNo)) {
1003 | continue;
1004 | }
1005 | if (measureNo >= allStMeasures[trackNo].length) {
1006 | survivors.delete(trackNo);
1007 | continue;
1008 | }
1009 |
1010 | const st = allStMeasures[trackNo][measureNo];
1011 | if (map.has(st)) {
1012 | console.assert(Array.isArray(map.get(st)));
1013 | map.get(st).push(trackNo);
1014 | } else {
1015 | map.set(st, [trackNo]);
1016 | }
1017 | }
1018 |
1019 | if (survivors.size === 0) {
1020 | break;
1021 | }
1022 |
1023 | // Chooses this measure's step time by "majority vote".
1024 | const entries = [...map.entries()];
1025 | const matchNum = Math.max(...entries.map(([_, trackNos]) => trackNos.length));
1026 | const [st, trackNos] = entries.find(([_, trackNos]) => trackNos.length === matchNum);
1027 | wholeStMeasures.push(st);
1028 | survivors = new Set(trackNos);
1029 | }
1030 |
1031 | return wholeStMeasures;
1032 | }
1033 |
1034 | export function convertRcmToSeq(rcm, options) {
1035 | // Checks the arguments.
1036 | if (!rcm) {
1037 | throw new Error(`Invalid argument: ${rcm}`);
1038 | }
1039 |
1040 | // Makes a settings object from the default settings and the specified ones.
1041 | const settings = {...defaultSettings, ...options};
1042 | const bitsTable = {none: 0b00, left: 0b01, right: 0b10, both: 0b11};
1043 |
1044 | // Checks the settings.
1045 | if (Object.keys(settings).filter((e) => /^trim/u.test(e)).some((e) => !Object.keys(bitsTable).includes(settings[e])) ||
1046 | Object.keys(settings).filter((e) => e in defaultSettings).some((e) => typeof settings[e] !== typeof defaultSettings[e]) ||
1047 | !['auto', 'signed', 'unsigned'].includes(settings.stPlus)) {
1048 | throw new Error(`Invalid settings: ${settings}`);
1049 | }
1050 |
1051 | // Makes functions from the settings.
1052 | const setMetaTrackName = (track, timestamp, bytes) => setEvent(track, timestamp, makeMetaText(0x03, rawTrim(bytes, bitsTable[settings.trimTrackName])));
1053 | const setMetaTextMemo = (!settings.metaTextMemo) ? nop : (track, timestamp, bytes) => setEvent(track, timestamp, makeMetaText(0x01, rawTrim(bytes, bitsTable[settings.trimTextMemo])));
1054 | const setMetaTextComment = (!settings.metaTextComment) ? nop : (track, timestamp, bytes) => setEvent(track, timestamp, makeMetaText(0x01, rawTrim(bytes, bitsTable[settings.trimTextComment])));
1055 | const setMetaTextUsrExc = (!settings.metaTextUsrExc) ? nop : (track, timestamp, bytes) => setEvent(track, timestamp, makeMetaText(0x01, rawTrim(bytes, bitsTable[settings.trimTextUsrExc])));
1056 | const setMetaCue = (!settings.metaCue) ? nop : (track, timestamp, bytes) => setEvent(track, timestamp, makeMetaText(0x07, bytes));
1057 |
1058 | const makeNoteOff = (settings.noteOff) ? (chNo, noteNo) => makeMidiEvent(0x8, chNo, noteNo, settings.noteOffVel) : (chNo, noteNo) => makeMidiEvent(0x9, chNo, noteNo, 0);
1059 |
1060 | const validateRange = (settings.ignoreOutOfRange) ? (isValid, message) => validateAndIgnore(isValid, message) : (isValid, message) => validateAndThrow(isValid, message);
1061 | const throwOrIgnore = (settings.ignoreWrongEvent) ? (message) => validateAndIgnore(false, message) : (message) => validateAndThrow(false, message);
1062 |
1063 | // SMF-related variables
1064 | let startTime = 0;
1065 | const seq = {
1066 | timeBase: rcm.header.timeBase,
1067 | tracks: [],
1068 | };
1069 | const usecPerBeat = 60 * 1000 * 1000 / rcm.header.tempo;
1070 |
1071 | // Adds a conductor track.
1072 | const conductorTrack = new Map();
1073 | seq.tracks.push(conductorTrack);
1074 |
1075 | // Sequence Name and Text Events
1076 | setMetaTrackName(conductorTrack, 0, rcm.header.title);
1077 | if (rcm.header.memoLines && rcm.header.memoLines.some((e) => rawTrim(e).length > 0)) {
1078 | for (const memoLine of rcm.header.memoLines) {
1079 | setMetaTextMemo(conductorTrack, 0, memoLine);
1080 | }
1081 | }
1082 |
1083 | // Time Signature
1084 | const initialBeat = {numer: 4, denom: 4};
1085 | if (rcm.header.beatD !== 0 && (rcm.header.beatD & (rcm.header.beatD - 1) === 0)) {
1086 | initialBeat.numer = rcm.header.beatN;
1087 | initialBeat.denom = rcm.header.beatD;
1088 | }
1089 | setEvent(conductorTrack, 0, makeMetaTimeSignature(initialBeat.numer, initialBeat.denom));
1090 |
1091 | // Key Signature
1092 | if ('key' in rcm.header) {
1093 | setEvent(conductorTrack, 0, convertKeySignature(rcm.header.key));
1094 | }
1095 |
1096 | // Adds a setup measure which consists of SysEx converted from control files.
1097 | if (rcm.header.sysExsMTD || rcm.header.sysExsCM6 || rcm.header.sysExsGSD || rcm.header.sysExsGSD2) {
1098 | console.assert(!settings.ignoreCtrlFile);
1099 | const allSysExs = [];
1100 |
1101 | // Adds SysEx for GS.
1102 | if (rcm.header.sysExsGSD || rcm.header.sysExsGSD2) {
1103 | // Inserts GS reset SysEx.
1104 | if (settings.resetBeforeCtrl) {
1105 | allSysExs.push([0xf0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7f, 0x00, 0x41, 0xf7]);
1106 | }
1107 | }
1108 | if (rcm.header.sysExsGSD) {
1109 | // Adds SysEx from GSD file.
1110 | allSysExs.push(...rcm.header.sysExsGSD);
1111 | }
1112 | if (rcm.header.sysExsGSD2) {
1113 | // Adds SysEx from GSD2 file.
1114 | // TODO: Support >16ch
1115 | allSysExs.push(...rcm.header.sysExsGSD2);
1116 | }
1117 |
1118 | // Adds SysEx for MT-32/CM-64.
1119 | if (rcm.header.sysExsMTD || rcm.header.sysExsCM6) {
1120 | // Inserts a reset SysEx.
1121 | if (settings.resetBeforeCtrl) {
1122 | allSysExs.push([0xf0, 0x41, 0x10, 0x16, 0x12, 0x7f, 0x00, 0x00, 0x00, 0x01, 0xf7]);
1123 | }
1124 | }
1125 | if (rcm.header.sysExsMTD) {
1126 | // Removes redundant SysEx. (For User Patch)
1127 | const keys = new Set();
1128 | const newSysExs = rcm.header.sysExsMTD.reduce((p, c) => {
1129 | const key = c.slice(5, 8).map((e) => `${e}`).join(',');
1130 | if (!keys.has(key)) {
1131 | p.push(c);
1132 | keys.add(key);
1133 | }
1134 | return p;
1135 | }, []);
1136 | // Adds SysEx from MTD file.
1137 | allSysExs.push(...newSysExs);
1138 | } else if (rcm.header.sysExsCM6) {
1139 | // Adds SysEx from CM6 file.
1140 | allSysExs.push(...rcm.header.sysExsCM6);
1141 | }
1142 |
1143 | // Removes unnecessary SysEx.
1144 | const sysExs = (settings.optimizeCtrl) ? allSysExs.filter((e) => !isSysExRedundant(e)) : allSysExs;
1145 |
1146 | if (sysExs.length > 0) {
1147 | // Decides each interval between SysExs.
1148 | const extraTick = calcSetupMeasureTick(initialBeat.numer, initialBeat.denom, seq.timeBase, sysExs.length);
1149 | const timings = spaceEachSysEx(sysExs, extraTick, seq.timeBase, settings.extraSysexWait);
1150 | const maxUsecPerBeat = Math.max(...timings.map((e) => e.usecPerBeat));
1151 |
1152 | // Sets tempo slow during sending SysExs if necessary.
1153 | setEvent(conductorTrack, 0, makeMetaTempo((maxUsecPerBeat > usecPerBeat) ? maxUsecPerBeat : usecPerBeat));
1154 |
1155 | // Adds a new track for SysEx.
1156 | const track = new Map();
1157 | seq.tracks.push(track);
1158 | setMetaTrackName(track, 0, new Uint8Array([...'SysEx from control file'.split('').map((e) => e.charCodeAt(0))]));
1159 |
1160 | // Inserts SysExs from control files.
1161 | let timestamp = startTime;
1162 | for (const timing of timings) {
1163 | setEvent(track, timestamp, timing.sysEx);
1164 | timestamp += timing.tick;
1165 | }
1166 |
1167 | // Sets original tempo.
1168 | if (maxUsecPerBeat > usecPerBeat) {
1169 | setEvent(conductorTrack, timestamp, makeMetaTempo(usecPerBeat));
1170 | }
1171 |
1172 | // Shifts the start time.
1173 | startTime += extraTick;
1174 |
1175 | // Adds an End of Track event to the SysEx track.
1176 | setEvent(track, startTime, [0xff, 0x2f, 0x00]);
1177 |
1178 | } else {
1179 | // Set Tempo
1180 | setEvent(conductorTrack, 0, makeMetaTempo(usecPerBeat));
1181 | }
1182 |
1183 | } else {
1184 | // Set Tempo
1185 | setEvent(conductorTrack, 0, makeMetaTempo(usecPerBeat));
1186 | }
1187 |
1188 | // Adds Time Signature meta events from each measure's step time.
1189 | if (settings.metaTimeSignature) {
1190 | const stMeasures = getMeasureSt(rcm);
1191 | const maxMeasureSt = Math.max(seq.timeBase * initialBeat.numer * 2 / initialBeat.denom, 192 * 2);
1192 | const maxDenom = 16;
1193 | const minBeatSt = seq.timeBase * 4 / maxDenom;
1194 | if (stMeasures.every((st) => st <= maxMeasureSt) && stMeasures.every((st) => st % minBeatSt === 0)) {
1195 | // Makes each measure's time signature.
1196 | const beats = stMeasures.map((st) => {
1197 | for (let denom = initialBeat.denom; denom <= maxDenom; denom *= 2) {
1198 | const beatSt = seq.timeBase * 4 / denom;
1199 | const numer = st / beatSt;
1200 | if (Number.isInteger(numer)) {
1201 | return {numer, denom};
1202 | }
1203 | }
1204 | console.assert(false);
1205 | return null;
1206 | });
1207 | const dedupedBeats = beats.map((e, i, a) => {
1208 | const p = (i > 0) ? a[i - 1] : initialBeat;
1209 | return (e.numer === p.numer && e.denom === p.denom) ? null : e;
1210 | });
1211 |
1212 | // Adds Time Signature meta events.
1213 | let timestamp = 0;
1214 | console.assert(stMeasures.length === dedupedBeats.length);
1215 | for (let i = 0; i < stMeasures.length; i++) {
1216 | if (dedupedBeats[i]) {
1217 | setEvent(conductorTrack, startTime + timestamp, makeMetaTimeSignature(dedupedBeats[i].numer, dedupedBeats[i].denom));
1218 | }
1219 | timestamp += stMeasures[i];
1220 | }
1221 | }
1222 | }
1223 |
1224 | // Converts each track.
1225 | const EVENT = (rcm.header.isMCP) ? EVENT_MCP : EVENT_RCP;
1226 | const isAllPortSame = ((new Set(rcm.tracks.map((e) => e.portNo))).size === 1);
1227 | const isNoteOff = (rcm.header.isMCP) ? ((gt, st) => (gt < st)) : ((gt, st) => (gt <= st));
1228 | let maxDuration = 0;
1229 | const tempoEventMap = new Map();
1230 | for (const rcmTrack of rcm.tracks) {
1231 | // Skips the track if it is empty or muted.
1232 | if (!rcmTrack.extractedEvents || rcmTrack.extractedEvents.length <= 1 || (rcmTrack.mode & 0x01) !== 0) {
1233 | continue;
1234 | }
1235 |
1236 | const smfTrack = new Map();
1237 | const noteGts = new Array(128).fill(-1);
1238 | const patchNos = [...Array(128).keys()];
1239 | const keyShift = (rcm.header.isMCP || (rcmTrack.keyShift & 0x80) !== 0) ? 0 : rcm.header.playBias + rcmTrack.keyShift - ((rcmTrack.keyShift >= 0x40) ? 0x80 : 0);
1240 | let timestamp = startTime + (rcmTrack.stShift || 0);
1241 | let {chNo, portNo, midiCh} = rcmTrack;
1242 | let rolDev, rolBase, yamDev, yamBase; // TODO: Investigate whether they belong to track or global.
1243 |
1244 | // Track name
1245 | setMetaTrackName(smfTrack, 0, rcmTrack.memo);
1246 |
1247 | // If any port No. are not same among all the track, adds an unofficial MIDI Port meta event. (FF 21 01 pp)
1248 | if (!isAllPortSame) {
1249 | setEvent(smfTrack, 0, [0xff, 0x21, 0x01, portNo]);
1250 | }
1251 |
1252 | // Converts each RCM event to MIDI/SysEx/meta event.
1253 | for (const event of rcmTrack.extractedEvents) {
1254 | const [cmd, stOrg, gt, vel] = event;
1255 | let st = stOrg;
1256 |
1257 | if (cmd < 0x80) {
1258 | // Note event
1259 | if (chNo >= 0 && gt > 0 && vel > 0) {
1260 | if (validateRange(isIn7bitRange(vel), `Invalid note-on event: [${hexStr(event)}]`)) {
1261 | const noteNo = cmd + keyShift;
1262 | if (0 <= noteNo && noteNo < 0x80) {
1263 | // Note on or tie
1264 | if (noteGts[noteNo] < 0) {
1265 | setEvent(smfTrack, timestamp, makeMidiEvent(0x9, chNo, noteNo, vel));
1266 | }
1267 | noteGts[noteNo] = gt;
1268 | } else {
1269 | console.warn(`Note No. of note-on event is out of range due to KEY+ and/or PLAY BIAS: (${cmd} -> ${noteNo}) Ignored.`);
1270 | }
1271 | }
1272 | }
1273 |
1274 | } else {
1275 | // Command event
1276 | switch (cmd) {
1277 | // MIDI messages
1278 | case EVENT.CONTROL:
1279 | if (chNo >= 0) {
1280 | if (validateRange(isIn7bitRange(gt, vel), `Invalid CONTROL event: [${hexStr(event)}]`)) {
1281 | setEvent(smfTrack, timestamp, makeMidiEvent(0xb, chNo, gt, vel));
1282 | }
1283 | }
1284 | break;
1285 | case EVENT.PITCH:
1286 | if (chNo >= 0) {
1287 | if (validateRange(isIn7bitRange(gt, vel), `Invalid PITCH event: [${hexStr(event)}]`)) {
1288 | setEvent(smfTrack, timestamp, makeMidiEvent(0xe, chNo, gt, vel));
1289 | }
1290 | }
1291 | break;
1292 | case EVENT.AFTER_C:
1293 | if (chNo >= 0) {
1294 | if (validateRange(isIn7bitRange(gt), `Invalid AFTER C. event: [${hexStr(event)}]`)) {
1295 | setEvent(smfTrack, timestamp, makeMidiEvent(0xd, chNo, gt));
1296 | }
1297 | }
1298 | break;
1299 | case EVENT.AFTER_K:
1300 | if (chNo >= 0) {
1301 | if (validateRange(isIn7bitRange(gt, vel), `Invalid AFTER K. event: [${hexStr(event)}]`)) {
1302 | setEvent(smfTrack, timestamp, makeMidiEvent(0xa, chNo, gt, vel));
1303 | }
1304 | }
1305 | break;
1306 | case EVENT.PROGRAM:
1307 | if (chNo >= 0) {
1308 | if (validateRange(isIn7bitRange(gt), `Invalid PROGRAM event: [${hexStr(event)}]`)) {
1309 | setEvent(smfTrack, timestamp, makeMidiEvent(0xc, chNo, gt));
1310 | }
1311 | }
1312 | break;
1313 | case EVENT.BankPrgL:
1314 | case EVENT.BankPrg:
1315 | if (chNo >= 0) {
1316 | if (validateRange(isIn7bitRange(gt, vel), `Invalid BankPrg event: [${hexStr(event)}]`)) {
1317 | // Note: According to the MIDI spec, Bank Select must be transmitted as a pair of MSB and LSB.
1318 | // But, a BankPrg event is converted to a single MSB or LSB at the current implementation.
1319 | setEvent(smfTrack, timestamp, makeMidiEvent(0xb, chNo, (cmd === EVENT.BankPrg) ? 0 : 32, vel));
1320 | setEvent(smfTrack, timestamp, makeMidiEvent(0xc, chNo, gt));
1321 | }
1322 | }
1323 | break;
1324 | case EVENT.UserPrg:
1325 | if (chNo >= 0) {
1326 | if (validateRange((0 <= gt && gt < 192), `Invalid PROGRAM (User Program) event: [${hexStr(event)}]`)) {
1327 | // Inserts a SysEx to set Patch Memory if necessary.
1328 | const progNo = gt & 0x7f;
1329 | if (patchNos[progNo] !== gt && rcm.header.patches) {
1330 | const addr = progNo * 8;
1331 | const bytes = [0x41, 0x10, 0x16, 0x12, 0x83, 0x05, (addr >> 7) & 0x7f, addr & 0x7f, ...rcm.header.patches[gt], 0x84];
1332 | console.assert(bytes.length === 17);
1333 | setEvent(smfTrack, timestamp, convertSysEx(bytes, 0, 0, 0));
1334 | patchNos[progNo] = gt;
1335 | }
1336 |
1337 | setEvent(smfTrack, timestamp, [0xc0 | chNo, progNo]);
1338 | }
1339 | }
1340 | break;
1341 |
1342 | // SysEx
1343 | case EVENT.UsrExc0:
1344 | case EVENT.UsrExc1:
1345 | case EVENT.UsrExc2:
1346 | case EVENT.UsrExc3:
1347 | case EVENT.UsrExc4:
1348 | case EVENT.UsrExc5:
1349 | case EVENT.UsrExc6:
1350 | case EVENT.UsrExc7:
1351 | if (validateRange(isIn7bitRange(gt, vel), `Invalid UsrExc event: [${hexStr(event)}]`)) {
1352 | const index = cmd - EVENT.UsrExc0;
1353 | const {bytes, memo} = rcm.header.userSysExs[index];
1354 | const sysEx = convertSysEx(bytes, (isAllPortSame) ? chNo : midiCh, gt, vel);
1355 | if (validateRange(sysEx && isIn7bitRange(sysEx.slice(1, -1)), `Invalid definition of UsrExc${index}: [${hexStr(bytes)}]`)) {
1356 | setMetaTextUsrExc(smfTrack, timestamp, memo);
1357 | setEvent(smfTrack, timestamp, sysEx);
1358 | }
1359 | }
1360 | break;
1361 | case EVENT.TrExcl:
1362 | if (validateRange(isIn7bitRange(gt, vel), `Invalid Tr.Excl event: [${hexStr(event)}]`)) {
1363 | const bytes = event.slice(4);
1364 | if (bytes.length > 0) {
1365 | const sysEx = convertSysEx(bytes, (isAllPortSame) ? chNo : midiCh, gt, vel);
1366 | if (validateRange(sysEx && isIn7bitRange(sysEx.slice(1, -1)), `Invalid definition of Tr.Excl: [${hexStr(bytes)}]`)) {
1367 | setEvent(smfTrack, timestamp, sysEx);
1368 | }
1369 | }
1370 | }
1371 | break;
1372 |
1373 | // 1-byte DT1 SysEx for Roland devices
1374 | case EVENT.RolBase:
1375 | if (validateRange(isIn7bitRange(gt, vel), `Invalid RolBase event: [${hexStr(event)}]`)) {
1376 | rolBase = [gt, vel];
1377 | }
1378 | break;
1379 | case EVENT.RolDev:
1380 | if (validateRange(isIn7bitRange(gt, vel), `Invalid RolDev# event: [${hexStr(event)}]`)) {
1381 | rolDev = [gt, vel];
1382 | }
1383 | break;
1384 | case EVENT.RolPara:
1385 | if (validateRange(isIn7bitRange(gt, vel), `Invalid RolPara event: [${hexStr(event)}]`)) {
1386 | // Initializes RolDev# and RolBase if they have not been set yet.
1387 | if (!rolDev) {
1388 | rolDev = [settings.rolandDevId, settings.rolandModelId];
1389 | console.warn(`RolDev# has not been set yet. Initialized to [${hexStr(rolDev)}].`);
1390 | }
1391 | if (!rolBase) {
1392 | rolBase = [settings.rolandBaseAddrH, settings.rolandBaseAddrM];
1393 | console.warn(`RolBase has not been set yet. Initialized to [${hexStr(rolBase)}].`);
1394 | }
1395 | // Makes a SysEx by UsrExcl/Tr.Excl parser.
1396 | const bytes = [0x41, ...rolDev, 0x12, 0x83, ...rolBase, 0x80, 0x81, 0x84];
1397 | console.assert(bytes.length === 10);
1398 | setEvent(smfTrack, timestamp, convertSysEx(bytes, 0, gt, vel));
1399 | }
1400 | break;
1401 |
1402 | // 1-byte parameter change SysEx for Yamaha XG devices
1403 | case EVENT.YamBase:
1404 | if (validateRange(isIn7bitRange(gt, vel), `Invalid YamBase event: [${hexStr(event)}]`)) {
1405 | yamBase = [gt, vel];
1406 | }
1407 | break;
1408 | case EVENT.YamDev:
1409 | if (validateRange(isIn7bitRange(gt, vel), `Invalid YamDev# event: [${hexStr(event)}]`)) {
1410 | yamDev = [gt, vel];
1411 | }
1412 | break;
1413 | case EVENT.YamPara:
1414 | if (validateRange(isIn7bitRange(gt, vel), `Invalid YamPara event: [${hexStr(event)}]`)) {
1415 | // Initializes YamDev# and YamBase if they have not been set yet.
1416 | if (!yamDev) {
1417 | yamDev = [settings.yamahaDevId, settings.yamahaModelId];
1418 | console.warn(`YamDev# has not been set yet. Initialized to [${hexStr(yamDev)}].`);
1419 | }
1420 | if (!yamBase) {
1421 | yamBase = [settings.yamahaBaseAddrH, settings.yamahaBaseAddrM];
1422 | console.warn(`YamBase has not been set yet. Initialized to [${hexStr(yamBase)}].`);
1423 | }
1424 | // Makes a SysEx by UsrExcl/Tr.Excl parser.
1425 | const bytes = [0x43, ...yamDev, 0x83, ...yamBase, 0x80, 0x81, 0x84];
1426 | console.assert(bytes.length === 9);
1427 | setEvent(smfTrack, timestamp, convertSysEx(bytes, 0, gt, vel));
1428 | }
1429 | break;
1430 | case EVENT.XGPara:
1431 | if (validateRange(isIn7bitRange(gt, vel), `Invalid XGPara event: [${hexStr(event)}]`)) {
1432 | // Initializes YamDev# and YamBase if they have not been set yet.
1433 | if (!yamDev) {
1434 | yamDev = [settings.yamahaDevId, settings.yamahaModelId];
1435 | console.warn(`YamDev# has not been set yet. Initialized to [${hexStr(yamDev)}].`);
1436 | }
1437 | if (!yamBase) {
1438 | yamBase = [settings.yamahaBaseAddrH, settings.yamahaBaseAddrM];
1439 | console.warn(`YamBase has not been set yet. Initialized to [${hexStr(yamBase)}].`);
1440 | }
1441 | // Makes a SysEx.
1442 | const bytes = [0xf0, 0x43, ...yamDev, ...yamBase, gt, vel, 0xf7];
1443 | console.assert(bytes.length === 9);
1444 | setEvent(smfTrack, timestamp, bytes);
1445 | }
1446 | break;
1447 |
1448 | // Meta events
1449 | case EVENT.MIDI_CH:
1450 | if (validateRange((0 <= gt && gt <= 32), `Invalid MIDI CH. event: [${hexStr(event)}]`)) {
1451 | const oldPortNo = portNo;
1452 | midiCh = gt - 1; // The internal representations of MIDI CH. are different between track headers and event.
1453 | chNo = (midiCh >= 0) ? midiCh % 16 : -1;
1454 | portNo = (midiCh >= 0) ? Math.trunc(midiCh / 16) : portNo;
1455 |
1456 | // Adds an unofficial MIDI Port meta event if necessary.
1457 | if (portNo !== oldPortNo) {
1458 | // TODO: Investigate whether this event can be appeared in the song body.
1459 | setEvent(smfTrack, timestamp, [0xff, 0x21, 0x01, portNo]);
1460 | }
1461 | }
1462 | break;
1463 |
1464 | case EVENT.TEMPO:
1465 | if (validateRange((gt > 0), `Invalid tempo rate: ${gt}`)) { // Note: It can be greater than 255 in G36.
1466 | tempoEventMap.set(timestamp, event);
1467 | }
1468 | break;
1469 |
1470 | case EVENT.MusicKey:
1471 | setEvent(conductorTrack, timestamp, convertKeySignature(stOrg));
1472 | st = 0;
1473 | break;
1474 |
1475 | case EVENT.Comment:
1476 | setMetaTextComment(smfTrack, timestamp, event.slice(1));
1477 | st = 0;
1478 | break;
1479 |
1480 | case EVENT.ExtCmd:
1481 | {
1482 | const kind = (gt === 0x00) ? 'MCI: ' : (gt === 0x01) ? 'RUN: ' : '???: ';
1483 | setMetaCue(conductorTrack, timestamp, [...strToBytes(kind), ...event.slice(4)]);
1484 | }
1485 | break;
1486 |
1487 | case EVENT.KeyScan:
1488 | {
1489 | const cue = {
1490 | 12: 'Suspend playing',
1491 | 18: 'Increase play bias',
1492 | 23: 'Stop playing',
1493 | 32: 'Show main screen',
1494 | 33: 'Show 11th track',
1495 | 34: 'Show 12th track',
1496 | 35: 'Show 13th track',
1497 | 36: 'Show 14th track',
1498 | 37: 'Show 15th track',
1499 | 38: 'Show 16th track',
1500 | 39: 'Show 17th track',
1501 | 40: 'Show 18th track',
1502 | 48: 'Show 10th track',
1503 | 49: 'Show 1st track',
1504 | 50: 'Show 2nd track',
1505 | 51: 'Show 3rd track',
1506 | 52: 'Show 4th track',
1507 | 53: 'Show 5th track',
1508 | 54: 'Show 6th track',
1509 | 55: 'Show 7th track',
1510 | 56: 'Show 8th track',
1511 | 57: 'Show 9th track',
1512 | 61: 'Mute 1st track',
1513 | }[gt] || 'Unknown';
1514 | setMetaCue(conductorTrack, timestamp, [...strToBytes(`KeyScan: ${cue}`)]);
1515 | }
1516 | break;
1517 |
1518 | // RCM commands
1519 | case EVENT.MeasEnd:
1520 | st = 0;
1521 | break;
1522 | case EVENT.TrackEnd:
1523 | // Expands the current step time to wait for all of note-off.
1524 | st = Math.max(...noteGts, 0);
1525 | break;
1526 |
1527 | case EVENT.SecondEvt:
1528 | case EVENT.LoopEnd:
1529 | case EVENT.LoopStart:
1530 | case EVENT.SameMeas:
1531 | console.assert(false, 'Such kind of events must be resolved in the previous phase', {event});
1532 | throwOrIgnore(`Unexpected event: [${hexStr(event)}]`);
1533 | break;
1534 |
1535 | // Special commands for particular devices
1536 | case EVENT.DX7FUNC:
1537 | case EVENT.DX_PARA:
1538 | case EVENT.DX_PERF:
1539 | case EVENT.TX_FUNC:
1540 | case EVENT.FB_01_P:
1541 | case EVENT.FB_01_S:
1542 | case EVENT.TX81Z_V:
1543 | case EVENT.TX81Z_A:
1544 | case EVENT.TX81Z_P:
1545 | case EVENT.TX81Z_S:
1546 | case EVENT.TX81Z_E:
1547 | case EVENT.DX7_2_R:
1548 | case EVENT.DX7_2_A:
1549 | case EVENT.DX7_2_P:
1550 | case EVENT.TX802_P:
1551 | case EVENT.MKS_7:
1552 | if (chNo >= 0) {
1553 | const eventName = Object.entries(EVENT).find((e) => e[1] === cmd)[0];
1554 | console.assert(eventName);
1555 | if (validateRange(isIn7bitRange(gt, vel), `Invalid ${eventName} event: [${hexStr(event)}]`)) {
1556 | const bytes = {
1557 | DX7FUNC: [0xf0, 0x43, 0x10 | chNo, 0x08, gt, vel, 0xf7],
1558 | DX_PARA: [0xf0, 0x43, 0x10 | chNo, 0x00, gt, vel, 0xf7],
1559 | DX_PERF: [0xf0, 0x43, 0x10 | chNo, 0x04, gt, vel, 0xf7],
1560 | TX_FUNC: [0xf0, 0x43, 0x10 | chNo, 0x11, gt, vel, 0xf7],
1561 | FB_01_P: [0xf0, 0x43, 0x10 | chNo, 0x15, gt, vel, 0xf7],
1562 | FB_01_S: [0xf0, 0x43, 0x75, chNo, 0x10, gt, vel, 0xf7],
1563 | TX81Z_V: [0xf0, 0x43, 0x10 | chNo, 0x12, gt, vel, 0xf7],
1564 | TX81Z_A: [0xf0, 0x43, 0x10 | chNo, 0x13, gt, vel, 0xf7],
1565 | TX81Z_P: [0xf0, 0x43, 0x10 | chNo, 0x10, gt, vel, 0xf7],
1566 | TX81Z_S: [0xf0, 0x43, 0x10 | chNo, 0x10, 0x7b, gt, vel, 0xf7],
1567 | TX81Z_E: [0xf0, 0x43, 0x10 | chNo, 0x10, 0x7c, gt, vel, 0xf7],
1568 | DX7_2_R: [0xf0, 0x43, 0x10 | chNo, 0x1b, gt, vel, 0xf7],
1569 | DX7_2_A: [0xf0, 0x43, 0x10 | chNo, 0x18, gt, vel, 0xf7],
1570 | DX7_2_P: [0xf0, 0x43, 0x10 | chNo, 0x19, gt, vel, 0xf7],
1571 | TX802_P: [0xf0, 0x43, 0x10 | chNo, 0x1a, gt, vel, 0xf7],
1572 | MKS_7: [0xf0, 0x41, 0x32, chNo, gt, vel, 0xf7],
1573 | }[eventName];
1574 | console.assert(Array.isArray(bytes));
1575 | setEvent(smfTrack, timestamp, bytes);
1576 | }
1577 | }
1578 | break;
1579 | case EVENT.CMU_800:
1580 | console.warn(`CMU-800 is not supported: ${gt}`);
1581 | break;
1582 |
1583 | default:
1584 | throwOrIgnore(`Unknown event: [${hexStr(event)}]`);
1585 | st = 0;
1586 | break;
1587 | }
1588 | }
1589 |
1590 | // Note off
1591 | if (chNo >= 0) {
1592 | for (let noteNo = 0; noteNo < noteGts.length; noteNo++) {
1593 | const noteGt = noteGts[noteNo];
1594 | if (noteGt < 0) {
1595 | continue;
1596 | }
1597 |
1598 | if (isNoteOff(noteGt, st)) {
1599 | setEvent(smfTrack, timestamp + noteGt, makeNoteOff(chNo, noteNo));
1600 | noteGts[noteNo] = -1;
1601 | } else {
1602 | noteGts[noteNo] -= st;
1603 | }
1604 | }
1605 | }
1606 |
1607 | timestamp += st;
1608 | }
1609 |
1610 | // End of Track
1611 | setEvent(smfTrack, timestamp, [0xff, 0x2f, 0x00]);
1612 | if (timestamp > maxDuration) {
1613 | maxDuration = timestamp;
1614 | }
1615 |
1616 | seq.tracks.push(smfTrack);
1617 | }
1618 |
1619 | // End of Track for the conductor track
1620 | setEvent(conductorTrack, maxDuration, [0xff, 0x2f, 0x00]);
1621 |
1622 | // Makes tempo meta events.
1623 | addTempoEvents(tempoEventMap);
1624 |
1625 | return seq;
1626 |
1627 | // Note: The process of the tempo graduation is different from the original Recomposer's algorithm.
1628 | function addTempoEvents(tempoEventMap) {
1629 | // Table of step time during graduation from CVS.EXE Ver 5.06 (1995-08-29) [0x00dcd8-0x00ddd7]
1630 | const gradSteps = [
1631 | NaN, 255, 225, 208, 195, 186, 178, 171, 165, 160, 156, 151, 148, 144, 141, 138,
1632 | 135, 132, 130, 128, 125, 123, 121, 119, 117, 116, 114, 112, 111, 109, 108, 106,
1633 | 105, 104, 102, 101, 100, 99, 98, 96, 95, 94, 93, 92, 91, 90, 89, 88,
1634 | 87, 86, 86, 85, 84, 83, 82, 81, 81, 80, 79, 78, 78, 77, 76, 76,
1635 | 75, 74, 74, 73, 72, 72, 71, 70, 70, 69, 69, 68, 67, 67, 66, 66,
1636 | 65, 65, 64, 64, 63, 63, 62, 62, 61, 61, 60, 60, 59, 59, 58, 58,
1637 | 57, 57, 56, 56, 56, 55, 55, 54, 54, 53, 53, 53, 52, 52, 51, 51,
1638 | 51, 50, 50, 49, 49, 49, 48, 48, 48, 47, 47, 47, 46, 46, 45, 45,
1639 | 45, 44, 44, 44, 43, 43, 43, 42, 42, 42, 42, 41, 41, 41, 40, 40,
1640 | 40, 39, 39, 39, 38, 38, 38, 38, 37, 37, 37, 36, 36, 36, 36, 35,
1641 | 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 31, 31,
1642 | 31, 31, 30, 30, 30, 30, 29, 29, 29, 29, 29, 28, 28, 28, 28, 27,
1643 | 27, 27, 27, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 24, 24, 24,
1644 | 24, 23, 23, 23, 23, 23, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21,
1645 | 20, 20, 20, 20, 20, 20, 19, 19, 19, 19, 19, 18, 18, 18, 18, 18,
1646 | 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15,
1647 | ];
1648 |
1649 | let currentTempo = rcm.header.tempo;
1650 | let gradTempoMap = null;
1651 | for (let timestamp = 0; timestamp < maxDuration; timestamp++) {
1652 | const oldTempo = currentTempo;
1653 |
1654 | // Checks if a tempo event exists.
1655 | if (tempoEventMap.has(timestamp)) {
1656 | const [cmd, _, gt, vel] = tempoEventMap.get(timestamp);
1657 | console.assert(cmd === EVENT.TEMPO);
1658 |
1659 | if (vel === 0) { // Normal tempo change
1660 | gradTempoMap = null; // Cancels tempo graduation.
1661 | currentTempo = Math.trunc(rcm.header.tempo * gt / 64.0);
1662 |
1663 | } else { // Tempo change with graduation
1664 | const targetTempo = Math.trunc(rcm.header.tempo * gt / 64.0);
1665 | const gradSt = gradSteps[vel];
1666 |
1667 | // Calculates future tempo values.
1668 | gradTempoMap = new Map();
1669 | for (let i = 0; i < gradSt; i += 2) {
1670 | const gradTimestamp = timestamp + Math.trunc(i * rcm.header.timeBase / 48.0);
1671 | const gradTempo = Math.trunc(currentTempo + (targetTempo - currentTempo) * i / gradSt);
1672 | gradTempoMap.set(gradTimestamp, gradTempo);
1673 | }
1674 | gradTempoMap.set(timestamp + Math.trunc(gradSt * rcm.header.timeBase / 48.0), targetTempo);
1675 | }
1676 | }
1677 |
1678 | // If in the "Tempo graduation", updates the current tempo with the pre-calculated tempo values.
1679 | if (gradTempoMap) {
1680 | if (gradTempoMap.has(timestamp)) {
1681 | currentTempo = gradTempoMap.get(timestamp);
1682 | }
1683 | }
1684 |
1685 | // Adds tempo meta event if necessary.
1686 | if (currentTempo !== oldTempo) {
1687 | setEvent(conductorTrack, timestamp, makeMetaTempo(60 * 1000 * 1000 / currentTempo));
1688 | }
1689 | }
1690 | }
1691 |
1692 | function setEvent(map, timestamp, bytes) {
1693 | console.assert(map instanceof Map, 'Invalid argument', {map});
1694 | console.assert(Number.isInteger(timestamp), 'Invalid argument', {timestamp});
1695 | console.assert(bytes && bytes.length, 'Invalid argument', {bytes});
1696 |
1697 | if (timestamp < 0) {
1698 | console.warn(`An event appeared previous to the zero point due to ST+. Adjusted it to zero: (${timestamp} -> 0)`);
1699 | timestamp = 0;
1700 | }
1701 | if (!map.has(timestamp)) {
1702 | map.set(timestamp, []);
1703 | }
1704 | map.get(timestamp).push(bytes);
1705 | }
1706 |
1707 | function convertSysEx(bytes, ch, gt, vel) {
1708 | console.assert(bytes && bytes.length, 'Invalid argument', {bytes});
1709 |
1710 | const sysEx = [0xf0];
1711 | let checkSum = 0;
1712 | loop: for (const byte of bytes) {
1713 | let value = byte;
1714 | switch (byte) {
1715 | case 0x80: // [gt]
1716 | value = gt;
1717 | break;
1718 | case 0x81: // [ve]
1719 | value = vel;
1720 | break;
1721 | case 0x82: // [ch]
1722 | if (ch < 0) {
1723 | return null;
1724 | }
1725 | value = ch;
1726 | break;
1727 | case 0x83: // [cs]
1728 | checkSum = 0;
1729 | continue;
1730 | case 0x84: // [ss]
1731 | value = (0x80 - checkSum) & 0x7f;
1732 | break;
1733 | case 0xf7:
1734 | break loop;
1735 | default:
1736 | break;
1737 | }
1738 |
1739 | if (!isIn7bitRange(value)) {
1740 | return null;
1741 | }
1742 |
1743 | // Adds a value and updates the checksum.
1744 | sysEx.push(value);
1745 | checkSum = (checkSum + value) & 0x7f;
1746 | }
1747 |
1748 | // Adds trailing 0xf7.
1749 | console.assert(sysEx[sysEx.length - 1] !== 0xf7);
1750 | sysEx.push(0xf7);
1751 |
1752 | return sysEx;
1753 | }
1754 |
1755 | function convertKeySignature(value) {
1756 | console.assert(Number.isInteger(value), 'Invalid argument', {value});
1757 | const tmp = value & 0x0f;
1758 | const sf = (tmp < 8) ? tmp : 8 - tmp;
1759 | console.assert(-7 <= sf && sf <= 7);
1760 | const mi = ((value & 0x10) === 0) ? 0x00 : 0x01;
1761 | return [0xff, 0x59, 0x02, (sf + 0x100) & 0xff, mi];
1762 | }
1763 |
1764 | function makeMidiEvent(kind, ch, ...values) {
1765 | console.assert((0x8 <= kind && kind <= 0xe), 'Invalid argument', {kind});
1766 | console.assert((0 <= ch && ch < 16), 'Invalid argument', {ch});
1767 | console.assert(values && values.length === [2, 2, 2, 2, 1, 1, 2][kind - 0x8], 'Invalid argument', {values});
1768 | console.assert(values.some((e) => Number.isInteger(e) && (e & ~0x7f) === 0), 'Invalid argument', {values});
1769 | return [(kind << 4) | ch, ...values];
1770 | }
1771 |
1772 | function makeMetaText(kind, bytes) {
1773 | console.assert((0x01 <= kind && kind <= 0x0f), 'Invalid argument', {kind});
1774 | console.assert(bytes && 'length' in bytes, 'Invalid argument', {bytes});
1775 | return [0xff, kind, ...varNum(bytes.length), ...bytes];
1776 | }
1777 |
1778 | function makeMetaTempo(usecPerBeat) {
1779 | console.assert(Number.isFinite(usecPerBeat), 'Invalid argument', {usecPerBeat});
1780 | const bytes = new Uint8Array(4);
1781 | (new DataView(bytes.buffer)).setUint32(0, Math.trunc(usecPerBeat));
1782 | return [0xff, 0x51, 0x03, ...bytes.slice(1)];
1783 | }
1784 |
1785 | function makeMetaTimeSignature(numer, denom) {
1786 | console.assert(Number.isInteger(numer), 'Invalid argument', {numer});
1787 | console.assert(Number.isInteger(denom) && (denom & (denom - 1)) === 0, 'Invalid argument', {denom});
1788 | return [0xff, 0x58, 0x04, numer, Math.log2(denom), 0x18, 0x08];
1789 | }
1790 | }
1791 |
1792 | export function convertSeqToSmf(seq) {
1793 | console.assert(seq, 'Invalid argument', {seq});
1794 |
1795 | // Makes a header chunk.
1796 | const mthd = [
1797 | ...strToBytes('MThd'),
1798 | ...uintBE(2 + 2 + 2, 4),
1799 | ...uintBE(1, 2),
1800 | ...uintBE(seq.tracks.length, 2),
1801 | ...uintBE(seq.timeBase, 2),
1802 | ];
1803 |
1804 | // Makes track chunks.
1805 | const mtrks = seq.tracks.map((smfTrack) => {
1806 | let prevTime = 0;
1807 | let lastStatus = 0;
1808 | const mtrk = [...smfTrack.entries()].sort((a, b) => a[0] - b[0]).reduce((p, c) => {
1809 | const [timestamp, events] = c;
1810 |
1811 | // Makes MTrk events.
1812 | const bytes = [];
1813 | for (const event of events) {
1814 | // Delta time
1815 | const deltaTime = timestamp - prevTime;
1816 | bytes.push(...varNum(deltaTime));
1817 | prevTime = timestamp;
1818 |
1819 | // Event
1820 | const status = event[0];
1821 | if (status < 0xf0) {
1822 | // Channel messages
1823 | console.assert(status >= 0x80);
1824 | if (status === lastStatus) {
1825 | // Applies running status rule.
1826 | bytes.push(...event.slice(1));
1827 | } else {
1828 | bytes.push(...event);
1829 | }
1830 | lastStatus = status;
1831 |
1832 | } else if (status === 0xf0) {
1833 | // SysEx
1834 | bytes.push(0xf0, ...varNum(event.length - 1), ...event.slice(1));
1835 | lastStatus = 0;
1836 |
1837 | } else {
1838 | // Meta events
1839 | console.assert(status === 0xff); // This converter doesn't generate F7 SysEx.
1840 | bytes.push(...event);
1841 | lastStatus = 0;
1842 | }
1843 | }
1844 |
1845 | p.push(...bytes);
1846 | return p;
1847 | }, []);
1848 |
1849 | // Extracts MTrk events with a leading MTrk header.
1850 | return [...strToBytes('MTrk'), ...uintBE(mtrk.length, 4), ...mtrk];
1851 | });
1852 |
1853 | // Extracts track events with a leading header chunk.
1854 | const smf = mthd.concat(...mtrks);
1855 |
1856 | return new Uint8Array(smf);
1857 |
1858 | function uintBE(value, width) {
1859 | console.assert(Number.isInteger(value) && (width === 2 || width === 4), 'Invalid argument', {value, width});
1860 | const bytes = [];
1861 | for (let i = 0; i < width; i++) {
1862 | bytes.unshift(value & 0xff);
1863 | value >>= 8;
1864 | }
1865 | console.assert(value === 0);
1866 | return bytes;
1867 | }
1868 | }
1869 |
1870 | function varNum(value) {
1871 | console.assert(Number.isInteger(value) && (0 <= value && value < 0x10000000), 'Invalid argument', {value});
1872 | if (value < 0x80) {
1873 | return [value];
1874 | } else if (value < 0x4000) {
1875 | return [(value >> 7) | 0x80, value & 0x7f];
1876 | } else if (value < 0x200000) {
1877 | return [(value >> 14) | 0x80, ((value >> 7) & 0x7f) | 0x80, value & 0x7f];
1878 | } else {
1879 | return [(value >> 21) | 0x80, ((value >> 14) & 0x7f) | 0x80, ((value >> 7) & 0x7f) | 0x80, value & 0x7f];
1880 | }
1881 | }
1882 |
1883 | function hexStr(bytes) {
1884 | console.assert(bytes && 'length' in bytes, 'Invalid argument', {bytes});
1885 | return [...bytes].map((e) => e.toString(16).padStart(2, '0')).join(' ');
1886 | }
1887 |
1888 | function strToBytes(str) {
1889 | console.assert(typeof str === 'string' && /^[\x20-\x7E]*$/u.test(str), 'Invalid argument', {str});
1890 | return str.split('').map((e) => e.codePointAt(0));
1891 | }
1892 |
1893 | function rawTrim(bytes, bits = 0b11) {
1894 | console.assert(bytes && 'length' in bytes, 'Invalid argument', {bytes});
1895 | console.assert(Number.isInteger(bits) && (bits & ~0b11) === 0, 'Invalid argument', {bits});
1896 |
1897 | if (bytes.every((e) => e === 0x20)) {
1898 | return new Uint8Array();
1899 | }
1900 |
1901 | const begin = ((bits & 0b01) === 0) ? 0 : bytes.findIndex((e) => e !== 0x20);
1902 | const end = ((bits & 0b10) === 0) ? undefined : String.fromCharCode(...bytes).replace(/\x20+$/u, '').length;
1903 |
1904 | return bytes.slice(begin, end);
1905 | }
1906 |
1907 | function rawTrimNul(bytes) {
1908 | console.assert(bytes && 'length' in bytes, 'Invalid argument', {bytes});
1909 |
1910 | const index = bytes.indexOf(0x00);
1911 | if (index < 0) {
1912 | return bytes;
1913 | } else {
1914 | return bytes.slice(0, index);
1915 | }
1916 | }
1917 |
1918 | function isIn7bitRange(...values) {
1919 | console.assert(values && values.length, 'Invalid argument', {values});
1920 | return values.every((e) => (e & ~0x7f) === 0);
1921 | }
1922 |
1923 | function validateAndThrow(isValid, message) {
1924 | if (!isValid) {
1925 | throw new Error(message);
1926 | }
1927 | return true;
1928 | }
1929 |
1930 | function validateAndIgnore(isValid, message) {
1931 | if (!isValid) {
1932 | console.warn(`${message} Ignored.`);
1933 | }
1934 | return isValid;
1935 | }
1936 |
1937 | function nop() {
1938 | /* EMPTY */
1939 | }
1940 |
--------------------------------------------------------------------------------