├── 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 | 1321 | 1322 | 1323 | 1341 | 1342 | 1343 | 1385 | 1386 | 1387 | 1394 | 1395 | 1396 | 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 | --------------------------------------------------------------------------------