/** * name: backend * version: v1.0.0 * build time: 2/20/2026, 8:53:44 PM * system user: camus * git user name: k.l.lambda * git user email: k.l.lambda@gmail.com **/ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var msgpackr = require('msgpackr'); var zeromq = require('zeromq'); var portfinder = require('portfinder'); var pythonShell = require('python-shell'); var require$$0 = require('lodash'); var sha1 = require('js-sha1'); var pick$1 = require('lodash/pick'); var erf = require('math-erf'); var weakLruCache = require('weak-lru-cache'); var SparkMD5 = require('spark-md5'); var sharp = require('sharp'); var got = require('got'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var require$$0__default = /*#__PURE__*/_interopDefaultLegacy(require$$0); var sha1__default = /*#__PURE__*/_interopDefaultLegacy(sha1); var pick__default = /*#__PURE__*/_interopDefaultLegacy(pick$1); var erf__default = /*#__PURE__*/_interopDefaultLegacy(erf); var SparkMD5__default = /*#__PURE__*/_interopDefaultLegacy(SparkMD5); var sharp__default = /*#__PURE__*/_interopDefaultLegacy(sharp); var got__default = /*#__PURE__*/_interopDefaultLegacy(got); globalThis.btoa = (str) => Buffer.from(str, 'binary').toString('base64'); globalThis.atob = (str) => Buffer.from(str, 'base64').toString('binary'); function commonjsRequire (path) { throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.'); } var events = {exports: {}}; var R = typeof Reflect === 'object' ? Reflect : null; var ReflectApply = R && typeof R.apply === 'function' ? R.apply : function ReflectApply(target, receiver, args) { return Function.prototype.apply.call(target, receiver, args); }; var ReflectOwnKeys; if (R && typeof R.ownKeys === 'function') { ReflectOwnKeys = R.ownKeys; } else if (Object.getOwnPropertySymbols) { ReflectOwnKeys = function ReflectOwnKeys(target) { return Object.getOwnPropertyNames(target) .concat(Object.getOwnPropertySymbols(target)); }; } else { ReflectOwnKeys = function ReflectOwnKeys(target) { return Object.getOwnPropertyNames(target); }; } function ProcessEmitWarning(warning) { if (console && console.warn) console.warn(warning); } var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { return value !== value; }; function EventEmitter() { EventEmitter.init.call(this); } events.exports = EventEmitter; events.exports.once = once; // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; EventEmitter.prototype._eventsCount = 0; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. var defaultMaxListeners = 10; function checkListener(listener) { if (typeof listener !== 'function') { throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); } } Object.defineProperty(EventEmitter, 'defaultMaxListeners', { enumerable: true, get: function() { return defaultMaxListeners; }, set: function(arg) { if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); } defaultMaxListeners = arg; } }); EventEmitter.init = function() { if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { this._events = Object.create(null); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); } this._maxListeners = n; return this; }; function _getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return _getMaxListeners(this); }; EventEmitter.prototype.emit = function emit(type) { var args = []; for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); var doError = (type === 'error'); var events = this._events; if (events !== undefined) doError = (doError && events.error === undefined); else if (!doError) return false; // If there is no 'error' event listener then throw. if (doError) { var er; if (args.length > 0) er = args[0]; if (er instanceof Error) { // Note: The comments on the `throw` lines are intentional, they show // up in Node's output if this results in an unhandled exception. throw er; // Unhandled 'error' event } // At least give some kind of context to the user var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); err.context = er; throw err; // Unhandled 'error' event } var handler = events[type]; if (handler === undefined) return false; if (typeof handler === 'function') { ReflectApply(handler, this, args); } else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; checkListener(listener); events = target._events; if (events === undefined) { events = target._events = Object.create(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. } else if (prepend) { existing.unshift(listener); } else { existing.push(listener); } // Check for listener leak m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; // No error code for this since it is a Warning // eslint-disable-next-line no-restricted-syntax var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + String(type) + ' listeners ' + 'added. Use emitter.setMaxListeners() to ' + 'increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; ProcessEmitWarning(w); } } return target; } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function onceWrapper() { if (!this.fired) { this.target.removeListener(this.type, this.wrapFn); this.fired = true; if (arguments.length === 0) return this.listener.call(this.target); return this.listener.apply(this.target, arguments); } } function _onceWrap(target, type, listener) { var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; var wrapped = onceWrapper.bind(state); wrapped.listener = listener; state.wrapFn = wrapped; return wrapped; } EventEmitter.prototype.once = function once(type, listener) { checkListener(listener); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { checkListener(listener); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // Emits a 'removeListener' event if and only if the listener was removed. EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; checkListener(listener); events = this._events; if (events === undefined) return this; list = events[type]; if (list === undefined) return this; if (list === listener || list.listener === listener) { if (--this._eventsCount === 0) this._events = Object.create(null); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (position === 0) list.shift(); else { spliceOne(list, position); } if (list.length === 1) events[type] = list[0]; if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener); } return this; }; EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events, i; events = this._events; if (events === undefined) return this; // not listening for removeListener, no need to emit if (events.removeListener === undefined) { if (arguments.length === 0) { this._events = Object.create(null); this._eventsCount = 0; } else if (events[type] !== undefined) { if (--this._eventsCount === 0) this._events = Object.create(null); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); var key; for (i = 0; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = Object.create(null); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners !== undefined) { // LIFO order for (i = listeners.length - 1; i >= 0; i--) { this.removeListener(type, listeners[i]); } } return this; }; function _listeners(target, type, unwrap) { var events = target._events; if (events === undefined) return []; var evlistener = events[type]; if (evlistener === undefined) return []; if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener]; return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); } EventEmitter.prototype.listeners = function listeners(type) { return _listeners(this, type, true); }; EventEmitter.prototype.rawListeners = function rawListeners(type) { return _listeners(this, type, false); }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events !== undefined) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener !== undefined) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; }; function arrayClone(arr, n) { var copy = new Array(n); for (var i = 0; i < n; ++i) copy[i] = arr[i]; return copy; } function spliceOne(list, index) { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; list.pop(); } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } function once(emitter, name) { return new Promise(function (resolve, reject) { function errorListener(err) { emitter.removeListener(name, resolver); reject(err); } function resolver() { if (typeof emitter.removeListener === 'function') { emitter.removeListener('error', errorListener); } resolve([].slice.call(arguments)); } eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); if (name !== 'error') { addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); } }); } function addErrorHandlerIfEventEmitter(emitter, handler, flags) { if (typeof emitter.on === 'function') { eventTargetAgnosticAddListener(emitter, 'error', handler, flags); } } function eventTargetAgnosticAddListener(emitter, name, listener, flags) { if (typeof emitter.on === 'function') { if (flags.once) { emitter.once(name, listener); } else { emitter.on(name, listener); } } else if (typeof emitter.addEventListener === 'function') { // EventTarget does not have `error` event semantics like Node // EventEmitters, we do not listen for `error` events here. emitter.addEventListener(name, function wrapListener(arg) { // IE does not have builtin `{ once: true }` support so we // have to do it manually. if (flags.once) { emitter.removeEventListener(name, wrapListener); } listener(arg); }); } else { throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); } } function destructPromise(options = {}) { const { timeout } = options; let rs; let rj; return [ new Promise((resolve, reject) => { rs = resolve; rj = reject; if (timeout >= 0) setTimeout(rj, timeout, 'timeout'); }), rs, rj, ]; } class AsyncQueue extends events.exports.EventEmitter { constructor() { super(); this.working = false; this.working = false; this.tasks = []; process.nextTick(() => { this.emit('idle'); }); } async _digest(item) { this.working = true; const [taskFn, payload, resolve, reject] = item; await taskFn(payload).then(resolve, reject); if (this.tasks.length > 0) { await this._digest(this.tasks.shift()); } else { this.working = false; this.emit('idle'); } } /** * 添加队列任务 * @param task * @param options */ addTask(task, { timeout = 600000 } = {}) { const [promise, resolve, reject] = destructPromise({ timeout }); if (this.working) { this.tasks.push([...task, resolve, reject]); } else { this._digest([...task, resolve, reject]); } return promise; } } class ZeroClient { constructor(logger = console) { this.queue = new AsyncQueue(); this.logger = logger; } bind(url) { url && (this.url = url); this.socket = new zeromq.Request({ sendTimeout: 15e3, receiveTimeout: 300e3, }); this.socket.connect(this.url); } __request(payload) { let retryTimes = 0; const req = async (data) => { try { if (this.socket.closed) this.bind(); return await this.socket.send(msgpackr.pack(data)).then(() => this.socket.receive()); } catch (err) { if (retryTimes < 2) { retryTimes++; console.log(`请求失败,${err.stack}`); console.error(`3s后重试第${retryTimes}次`); this.socket.close(); await new Promise((resolve) => setTimeout(resolve, 3000)); return req(data); } else { throw err; } } }; return req(payload); } async request(method, args = null, kwargs = null) { const [args_, kwargs_] = Array.isArray(args) ? [args, kwargs] : [undefined, args]; const msg = { method }; if (args_) msg.args = args_; if (kwargs_) msg.kwargs = kwargs_; return this.queue.addTask([ async (opt) => { const [result] = await this.__request(opt); const obj = msgpackr.unpack(result); if (obj.code === 0) { return obj.data; } else { return Promise.reject(obj.msg); } }, msg, ]); } } class PyProcessor extends ZeroClient { constructor(scriptPath, options = {}, logger = console) { super(logger); this.retryCount = 0; this.retryDelay = 3000; this.scriptPath = scriptPath; this.options = options; } async bind(port) { const freePort = port || (await portfinder.getPortPromise({ port: 12022, stopPort: 12122, })); // "./streamPredictor.py", "--inspect" const options = require$$0.defaultsDeep({ args: [...(this.options.args || []), '-p', `${freePort}`], }, this.options); this.logger.info(`[python-shell]: starting python shell. path: ${this.scriptPath}`); this.pyShell = new pythonShell.PythonShell(this.scriptPath, options); this.pyShell.stdout.on('data', (data) => this.logger.info(data)); this.pyShell.on('pythonError', (err) => this.logger.error(`[python-shell]: ${this.scriptPath} pythonError:`, err)); this.pyShell.on('stderr', (err) => this.logger.error(`[python-shell]: ${this.scriptPath} stderr:`, err)); this.pyShell.on('error', (err) => this.logger.error(`[python-shell]: ${this.scriptPath} error:`, err)); this.pyShell.on('close', () => { // python子进程关闭事件 if (this.retryCount < 5) { this.retryCount++; this.logger.info(`[python-shell]: ${this.scriptPath} will retry ${this.retryCount}th time after 3 seconds`); setTimeout(() => { this.bind(); }, this.retryDelay); } }); super.bind(`tcp://127.0.0.1:${freePort}`); } } var util$1 = {}; var isBuffer = function isBuffer(arg) { return arg instanceof Buffer; }; var inherits = {exports: {}}; var inherits_browser = {exports: {}}; if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module inherits_browser.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; } else { // old school shim for old browsers inherits_browser.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor; var TempCtor = function () {}; TempCtor.prototype = superCtor.prototype; ctor.prototype = new TempCtor(); ctor.prototype.constructor = ctor; }; } try { var util = require('util'); if (typeof util.inherits !== 'function') throw ''; inherits.exports = util.inherits; } catch (e) { inherits.exports = inherits_browser.exports; } (function (exports) { // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors(obj) { var keys = Object.keys(obj); var descriptors = {}; for (var i = 0; i < keys.length; i++) { descriptors[keys[i]] = Object.getOwnPropertyDescriptor(obj, keys[i]); } return descriptors; }; var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { var objects = []; for (var i = 0; i < arguments.length; i++) { objects.push(inspect(arguments[i])); } return objects.join(' '); } var i = 1; var args = arguments; var len = args.length; var str = String(f).replace(formatRegExp, function(x) { if (x === '%%') return '%'; if (i >= len) return x; switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); case '%j': try { return JSON.stringify(args[i++]); } catch (_) { return '[Circular]'; } default: return x; } }); for (var x = args[i]; i < len; x = args[++i]) { if (isNull(x) || !isObject(x)) { str += ' ' + x; } else { str += ' ' + inspect(x); } } return str; }; // Mark that a method should not be used. // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. exports.deprecate = function(fn, msg) { if (typeof process !== 'undefined' && process.noDeprecation === true) { return fn; } // Allow for deprecating things in the process of starting up. if (typeof process === 'undefined') { return function() { return exports.deprecate(fn, msg).apply(this, arguments); }; } var warned = false; function deprecated() { if (!warned) { if (process.throwDeprecation) { throw new Error(msg); } else if (process.traceDeprecation) { console.trace(msg); } else { console.error(msg); } warned = true; } return fn.apply(this, arguments); } return deprecated; }; var debugs = {}; var debugEnviron; exports.debuglog = function(set) { if (isUndefined(debugEnviron)) debugEnviron = process.env.NODE_DEBUG || ''; set = set.toUpperCase(); if (!debugs[set]) { if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { var pid = process.pid; debugs[set] = function() { var msg = exports.format.apply(exports, arguments); console.error('%s %d: %s', set, pid, msg); }; } else { debugs[set] = function() {}; } } return debugs[set]; }; /** * Echos the value of a value. Trys to print the value out * in the best way possible given the different types. * * @param {Object} obj The object to print out. * @param {Object} opts Optional options object that alters the output. */ /* legacy: obj, showHidden, depth, colors*/ function inspect(obj, opts) { // default options var ctx = { seen: [], stylize: stylizeNoColor }; // legacy... if (arguments.length >= 3) ctx.depth = arguments[2]; if (arguments.length >= 4) ctx.colors = arguments[3]; if (isBoolean(opts)) { // legacy... ctx.showHidden = opts; } else if (opts) { // got an "options" object exports._extend(ctx, opts); } // set default options if (isUndefined(ctx.showHidden)) ctx.showHidden = false; if (isUndefined(ctx.depth)) ctx.depth = 2; if (isUndefined(ctx.colors)) ctx.colors = false; if (isUndefined(ctx.customInspect)) ctx.customInspect = true; if (ctx.colors) ctx.stylize = stylizeWithColor; return formatValue(ctx, obj, ctx.depth); } exports.inspect = inspect; // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics inspect.colors = { 'bold' : [1, 22], 'italic' : [3, 23], 'underline' : [4, 24], 'inverse' : [7, 27], 'white' : [37, 39], 'grey' : [90, 39], 'black' : [30, 39], 'blue' : [34, 39], 'cyan' : [36, 39], 'green' : [32, 39], 'magenta' : [35, 39], 'red' : [31, 39], 'yellow' : [33, 39] }; // Don't use 'blue' not visible on cmd.exe inspect.styles = { 'special': 'cyan', 'number': 'yellow', 'boolean': 'yellow', 'undefined': 'grey', 'null': 'bold', 'string': 'green', 'date': 'magenta', // "name": intentionally not styling 'regexp': 'red' }; function stylizeWithColor(str, styleType) { var style = inspect.styles[styleType]; if (style) { return '\u001b[' + inspect.colors[style][0] + 'm' + str + '\u001b[' + inspect.colors[style][1] + 'm'; } else { return str; } } function stylizeNoColor(str, styleType) { return str; } function arrayToHash(array) { var hash = {}; array.forEach(function(val, idx) { hash[val] = true; }); return hash; } function formatValue(ctx, value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it if (ctx.customInspect && value && isFunction(value.inspect) && // Filter out the util module, it's inspect function is special value.inspect !== exports.inspect && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { var ret = value.inspect(recurseTimes, ctx); if (!isString(ret)) { ret = formatValue(ctx, ret, recurseTimes); } return ret; } // Primitive types cannot have properties var primitive = formatPrimitive(ctx, value); if (primitive) { return primitive; } // Look up the keys of the object. var keys = Object.keys(value); var visibleKeys = arrayToHash(keys); if (ctx.showHidden) { keys = Object.getOwnPropertyNames(value); } // IE doesn't make error fields non-enumerable // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx if (isError(value) && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { return formatError(value); } // Some type of object without properties can be shortcutted. if (keys.length === 0) { if (isFunction(value)) { var name = value.name ? ': ' + value.name : ''; return ctx.stylize('[Function' + name + ']', 'special'); } if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } if (isDate(value)) { return ctx.stylize(Date.prototype.toString.call(value), 'date'); } if (isError(value)) { return formatError(value); } } var base = '', array = false, braces = ['{', '}']; // Make Array say that they are Array if (isArray(value)) { array = true; braces = ['[', ']']; } // Make functions say that they are functions if (isFunction(value)) { var n = value.name ? ': ' + value.name : ''; base = ' [Function' + n + ']'; } // Make RegExps say that they are RegExps if (isRegExp(value)) { base = ' ' + RegExp.prototype.toString.call(value); } // Make dates with properties first say the date if (isDate(value)) { base = ' ' + Date.prototype.toUTCString.call(value); } // Make error with message first say the error if (isError(value)) { base = ' ' + formatError(value); } if (keys.length === 0 && (!array || value.length == 0)) { return braces[0] + base + braces[1]; } if (recurseTimes < 0) { if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } else { return ctx.stylize('[Object]', 'special'); } } ctx.seen.push(value); var output; if (array) { output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); } else { output = keys.map(function(key) { return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); }); } ctx.seen.pop(); return reduceToSingleString(output, base, braces); } function formatPrimitive(ctx, value) { if (isUndefined(value)) return ctx.stylize('undefined', 'undefined'); if (isString(value)) { var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\''; return ctx.stylize(simple, 'string'); } if (isNumber(value)) return ctx.stylize('' + value, 'number'); if (isBoolean(value)) return ctx.stylize('' + value, 'boolean'); // For some reason typeof null is "object", so special case here. if (isNull(value)) return ctx.stylize('null', 'null'); } function formatError(value) { return '[' + Error.prototype.toString.call(value) + ']'; } function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { if (hasOwnProperty(value, String(i))) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, String(i), true)); } else { output.push(''); } } keys.forEach(function(key) { if (!key.match(/^\d+$/)) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, key, true)); } }); return output; } function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { var name, str, desc; desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; if (desc.get) { if (desc.set) { str = ctx.stylize('[Getter/Setter]', 'special'); } else { str = ctx.stylize('[Getter]', 'special'); } } else { if (desc.set) { str = ctx.stylize('[Setter]', 'special'); } } if (!hasOwnProperty(visibleKeys, key)) { name = '[' + key + ']'; } if (!str) { if (ctx.seen.indexOf(desc.value) < 0) { if (isNull(recurseTimes)) { str = formatValue(ctx, desc.value, null); } else { str = formatValue(ctx, desc.value, recurseTimes - 1); } if (str.indexOf('\n') > -1) { if (array) { str = str.split('\n').map(function(line) { return ' ' + line; }).join('\n').substr(2); } else { str = '\n' + str.split('\n').map(function(line) { return ' ' + line; }).join('\n'); } } } else { str = ctx.stylize('[Circular]', 'special'); } } if (isUndefined(name)) { if (array && key.match(/^\d+$/)) { return str; } name = JSON.stringify('' + key); if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { name = name.substr(1, name.length - 2); name = ctx.stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") .replace(/\\"/g, '"') .replace(/(^"|"$)/g, "'"); name = ctx.stylize(name, 'string'); } } return name + ': ' + str; } function reduceToSingleString(output, base, braces) { var length = output.reduce(function(prev, cur) { if (cur.indexOf('\n') >= 0) ; return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; }, 0); if (length > 60) { return braces[0] + (base === '' ? '' : base + '\n ') + ' ' + output.join(',\n ') + ' ' + braces[1]; } return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } // NOTE: These type checking functions intentionally don't use `instanceof` // because it is fragile and can be easily faked with `Object.create()`. function isArray(ar) { return Array.isArray(ar); } exports.isArray = isArray; function isBoolean(arg) { return typeof arg === 'boolean'; } exports.isBoolean = isBoolean; function isNull(arg) { return arg === null; } exports.isNull = isNull; function isNullOrUndefined(arg) { return arg == null; } exports.isNullOrUndefined = isNullOrUndefined; function isNumber(arg) { return typeof arg === 'number'; } exports.isNumber = isNumber; function isString(arg) { return typeof arg === 'string'; } exports.isString = isString; function isSymbol(arg) { return typeof arg === 'symbol'; } exports.isSymbol = isSymbol; function isUndefined(arg) { return arg === void 0; } exports.isUndefined = isUndefined; function isRegExp(re) { return isObject(re) && objectToString(re) === '[object RegExp]'; } exports.isRegExp = isRegExp; function isObject(arg) { return typeof arg === 'object' && arg !== null; } exports.isObject = isObject; function isDate(d) { return isObject(d) && objectToString(d) === '[object Date]'; } exports.isDate = isDate; function isError(e) { return isObject(e) && (objectToString(e) === '[object Error]' || e instanceof Error); } exports.isError = isError; function isFunction(arg) { return typeof arg === 'function'; } exports.isFunction = isFunction; function isPrimitive(arg) { return arg === null || typeof arg === 'boolean' || typeof arg === 'number' || typeof arg === 'string' || typeof arg === 'symbol' || // ES6 symbol typeof arg === 'undefined'; } exports.isPrimitive = isPrimitive; exports.isBuffer = isBuffer; function objectToString(o) { return Object.prototype.toString.call(o); } function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 26 Feb 16:19:34 function timestamp() { var d = new Date(); var time = [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } // log is just a thin wrapper to console.log that prepends a timestamp exports.log = function() { console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); }; /** * Inherit the prototype methods from one constructor into another. * * The Function.prototype.inherits from lang.js rewritten as a standalone * function (not on Function.prototype). NOTE: If this file is to be loaded * during bootstrapping this function needs to be rewritten using some native * functions as prototype setup using normal JavaScript does not work as * expected during bootstrapping (see mirror.js in r114903). * * @param {function} ctor Constructor function which needs to inherit the * prototype. * @param {function} superCtor Constructor function to inherit prototype from. */ exports.inherits = inherits.exports; exports._extend = function(origin, add) { // Don't do anything if add isn't an object if (!add || !isObject(add)) return origin; var keys = Object.keys(add); var i = keys.length; while (i--) { origin[keys[i]] = add[keys[i]]; } return origin; }; function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } var kCustomPromisifiedSymbol = typeof Symbol !== 'undefined' ? Symbol('util.promisify.custom') : undefined; exports.promisify = function promisify(original) { if (typeof original !== 'function') throw new TypeError('The "original" argument must be of type Function'); if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { var fn = original[kCustomPromisifiedSymbol]; if (typeof fn !== 'function') { throw new TypeError('The "util.promisify.custom" argument must be of type Function'); } Object.defineProperty(fn, kCustomPromisifiedSymbol, { value: fn, enumerable: false, writable: false, configurable: true }); return fn; } function fn() { var promiseResolve, promiseReject; var promise = new Promise(function (resolve, reject) { promiseResolve = resolve; promiseReject = reject; }); var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); } args.push(function (err, value) { if (err) { promiseReject(err); } else { promiseResolve(value); } }); try { original.apply(this, args); } catch (err) { promiseReject(err); } return promise; } Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, { value: fn, enumerable: false, writable: false, configurable: true }); return Object.defineProperties( fn, getOwnPropertyDescriptors(original) ); }; exports.promisify.custom = kCustomPromisifiedSymbol; function callbackifyOnRejected(reason, cb) { // `!reason` guard inspired by bluebird (Ref: https://goo.gl/t5IS6M). // Because `null` is a special error value in callbacks which means "no error // occurred", we error-wrap so the callback consumer can distinguish between // "the promise rejected with null" or "the promise fulfilled with undefined". if (!reason) { var newReason = new Error('Promise was rejected with a falsy value'); newReason.reason = reason; reason = newReason; } return cb(reason); } function callbackify(original) { if (typeof original !== 'function') { throw new TypeError('The "original" argument must be of type Function'); } // We DO NOT return the promise as it gives the user a false sense that // the promise is actually somehow related to the callback's execution // and that the callback throwing will reject the promise. function callbackified() { var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); } var maybeCb = args.pop(); if (typeof maybeCb !== 'function') { throw new TypeError('The last argument must be of type Function'); } var self = this; var cb = function() { return maybeCb.apply(self, arguments); }; // In true node style we process the callback on `nextTick` with all the // implications (stack, `uncaughtException`, `async_hooks`) original.apply(this, args) .then(function(ret) { process.nextTick(cb, null, ret); }, function(rej) { process.nextTick(callbackifyOnRejected, rej, cb); }); } Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original)); Object.defineProperties(callbackified, getOwnPropertyDescriptors(original)); return callbackified; } exports.callbackify = callbackify; }(util$1)); const getPortPromise = util$1.promisify(portfinder.getPort); class PyClients { constructor(options, logger = console) { this.options = options; this.logger = logger; this.clients = new Map(); } async getClient(type) { if (this.clients.has(type)) { return this.clients.get(type); } const [promise, resolve, reject] = destructPromise(); const opt = this.options[type]; if (!opt) { throw new Error(`no config for client \`${type}\` found`); } try { if (typeof opt === 'string') { const client = new ZeroClient(); client.bind(opt); resolve(client); } else { const { scriptPath, ...option } = opt; const client = new PyProcessor(scriptPath, option, this.logger); await client.bind(`${await getPortPromise()}`); resolve(client); } this.logger.info(`PyClients: ${type} started`); } catch (err) { this.logger.error(`PyClients: ${type} start fail: ${JSON.stringify(err)}`); reject(err); } this.clients.set(type, promise); return promise; } async checkHost(type) { const client = await this.getClient(type); return client.request('checkHost'); } async warmup() { const opts = Object.keys(this.options); await Promise.all(opts.map((type) => this.getClient(type))); } /** * 模型预测 * @param type layout | mask | gauge | semantic * @param args */ async predictScoreImages(type, ...args) { const clientType = type.split('$')[0]; const client = await this.getClient(clientType); let res = null; this.logger.info(`[predictor]: ${type} py start..`); const start = Date.now(); switch (type) { case 'layout': res = await client.request('predictDetection', args); break; case 'layout$reinforce': res = await client.request('predictReinforce', args); break; case 'gauge': case 'mask': res = await client.request('predict', args, { by_buffer: true }); break; case 'semantic': case 'textLoc': res = await client.request('predict', args); break; case 'textOcr': case 'brackets': case 'topo': case 'gaugeRenderer': case 'jianpu': res = await client.request('predict', ...args); break; default: this.logger.error(`[predictor]: no predictor ${type}`); } this.logger.info(`[predictor]: ${type} py duration: ${Date.now() - start}ms`); return res; } } var PageLayoutMethod; (function (PageLayoutMethod) { PageLayoutMethod["ByLines"] = "ByLines"; PageLayoutMethod["ByBlocks"] = "ByBlocks"; })(PageLayoutMethod || (PageLayoutMethod = {})); var TextType; (function (TextType) { TextType["Title"] = "Title"; TextType["Author"] = "Author"; TextType["TempoText"] = "TempoText"; TextType["TempoNumeral"] = "TempoNumeral"; TextType["TextualMark"] = "TextualMark"; TextType["Lyric"] = "Lyric"; TextType["Instrument"] = "Instrument"; TextType["MeasureNumber"] = "MeasureNumber"; TextType["Times"] = "Times"; TextType["Alternation1"] = "Alternation1"; TextType["Alternation2"] = "Alternation2"; TextType["Chord"] = "Chord"; TextType["PageMargin"] = "PageMargin"; TextType["Other"] = "Other"; })(TextType || (TextType = {})); var SemanticType; (function (SemanticType) { // clefs SemanticType["ClefG"] = "ClefG"; SemanticType["ClefF"] = "ClefF"; SemanticType["ClefC"] = "ClefC"; // noteheads SemanticType["NoteheadS0"] = "NoteheadS0"; SemanticType["NoteheadS1"] = "NoteheadS1"; SemanticType["NoteheadS2"] = "NoteheadS2"; SemanticType["NoteheadS1stemU"] = "NoteheadS1stemU"; SemanticType["NoteheadS1stemD"] = "NoteheadS1stemD"; SemanticType["NoteheadS2stemU"] = "NoteheadS2stemU"; SemanticType["NoteheadS2stemD"] = "NoteheadS2stemD"; SemanticType["vline_Stem"] = "vline_Stem"; // flags SemanticType["Flag3"] = "Flag3"; // beams SemanticType["BeamLeft"] = "BeamLeft"; SemanticType["BeamContinue"] = "BeamContinue"; SemanticType["BeamRight"] = "BeamRight"; // tremolos SemanticType["TremoloLeft"] = "TremoloLeft"; SemanticType["TremoloRight"] = "TremoloRight"; SemanticType["TremoloMiddle"] = "TremoloMiddle"; // dots (duration) SemanticType["Dot"] = "Dot"; // rests SemanticType["Rest0"] = "Rest0"; SemanticType["Rest1"] = "Rest1"; SemanticType["Rest2"] = "Rest2"; SemanticType["Rest3"] = "Rest3"; SemanticType["Rest4"] = "Rest4"; SemanticType["Rest5"] = "Rest5"; SemanticType["Rest6"] = "Rest6"; SemanticType["Rest0W"] = "Rest0W"; SemanticType["RestM1"] = "RestM1"; // accidentals SemanticType["AccNatural"] = "AccNatural"; SemanticType["AccSharp"] = "AccSharp"; SemanticType["AccDoublesharp"] = "AccDoublesharp"; SemanticType["AccFlat"] = "AccFlat"; SemanticType["AccFlatflat"] = "AccFlatflat"; // volta SemanticType["vline_VoltaLeft"] = "vline_VoltaLeft"; SemanticType["vline_VoltaRight"] = "vline_VoltaRight"; SemanticType["VoltaLeft"] = "VoltaLeft"; SemanticType["VoltaRight"] = "VoltaRight"; SemanticType["VoltaAlternativeBegin"] = "VoltaAlternativeBegin"; //VoltaAlternativeEnd = "VoltaAlternativeEnd", // vertical bars SemanticType["BarMeasure"] = "BarMeasure"; SemanticType["vline_BarMeasure"] = "vline_BarMeasure"; SemanticType["vline_BarTerminal"] = "vline_BarTerminal"; SemanticType["vline_BarSegment"] = "vline_BarSegment"; // slur & tie SemanticType["SlurBegin"] = "SlurBegin"; SemanticType["SlurEnd"] = "SlurEnd"; // time signature SemanticType["TimesigC44"] = "TimesigC44"; SemanticType["TimesigC22"] = "TimesigC22"; SemanticType["TimesigZero"] = "TimesigZero"; SemanticType["TimesigOne"] = "TimesigOne"; SemanticType["TimesigTwo"] = "TimesigTwo"; SemanticType["TimesigThree"] = "TimesigThree"; SemanticType["TimesigFour"] = "TimesigFour"; SemanticType["TimesigFive"] = "TimesigFive"; SemanticType["TimesigSix"] = "TimesigSix"; SemanticType["TimesigSeven"] = "TimesigSeven"; SemanticType["TimesigEight"] = "TimesigEight"; SemanticType["TimesigNine"] = "TimesigNine"; // octave shifts SemanticType["OctaveShift8va"] = "OctaveShift8va"; SemanticType["OctaveShift8vb"] = "OctaveShift8vb"; SemanticType["OctaveShift8"] = "OctaveShift8"; SemanticType["OctaveShift0"] = "OctaveShift0"; // numbers SemanticType["Zero"] = "Zero"; SemanticType["One"] = "One"; SemanticType["Two"] = "Two"; SemanticType["Three"] = "Three"; SemanticType["Four"] = "Four"; SemanticType["Five"] = "Five"; SemanticType["Six"] = "Six"; SemanticType["Seven"] = "Seven"; SemanticType["Eight"] = "Eight"; SemanticType["Nine"] = "Nine"; // dynamics SemanticType["f"] = "f"; SemanticType["p"] = "p"; SemanticType["m"] = "m"; SemanticType["n"] = "n"; SemanticType["r"] = "r"; SemanticType["s"] = "s"; SemanticType["z"] = "z"; SemanticType["CrescendoBegin"] = "CrescendoBegin"; SemanticType["CrescendoEnd"] = "CrescendoEnd"; SemanticType["DecrescendoBegin"] = "DecrescendoBegin"; SemanticType["DecrescendoEnd"] = "DecrescendoEnd"; // scripts SemanticType["ScriptFermata"] = "ScriptFermata"; SemanticType["ScriptShortFermata"] = "ScriptShortFermata"; SemanticType["ScriptSforzato"] = "ScriptSforzato"; SemanticType["ScriptStaccato"] = "ScriptStaccato"; SemanticType["ScriptStaccatissimo"] = "ScriptStaccatissimo"; SemanticType["ScriptTurn"] = "ScriptTurn"; SemanticType["ScriptTrill"] = "ScriptTrill"; SemanticType["ScriptSegno"] = "ScriptSegno"; SemanticType["ScriptCoda"] = "ScriptCoda"; SemanticType["ScriptArpeggio"] = "ScriptArpeggio"; SemanticType["ScriptPrall"] = "ScriptPrall"; SemanticType["ScriptMordent"] = "ScriptMordent"; SemanticType["ScriptMarcato"] = "ScriptMarcato"; SemanticType["ScriptTenuto"] = "ScriptTenuto"; SemanticType["ScriptPortato"] = "ScriptPortato"; // pedal SemanticType["PedalStar"] = "PedalStar"; SemanticType["PedalPed"] = "PedalPed"; // additional annotation SemanticType["KeyAcc"] = "KeyAcc"; SemanticType["TempoNotehead"] = "TempoNotehead"; SemanticType["GraceNotehead"] = "GraceNotehead"; SemanticType["SignLined"] = "SignLined"; SemanticType["SignInterval"] = "SignInterval"; SemanticType["rect_Text"] = "rect_Text"; SemanticType["rect_Lyric"] = "rect_Lyric"; })(SemanticType || (SemanticType = {})); const glyphSemanticMapping = { 'rests.1': 'Rest1', 'rests.0o': 'Rest0', 'rests.1o': 'Rest1', 'rests.M1': 'RestM1', 'rests.2': 'Rest2', 'rests.3': 'Rest3', 'rests.4': 'Rest4', 'rests.5': 'Rest5', 'rests.6': 'Rest6', 'accidentals.sharp': 'AccSharp', 'accidentals.doublesharp': 'AccDoublesharp', 'accidentals.natural': 'AccNatural', 'accidentals.flat': 'AccFlat', 'accidentals.flatflat': 'AccFlatflat', 'dots.dot': 'Dot', 'scripts.ufermata': 'ScriptFermata', 'scripts.dfermata': 'ScriptFermata', 'scripts.ushortfermata': 'ScriptShortFermata', 'scripts.dshortfermata': 'ScriptShortFermata', 'scripts.staccato': 'ScriptStaccato', 'scripts.ustaccatissimo': 'ScriptStaccatissimo', 'scripts.dstaccatissimo': 'ScriptStaccatissimo', 'scripts.turn': 'ScriptTurn', 'scripts.trill': 'ScriptTrill', 'scripts.segno': 'ScriptSegno', 'scripts.coda': 'ScriptCoda', 'scripts.arpeggio': 'ScriptArpeggio', 'scripts.prall': 'ScriptPrall', 'scripts.mordent': 'ScriptMordent', 'scripts.umarcato': 'ScriptMarcato', 'scripts.dmarcato': 'ScriptMarcato', 'scripts.uportato': 'ScriptPortato', 'scripts.dportato': 'ScriptPortato', 'scripts.tenuto': 'ScriptTenuto', 'scripts.sforzato': 'ScriptSforzato', 'clefs.C': 'ClefC', 'clefs.F': 'ClefF', 'clefs.G': 'ClefG', 'clefs.F_change': 'ClefF', 'clefs.G_change': 'ClefG', 'timesig.C44': 'TimesigC44', 'timesig.C22': 'TimesigC22', 'pedal.*': 'PedalStar', 'pedal.Ped': 'PedalPed', 'noteheads.s0': 'NoteheadS0', 'noteheads.s1': 'NoteheadS1', 'noteheads.s2': 'NoteheadS2', f: 'f', m: 'm', p: 'p', r: 'r', s: 's', z: 'z', }; const semanticPriorities = { ClefG: 0, ClefF: 0, TimesigFour: 0, TimesigThree: 0, TimesigTwo: 0, NoteheadS0: 0, NoteheadS1: 0, NoteheadS2: 0, Dot: 0, vline_BarMeasure: 0, vline_Stem: 0, Flag3: 0, TimesigC44: 1, TimesigC22: 1, TimesigEight: 1, TimesigSix: 1, AccNatural: 1, AccSharp: 1, AccFlat: 1, KeyAcc: 1, Rest0: 1, Rest1: 1, Rest2: 1, Rest3: 1, Rest4: 1, OctaveShift8: 1, OctaveShift0: 1, AccDoublesharp: 2, AccFlatflat: 2, TimesigOne: 2, TimesigNine: 2, Rest5: 2, Rest6: 2, SlurBegin: 2, SlurEnd: 2, VoltaLeft: 2, VoltaRight: 2, //VoltaAlternativeBegin: 2, vline_BarTerminal: 2, vline_BarSegment: 2, TempoNotehead: 2, GraceNotehead: 2, SignLined: 2, SignInterval: 2, BeamLeft: 2, BeamRight: 2, BeamContinue: 2, TremoloLeft: 2, TremoloRight: 2, TremoloMiddle: 2, StemTip: 2, StemHead: 2, //Rest0W: 3, f: 3, p: 3, m: 3, ScriptFermata: 3, ScriptSforzato: 3, ScriptStaccato: 3, ScriptStaccatissimo: 3, ScriptTurn: 3, ScriptTrill: 3, ScriptSegno: 3, ScriptCoda: 3, ScriptArpeggio: 3, ScriptPrall: 3, ScriptMordent: 3, ScriptTenuto: 3, PedalStar: 3, PedalPed: 3, TimesigFive: 3, TimesigSeven: 3, TimesigZero: 3, One: 3, Two: 3, Three: 3, Four: 3, Five: 3, rect_Text: 3, rect_Lyric: 3, CrescendoBegin: 3, CrescendoEnd: 3, DecrescendoBegin: 3, DecrescendoEnd: 3, RestM1: 4, ClefC: 4, ScriptShortFermata: 4, ScriptMarcato: 4, ScriptPortato: 4, s: 4, r: 4, z: 4, Zero: 4, Six: 4, Seven: 4, Eight: 4, Nine: 4, }; const NOTEHEAD_WIDTHS = { NoteheadS0: 0.913 * 2, NoteheadS1: 0.632 * 2, NoteheadS2: 0.599 * 2, }; const glyphCenters = { //"clefs.C": { x: 1.3 }, 'clefs.F': { x: 1.06 }, 'clefs.G': { x: 1.3 }, 'clefs.F_change': { x: 0.87 }, 'clefs.G_change': { x: 1.07 }, 'timesig.C44': { x: 0.9 }, 'timesig.C22': { x: 0.9 }, zero: { x: 0.7, y: -1 }, one: { x: 0.7, y: -1 }, two: { x: 0.7, y: -1 }, three: { x: 0.7, y: -1 }, four: { x: 0.7, y: -1 }, five: { x: 0.7, y: -1 }, six: { x: 0.7, y: -1 }, seven: { x: 0.7, y: -1 }, eight: { x: 0.7, y: -1 }, nine: { x: 0.7, y: -1 }, 'accidentals.sharp': { x: 0.55 }, 'accidentals.doublesharp': { x: 0.5 }, 'accidentals.natural': { x: 0.3 }, 'accidentals.flat': { x: 0.3 }, 'accidentals.flatflat': { x: 0.5 }, 'noteheads.s0': { x: NOTEHEAD_WIDTHS.NoteheadS0 / 2 }, 'noteheads.s1': { x: NOTEHEAD_WIDTHS.NoteheadS1 / 2 }, 'noteheads.s2': { x: NOTEHEAD_WIDTHS.NoteheadS2 / 2 }, 'rests.0': { x: 0.75, y: 1 }, 'rests.1': { x: 0.75 }, 'rests.0o': { x: 0.75, y: 1 }, 'rests.1o': { x: 0.75 }, 'rests.M1': { x: 0.75, y: 1 }, 'rests.2': { x: 0.5 }, 'rests.3': { x: 0.5 }, 'rests.4': { x: 0.5 }, 'rests.5': { x: 0.5 }, 'rests.6': { x: 0.5 }, f: { x: 0.6, y: -0.5 }, m: { x: 0.9, y: -0.5 }, p: { x: 0.5, y: -0.5 }, r: { x: 0.5, y: -0.5 }, s: { x: 0.5, y: -0.5 }, z: { x: 0.5, y: -0.5 }, 'scripts.trill': { y: -0.5 }, 'scripts.segno': { x: 0, y: 0 }, 'scripts.coda': { x: 0, y: 0 }, 'scripts.arpeggio': { x: 0.5, y: -0.5 }, 'pedal.*': { x: 0.78, y: -0.78 }, 'pedal.Ped': { x: 1.6, y: -0.7 }, }; const ONE_D_SEMANTICS = [ 'OctaveShift8va', 'OctaveShift8vb', 'OctaveShift8', 'OctaveShift0', 'vline_VoltaLeft', 'vline_VoltaRight', 'VoltaAlternativeBegin', 'vline_BarMeasure', 'vline_BarTerminal', 'vline_BarSegment', ]; const SYSTEM_SEMANTIC_TYPES = [ SemanticType.BarMeasure, SemanticType.vline_BarMeasure, SemanticType.vline_BarTerminal, SemanticType.vline_BarSegment, SemanticType.vline_VoltaLeft, SemanticType.vline_VoltaRight, SemanticType.VoltaAlternativeBegin, ]; const st = SemanticType; const CONFLICTION_GROUPS = [ [st.NoteheadS0, st.NoteheadS1, st.NoteheadS2], [st.Zero, st.One, st.Two, st.Three, st.Four, st.Five, st.Six, st.Seven, st.Eight, st.Nine, st.ScriptStaccatissimo], [ st.TimesigZero, st.TimesigOne, st.TimesigTwo, st.TimesigThree, st.TimesigFour, st.TimesigFive, st.TimesigSix, st.TimesigSeven, st.TimesigEight, st.TimesigNine, ], [st.Rest0, st.Rest1, st.Rest2, st.Rest3, st.Rest4, st.Rest5, st.Rest6, st.Rest0W, st.RestM1], [st.SignInterval, st.SignLined], [st.BeamLeft, st.BeamContinue, st.BeamRight], ]; const STAMP_SEMANTICS = [ st.ClefG, st.ClefF, st.ClefC, st.NoteheadS0, st.NoteheadS1, st.NoteheadS2, st.Dot, st.Rest0, st.Rest1, st.Rest2, st.Rest3, st.Rest4, st.Rest5, st.Rest6, st.RestM1, st.AccNatural, st.AccSharp, st.AccDoublesharp, st.AccFlat, st.AccFlatflat, st.TimesigC44, st.TimesigC22, st.TimesigZero, st.TimesigOne, st.TimesigTwo, st.TimesigThree, st.TimesigFour, st.TimesigFive, st.TimesigSix, st.TimesigSeven, st.TimesigEight, st.TimesigNine, st.One, st.Two, st.Three, st.Four, st.Five, st.OctaveShift8, //st.OctaveShift15, st.OctaveShift0, st.f, st.p, st.m, st.n, st.r, st.s, st.z, st.ScriptFermata, st.ScriptShortFermata, st.ScriptSforzato, st.ScriptStaccato, st.ScriptStaccatissimo, st.ScriptTurn, st.ScriptTrill, st.ScriptSegno, st.ScriptCoda, st.ScriptArpeggio, st.ScriptPrall, st.ScriptMordent, st.ScriptMarcato, st.ScriptTenuto, st.ScriptPortato, st.PedalStar, st.PedalPed, ]; // [cx, cy, width, height] const STAMP_RECTS = { ClefG: [-0.0625, -1.125, 3.6, 8.6], ClefF: [0.25, 0.5625, 3.6, 3.8], ClefC: [0.25, 0, 3.25, 4.5], NoteheadS0: [0.0625, 0, 2.55, 1.4], NoteheadS1: [0.0625, 0, 1.8, 1.4], NoteheadS2: [0.0625, -0.0625, 1.65, 1.35], Dot: [0.25, 0, 0.6, 0.6], Rest0: [0, -0.75, 3.25, 0.9], Rest1: [0, -0.25, 3.25, 0.9], Rest2: [-0.0625, -0.1875, 1.6, 3.375], Rest3: [0, 0.0625, 1.2, 2.25], Rest4: [0.0625, 0.5625, 1.65, 3.375], Rest5: [0.0625, 0.0625, 1.95, 4.375], Rest6: [0.0625, 0.5625, 1.95, 5.375], RestM1: [-0.4375, -1.5, 0.75, 1.2], AccNatural: [0, 0, 0.9, 3.5], AccSharp: [0, 0, 1.5, 3.5], AccDoublesharp: [0, 0, 1.5, 1.5], AccFlat: [0, -0.5625, 1.2, 3.125], AccFlatflat: [0.1875, -0.5625, 1.95, 3.125], TimesigC44: [-0.0625, 0, 2.25, 2.3], TimesigC22: [-0.0625, 0, 2.25, 3.2], TimesigZero: [0, 0, 1.8, 2.2], TimesigOne: [-0.125, 0, 1.5, 2.2], TimesigTwo: [0, 0, 2.2, 2.2], TimesigThree: [-0.0625, 0, 1.9, 2.4], TimesigFour: [0.0625, 0, 1.95, 2.2], TimesigFive: [0, 0, 1.8, 2.3], TimesigSix: [0, 0, 2.0, 2.4], TimesigSeven: [0, 0, 1.8, 2.2], TimesigEight: [0, 0, 1.9, 2.2], TimesigNine: [0, 0, 1.9, 2.2], One: [-0.0625, 0, 0.75, 1.6], Two: [0, 0, 1.2, 1.6], Three: [0, 0, 1.2, 1.6], Four: [0, 0, 1.2, 1.6], Five: [0, 0, 1.2, 1.6], OctaveShift8: [2.125, -0.1875, 4.75, 3.6], OctaveShift0: [-0.4, 0, 1.8, 4.2], f: [0.0625, -0.125, 2.55, 3], p: [-0.0625, 0.25, 2.55, 2.1], m: [-0.125, -0.0625, 2.4, 1.35], n: [-0.3125, -0.0625, 1.95, 1.35], r: [0, -0.125, 1.5, 1.5], s: [0, -0.0625, 1.2, 1.35], z: [0.0625, 0, 1.35, 1.5], ScriptFermata: [0, 0, 3.25, 3.9], ScriptShortFermata: [0, 0, 2.4, 4.95], ScriptSforzato: [-0.0625, 0, 2.5, 1.2], ScriptStaccato: [0, -0.0625, 0.6, 0.45], ScriptStaccatissimo: [0, 0, 1.2, 2.6], ScriptTurn: [0, 0, 2.7, 1.5], ScriptTrill: [-0.125, -0.5, 3, 2.7], ScriptSegno: [0, 0, 2.4, 3.5], ScriptCoda: [0, 0, 2.7, 3.25], ScriptArpeggio: [-0.0625, 0, 1.05, 1.8], ScriptPrall: [0, 0, 2.4, 1.2], ScriptMordent: [0, 0, 2.4, 1.5], ScriptMarcato: [0, 0, 1.2, 2.475], ScriptTenuto: [0, -0.0625, 1.5, 0.15], ScriptPortato: [0, 0, 1.5, 1.65], PedalStar: [0, 0, 3.2, 3.2], PedalPed: [0, -0.25, 4.7, 2.4], }; const hashSemanticPoint = (systemIndex, staffIndex, point) => { const x = Math.round(point.x * 10); const y = Math.round(point.y * 10); const source = `${systemIndex}|${staffIndex}|${point.semantic}|${x}|${y}`; const hash = sha1__default["default"].array(source).slice(12); // clip to 12 bytes const id = globalThis.btoa(String.fromCharCode(...hash)).substring(0, 11); point.id = id; return id; }; const hashPageSemanticPoint = (pageName, point) => { const x = Math.round(point.x); const y = Math.round(point.y); const source = `p-${pageName}|${point.semantic}|${x}|${y}`; const hash = sha1__default["default"].array(source).slice(12); // clip to 12 bytes const id = globalThis.btoa(String.fromCharCode(...hash)).substring(0, 11); point.id = id; return id; }; var TokenType; (function (TokenType) { // clefs TokenType["ClefG"] = "clefs-G"; TokenType["ClefF"] = "clefs-F"; TokenType["ClefC"] = "clefs-C"; // time signature TokenType["TimesigC44"] = "timesig-C44"; TokenType["TimesigC22"] = "timesig-C22"; TokenType["TimesigZero"] = "zero|timesig0"; TokenType["TimesigOne"] = "one|timesig1"; TokenType["TimesigTwo"] = "two|timesig2"; TokenType["TimesigThree"] = "three|timesig3"; TokenType["TimesigFour"] = "four|timesig4"; TokenType["TimesigFive"] = "five|timesig5"; TokenType["TimesigSix"] = "six|timesig6"; TokenType["TimesigSeven"] = "seven|timesig7"; TokenType["TimesigEight"] = "eight|timesig8"; TokenType["TimesigNine"] = "nine|timesig9"; // octave shifts TokenType["OctaveShift8va"] = "octave-a"; TokenType["OctaveShift8vb"] = "octave-b"; TokenType["OctaveShift0"] = "octave-0"; // numbers TokenType["Zero"] = "zero|n0"; TokenType["One"] = "one|n1"; TokenType["Two"] = "two|n2"; TokenType["Three"] = "three|n3"; TokenType["Four"] = "four|n4"; TokenType["Five"] = "five|n5"; TokenType["Six"] = "six|n6"; TokenType["Seven"] = "seven|n7"; TokenType["Eight"] = "eight|n8"; TokenType["Nine"] = "nine|n9"; // accidentals TokenType["AccNatural"] = "accidentals-natural"; TokenType["AccSharp"] = "accidentals-sharp"; TokenType["AccDoublesharp"] = "accidentals-doublesharp"; TokenType["AccFlat"] = "accidentals-flat"; TokenType["AccFlatflat"] = "accidentals-flatflat"; TokenType["KeyNatural"] = "accidentals-natural|key-natural"; TokenType["KeySharp"] = "accidentals-sharp|key-sharp"; TokenType["KeyFlat"] = "accidentals-flat|key-flat"; // noteheads TokenType["NoteheadS0"] = "noteheads-s0"; TokenType["NoteheadS1"] = "noteheads-s1"; TokenType["NoteheadS2"] = "noteheads-s2"; TokenType["NoteheadS1stemU"] = "noteheads-s1|noteheads-s1-u"; TokenType["NoteheadS1stemD"] = "noteheads-s1|noteheads-s1-d"; TokenType["NoteheadS2stemU"] = "noteheads-s2|noteheads-s2-u"; TokenType["NoteheadS2stemD"] = "noteheads-s2|noteheads-s2-d"; // rests TokenType["Rest0"] = "rests-0o"; TokenType["Rest1"] = "rests-1o"; TokenType["Rest2"] = "rests-2"; TokenType["Rest3"] = "rests-3"; TokenType["Rest4"] = "rests-4"; TokenType["Rest5"] = "rests-5"; TokenType["Rest6"] = "rests-6"; TokenType["Rest0W"] = "rests-0"; TokenType["RestM1"] = "rests-M1"; // flags TokenType["Flag3"] = "flags-u3"; TokenType["Flag4"] = "flags-u4"; TokenType["Flag5"] = "flags-u5"; TokenType["Flag6"] = "flags-u6"; TokenType["Flag7"] = "flags-u7"; TokenType["Flag8"] = "flags-u8"; // beams TokenType["BeamLeft"] = "|beam-left"; TokenType["BeamRight"] = "|beam-right"; TokenType["BeamContinue"] = "|beam-continue"; // tremolos TokenType["TremoloLeft"] = "|tremolo-left"; TokenType["TremoloRight"] = "|tremolo-right"; TokenType["TremoloMiddle"] = "|tremolo-middle"; // slur & tie TokenType["SlurBegin"] = "|slur-begin"; TokenType["SlurEnd"] = "|slur-end"; TokenType["TieBegin"] = "|tie-begin"; TokenType["TieEnd"] = "|tie-end"; // volta TokenType["VoltaLeft"] = "|volta-left"; TokenType["VoltaRight"] = "|volta-right"; TokenType["VoltaAlternativeBegin"] = "|volta-alter-begin"; //VoltaAlternativeEnd = "|volta-alter-end", // vertical bars //BarMeasure = "|bar-measure", TokenType["BarTerminal"] = "|bar-terminal"; TokenType["BarSegment"] = "|bar-segment"; // dots (duration) TokenType["Dot"] = "|dot"; TokenType["DotDot"] = "|dotdot"; // dynamics TokenType["f"] = "f"; TokenType["p"] = "p"; TokenType["m"] = "m"; TokenType["r"] = "r"; TokenType["s"] = "s"; TokenType["z"] = "z"; // TokenType["WedgeCrescendo"] = "|wedge-crescendo"; TokenType["WedgeDiminuendo"] = "|wedge-diminuendo"; TokenType["WedgeClose"] = "|wedge-close"; TokenType["CrescendoBegin"] = "|wedge-crescendo"; TokenType["DecrescendoBegin"] = "|wedge-diminuendo"; TokenType["CrescendoEnd"] = "|wedge-close"; TokenType["DecrescendoEnd"] = "|wedge-close"; // scripts TokenType["ScriptFermata"] = "scripts-ufermata"; TokenType["ScriptShortFermata"] = "scripts-ushortfermata"; TokenType["ScriptSforzato"] = "scripts-sforzato"; TokenType["ScriptStaccato"] = "scripts-staccato"; TokenType["ScriptStaccatissimo"] = "scripts-ustaccatissimo"; TokenType["ScriptTurn"] = "scripts-turn"; TokenType["ScriptTrill"] = "scripts-trill"; TokenType["ScriptSegno"] = "scripts-segno"; TokenType["ScriptCoda"] = "scripts-coda"; TokenType["ScriptArpeggio"] = "scripts-arpeggio"; TokenType["ScriptPrall"] = "scripts-prall"; TokenType["ScriptMordent"] = "scripts-mordent"; TokenType["ScriptMarcato"] = "scripts-umarcato"; TokenType["ScriptTenuto"] = "scripts-tenuto"; TokenType["ScriptPortato"] = "scripts-uportato"; // pedal TokenType["PedalStar"] = "pedal-star"; TokenType["PedalPed"] = "pedal-Ped"; TokenType["Text"] = "|text"; TokenType["GraceNotehead"] = "|grace-notehead"; })(TokenType || (TokenType = {})); // alias const tt = TokenType; const TokenTypes = Object.values(TokenType); const TokenClefs = TokenTypes.filter((t) => /clefs-/.test(t)); const TokenTimesigs = TokenTypes.filter((t) => /timesig/.test(t)); const TokenTimesigsC = TokenTypes.filter((t) => /timesig-/.test(t)); const TokenTimesigsN = TokenTypes.filter((t) => /timesig\d/.test(t)); const TokenOctshifts = TokenTypes.filter((t) => /octave-/.test(t)); const TokenNumbers = TokenTypes.filter((t) => /n\d/.test(t)); const TokenAccidentals = TokenTypes.filter((t) => /accidentals-/.test(t)); const TokenNoteheads = TokenTypes.filter((t) => /noteheads-/.test(t)); const TokenBareNoteheads = [tt.NoteheadS0, tt.NoteheadS1, tt.NoteheadS2]; const TokenDirectionalNoteheads = TokenTypes.filter((t) => /noteheads-.+-[ud]/.test(t)); const TokenRests = TokenTypes.filter((t) => /rests-/.test(t)); const TokenFlags = TokenTypes.filter((t) => /flags-/.test(t)); const TokenVolta = TokenTypes.filter((t) => /volta-/.test(t)); const TokenDynamics = TokenTypes.filter((t) => /^[a-z]$/.test(t)); const TokenScripts = TokenTypes.filter((t) => /scripts-/.test(t)); const TokenPedals = TokenTypes.filter((t) => /pedal-/.test(t)); const TokenDots = [tt.Dot, tt.DotDot]; const TokenArcs = [tt.SlurBegin, tt.SlurEnd, tt.TieBegin, tt.TieEnd]; const TokenBeams = TokenTypes.filter((t) => /beam-/.test(t)); const TokenWedges = TokenTypes.filter((t) => /wedge-/.test(t)); const TokenAccessories = [ ...TokenNumbers, ...TokenDynamics, ...TokenWedges, ...TokenPedals, ...TokenArcs, tt.ScriptFermata, tt.ScriptShortFermata, tt.ScriptSforzato, tt.ScriptStaccato, tt.ScriptStaccatissimo, tt.ScriptTurn, tt.ScriptTrill, tt.ScriptPrall, tt.ScriptMordent, tt.ScriptMarcato, tt.ScriptTenuto, tt.ScriptPortato, ]; const TokenDirectionless = [...TokenPedals]; const TokenGlyphs = [ ...TokenClefs, ...TokenTimesigs, ...TokenNumbers, ...TokenAccidentals, tt.NoteheadS0, tt.NoteheadS1, tt.NoteheadS2, ...TokenRests, ...TokenDynamics, ...TokenScripts, ...TokenPedals, ...TokenDots, ]; const TOKEN_Y_ROUND = {}; TokenClefs.forEach((t) => (TOKEN_Y_ROUND[t] = 1)); TokenTimesigsN.forEach((t) => (TOKEN_Y_ROUND[t] = 1)); TokenAccidentals.forEach((t) => (TOKEN_Y_ROUND[t] = 0.5)); TokenNoteheads.forEach((t) => (TOKEN_Y_ROUND[t] = 0.5)); TokenRests.forEach((t) => (TOKEN_Y_ROUND[t] = 0.5)); TokenDots.forEach((t) => (TOKEN_Y_ROUND[t] = 0.5)); const TOKEN_Y_FIXED = {}; TokenTimesigsC.forEach((t) => (TOKEN_Y_FIXED[t] = 0)); TokenVolta.forEach((t) => (TOKEN_Y_FIXED[t] = 0)); class Token { constructor(data) { Object.assign(this, data); } get typeId() { return this.type.split('|').reverse()[0]; } get isPredicted() { return Number.isFinite(this.confidence); } get isNotehead() { return TokenDirectionalNoteheads.includes(this.type) || this.type === TokenType.NoteheadS0; } get isContexted() { return (TokenClefs.includes(this.type) || TokenTimesigs.includes(this.type) || TokenOctshifts.includes(this.type) || TokenAccidentals.includes(this.type)); } get isAccessory() { return TokenNumbers.includes(this.type) || TokenDynamics.includes(this.type) || TokenScripts.includes(this.type) || TokenPedals.includes(this.type); } get division() { switch (this.type) { case tt.NoteheadS0: return 0; case tt.NoteheadS1stemU: case tt.NoteheadS1stemD: return 1; case tt.NoteheadS2stemU: case tt.NoteheadS2stemD: return 2; case tt.Flag3: return 3; case tt.Flag4: return 4; case tt.Flag5: return 5; case tt.Flag6: return 6; case tt.Flag7: return 7; case tt.Flag8: return 8; case tt.RestM1: return -1; case tt.Rest0: return 0; case tt.Rest1: return 1; case tt.Rest2: return 2; case tt.Rest3: return 3; case tt.Rest4: return 4; case tt.Rest5: return 5; case tt.Rest6: return 6; // TODO: //case tt.Rest0W: // return 0; } return null; } get dots() { switch (this.type) { case tt.Dot: return 1; case tt.DotDot: return 2; } return null; } get direction() { switch (this.type) { case tt.NoteheadS1stemU: case tt.NoteheadS2stemU: return 'u'; case tt.NoteheadS1stemD: case tt.NoteheadS2stemD: return 'd'; } return null; } get width() { switch (this.type) { case tt.NoteheadS0: return NOTEHEAD_WIDTHS.NoteheadS0; case tt.NoteheadS1stemU: case tt.NoteheadS1stemD: return NOTEHEAD_WIDTHS.NoteheadS1; case tt.NoteheadS2stemU: case tt.NoteheadS2stemD: return NOTEHEAD_WIDTHS.NoteheadS2; } } get left() { switch (this.type) { case tt.NoteheadS0: return this.x - this.width / 2; case tt.NoteheadS1stemU: case tt.NoteheadS2stemU: return this.x - this.width; case tt.NoteheadS1stemD: case tt.NoteheadS2stemD: return this.x; } return this.x; } get right() { switch (this.type) { case tt.NoteheadS0: return this.x + this.width / 2; case tt.NoteheadS1stemU: case tt.NoteheadS2stemU: return this.x; case tt.NoteheadS1stemD: case tt.NoteheadS2stemD: return this.x + this.width; } return this.x; } get voiceIndices() { if (!this.voice || this.voice < 0) return []; return Array(Math.floor(Math.log2(this.voice)) + 1) .fill(null) .reduce((indices, _, i) => (this.voice & (1 << i) ? [i + 1, ...indices] : indices), []); } } Token.className = 'Token'; class TextToken extends Token { constructor(data) { super(data); Object.assign(this, data); } get width() { return this.width_; } set width(value) { this.width_ = value; } } const recoverJSON = (json, classDict) => { if (typeof json === 'object') json = JSON.stringify(json); return JSON.parse(json, (_, value) => { if (value && typeof value === 'object' && value.__prototype) { const Class = classDict[value.__prototype]; if (Class) { const { __prototype, ...fields } = value; return new Class(fields); } } return value; }); }; const deepCopy = (o, dict = null) => { dict = dict || new Map(); if (dict.get(o)) return dict.get(o); if (Array.isArray(o)) { const result = []; dict.set(o, result); o.forEach((e) => result.push(deepCopy(e, dict))); return result; } else if (o && typeof o === 'object') { const result = {}; dict.set(o, result); Object.entries(o).forEach(([key, value]) => (result[key] = deepCopy(value, dict))); Object.setPrototypeOf(result, o.__proto__); return result; } return o; }; class SimpleClass { assign(data) { if (data) Object.assign(this, data); } toJSON() { const cls = this.constructor; const serializedKeys = cls.serializedKeys || (cls.blackKeys && Object.keys(this).filter((key) => !cls.blackKeys.includes(key))); const fields = serializedKeys ? pick__default["default"](this, serializedKeys) : this; return { __prototype: cls.className, ...fields, }; } deepCopy() { return deepCopy(this); } } var LayoutType; (function (LayoutType) { LayoutType["Ordinary"] = "ordinary"; LayoutType["Full"] = "full"; LayoutType["Conservative"] = "conservative"; LayoutType["Once"] = "once"; })(LayoutType || (LayoutType = {})); const spreadMeasureSeq = (seq, type = LayoutType.Ordinary) => [].concat(...seq.map((layout) => layout.serialize(type))); const seqToCode = (seq, { withBrackets = false } = {}) => { //const code = seq.map(layout => layout.code).join(", "); let code = ''; let inRange = false; for (let i = 0; i < seq.length; ++i) { const middle = seq[i - 1] instanceof SingleMLayout && seq[i] instanceof SingleMLayout && seq[i + 1] instanceof SingleMLayout; if (middle) { if (!inRange) { code += '..'; inRange = true; } } else { if (i > 0 && !inRange) code += ', '; inRange = false; code += seq[i].code; } } return withBrackets ? `[${code}]` : code; }; class SingleMLayout extends SimpleClass { static from(measure) { const layout = new SingleMLayout(); layout.measure = measure; return layout; } constructor(data = undefined) { super(); this.assign(data); } serialize() { return [this.measure]; } get seq() { return [this]; } get code() { return this.measure.toString(); } } SingleMLayout.className = 'SingleMLayout'; class BlockMLayout extends SimpleClass { static trimSeq(seq) { const seq2 = []; for (const layout of seq) { if (layout instanceof BlockMLayout) { for (const sub of layout.seq) seq2.push(sub); } else seq2.push(layout); } // reduce duplicated or backwards single measures const seq3 = []; let measure = null; for (const layout of seq2) { if (layout instanceof SingleMLayout) { if (layout.measure > measure) { seq3.push(layout); measure = layout.measure; } } else seq3.push(layout); } return seq3; } static fromSeq(seq) { const layout = new BlockMLayout(); layout.seq = BlockMLayout.trimSeq(seq); return layout; } constructor(data = undefined) { super(); this.assign(data); } serialize(type) { return spreadMeasureSeq(this.seq, type); } get code() { return seqToCode(this.seq, { withBrackets: true }); } } BlockMLayout.className = 'BlockMLayout'; class VoltaMLayout extends SimpleClass { constructor(data = undefined) { super(); this.assign(data); } serialize(type) { const bodySeq = spreadMeasureSeq(this.body); if (this.alternates) { const alternateSeqs = this.alternates.map((seq) => spreadMeasureSeq(seq)); const lastAlternateSeq = alternateSeqs[alternateSeqs.length - 1]; switch (type) { case LayoutType.Ordinary: return bodySeq.concat(...alternateSeqs); case LayoutType.Conservative: case LayoutType.Full: { const priorSeq = [].concat(...Array(this.times - 1) .fill(null) .map((_, i) => [...bodySeq, ...alternateSeqs[i % (this.times - 1)]])); return [...priorSeq, ...bodySeq, ...lastAlternateSeq]; } case LayoutType.Once: return [...bodySeq, ...lastAlternateSeq]; } } else { switch (type) { case LayoutType.Ordinary: case LayoutType.Conservative: case LayoutType.Once: return bodySeq; case LayoutType.Full: return [].concat(...Array(this.times) .fill(null) .map(() => bodySeq)); } } console.warn('the current case not handled:', type, this); } get seq() { const alternates = this.alternates ? this.alternates[this.alternates.length - 1] : []; return [...this.body, ...alternates]; } get code() { const body = seqToCode(this.body, { withBrackets: true }); let code = `${this.times}*${body}`; if (this.alternates) code += '{' + this.alternates.map((seq) => seqToCode(seq, { withBrackets: seq.length > 1 })).join(', ') + '}'; return code; } } VoltaMLayout.className = 'VoltaMLayout'; class ABAMLayout extends SimpleClass { constructor(data = undefined) { super(); this.assign(data); } serialize(type) { const seqA = this.main.serialize(type); const seqA_ = spreadMeasureSeq(this.main.seq, LayoutType.Once); const seqB = spreadMeasureSeq(this.rest, type); switch (type) { case LayoutType.Ordinary: // A B return [...seqA, ...seqB]; case LayoutType.Once: // B A' return [...seqB, ...seqA_]; case LayoutType.Conservative: // A B A' case LayoutType.Full: // A B A' return [...seqA, ...seqB, ...seqA_]; default: console.warn('the current case not handled:', type, this); } } get seq() { return [this.main, ...this.rest]; } get code() { return '<' + this.main.code + ', ' + seqToCode(this.rest) + '>'; } } ABAMLayout.className = 'ABAMLayout'; var measureLayout = /*#__PURE__*/Object.freeze({ __proto__: null, get LayoutType () { return LayoutType; }, SingleMLayout: SingleMLayout, BlockMLayout: BlockMLayout, VoltaMLayout: VoltaMLayout, ABAMLayout: ABAMLayout }); /* parser generated by jison 0.4.18 */ /* Returns a Parser object of the following structure: Parser: { yy: {} } Parser.prototype: { yy: {}, trace: function(), symbols_: {associative list: name ==> number}, terminals_: {associative list: number ==> name}, productions_: [...], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), table: [...], defaultActions: {...}, parseError: function(str, hash), parse: function(input), lexer: { EOF: 1, parseError: function(str, hash), setInput: function(input), input: function(), unput: function(str), more: function(), less: function(n), pastInput: function(), upcomingInput: function(), showPosition: function(), test_match: function(regex_match_array, rule_index), next: function(), lex: function(), begin: function(condition), popState: function(), _currentRules: function(), topState: function(), pushState: function(condition), options: { ranges: boolean (optional: true ==> token location info will include a .range[] member) flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) }, performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), rules: [...], conditions: {associative list: name ==> set}, } } token location info (@$, _$, etc.): { first_line: n, last_line: n, first_column: n, last_column: n, range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) } the parseError function receives a 'hash' object with these members for lexer and parser errors: { text: (matched text) token: (the produced terminal token, if any) line: (yylineno) } while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { loc: (yylloc) expected: (string describing the set of expected tokens) recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ var parser$1 = (function () { var o = function (k, v, o, l) { for (o = o || {}, l = k.length; l--; o[k[l]] = v) ; return o; }, $V0 = [1, 13], $V1 = [1, 16], $V2 = [1, 15], $V3 = [1, 26], $V4 = [1, 29], $V5 = [1, 28], $V6 = [1, 30], $V7 = [5, 13, 22, 27, 29], $V8 = [2, 15], $V9 = [1, 32], $Va = [5, 14, 21, 22, 27, 28, 29]; var parser = { trace: function trace() { }, yy: {}, symbols_: { error: 2, start_symbol: 3, measure_layout: 4, EOF: 5, index_wise_measure_layout: 6, 'i:': 7, 's:': 8, segment_wise_measure_layout: 9, iw_sequence: 10, iw_item: 11, range: 12, ',': 13, UNSIGNED: 14, '..': 15, single: 16, iw_block_item: 17, iw_volta: 18, iw_aba: 19, iw_block: 20, '[': 21, ']': 22, '*': 23, iw_optional_alternates: 24, iw_alternates: 25, '{': 26, '}': 27, '<': 28, '>': 29, sw_sequence: 30, sw_item: 31, segment: 32, sw_block_item: 33, sw_volta: 34, sw_aba: 35, sw_block: 36, sw_optional_alternates: 37, sw_alternates: 38, $accept: 0, $end: 1, }, terminals_: { 2: 'error', 5: 'EOF', 7: 'i:', 8: 's:', 13: ',', 14: 'UNSIGNED', 15: '..', 21: '[', 22: ']', 23: '*', 26: '{', 27: '}', 28: '<', 29: '>', }, productions_: [ 0, [3, 2], [4, 1], [4, 2], [4, 2], [6, 1], [10, 1], [10, 1], [10, 3], [10, 3], [12, 3], [11, 1], [11, 1], [11, 1], [11, 1], [16, 1], [17, 1], [20, 3], [18, 4], [24, 0], [24, 1], [25, 3], [19, 5], [9, 1], [30, 1], [30, 2], [31, 1], [31, 1], [31, 1], [31, 1], [32, 1], [33, 1], [36, 3], [34, 4], [37, 0], [37, 1], [38, 3], [35, 4], ], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { /* this == yyval */ var $0 = $$.length - 1; switch (yystate) { case 1: return $$[$0 - 1]; case 2: this.$ = root(null, $$[$0]); break; case 3: this.$ = root('index-wise', $$[$0]); break; case 4: this.$ = root('segment-wise', serialize($$[$0])); break; case 5: case 23: if ($$[$0].length === 1 && $$[$0][0].__prototype === 'BlockMLayout') this.$ = $$[$0][0]; else this.$ = blockLayout($$[$0]); break; case 6: case 24: this.$ = [$$[$0]]; break; case 7: case 11: case 12: case 13: case 14: case 20: case 27: case 28: case 29: case 35: this.$ = $$[$0]; break; case 8: this.$ = [...$$[$0 - 2], $$[$0]]; break; case 9: this.$ = [...$$[$0 - 2], ...$$[$0]]; break; case 10: this.$ = range($$[$0 - 2], $$[$0]); break; case 15: this.$ = singleLayout($$[$0]); break; case 16: case 31: this.$ = blockLayout($$[$0]); break; case 17: case 32: this.$ = $$[$0 - 1]; break; case 18: case 33: this.$ = voltaBlock($$[$0 - 3], $$[$0 - 1], $$[$0]); break; case 19: case 34: this.$ = null; break; case 21: case 36: this.$ = alternates($$[$0 - 1]); break; case 22: this.$ = abaBlock($$[$0 - 3], $$[$0 - 1]); break; case 25: this.$ = [...$$[$0 - 1], $$[$0]]; break; case 26: this.$ = blockLayout([$$[$0]]); break; case 30: this.$ = segment($$[$0]); break; case 37: this.$ = abaBlock($$[$0 - 2], $$[$0 - 1]); break; } }, table: [ { 3: 1, 4: 2, 6: 3, 7: [1, 4], 8: [1, 5], 10: 6, 11: 7, 12: 8, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 1: [3] }, { 5: [1, 17] }, { 5: [2, 2] }, { 6: 18, 10: 6, 11: 7, 12: 8, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 9: 19, 14: $V3, 21: $V4, 28: $V5, 30: 20, 31: 21, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, { 5: [2, 5], 13: $V6 }, o($V7, [2, 6]), o($V7, [2, 7]), o($V7, [2, 11]), o($V7, [2, 12]), o($V7, [2, 13]), o($V7, [2, 14]), o($V7, $V8, { 15: [1, 31], 23: $V9 }), o($V7, [2, 16]), { 11: 33, 14: [1, 34], 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 10: 35, 11: 7, 12: 8, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 1: [2, 1] }, { 5: [2, 3] }, { 5: [2, 4] }, { 5: [2, 23], 14: $V3, 21: $V4, 28: $V5, 31: 36, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, o($Va, [2, 24]), o($Va, [2, 26]), o($Va, [2, 27]), o($Va, [2, 28]), o($Va, [2, 29]), o($Va, [2, 30], { 23: [1, 37] }), o($Va, [2, 31]), { 14: $V3, 21: $V4, 28: $V5, 31: 38, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, { 14: $V3, 21: $V4, 28: $V5, 30: 39, 31: 21, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, { 11: 40, 12: 41, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 14: [1, 42] }, { 20: 43, 21: $V1 }, { 13: [1, 44] }, { 13: $V8, 23: $V9 }, { 13: $V6, 22: [1, 45] }, o($Va, [2, 25]), { 21: $V4, 36: 46 }, { 14: $V3, 21: $V4, 28: $V5, 30: 47, 31: 21, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, { 14: $V3, 21: $V4, 22: [1, 48], 28: $V5, 31: 36, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, o($V7, [2, 8]), o($V7, [2, 9]), o($V7, [2, 10]), o($V7, [2, 19], { 24: 49, 25: 50, 26: [1, 51] }), { 10: 52, 11: 7, 12: 8, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, o([5, 13, 22, 26, 27, 29], [2, 17]), o($Va, [2, 34], { 37: 53, 38: 54, 26: [1, 55] }), { 14: $V3, 21: $V4, 28: $V5, 29: [1, 56], 31: 36, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, o([5, 14, 21, 22, 26, 27, 28, 29], [2, 32]), o($V7, [2, 18]), o($V7, [2, 20]), { 10: 57, 11: 7, 12: 8, 14: $V0, 16: 9, 17: 10, 18: 11, 19: 12, 20: 14, 21: $V1, 28: $V2 }, { 13: $V6, 29: [1, 58] }, o($Va, [2, 33]), o($Va, [2, 35]), { 14: $V3, 21: $V4, 28: $V5, 30: 59, 31: 21, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, o($Va, [2, 37]), { 13: $V6, 27: [1, 60] }, o($V7, [2, 22]), { 14: $V3, 21: $V4, 27: [1, 61], 28: $V5, 31: 36, 32: 22, 33: 23, 34: 24, 35: 25, 36: 27 }, o($V7, [2, 21]), o($Va, [2, 36]), ], defaultActions: { 3: [2, 2], 17: [2, 1], 18: [2, 3], 19: [2, 4] }, parseError: function parseError(str, hash) { if (hash.recoverable) { this.trace(str); } else { var error = new Error(str); error.hash = hash; throw error; } }, parse: function parse(input) { var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); var lexer = Object.create(this.lexer); var sharedState = { yy: {} }; for (var k in this.yy) { if (Object.prototype.hasOwnProperty.call(this.yy, k)) { sharedState.yy[k] = this.yy[k]; } } lexer.setInput(input, sharedState.yy); sharedState.yy.lexer = lexer; sharedState.yy.parser = this; if (typeof lexer.yylloc == 'undefined') { lexer.yylloc = {}; } var yyloc = lexer.yylloc; lstack.push(yyloc); var ranges = lexer.options && lexer.options.ranges; if (typeof sharedState.yy.parseError === 'function') { this.parseError = sharedState.yy.parseError; } else { this.parseError = Object.getPrototypeOf(this).parseError; } var lex = function () { var token; token = lexer.lex() || EOF; if (typeof token !== 'number') { token = self.symbols_[token] || token; } return token; }; var symbol, state, action, r, yyval = {}, p, len, newState, expected; while (true) { state = stack[stack.length - 1]; if (this.defaultActions[state]) { action = this.defaultActions[state]; } else { if (symbol === null || typeof symbol == 'undefined') { symbol = lex(); } action = table[state] && table[state][symbol]; } if (typeof action === 'undefined' || !action.length || !action[0]) { var errStr = ''; expected = []; for (p in table[state]) { if (this.terminals_[p] && p > TERROR) { expected.push("'" + this.terminals_[p] + "'"); } } if (lexer.showPosition) { errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ", got '" + (this.terminals_[symbol] || symbol) + "'"; } else { errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'"); } this.parseError(errStr, { text: lexer.match, token: this.terminals_[symbol] || symbol, line: lexer.yylineno, loc: yyloc, expected: expected, }); } if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); } switch (action[0]) { case 1: stack.push(symbol); vstack.push(lexer.yytext); lstack.push(lexer.yylloc); stack.push(action[1]); symbol = null; { yyleng = lexer.yyleng; yytext = lexer.yytext; yylineno = lexer.yylineno; yyloc = lexer.yylloc; } break; case 2: len = this.productions_[action[1]][1]; yyval.$ = vstack[vstack.length - len]; yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column, }; if (ranges) { yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; } r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); if (typeof r !== 'undefined') { return r; } if (len) { stack = stack.slice(0, -1 * len * 2); vstack = vstack.slice(0, -1 * len); lstack = lstack.slice(0, -1 * len); } stack.push(this.productions_[action[1]][0]); vstack.push(yyval.$); lstack.push(yyval._$); newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; stack.push(newState); break; case 3: return true; } } return true; }, }; const root = (type, data) => ({ __prototype: 'MesaureLayout', type, data }); const singleLayout = (n) => ({ __prototype: 'SingleMLayout', measure: Number(n) }); const blockLayout = (seq) => ({ __prototype: 'BlockMLayout', seq }); const voltaBlock = (times, body, alternates) => ({ __prototype: 'VoltaMLayout', times: Number(times), body, alternates }); const abaBlock = (main, rest) => ({ __prototype: 'ABAMLayout', main, rest }); const segment = (n) => ({ segment: true, length: Number(n) }); const alternates = (items) => items.map((item) => { if (item.__prototype === 'BlockMLayout') return item.seq; return [item]; }); const range = (start, end) => { start = Number(start); end = Number(end); if (!(end >= start)) throw new Error(`invalid measure range: ${start}..${end}`); return Array(end + 1 - start) .fill(0) .map((_, i) => singleLayout(start + i)); }; const serializeSeq = (item, options) => { if (item.segment) { const index = options.index; options.index += item.length; return Array(item.length) .fill(0) .map((_, i) => singleLayout(index + i)); } return [serialize(item, options)]; }; const serialize = (item, options = { index: 1 }) => { const speard = (seq) => [].concat(...seq.map((it) => serializeSeq(it, options))); switch (item.__prototype) { case 'BlockMLayout': item.seq = speard(item.seq); break; case 'VoltaMLayout': item.body = speard(item.body); item.alternates = item.alternates && item.alternates.map(speard); break; case 'ABAMLayout': item.main = serialize(item.main, options); item.rest = speard(item.rest); break; } return item; }; /* generated by jison-lex 0.3.4 */ var lexer = (function () { var lexer = { EOF: 1, parseError: function parseError(str, hash) { if (this.yy.parser) { this.yy.parser.parseError(str, hash); } else { throw new Error(str); } }, // resets the lexer, sets new input setInput: function (input, yy) { this.yy = yy || this.yy || {}; this._input = input; this._more = this._backtrack = this.done = false; this.yylineno = this.yyleng = 0; this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0, }; if (this.options.ranges) { this.yylloc.range = [0, 0]; } this.offset = 0; return this; }, // consumes and returns one char from the input input: function () { var ch = this._input[0]; this.yytext += ch; this.yyleng++; this.offset++; this.match += ch; this.matched += ch; var lines = ch.match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno++; this.yylloc.last_line++; } else { this.yylloc.last_column++; } if (this.options.ranges) { this.yylloc.range[1]++; } this._input = this._input.slice(1); return ch; }, // unshifts one char (or a string) into the input unput: function (ch) { var len = ch.length; var lines = ch.split(/(?:\r\n?|\n)/g); this._input = ch + this._input; this.yytext = this.yytext.substr(0, this.yytext.length - len); //this.yyleng -= len; this.offset -= len; var oldLines = this.match.split(/(?:\r\n?|\n)/g); this.match = this.match.substr(0, this.match.length - 1); this.matched = this.matched.substr(0, this.matched.length - 1); if (lines.length - 1) { this.yylineno -= lines.length - 1; } var r = this.yylloc.range; this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len, }; if (this.options.ranges) { this.yylloc.range = [r[0], r[0] + this.yyleng - len]; } this.yyleng = this.yytext.length; return this; }, // When called from action, caches matched text and appends it on next action more: function () { this._more = true; return this; }, // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. reject: function () { if (this.options.backtrack_lexer) { this._backtrack = true; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { text: '', token: null, line: this.yylineno, }); } return this; }, // retain first n characters of the match less: function (n) { this.unput(this.match.slice(n)); }, // displays already matched input, i.e. for error messages pastInput: function () { var past = this.matched.substr(0, this.matched.length - this.match.length); return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ''); }, // displays upcoming input, i.e. for error messages upcomingInput: function () { var next = this.match; if (next.length < 20) { next += this._input.substr(0, 20 - next.length); } return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ''); }, // displays the character position where the lexing error occurred, i.e. for error messages showPosition: function () { var pre = this.pastInput(); var c = new Array(pre.length + 1).join('-'); return pre + this.upcomingInput() + '\n' + c + '^'; }, // test the lexed token: return FALSE when not a match, otherwise return token test_match: function (match, indexed_rule) { var token, lines, backup; if (this.options.backtrack_lexer) { // save context backup = { yylineno: this.yylineno, yylloc: { first_line: this.yylloc.first_line, last_line: this.last_line, first_column: this.yylloc.first_column, last_column: this.yylloc.last_column, }, yytext: this.yytext, match: this.match, matches: this.matches, matched: this.matched, yyleng: this.yyleng, offset: this.offset, _more: this._more, _input: this._input, yy: this.yy, conditionStack: this.conditionStack.slice(0), done: this.done, }; if (this.options.ranges) { backup.yylloc.range = this.yylloc.range.slice(0); } } lines = match[0].match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno += lines.length; } this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length, }; this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; if (this.options.ranges) { this.yylloc.range = [this.offset, (this.offset += this.yyleng)]; } this._more = false; this._backtrack = false; this._input = this._input.slice(match[0].length); this.matched += match[0]; token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); if (this.done && this._input) { this.done = false; } if (token) { return token; } else if (this._backtrack) { // recover context for (var k in backup) { this[k] = backup[k]; } return false; // rule action called reject() implying the next rule should be tested instead. } return false; }, // return next match in input next: function () { if (this.done) { return this.EOF; } if (!this._input) { this.done = true; } var token, match, tempMatch, index; if (!this._more) { this.yytext = ''; this.match = ''; } var rules = this._currentRules(); for (var i = 0; i < rules.length; i++) { tempMatch = this._input.match(this.rules[rules[i]]); if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { match = tempMatch; index = i; if (this.options.backtrack_lexer) { token = this.test_match(tempMatch, rules[i]); if (token !== false) { return token; } else if (this._backtrack) { match = false; continue; // rule action called reject() implying a rule MISmatch. } else { // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } } else if (!this.options.flex) { break; } } } if (match) { token = this.test_match(match, rules[index]); if (token !== false) { return token; } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } if (this._input === '') { return this.EOF; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: '', token: null, line: this.yylineno, }); } }, // return next match that has a token lex: function lex() { var r = this.next(); if (r) { return r; } else { return this.lex(); } }, // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) begin: function begin(condition) { this.conditionStack.push(condition); }, // pop the previously active lexer condition state off the condition stack popState: function popState() { var n = this.conditionStack.length - 1; if (n > 0) { return this.conditionStack.pop(); } else { return this.conditionStack[0]; } }, // produce the lexer rule set which is active for the currently active lexer condition state _currentRules: function _currentRules() { if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; } else { return this.conditions['INITIAL'].rules; } }, // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available topState: function topState(n) { n = this.conditionStack.length - 1 - Math.abs(n || 0); if (n >= 0) { return this.conditionStack[n]; } else { return 'INITIAL'; } }, // alias for begin(condition) pushState: function pushState(condition) { this.begin(condition); }, // return the number of states currently on the stack stateStackSize: function stateStackSize() { return this.conditionStack.length; }, options: {}, performAction: function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { switch ($avoiding_name_collisions) { case 0: break; case 1: return yy_.yytext; case 2: return 14; case 3: return yy_.yytext; case 4: return yy_.yytext; case 5: return 5; } }, rules: [/^(?:\s+)/, /^(?:([*,\[\]<>{}]))/, /^(?:(([1-9])([0-9])*))/, /^(?:(([a-z])+):)/, /^(?:\.\.)/, /^(?:$)/], conditions: { INITIAL: { rules: [0, 1, 2, 3, 4, 5], inclusive: true } }, }; return lexer; })(); parser.lexer = lexer; function Parser() { this.yy = {}; } Parser.prototype = parser; parser.Parser = Parser; return new Parser(); })(); parser$1.Parser; var parse$1 = function () { return parser$1.parse.apply(parser$1, arguments); }; var grammar$1 = { parser: parser$1, Parser: parser$1.Parser, parse: parse$1 }; const parseCode$1 = (code) => { const raw = grammar$1.parse(code); if (raw?.data) return recoverJSON(raw.data, measureLayout); return null; }; var StaffGroupType; (function (StaffGroupType) { StaffGroupType[StaffGroupType["Default"] = 0] = "Default"; StaffGroupType[StaffGroupType["Brace"] = 1] = "Brace"; StaffGroupType[StaffGroupType["Bracket"] = 2] = "Bracket"; StaffGroupType[StaffGroupType["Square"] = 3] = "Square"; })(StaffGroupType || (StaffGroupType = {})); var StaffConjunctionType; (function (StaffConjunctionType) { StaffConjunctionType[StaffConjunctionType["Blank"] = 0] = "Blank"; StaffConjunctionType[StaffConjunctionType["Dashed"] = 1] = "Dashed"; StaffConjunctionType[StaffConjunctionType["Solid"] = 2] = "Solid"; })(StaffConjunctionType || (StaffConjunctionType = {})); const singleGroup = (id) => ({ type: StaffGroupType.Default, staff: id }); const BOUNDS_TO_GROUPTYPE = { '{': StaffGroupType.Brace, '}': StaffGroupType.Brace, '<': StaffGroupType.Bracket, '>': StaffGroupType.Bracket, '[': StaffGroupType.Square, ']': StaffGroupType.Square, }; const OPEN_BOUNDS = '{<['; const CLOSE_BOUNDS = '}>]'; const CONJUNCTIONS_MAP = { ',': StaffConjunctionType.Blank, '-': StaffConjunctionType.Solid, '.': StaffConjunctionType.Dashed, }; const bracketCode = (type, partial = false) => { if (type === StaffGroupType.Default) return (inner) => inner; if (partial) { switch (type) { case StaffGroupType.Brace: return (inner) => `{${inner}`; case StaffGroupType.Bracket: return (inner) => `<${inner}`; case StaffGroupType.Square: return (inner) => `[${inner}`; default: return (inner) => inner; } } switch (type) { case StaffGroupType.Brace: return (inner) => `{${inner}}`; case StaffGroupType.Bracket: return (inner) => `<${inner}>`; case StaffGroupType.Square: return (inner) => `[${inner}]`; default: return (inner) => inner; } }; const randomB64 = () => { const code = btoa(Math.random().toString().substr(2)).replace(/=/g, ''); return code.split('').reverse().slice(0, 6).join(''); }; const makeUniqueName = (set, index, prefix) => { let name = prefix; if (!name) name = index.toString(); else if (set.has(name)) name += '_' + index.toString(); while (set.has(name)) name += '_' + randomB64(); return name; }; const makeGroupsFromRaw = (parent, seq) => { let remains = seq; while (remains.length) { const word = remains.shift(); const bound = BOUNDS_TO_GROUPTYPE[word]; if (bound) { if (CLOSE_BOUNDS.includes(word) && bound === parent.type) break; if (OPEN_BOUNDS.includes(word)) { const group = { type: bound, level: Number.isFinite(parent.level) ? parent.level + 1 : 0 }; remains = makeGroupsFromRaw(group, remains); parent.subs = parent.subs || []; parent.subs.push(group); } } else { parent.subs = parent.subs || []; parent.subs.push(singleGroup(word)); } } while (parent.type === StaffGroupType.Default && parent.subs && parent.subs.length === 1) { const sub = parent.subs[0]; parent.type = sub.type; parent.subs = sub.subs; parent.staff = sub.staff; parent.level = sub.level; } while (parent.subs && parent.subs.length === 1 && parent.subs[0].type === StaffGroupType.Default) { const sub = parent.subs[0]; parent.subs = sub.subs; parent.staff = sub.staff; } parent.grand = parent.type === StaffGroupType.Brace && parent.subs && parent.subs.every((sub) => sub.staff); return remains; }; const groupHead = (group) => { if (group.staff) return group.staff; else if (group.subs) return groupHead(group.subs[0]); }; const groupTail = (group) => { if (group.staff) return group.staff; else if (group.subs) return groupTail(group.subs[group.subs.length - 1]); }; const groupKey = (group) => { if (group.staff) return group.staff; else if (group.subs) return `${groupHead(group)}-${groupTail(group)}`; }; const groupDict = (group, dict) => { dict[groupKey(group)] = group; if (group.subs) group.subs.forEach((sub) => groupDict(sub, dict)); }; class StaffLayout { constructor(raw) { // make unique ids const ids = new Set(); raw.forEach((item, i) => { item.id = makeUniqueName(ids, i + 1, item.id); ids.add(item.id); }); this.staffIds = raw.map((item) => item.id); this.conjunctions = raw.slice(0, raw.length - 1).map((item) => (item.conjunction ? CONJUNCTIONS_MAP[item.conjunction] : StaffConjunctionType.Blank)); // make groups const seq = [].concat(...raw.map((item) => [...item.leftBounds, item.id, ...item.rightBounds])); this.group = { type: StaffGroupType.Default }; makeGroupsFromRaw(this.group, seq); const dict = {}; groupDict(this.group, dict); this.groups = Object.entries(dict).map(([key, group]) => { let ids = key.split('-'); if (ids.length === 1) ids = [ids[0], ids[0]]; const range = ids.map((id) => this.staffIds.indexOf(id)); return { group, range, key, }; }); this.maskCache = new Map(); } get stavesCount() { if (!this.staffIds) return null; return this.staffIds.length; } get partGroups() { const grands = this.groups.filter((g) => g.group.grand); const parts = this.groups.filter((g) => { if (g.group.grand) return true; if (g.range[0] === g.range[1]) { const index = g.range[0]; return !grands.some((g) => g.range[0] <= index && g.range[1] >= index); } return false; }); return parts; } get standaloneGroups() { const groups = []; const collect = (group) => { if (group.grand) groups.push(group.subs.map((sub) => sub.staff)); else if (group.staff) groups.push([group.staff]); else if (group.subs) group.subs.forEach((sub) => collect(sub)); }; collect(this.group); return groups; } conjunctionBetween(upStaff, downStaff) { if (downStaff <= upStaff) return null; let con = StaffConjunctionType.Solid; for (let i = upStaff; i < downStaff; i++) con = Math.min(con, this.conjunctions[i]); return con; } static makeMaskLayout(layout, mask) { const staffIds = layout.staffIds.filter((_, i) => mask & (1 << i)); if (staffIds.length === layout.staffIds.length) { return { staffIds: layout.staffIds, conjunctions: layout.conjunctions, groups: layout.groups, }; } const groups = layout.groups .map((g) => ({ ids: layout.staffIds.slice(g.range[0], g.range[1] + 1).filter((id) => staffIds.includes(id)), ...g })) .filter(({ ids }) => ids.length) .map(({ ids, ...g }) => ({ key: g.key, group: g.group, range: [staffIds.indexOf(ids[0]), staffIds.indexOf(ids[ids.length - 1])], })); const conjunctions = staffIds.slice(0, staffIds.length - 1).map((id, i) => { const nextId = staffIds[i + 1]; return layout.conjunctionBetween(layout.staffIds.indexOf(id), layout.staffIds.indexOf(nextId)); }); return { staffIds, conjunctions, groups, }; } mask(mask) { if (!this.maskCache.get(mask)) this.maskCache.set(mask, StaffLayout.makeMaskLayout(this, mask)); return this.maskCache.get(mask); } // {,} * 1,1 => {,} // {,} * 1,x => { // {,} * 0,x => // {,} * 0,1 => {} partialMaskCode(bits, withIds = false) { const staffStatus = this.staffIds .map((_, i) => (i < bits.length ? bits[i] : null)) .reduce((status, x, i) => { status[this.staffIds[i]] = x; return status; }, {}); const joinGroup = (group) => { if (group.staff) return [staffStatus[group.staff] ? group.staff : null, staffStatus[group.staff] === null]; const subs = group.subs.map((sub) => joinGroup(sub)); const subStr = subs .map((pair) => pair[0]) .filter(Boolean) .join(','); const partial = subs.some(([_, partial]) => partial); const code = subStr ? bracketCode(group.type, partial)(subStr) : null; return [code, partial]; }; let [code] = joinGroup(this.group); code = code || ''; if (!withIds) code = code.replace(/[_\w]+/g, ''); return code; } } /* parser generated by jison 0.4.18 */ /* Returns a Parser object of the following structure: Parser: { yy: {} } Parser.prototype: { yy: {}, trace: function(), symbols_: {associative list: name ==> number}, terminals_: {associative list: number ==> name}, productions_: [...], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), table: [...], defaultActions: {...}, parseError: function(str, hash), parse: function(input), lexer: { EOF: 1, parseError: function(str, hash), setInput: function(input), input: function(), unput: function(str), more: function(), less: function(n), pastInput: function(), upcomingInput: function(), showPosition: function(), test_match: function(regex_match_array, rule_index), next: function(), lex: function(), begin: function(condition), popState: function(), _currentRules: function(), topState: function(), pushState: function(condition), options: { ranges: boolean (optional: true ==> token location info will include a .range[] member) flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) }, performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), rules: [...], conditions: {associative list: name ==> set}, } } token location info (@$, _$, etc.): { first_line: n, last_line: n, first_column: n, last_column: n, range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) } the parseError function receives a 'hash' object with these members for lexer and parser errors: { text: (matched text) token: (the produced terminal token, if any) line: (yylineno) } while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { loc: (yylloc) expected: (string describing the set of expected tokens) recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ var parser = (function () { var o = function (k, v, o, l) { for (o = o || {}, l = k.length; l--; o[k[l]] = v) ; return o; }, $V0 = [1, 15], $V1 = [1, 16], $V2 = [1, 17], $V3 = [1, 11], $V4 = [1, 12], $V5 = [1, 13], $V6 = [1, 24], $V7 = [1, 25], $V8 = [1, 26], $V9 = [5, 11, 12, 13, 15, 16, 17, 21, 22, 23, 24], $Va = [15, 16, 17, 21, 22, 23, 24], $Vb = [11, 12, 13, 15, 16, 17, 21, 22, 23, 24], $Vc = [5, 11, 12, 13, 21, 22, 23, 24]; var parser = { trace: function trace() { }, yy: {}, symbols_: { error: 2, start_symbol: 3, staff_layout: 4, EOF: 5, seq: 6, seq_id: 7, seq_br: 8, seq_con: 9, bound_left: 10, '<': 11, '[': 12, '{': 13, bound_right: 14, '>': 15, ']': 16, '}': 17, bound_lefts: 18, bound_rights: 19, conjunction: 20, '-': 21, ',': 22, '.': 23, ID: 24, seq_bl: 25, $accept: 0, $end: 1, }, terminals_: { 2: 'error', 5: 'EOF', 11: '<', 12: '[', 13: '{', 15: '>', 16: ']', 17: '}', 21: '-', 22: ',', 23: '.', 24: 'ID' }, productions_: [ 0, [3, 2], [4, 1], [6, 0], [6, 1], [6, 1], [6, 1], [10, 1], [10, 1], [10, 1], [14, 1], [14, 1], [14, 1], [18, 1], [18, 2], [19, 1], [19, 2], [20, 1], [20, 1], [20, 1], [7, 1], [7, 2], [7, 2], [7, 2], [7, 2], [25, 1], [25, 2], [25, 2], [25, 2], [8, 2], [8, 2], [8, 2], [9, 1], [9, 2], [9, 2], [9, 2], [9, 2], ], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { /* this == yyval */ var $0 = $$.length - 1; switch (yystate) { case 1: return $$[$0 - 1]; case 2: $$[$0].next(); this.$ = $$[$0].toJSON(); break; case 3: this.$ = new Seq(); break; case 13: case 15: this.$ = [$$[$0]]; break; case 14: case 16: this.$ = [...$$[$0 - 1], $$[$0]]; break; case 20: this.$ = new Seq(); this.$.tip.i($$[$0]); break; case 21: case 23: $$[$0 - 1].next(); $$[$0 - 1].tip.i($$[$0]); this.$ = $$[$0 - 1]; break; case 22: case 24: $$[$0 - 1].tip.i($$[$0]); this.$ = $$[$0 - 1]; break; case 25: this.$ = new Seq(); this.$.tip.bl($$[$0]); break; case 26: case 27: $$[$0 - 1].next(); $$[$0 - 1].tip.bl($$[$0]); this.$ = $$[$0 - 1]; break; case 28: $$[$0 - 1].tip.bl($$[$0]); this.$ = $$[$0 - 1]; break; case 29: case 30: case 31: $$[$0 - 1].tip.br($$[$0]); this.$ = $$[$0 - 1]; break; case 32: this.$ = new Seq(); this.$.tip.con($$[$0]); this.$.next(); break; case 33: case 34: case 35: case 36: $$[$0 - 1].tip.con($$[$0]); $$[$0 - 1].next(); this.$ = $$[$0 - 1]; break; } }, table: [ { 3: 1, 4: 2, 5: [2, 3], 6: 3, 7: 4, 8: 5, 9: 6, 10: 14, 11: $V0, 12: $V1, 13: $V2, 18: 10, 20: 9, 21: $V3, 22: $V4, 23: $V5, 24: [1, 7], 25: 8 }, { 1: [3] }, { 5: [1, 18] }, { 5: [2, 2] }, { 5: [2, 4], 10: 14, 11: $V0, 12: $V1, 13: $V2, 14: 23, 15: $V6, 16: $V7, 17: $V8, 18: 22, 19: 20, 20: 21, 21: $V3, 22: $V4, 23: $V5, 24: [1, 19] }, { 5: [2, 5], 10: 14, 11: $V0, 12: $V1, 13: $V2, 18: 29, 20: 28, 21: $V3, 22: $V4, 23: $V5, 24: [1, 27] }, { 5: [2, 6], 10: 14, 11: $V0, 12: $V1, 13: $V2, 14: 23, 15: $V6, 16: $V7, 17: $V8, 18: 33, 19: 31, 20: 32, 21: $V3, 22: $V4, 23: $V5, 24: [1, 30] }, o($V9, [2, 20]), { 14: 23, 15: $V6, 16: $V7, 17: $V8, 19: 35, 20: 36, 21: $V3, 22: $V4, 23: $V5, 24: [1, 34] }, o($V9, [2, 32]), o($Va, [2, 25], { 10: 37, 11: $V0, 12: $V1, 13: $V2 }), o($V9, [2, 17]), o($V9, [2, 18]), o($V9, [2, 19]), o($Vb, [2, 13]), o($Vb, [2, 7]), o($Vb, [2, 8]), o($Vb, [2, 9]), { 1: [2, 1] }, o($V9, [2, 21]), o($Vc, [2, 29], { 14: 38, 15: $V6, 16: $V7, 17: $V8 }), o($V9, [2, 33]), o($Va, [2, 26], { 10: 37, 11: $V0, 12: $V1, 13: $V2 }), o($V9, [2, 15]), o($V9, [2, 10]), o($V9, [2, 11]), o($V9, [2, 12]), o($V9, [2, 23]), o($V9, [2, 35]), o($Va, [2, 27], { 10: 37, 11: $V0, 12: $V1, 13: $V2 }), o($V9, [2, 24]), o($Vc, [2, 31], { 14: 38, 15: $V6, 16: $V7, 17: $V8 }), o($V9, [2, 36]), o($Va, [2, 28], { 10: 37, 11: $V0, 12: $V1, 13: $V2 }), o($V9, [2, 22]), o($Vc, [2, 30], { 14: 38, 15: $V6, 16: $V7, 17: $V8 }), o($V9, [2, 34]), o($Vb, [2, 14]), o($V9, [2, 16]), ], defaultActions: { 3: [2, 2], 18: [2, 1] }, parseError: function parseError(str, hash) { if (hash.recoverable) { this.trace(str); } else { var error = new Error(str); error.hash = hash; throw error; } }, parse: function parse(input) { var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); var lexer = Object.create(this.lexer); var sharedState = { yy: {} }; for (var k in this.yy) { if (Object.prototype.hasOwnProperty.call(this.yy, k)) { sharedState.yy[k] = this.yy[k]; } } lexer.setInput(input, sharedState.yy); sharedState.yy.lexer = lexer; sharedState.yy.parser = this; if (typeof lexer.yylloc == 'undefined') { lexer.yylloc = {}; } var yyloc = lexer.yylloc; lstack.push(yyloc); var ranges = lexer.options && lexer.options.ranges; if (typeof sharedState.yy.parseError === 'function') { this.parseError = sharedState.yy.parseError; } else { this.parseError = Object.getPrototypeOf(this).parseError; } var lex = function () { var token; token = lexer.lex() || EOF; if (typeof token !== 'number') { token = self.symbols_[token] || token; } return token; }; var symbol, state, action, r, yyval = {}, p, len, newState, expected; while (true) { state = stack[stack.length - 1]; if (this.defaultActions[state]) { action = this.defaultActions[state]; } else { if (symbol === null || typeof symbol == 'undefined') { symbol = lex(); } action = table[state] && table[state][symbol]; } if (typeof action === 'undefined' || !action.length || !action[0]) { var errStr = ''; expected = []; for (p in table[state]) { if (this.terminals_[p] && p > TERROR) { expected.push("'" + this.terminals_[p] + "'"); } } if (lexer.showPosition) { errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ", got '" + (this.terminals_[symbol] || symbol) + "'"; } else { errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'"); } this.parseError(errStr, { text: lexer.match, token: this.terminals_[symbol] || symbol, line: lexer.yylineno, loc: yyloc, expected: expected, }); } if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); } switch (action[0]) { case 1: stack.push(symbol); vstack.push(lexer.yytext); lstack.push(lexer.yylloc); stack.push(action[1]); symbol = null; { yyleng = lexer.yyleng; yytext = lexer.yytext; yylineno = lexer.yylineno; yyloc = lexer.yylloc; } break; case 2: len = this.productions_[action[1]][1]; yyval.$ = vstack[vstack.length - len]; yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column, }; if (ranges) { yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; } r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); if (typeof r !== 'undefined') { return r; } if (len) { stack = stack.slice(0, -1 * len * 2); vstack = vstack.slice(0, -1 * len); lstack = lstack.slice(0, -1 * len); } stack.push(this.productions_[action[1]][0]); vstack.push(yyval.$); lstack.push(yyval._$); newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; stack.push(newState); break; case 3: return true; } } return true; }, }; class Item { constructor() { this.id = null; this.leftBounds = []; this.rightBounds = []; this.conjunction = null; } i(id) { this.id = id; return this; } bl(leftBounds) { this.leftBounds = leftBounds; return this; } br(rightBounds) { this.rightBounds = rightBounds; return this; } con(conjunction) { this.conjunction = conjunction; return this; } } class Seq { constructor() { this.body = []; this.tip = new Item(); } next() { this.body.push(this.tip); this.tip = new Item(); return this; } toJSON() { return this.body; } } /* generated by jison-lex 0.3.4 */ var lexer = (function () { var lexer = { EOF: 1, parseError: function parseError(str, hash) { if (this.yy.parser) { this.yy.parser.parseError(str, hash); } else { throw new Error(str); } }, // resets the lexer, sets new input setInput: function (input, yy) { this.yy = yy || this.yy || {}; this._input = input; this._more = this._backtrack = this.done = false; this.yylineno = this.yyleng = 0; this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0, }; if (this.options.ranges) { this.yylloc.range = [0, 0]; } this.offset = 0; return this; }, // consumes and returns one char from the input input: function () { var ch = this._input[0]; this.yytext += ch; this.yyleng++; this.offset++; this.match += ch; this.matched += ch; var lines = ch.match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno++; this.yylloc.last_line++; } else { this.yylloc.last_column++; } if (this.options.ranges) { this.yylloc.range[1]++; } this._input = this._input.slice(1); return ch; }, // unshifts one char (or a string) into the input unput: function (ch) { var len = ch.length; var lines = ch.split(/(?:\r\n?|\n)/g); this._input = ch + this._input; this.yytext = this.yytext.substr(0, this.yytext.length - len); //this.yyleng -= len; this.offset -= len; var oldLines = this.match.split(/(?:\r\n?|\n)/g); this.match = this.match.substr(0, this.match.length - 1); this.matched = this.matched.substr(0, this.matched.length - 1); if (lines.length - 1) { this.yylineno -= lines.length - 1; } var r = this.yylloc.range; this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len, }; if (this.options.ranges) { this.yylloc.range = [r[0], r[0] + this.yyleng - len]; } this.yyleng = this.yytext.length; return this; }, // When called from action, caches matched text and appends it on next action more: function () { this._more = true; return this; }, // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. reject: function () { if (this.options.backtrack_lexer) { this._backtrack = true; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { text: '', token: null, line: this.yylineno, }); } return this; }, // retain first n characters of the match less: function (n) { this.unput(this.match.slice(n)); }, // displays already matched input, i.e. for error messages pastInput: function () { var past = this.matched.substr(0, this.matched.length - this.match.length); return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ''); }, // displays upcoming input, i.e. for error messages upcomingInput: function () { var next = this.match; if (next.length < 20) { next += this._input.substr(0, 20 - next.length); } return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ''); }, // displays the character position where the lexing error occurred, i.e. for error messages showPosition: function () { var pre = this.pastInput(); var c = new Array(pre.length + 1).join('-'); return pre + this.upcomingInput() + '\n' + c + '^'; }, // test the lexed token: return FALSE when not a match, otherwise return token test_match: function (match, indexed_rule) { var token, lines, backup; if (this.options.backtrack_lexer) { // save context backup = { yylineno: this.yylineno, yylloc: { first_line: this.yylloc.first_line, last_line: this.last_line, first_column: this.yylloc.first_column, last_column: this.yylloc.last_column, }, yytext: this.yytext, match: this.match, matches: this.matches, matched: this.matched, yyleng: this.yyleng, offset: this.offset, _more: this._more, _input: this._input, yy: this.yy, conditionStack: this.conditionStack.slice(0), done: this.done, }; if (this.options.ranges) { backup.yylloc.range = this.yylloc.range.slice(0); } } lines = match[0].match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno += lines.length; } this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length, }; this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; if (this.options.ranges) { this.yylloc.range = [this.offset, (this.offset += this.yyleng)]; } this._more = false; this._backtrack = false; this._input = this._input.slice(match[0].length); this.matched += match[0]; token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); if (this.done && this._input) { this.done = false; } if (token) { return token; } else if (this._backtrack) { // recover context for (var k in backup) { this[k] = backup[k]; } return false; // rule action called reject() implying the next rule should be tested instead. } return false; }, // return next match in input next: function () { if (this.done) { return this.EOF; } if (!this._input) { this.done = true; } var token, match, tempMatch, index; if (!this._more) { this.yytext = ''; this.match = ''; } var rules = this._currentRules(); for (var i = 0; i < rules.length; i++) { tempMatch = this._input.match(this.rules[rules[i]]); if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { match = tempMatch; index = i; if (this.options.backtrack_lexer) { token = this.test_match(tempMatch, rules[i]); if (token !== false) { return token; } else if (this._backtrack) { match = false; continue; // rule action called reject() implying a rule MISmatch. } else { // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } } else if (!this.options.flex) { break; } } } if (match) { token = this.test_match(match, rules[index]); if (token !== false) { return token; } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } if (this._input === '') { return this.EOF; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: '', token: null, line: this.yylineno, }); } }, // return next match that has a token lex: function lex() { var r = this.next(); if (r) { return r; } else { return this.lex(); } }, // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) begin: function begin(condition) { this.conditionStack.push(condition); }, // pop the previously active lexer condition state off the condition stack popState: function popState() { var n = this.conditionStack.length - 1; if (n > 0) { return this.conditionStack.pop(); } else { return this.conditionStack[0]; } }, // produce the lexer rule set which is active for the currently active lexer condition state _currentRules: function _currentRules() { if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; } else { return this.conditions['INITIAL'].rules; } }, // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available topState: function topState(n) { n = this.conditionStack.length - 1 - Math.abs(n || 0); if (n >= 0) { return this.conditionStack[n]; } else { return 'INITIAL'; } }, // alias for begin(condition) pushState: function pushState(condition) { this.begin(condition); }, // return the number of states currently on the stack stateStackSize: function stateStackSize() { return this.conditionStack.length; }, options: {}, performAction: function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { switch ($avoiding_name_collisions) { case 0: break; case 1: return yy_.yytext; case 2: return 24; case 3: return 5; } }, rules: [/^(?:\s+)/, /^(?:([-,.\[\]<>{}]))/, /^(?:([a-zA-Z_0-9]+))/, /^(?:$)/], conditions: { INITIAL: { rules: [0, 1, 2, 3], inclusive: true } }, }; return lexer; })(); parser.lexer = lexer; function Parser() { this.yy = {}; } Parser.prototype = parser; parser.Parser = Parser; return new Parser(); })(); parser.Parser; var parse = function () { return parser.parse.apply(parser, arguments); }; var grammar = { parser: parser, Parser: parser.Parser, parse: parse }; const parseCode = (code) => { const raw = grammar.parse(code); return new StaffLayout(raw); }; class DummyLogger { debug(..._) { } group(..._) { } groupCollapsed(..._) { } groupEnd() { } info(..._) { } warn(..._) { } assert(..._) { } } const POINT_CONFLICTION_DISTANCE = 0.4; const roundNumber = (x, precision, min = -Infinity) => Math.max(Math.round(x / precision) * precision, min); const distance2D = (p1, p2) => { const dx = p1.x - p2.x; const dy = p1.y - p2.y; return Math.sqrt(dx * dx + dy * dy); }; const trans23 = (point, matrix) => ({ x: matrix[0] * point.x + matrix[2] * point.y + matrix[4], y: matrix[1] * point.x + matrix[3] * point.y + matrix[5], }); const gcd = (a, b) => { if (!(Number.isInteger(a) && Number.isInteger(b))) { console.error('non-integer gcd:', a, b); return 1; } return b === 0 ? a : gcd(b, a % b); }; const frac = (numerator, denominator) => ({ numerator, denominator }); const reducedFraction = (n, d) => { n = Math.round(n); d = Math.round(d); const g = n !== 0 ? gcd(n, d) : d; return frac(n / g, d / g); }; const printFraction = (f) => `${f.numerator}/${f.denominator}`; const fractionMul = (value, fraction) => (fraction ? (value * fraction.numerator) / fraction.denominator : value); const segmentPoints = (points, axis) => { const sorted = [...points].sort((p1, p2) => p1[axis] - p2[axis]); let seg = null; let lastP = null; return sorted.reduce((segments, p, i) => { if (!lastP) { lastP = p; seg = [p]; } else { if (p[axis] - lastP[axis] < POINT_CONFLICTION_DISTANCE) seg.push(p); else { if (seg.length > 1) segments.push(seg); lastP = p; seg = [p]; } } if (seg.length > 1 && i === sorted.length - 1) segments.push(seg); return segments; }, []); }; const filterWeekPoints = (points) => { //console.log("filterWeekPoints:", points.map(p => `${p.semantic}, ${p.x}, ${p.y}`)); //console.table(points.map(p => ({ ...p }))); if (points.length <= 1) return []; let rests = points.slice(1); const group = CONFLICTION_GROUPS.find((group) => group.includes(points[0].semantic)); if (!group) return filterWeekPoints(rests); const weeks = rests.filter((p) => group.includes(p.semantic)); rests = rests.filter((p) => !group.includes(p.semantic)); return [...weeks, ...filterWeekPoints(rests)]; }; const solveOverlapping = (points) => { const pset = new Set(points); const xClusters = segmentPoints(points, 'x'); const clusters = [].concat(...xClusters.map((c) => segmentPoints(c, 'y'))); clusters.forEach((ps) => ps.sort((p1, p2) => p2.confidence - p1.confidence)); clusters.forEach((ps) => { filterWeekPoints(ps).forEach((p) => pset.delete(p)); }); return Array.from(pset); }; const GROUP_N_TO_PITCH$1 = [0, 2, 4, 5, 7, 9, 11]; const MIDDLE_C$1 = 60; const mod7$1 = (x) => { let y = x % 7; while (y < 0) y += 7; return y; }; const noteToPitch = ({ note, alter }) => { const group = Math.floor(note / 7); const gn = mod7$1(note); return MIDDLE_C$1 + group * 12 + GROUP_N_TO_PITCH$1[gn] + alter; }; const argmax$1 = (data) => { const max = Math.max(...data); return data.indexOf(max); }; const WHOLE_DURATION = 128 * 3 * 5; const WHOLE_EXP2 = WHOLE_DURATION / 15; var AccessoryDirection; (function (AccessoryDirection) { AccessoryDirection["Up"] = "^"; AccessoryDirection["Down"] = "_"; AccessoryDirection["Middle"] = "-"; })(AccessoryDirection || (AccessoryDirection = {})); var GraceType; (function (GraceType) { GraceType["Grace"] = "grace"; GraceType["AfterGrace"] = "afterGrace"; GraceType["Acciaccatura"] = "acciaccatura"; GraceType["Appoggiatura"] = "appoggiatura"; GraceType["SlashedGrace"] = "slashedGrace"; })(GraceType || (GraceType = {})); var StemBeam; (function (StemBeam) { StemBeam["Open"] = "Open"; StemBeam["Close"] = "Close"; StemBeam["Continue"] = "Continue"; })(StemBeam || (StemBeam = {})); var TremoloLink; (function (TremoloLink) { TremoloLink["Pitcher"] = "Pitcher"; TremoloLink["Catcher"] = "Catcher"; TremoloLink["Pierced"] = "Pierced"; })(TremoloLink || (TremoloLink = {})); var GlissandoStyle; (function (GlissandoStyle) { GlissandoStyle["Normal"] = "normal"; GlissandoStyle["DashedLine"] = "dashed-line"; GlissandoStyle["DottedLine"] = "dotted-line"; GlissandoStyle["Zigzag"] = "zigzag"; GlissandoStyle["Trill"] = "trill"; })(GlissandoStyle || (GlissandoStyle = {})); var ArpeggioStyle; (function (ArpeggioStyle) { ArpeggioStyle["Normal"] = "Normal"; ArpeggioStyle["Bracket"] = "Bracket"; ArpeggioStyle["Parenthesis"] = "Parenthesis"; ArpeggioStyle["ParenthesisDashed"] = "ParenthesisDashed"; ArpeggioStyle["ArrowDown"] = "ArrowDown"; })(ArpeggioStyle || (ArpeggioStyle = {})); class Term extends SimpleClass { } const SCALE_NAMES = 'CDEFGAB'; class EventTerm extends Term { static space({ tick, duration }) { const term = new EventTerm({ rest: 's', tick, accessories: [], }); term.duration = Math.round(duration); return term; } constructor(data) { super(); super.assign(data); Object.assign(this, data); if (Number.isFinite(data.left) && Number.isFinite(data.right)) this.x = (this.left + this.right) / 2; if (!Number.isFinite(this.pivotX)) this.pivotX = this.x; //console.assert(Number.isFinite(this.x), "EventTerm: invalid x,", data); } get alignedTick() { return this.grace ? this.tick + this.duration : this.tick; } get mainDuration() { return WHOLE_DURATION * 2 ** -this.division * (2 - 2 ** -this.dots); } get duration() { let value = this.mainDuration; if (this.multiplier) value *= this.multiplier.numerator / this.multiplier.denominator; if (this.timeWarp) value *= this.timeWarp.numerator / this.timeWarp.denominator; return this.grace ? value / 8 : value; } set duration(value) { console.assert(Number.isFinite(value), 'invalid duration value:', value); const divider = gcd(value, WHOLE_EXP2); const division = Math.log2(WHOLE_EXP2 / divider); const multiplier = reducedFraction(value * 2 ** division, WHOLE_DURATION); this.division = division; this.dots = 0; if (multiplier.numerator !== multiplier.denominator) this.multiplier = multiplier; else this.multiplier = undefined; } get prior() { return this.tick; } get times() { if (!this.timeWarp) return null; return `${this.timeWarp.numerator}/${this.timeWarp.denominator}`; } get fullMeasureRest() { return this.rest === 'R'; } get tipX() { return this.tip ? this.tip.x : this.x; } get tipY() { return this.tip ? this.tip.y : this.ys ? this.ys[0] : 0; } get tremoloCatcher() { return this.tremoloLink === TremoloLink.Catcher; } get scaleChord() { return this.pitches.map((pitch) => SCALE_NAMES[(pitch.note + 700) % 7]).join(''); } get zeroHolder() { return !!this.grace || this.tremoloCatcher; } } EventTerm.className = 'EventTerm'; var ContextType; (function (ContextType) { ContextType[ContextType["Clef"] = 0] = "Clef"; ContextType[ContextType["KeyAcc"] = 1] = "KeyAcc"; ContextType[ContextType["Acc"] = 2] = "Acc"; ContextType[ContextType["OctaveShift"] = 3] = "OctaveShift"; ContextType[ContextType["TimeSignatureC"] = 4] = "TimeSignatureC"; ContextType[ContextType["TimeSignatureN"] = 5] = "TimeSignatureN"; })(ContextType || (ContextType = {})); class ContextedTerm extends Term { constructor(data) { super(); Object.assign(this, data); } get type() { if (TokenClefs.includes(this.tokenType)) return ContextType.Clef; if (/\|key-/.test(this.tokenType)) return ContextType.KeyAcc; if (/accidentals-/.test(this.tokenType)) return ContextType.Acc; if (TokenOctshifts.includes(this.tokenType)) return ContextType.OctaveShift; if (TokenTimesigsC.includes(this.tokenType)) return ContextType.TimeSignatureC; if (TokenTimesigsN.includes(this.tokenType)) return ContextType.TimeSignatureN; return null; } get staffLevel() { return [ContextType.OctaveShift, ContextType.Clef, ContextType.KeyAcc].includes(this.type); } get prior() { return this.tick - 0.1; } get clef() { switch (this.tokenType) { case TokenType.ClefG: return -this.y - 2; case TokenType.ClefF: return -this.y + 2; case TokenType.ClefC: return -this.y; } return null; } get alter() { switch (this.tokenType) { case TokenType.AccNatural: case TokenType.KeyNatural: return 0; case TokenType.AccSharp: case TokenType.KeySharp: return 1; case TokenType.AccFlat: case TokenType.KeyFlat: return -1; case TokenType.AccDoublesharp: return 2; case TokenType.AccFlatflat: return -2; } return null; } get octaveShift() { switch (this.tokenType) { case TokenType.OctaveShift8va: return -1; case TokenType.OctaveShift0: return 0; case TokenType.OctaveShift8vb: return 1; } return null; } get number() { switch (this.tokenType) { case TokenType.TimesigZero: return 0; case TokenType.TimesigOne: return 1; case TokenType.TimesigTwo: return 2; case TokenType.TimesigThree: return 3; case TokenType.TimesigFour: return 4; case TokenType.TimesigFive: return 5; case TokenType.TimesigSix: return 6; case TokenType.TimesigSeven: return 7; case TokenType.TimesigEight: return 8; case TokenType.TimesigNine: return 9; } return null; } } ContextedTerm.className = 'ContextedTerm'; //class BreakTerm extends Term { //}; class MarkTerm extends Term { get prior() { return this.tick + 0.01; } } MarkTerm.className = 'MarkTerm'; const MUSIC_NOTES = Array(7) .fill(0) .map((_, i) => String.fromCodePoint(0x1d15d + i)); class TempoTerm extends MarkTerm { static fromNumeralText(text) { if (/.+=.*\d+/.test(text)) { const [symbol, value] = text.split('='); let division = MUSIC_NOTES.findIndex((n) => symbol.includes(n)); division = division >= 0 ? division : 2; let duration = (2 ** division).toString(); if (symbol.includes('.')) duration += '.'; return new TempoTerm({ tick: 0, duration, beats: value }); } return null; } constructor(data) { super(); Object.assign(this, data); } get prior() { return this.tick - 0.01; } // a whole note equal to 1920 get durationMagnitude() { const [_, den, dot] = this.duration.match(/^(\d+)(\.)?$/); const magnitude = (WHOLE_DURATION / Number(den)) * (dot ? 1.5 : 1); return magnitude; } // beats per minute, suppose 1 beat = 480 ticks get bpm() { const [number] = this.beats.match(/\d+/) || [90]; const beats = Number(number); return (beats * this.durationMagnitude * 4) / WHOLE_DURATION; } isValid(range = [10, 400]) { const bpm = this.bpm; return Number.isFinite(this.bpm) && bpm >= range[0] && bpm < range[1]; } } TempoTerm.className = 'TempoTerm'; class GlyphTerm extends MarkTerm { constructor(data) { super(); Object.assign(this, data); } } GlyphTerm.className = 'GlyphTerm'; class TextTerm extends MarkTerm { constructor(data) { super(); Object.assign(this, data); } } TextTerm.className = 'TextTerm'; class LyricTerm extends MarkTerm { constructor(data) { super(); Object.assign(this, data); } } LyricTerm.className = 'LyricTerm'; class CommandTerm extends MarkTerm { constructor(data) { super(); Object.assign(this, data); } } CommandTerm.className = 'CommandTerm'; class ChordmodeTerm extends Term { constructor(data) { super(); Object.assign(this, data); } get prior() { return this.tick; } get duration() { const value = WHOLE_DURATION * 2 ** -this.division * (2 - 2 ** -this.dots); if (this.multiplier) return (value * this.multiplier.numerator) / this.multiplier.denominator; return value; } } ChordmodeTerm.className = 'ChordmodeTerm'; const BEAM_STATUS = { [StemBeam.Open]: 1, [StemBeam.Continue]: 0, [StemBeam.Close]: -1, }; const evaluateMeasure = (measure) => { if (!measure.regulated) return undefined; const eventMap = measure.eventMap; const events = measure.events.length; const validEvents = measure.voices.flat(1).length; const warpedEvents = measure.events.filter((e) => e.timeWarp).length; const warps = new Set(measure.events.filter((e) => e.timeWarp && !(e.rest && e.division === 0)).map((e) => `${e.timeWarp.numerator}/${e.timeWarp.denominator}`)); const irregularWarps = new Set(warps); irregularWarps.delete('2/3'); const fractionalWarp = measure.voices.some((voice) => { const events = voice.map((id) => eventMap[id]); if (!events.some((e) => e.timeWarp)) return false; let denominator = 0; let tickSum = 0; let eventN = 0; return events.some((event, i) => { const d = event.timeWarp ? event.timeWarp.denominator : 0; if (d !== denominator) { if (denominator > 0 && (tickSum % denominator || eventN < 2)) return true; tickSum = 0; eventN = 0; } denominator = d; tickSum += event.duration; ++eventN; if (i === events.length - 1) { if (denominator > 0 && (tickSum % denominator || eventN < 2)) return true; } return false; }); }); const tickOverlapped = measure.voices.some((voice) => { const events = voice.map((id) => eventMap[id]); let tick = 0; return events.some((event) => { if (event.grace) return false; if (event.tick < tick) return true; tick = event.tick + event.duration; return false; }); }); const fractionalTimes = new Set(measure.events.filter((e) => e.timeWarp && e.timeWarp.denominator > 3).map((e) => e.duration)); const complicatedTimewarp = fractionalTimes.size > 1; const literalDuration = fractionMul(WHOLE_DURATION, measure.timeSignature); const sigDuration = measure.doubtfulTimesig ? measure.duration : literalDuration; const inVoiceEvents = measure.voices.flat(1).map((id) => eventMap[id]); // Guard: detect corrupted event data in voices (e.g. missing division, NaN tick) const corruptedVoiceEvent = inVoiceEvents.some((event) => !event || !Number.isFinite(event.tick) || !Number.isFinite(event.division) || event.division < 0 || !Number.isFinite(event.duration) || event.duration <= 0); const overranged = inVoiceEvents.reduce((over, event) => over || event.tick < 0 || event.tick + event.duration > sigDuration, false); const overDuration = measure.duration > literalDuration; const graceInVoice = inVoiceEvents.some((event) => event.grace); const graceN = measure.events.filter((e) => e.grace).length; const graceDominant = graceN >= inVoiceEvents.length; const irregularTick = inVoiceEvents.some((event) => { let t = event.tick * 2 ** (event.division + 2); if (event.timeWarp) t *= event.timeWarp.denominator; if (!Number.isFinite(t)) return true; const fragment = gcd(Math.round(t), WHOLE_DURATION); //if (fragment < WHOLE_DURATION) // console.log("irregularTick:", event.tick, fragment); return fragment < WHOLE_DURATION; }); const beamStatus = measure.voices.map((voice) => voice.reduce(({ status, broken }, ei) => { const event = eventMap[ei]; if (event.beam) { status += BEAM_STATUS[event.beam]; broken = broken || !(status >= 0 && status <= 1); } return { status, broken }; }, { status: 0, broken: false })); const beamBroken = beamStatus.some(({ status, broken }) => status || broken); let spaceTime = 0; let surplusTime = 0; measure.voices.forEach((voice) => { const eventDuration = voice.reduce((sum, ei) => sum + eventMap[ei].duration, 0); spaceTime += Math.max(0, measure.duration - eventDuration); surplusTime += Math.max(0, eventDuration - measure.duration); }); spaceTime /= WHOLE_DURATION; const nullEvents = measure.events.filter((e) => !e.grace && !e.fullMeasureRest && !e.tremoloCatcher && (!e.predisposition || e.predisposition.fakeP < 0.1) && !Number.isFinite(e.tick)).length; const fakeEvents = measure.events.filter((event) => !event.fullMeasureRest && !event.grace && !event.tremoloCatcher && !inVoiceEvents.includes(event)).length; const { voiceRugged } = measure.voices.flat(1).reduce((result, ei) => { if (!result.voiceRugged) { if (result.es.has(ei)) return { voiceRugged: true, es: result.es }; } result.es.add(ei); return result; }, { voiceRugged: false, es: new Set() }); const tickTwist = measure.tickTwist || 0; const error = corruptedVoiceEvent || tickTwist >= 1 || tickOverlapped || voiceRugged || measure.tickRatesInStaves.some((rate) => rate < 0) || nullEvents > 2 || !measure.timeSignature || overranged || measure.duration > sigDuration || measure.events.some((event) => event.timeWarp && event.timeWarp.numerator / event.timeWarp.denominator <= 0.5); const perfect = !error && !overDuration && tickTwist < 0.2 && !fractionalWarp && !irregularWarps.size && !irregularTick && !spaceTime && !surplusTime && !!measure.voices.length && !beamBroken && !graceInVoice && !graceDominant && (measure.duration === sigDuration || (Number.isFinite(measure.estimatedDuration) && measure.estimatedDuration <= sigDuration * 0.75)); const fine = !error && !overDuration && tickTwist < 0.3 && !fractionalWarp && !irregularTick && !surplusTime && !beamBroken && !graceInVoice; let expectDuration = Math.min(sigDuration, WHOLE_DURATION * 2); if (Number.isFinite(measure.estimatedDuration)) expectDuration = Math.max(0, Math.min(expectDuration, measure.estimatedDuration)); const durationRate = measure.duration / expectDuration; let qualityScore = 0; if (measure.patched && !corruptedVoiceEvent) qualityScore = 1; else if (!error) { const spaceLoss = Math.tanh(Math.abs(spaceTime / Math.max(1, measure.voices.length)) * 1); let expectDuration = Math.min(sigDuration, WHOLE_DURATION * 2); if (Number.isFinite(measure.estimatedDuration)) expectDuration = Math.max(0, Math.min(expectDuration, measure.estimatedDuration)); const durationLoss = expectDuration ? Math.max(0, 1 - durationRate) ** 2 : 0; const warpsLoss = Math.tanh(irregularWarps.size); qualityScore = (1 - spaceLoss) * (1 - durationLoss) * (1 - warpsLoss) * (1 - tickTwist ** 2); } return { events, validEvents, voiceRugged, nullEvents, fakeEvents, warpedEvents, complicatedTimewarp, spaceTime, surplusTime, durationRate, beamBroken, fractionalWarp, irregularWarpsN: irregularWarps.size, irregularTick, tickTwist, tickOverlapped, graceInVoice, graceN, graceDominant, perfect, fine, error, qualityScore, }; }; //import { staffSvg } from "@kelvinnxu/lotus"; class SemanticGraph extends SimpleClass { constructor(data) { super(); super.assign(data); } /*static fromSheetStaff(staff: staffSvg.SheetStaff, hashTable: {[key: string]: any}): SemanticGraph { const tokens = [].concat(...staff.measures.map(measure => measure.tokens)); const voltaRightXs = []; const points = []; tokens.forEach(token => { const def = hashTable[token.hash]; if (token.glyph) { const glyph = token.glyph as string; let semantic = null; const isKey = /^\\key/.test(token.source) || token.is("KEY"); let { x: cx = 0, y: cy = 0 } = glyphCenters[glyph] || { x: 0, y: 0 }; if (token.scale2) { cx *= token.scale2.x; cy *= token.scale2.y; } let x = token.x + cx; const y = token.y + cy; switch (glyph) { case "rests.0": if (/^R/.test(token.source)) semantic = "Rest0W"; else semantic = "Rest0"; break; case "accidentals.flat": semantic = glyphSemanticMapping[glyph]; if (isKey) { points.push({ semantic: SemanticType.KeyAcc, x, y, }); } break; case "accidentals.natural": semantic = glyphSemanticMapping[glyph]; if (isKey) { points.push({ semantic: SemanticType.KeyAcc, x, y, }); } break; case "accidentals.sharp": semantic = glyphSemanticMapping[glyph]; if (isKey) { points.push({ semantic: SemanticType.KeyAcc, x, y, }); } break; case "dots.dot": if (token.is("VOLTA")) { x += 0.24; // dot glyph center X offset if (token.is("LEFT")) semantic = SemanticType.VoltaLeft; else if (token.is("RIGHT")) { voltaRightXs.push(x); semantic = SemanticType.VoltaRight; } } else semantic = "Dot"; break; case "zero": case "one": case "two": case "three": case "four": case "five": case "six": case "seven": case "eight": case "nine": { const upper = glyph[0].toUpperCase() + glyph.substr(1); semantic = token.is("TIME_SIG") ? "Timesig" + upper : upper; } break; default: semantic = glyphSemanticMapping[glyph]; } if (semantic) { points.push({ semantic, x, y, }); } if (token.is("TEMPO_NOTEHEAD")) { points.push({ semantic: SemanticType.TempoNotehead, x, y, }); } // grace noteheads if (token.is("NOTEHEAD") && Number.isFinite(token.scale) && token.scale < 0.75) { points.push({ semantic: SemanticType.GraceNotehead, x, y, }); } } // semantic from token symbol let semantic = null; const cx = 0; let cy = 0; if (token.is("OCTAVE")) { if (token.is("_8")) { semantic = SemanticType.OctaveShift8; cy = token.is("B") ? -0.7512 : -0.7256; } else if (token.is("CLOSE")) { semantic = SemanticType.OctaveShift0; cy = 0; } } else if (/^flags/.test(token.glyph)) { let direction = 0; if (/\.u\d/.test(token.glyph)) direction = 1; if (/\.d\d/.test(token.glyph)) direction = -1; if (direction) { const [n] = token.glyph.match(/\d+/); const flagCount = Number(n) - 2; //console.log("flags:", token.glyph, flagCount); for (let i = 0; i < flagCount; ++i) { const y = token.y + (i + 0.5) * direction; points.push({ semantic: SemanticType.Flag3, x: token.x, y, }); //console.log("flags.1:", token.x, y); } } } else if (token.is("SLUR")) { const d = def && def.d; if (d) { const numbers = d.match(/-?[\d.]+/g).map(Number); //console.log("slur:", numbers); const x1 = token.x + numbers[0]; const y1 = token.y + numbers[1]; const x2 = token.x + numbers[6]; const y2 = token.y + numbers[7]; points.push({ semantic: SemanticType.SlurBegin, x: x1, y: y1, }); points.push({ semantic: SemanticType.SlurEnd, x: x2, y: y2, }); } } else if (token.is("NOTE_STEM")) { points.push({ semantic: SemanticType.vline_Stem, x: token.x + def.width / 2, y: token.y, extension: { y1: token.y, y2: token.y + token.height, }, }); } else if (token.is("TEXT") || token.is("CHORD_TEXT")) { if (/\S/.test(token.text)) { // NOTE: text rect computation is delayed to sheet rendering points.push({ semantic: SemanticType.rect_Text, x: token.x, y: token.y, extension: { index: token.index, text: token.text, }, }); } } if (semantic) { points.push({ semantic, x: token.x + cx, y: token.y + cy, }); } }); // beams const stems = tokens.filter(token => token.is("NOTE_STEM")).map(stem => ({ x: stem.x + stem.width / 2, y1: stem.y, y2: stem.y + stem.height, })); const beams = tokens.filter(token => token.is("NOTETAIL") && token.is("JOINT")) .map(beam => { const def = hashTable[beam.hash]; const points = def && def.points; if (points) { const floats = points.split(" ").map(Number); const x1 = beam.x + floats[4]; const x2 = beam.x + floats[0]; const y1 = beam.y + (floats[5] + floats[7]) / 2; const y2 = beam.y + (floats[1] + floats[3]) / 2; const k = (y2 - y1) / (x2 - x1); return { x1, x2, y1, y2, k, capital: beam.is("CAPITAL_BEAM") }; } return null; }).filter(Boolean); //console.log("beams:", beams); beams.forEach(beam => { const innerStems = stems.filter(stem => stem.x > beam.x1 - 0.2 && stem.x < beam.x2 + 0.2); //console.log("innerStems:", beam, innerStems); let lines = 0; innerStems.forEach(stem => { const beamY = beam.y1 + (stem.x - beam.x1) * beam.k; //console.log("beamY:", beamY, Math.min(Math.abs(beamY - beam.y1), Math.abs(beamY - beam.y2))); if (beamY >= stem.y1 - 0.1 && beamY <= stem.y2 + 0.1) { points.push({ semantic: SemanticType.Flag3, x: stem.x, y: beamY, }); ++lines; // beam semantics if (beam.capital) { let semantic = SemanticType.BeamContinue; if (Math.abs(stem.x - beam.x1) < 0.2) semantic = SemanticType.BeamLeft; else if (Math.abs(stem.x - beam.x2) < 0.2) semantic = SemanticType.BeamRight; points.push({ semantic, x: stem.x, y: beamY, }); } } }); if (!lines) console.warn("empty beam:", beam, innerStems, stems); //else if (lines < 2) // console.debug("single beam:", beam, innerStems, stems); }); // wedges (crescendo & decrescendo) const crescendos = tokens.filter(token => token.is("WEDGE CRESCENDO TOP")); const crescendoBottoms = tokens.filter(token => token.is("WEDGE CRESCENDO BOTTOM")); const decrescendos = tokens.filter(token => token.is("WEDGE DECRESCENDO TOP")); const decrescendoBottoms = tokens.filter(token => token.is("WEDGE DECRESCENDO BOTTOM")); crescendos.forEach(line => { const partner = crescendoBottoms.find(b => b.x === line.x && Math.abs(b.y - line.y) < 0.06); if (partner) { points.push({ semantic: SemanticType.CrescendoBegin, x: line.x, y: line.y, }); } //else // console.log("unpaired crescendo:", line, crescendoBottoms); points.push({ semantic: SemanticType.CrescendoEnd, x: line.x + line.target.x, y: line.y + line.target.y, }); }); decrescendos.forEach(line => { const partner = decrescendoBottoms.find(b => b.x + b.target.x === line.x + line.target.x && Math.abs(b.y + b.target.y - (line.y + line.target.y)) < 0.06); points.push({ semantic: SemanticType.DecrescendoBegin, x: line.x, y: line.y, }); if (partner) { points.push({ semantic: SemanticType.DecrescendoEnd, x: line.x + line.target.x, y: line.y + line.target.y, }); } //else // console.log("unpaired decrescendo:", line, decrescendoBottoms); }); // TODO: volta alternative // measure bars const measureSeparators = staff.tokens.filter(token => token.is("MEASURE_SEPARATOR")); const singleBars = []; const groupBars = []; for (let i = 0; i < measureSeparators.length; ++i) { const bar = measureSeparators[i]; const nextBar = measureSeparators[i + 1]; const inteval = nextBar ? nextBar.x - bar.x : Infinity; if (inteval < 1) { groupBars.push([bar, nextBar]); ++i; } else singleBars.push(bar); }; //console.log("bars:", singleBars, groupBars); singleBars.forEach(bar => { points.push({ semantic: SemanticType.vline_BarMeasure, x: bar.x + bar.sw / 2, y: 0, extension: { y1: -2, y2: 2, }, }); }); groupBars.forEach(group => { let x = (group[0].x + group[1].x) / 2; const bold0 = group[0].is("BOLD"); const bold1 = group[1].is("BOLD"); let semantic = null; if (!bold0 && bold1) { x = group[0].x; if (!voltaRightXs.some(vx => x - vx < 2)) semantic = SemanticType.vline_BarTerminal; } else if (bold0 && !bold1) x = group[1].x; else if (!bold0 && !bold1) semantic = SemanticType.vline_BarSegment; //console.log("group:", group[0].x, group[1].x, x); points.push({ semantic: SemanticType.vline_BarMeasure, x, y: 0, extension: { y1: -2, y2: 2, }, }); if (semantic) { points.push({ semantic, x, y: 0, extension: { y1: -2, y2: 2, }, }); } }); const graph = new SemanticGraph(); graph.points = points; return graph; }*/ static fromPoints(points = []) { const graph = new SemanticGraph(); graph.points = points; return graph; } getLayer(semantic) { return this.points.filter((p) => p.semantic === semantic); } getConfidentLayer(semantic, threshold) { return this.points.filter((p) => p.semantic === semantic && (!Number.isFinite(p.confidence) || p.confidence >= threshold)); } getSystemPoints() { return this.points.filter((point) => SYSTEM_SEMANTIC_TYPES.includes(point.semantic)); } getStaffPoints() { return this.points.filter((point) => !SYSTEM_SEMANTIC_TYPES.includes(point.semantic)); } offset(x, y) { this.points.forEach((point) => { point.x += x; point.y += y; }); } scale(factor) { this.points.forEach((point) => { point.x *= factor; point.y *= factor; }); } // multipy 3x2 matrix transform(matrix) { this.points.forEach((point) => { let x = point.x * matrix[0][0] + point.y * matrix[1][0] + matrix[2][0]; const y = point.x * matrix[0][1] + point.y * matrix[1][1] + matrix[2][1]; if (point.extension) { if (Number.isFinite(point.extension.y1)) { const y1 = point.x * matrix[0][1] + point.extension.y1 * matrix[1][1] + matrix[2][1]; const y2 = point.x * matrix[0][1] + point.extension.y2 * matrix[1][1] + matrix[2][1]; x = point.x * matrix[0][0] + (point.extension.y1 + point.extension.y2) * 0.5 * matrix[1][0] + matrix[2][0]; point.extension.y1 = y1; point.extension.y2 = y2; } if (Number.isFinite(point.extension.width)) { const scaling = Math.sqrt(matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]); point.extension.width *= scaling; point.extension.height *= scaling; } } point.x = x; point.y = y; }); } } SemanticGraph.className = 'SemanticGraph'; const CHORD_X_TOLERANCE = 0.2; //const EVENT_X_TOLERANCE = 0.8; const STEM_LENGTH_MAX = 6; const INDENT_THRESHOLD = 2; const MEASURE_SEMANTICS = [ SemanticType.ClefG, SemanticType.ClefF, SemanticType.ClefC, SemanticType.TimesigC44, SemanticType.TimesigC22, SemanticType.TimesigZero, SemanticType.TimesigOne, SemanticType.TimesigTwo, SemanticType.TimesigThree, SemanticType.TimesigFour, SemanticType.TimesigFive, SemanticType.TimesigSix, SemanticType.TimesigSeven, SemanticType.TimesigEight, SemanticType.TimesigNine, SemanticType.OctaveShift8va, SemanticType.OctaveShift8vb, SemanticType.OctaveShift0, SemanticType.Zero, SemanticType.One, SemanticType.Two, SemanticType.Three, SemanticType.Four, SemanticType.Five, SemanticType.Six, SemanticType.Seven, SemanticType.Eight, SemanticType.Nine, SemanticType.AccNatural, SemanticType.AccSharp, SemanticType.AccDoublesharp, SemanticType.AccFlat, SemanticType.AccFlatflat, SemanticType.NoteheadS0, SemanticType.NoteheadS1, SemanticType.NoteheadS2, SemanticType.NoteheadS1stemU, SemanticType.NoteheadS1stemD, SemanticType.NoteheadS2stemU, SemanticType.NoteheadS2stemD, SemanticType.Rest0, SemanticType.Rest1, SemanticType.Rest2, SemanticType.Rest3, SemanticType.Rest4, SemanticType.Rest5, SemanticType.Rest6, SemanticType.Rest0W, SemanticType.RestM1, SemanticType.SlurBegin, SemanticType.SlurEnd, SemanticType.Dot, SemanticType.f, SemanticType.p, SemanticType.m, SemanticType.n, SemanticType.r, SemanticType.s, SemanticType.z, SemanticType.ScriptFermata, SemanticType.ScriptShortFermata, SemanticType.ScriptSforzato, SemanticType.ScriptStaccato, SemanticType.ScriptStaccatissimo, SemanticType.ScriptTurn, SemanticType.ScriptTrill, SemanticType.ScriptSegno, SemanticType.ScriptCoda, SemanticType.ScriptArpeggio, SemanticType.ScriptPrall, SemanticType.ScriptMordent, SemanticType.ScriptMarcato, SemanticType.ScriptTenuto, SemanticType.ScriptPortato, SemanticType.PedalStar, SemanticType.PedalPed, SemanticType.GraceNotehead, SemanticType.BeamLeft, SemanticType.BeamRight, SemanticType.BeamContinue, SemanticType.CrescendoBegin, SemanticType.CrescendoEnd, SemanticType.DecrescendoBegin, SemanticType.DecrescendoEnd, SemanticType.TremoloLeft, SemanticType.TremoloRight, SemanticType.TremoloMiddle, ]; const STAFF_LINED_SEMANTICS = [ SemanticType.AccNatural, SemanticType.AccSharp, SemanticType.AccDoublesharp, SemanticType.AccFlat, SemanticType.AccFlatflat, SemanticType.NoteheadS0, SemanticType.NoteheadS1, SemanticType.NoteheadS2, SemanticType.NoteheadS1stemU, SemanticType.NoteheadS1stemD, SemanticType.NoteheadS2stemU, SemanticType.NoteheadS2stemD, ]; const LINED_INTERVAL_SEMANTICS = [SemanticType.SignLined, SemanticType.SignInterval]; const NOTEHEAD_FOR_STEM_SEMANTICS = [SemanticType.NoteheadS1, SemanticType.NoteheadS2]; const KEYACC_CANDIDATE_SEMANTICS = { AccSharp: TokenType.KeySharp, AccNatural: TokenType.KeyNatural, AccFlat: TokenType.KeyFlat, }; const NOTEHEAD_TABLE = { [SemanticType.NoteheadS1]: { up: SemanticType.NoteheadS1stemU, down: SemanticType.NoteheadS1stemD, }, [SemanticType.NoteheadS2]: { up: SemanticType.NoteheadS2stemU, down: SemanticType.NoteheadS2stemD, }, }; const REST_SEMANTICS = [ SemanticType.Rest0, SemanticType.Rest1, SemanticType.Rest2, SemanticType.Rest3, SemanticType.Rest4, SemanticType.Rest5, SemanticType.Rest6, ]; const TOKEN_TO_STEMBEAM = { [TokenType.BeamLeft]: 'Open', [TokenType.BeamRight]: 'Close', [TokenType.BeamContinue]: 'Continue', }; const TEXT_TYPE_ALIAS = { Alter1: TextType.Alternation1, Alter2: TextType.Alternation2, }; const noteheadsXPivot = (xs, direction) => { switch (xs.length) { case 0: return undefined; case 1: return xs[0]; case 2: return direction === 'u' ? Math.min(...xs) : Math.max(...xs); default: { const mean = xs.reduce((sum, x) => sum + x, 0) / xs.length; xs.sort((x1, x2) => Math.abs(x1 - mean) - Math.abs(x2 - mean)); return noteheadsXPivot(xs.slice(0, xs.length - 1), direction); } } }; const noteheadsPivot = (nhs) => noteheadsXPivot(nhs.map((nh) => (Number.isFinite(nh.pivotX) ? nh.pivotX : nh.x)), nhs[0].direction); class Measure extends SimpleClass { constructor(data) { super(); super.assign(data); this.tokens = this.tokens || []; this.antiTokens = this.antiTokens || []; this.barTypes = this.barTypes || {}; } get right() { return this.left + this.width; } get noteheads() { return this.tokens.filter((t) => t.isNotehead).sort((n1, n2) => n1.x - n2.x); } get chordRects() { const noteheads = this.noteheads.filter((nh) => [TokenType.NoteheadS0, TokenType.NoteheadS1stemU, TokenType.NoteheadS2stemU, TokenType.NoteheadS1stemD, TokenType.NoteheadS2stemD].includes(nh.type)); let nulN = 0; const nhmap = noteheads.reduce((map, nh) => { const tip = nh.tip ? `${nh.tip.x}|${nh.tip.y}` : `nul${nulN}`; let key = `${nh.type}|${tip}`; if (!nh.tip && map[key]) { if (!map[key].some((hh) => Math.abs(hh.x - nh.x) < NOTEHEAD_WIDTHS.NoteheadS0)) { ++nulN; key = `${nh.type}|nul${nulN}`; } } map[key] = map[key] || []; map[key].push(nh); return map; }, {}); return Object.values(nhmap).map((nhs) => { const left = Math.min(...nhs.map((nh) => nh.x)); const right = Math.max(...nhs.map((nh) => nh.x)); const top = Math.min(...nhs.map((nh) => nh.y)); const bottom = Math.max(...nhs.map((nh) => nh.y)); const nh0 = nhs[0]; const stemX = nh0 && nh0.tip ? nh0.tip.x : left; let x = left; let width = right - left; let stemDirection = null; switch (nh0.type) { case TokenType.NoteheadS0: x -= NOTEHEAD_WIDTHS.NoteheadS0 / 2; width += NOTEHEAD_WIDTHS.NoteheadS0; break; case TokenType.NoteheadS1stemU: case TokenType.NoteheadS2stemU: stemDirection = 'u'; x -= NOTEHEAD_WIDTHS.NoteheadS1; width += NOTEHEAD_WIDTHS.NoteheadS1; break; case TokenType.NoteheadS1stemD: case TokenType.NoteheadS2stemD: stemDirection = 'd'; width += NOTEHEAD_WIDTHS.NoteheadS1; break; } return { x, width, stemX, stemDirection, top, bottom, tip: nh0.tip, }; }); } get timeWarped() { return this.tokens && this.tokens.some((token) => token.timeWarped); } get additionalLines() { const chords = this.getChords(); const up = chords .filter((chord) => chord.ys.some((y) => y <= -3)) .map((chord) => ({ left: chord.left, right: chord.right, n: Math.ceil(Math.min(...chord.ys)) + 2, })); const down = chords .filter((chord) => chord.ys.some((y) => y >= 3)) .map((chord) => ({ left: chord.left, right: chord.right, n: Math.floor(Math.max(...chord.ys)) - 2, })); return [...up, ...down].map((stack) => ({ left: stack.left - 0.28, right: stack.right + 0.28, n: stack.n, })); } getChords() { const flags = this.tokens.filter((t) => TokenFlags.includes(t.type)); const dots = this.tokens.filter((t) => TokenDots.includes(t.type)); const beams = this.tokens.filter((t) => TokenBeams.includes(t.type)); const chordRcs = this.chordRects .map((rect) => { const noteheads = this.noteheads.filter((nh) => nh.direction === rect.stemDirection && nh.left >= rect.x && nh.right <= rect.x + rect.width + CHORD_X_TOLERANCE && nh.y >= rect.top && nh.y <= rect.bottom); noteheads.sort((n1, n2) => n2.y - n1.y); const ys = noteheads.map((nh) => nh.y); const noteIds = noteheads.map((nh) => nh.id); const division = noteheads.reduce((d, nh) => Math.max(d, nh.division), 0); return { rect, left: rect.x, right: rect.x + rect.width, pivotX: noteheadsPivot(noteheads), ys, tip: rect.tip, noteIds, division, dots: null, rest: false, stemDirection: rect.stemDirection, beam: null, }; }) .sort((c1, c2) => c2.left - c1.left); const accs = new Set(); const chords = chordRcs.map(({ rect, ...chord }) => { if (chord.division >= 1) { // NOTE: notehead-s1 may have flags too const flagRange = [rect.bottom, rect.top]; switch (rect.stemDirection) { case 'u': flagRange[0] = rect.tip ? rect.tip.y - 0.2 : rect.top - STEM_LENGTH_MAX - 0.5; break; case 'd': flagRange[1] = rect.tip ? rect.tip.y + 0.2 : rect.bottom + STEM_LENGTH_MAX + 0.5; break; } const nearbyFlags = flags.filter((flag) => !accs.has(flag.id) && flag.x > rect.stemX - CHORD_X_TOLERANCE && flag.x < rect.stemX + CHORD_X_TOLERANCE && flag.y > flagRange[0] && flag.y < flagRange[1]); chord.division = nearbyFlags.reduce((d, flag) => Math.max(d, flag.division), chord.division); nearbyFlags.forEach((flag) => accs.add(flag.id)); const beamToken = rect.tip && beams.find((t) => Math.abs(rect.tip.x - t.x) < 0.3 && Math.abs(rect.tip.y - t.y) < 0.7); if (beamToken) chord.beam = TOKEN_TO_STEMBEAM[beamToken.type]; } const nearbyDots = dots.filter((dot) => !accs.has(dot.id) && dot.x > rect.x + rect.width - 0.2 && dot.x < rect.x + rect.width + 1.2 && dot.y > rect.top - 1 && dot.y <= rect.bottom + 0.5); chord.dots = nearbyDots.reduce((v, dot) => Math.max(v, dot.dots), 0); nearbyDots.forEach((dot) => accs.add(dot.id)); return chord; }); chords.reverse(); return chords; } getRests() { const rests = this.tokens.filter((t) => TokenRests.includes(t.type)); const dots = this.tokens.filter((t) => TokenDots.includes(t.type)); return rests.map((rest) => { const nearbyDots = dots.filter((dot) => dot.x > rest.x + 0.5 && dot.x < rest.x + 2 && dot.y > rest.y - 1 && dot.y < rest.y + 0.5); const dotValue = nearbyDots.reduce((v, dot) => Math.max(v, dot.dots), 0); return { left: rest.x - 0.75, right: rest.x + 0.75, pivotX: rest.x, rest: true, ys: [rest.y], noteIds: [rest.id], dots: dotValue, division: rest.division, stemDirection: null, }; }); } getEvents() { return [...this.getChords(), ...this.getRests()].sort((e1, e2) => e1.left - e2.left); } getContexts(fields = {}) { return this.tokens .filter((t) => t.isContexted) .sort((n1, n2) => n1.x - n2.x) .map((token) => new ContextedTerm({ x: token.x, y: token.y, tokenType: token.type, ...fields, })); } assignAccessoriesOnEvents(events) { events.forEach((event) => (event.accessories = event.accessories || [])); const accessories = this.tokens.filter((token) => TokenAccessories.includes(token.type)); //console.log("accessories:", accessories); accessories.forEach((accessory) => { const relatedEvents = events.filter((event) => accessory.x > event.left - 1 && accessory.x < event.right + 1); if (relatedEvents.length > 0) { let owner = relatedEvents[0]; if (relatedEvents.length > 1) { owner = relatedEvents .map((event) => ({ event, d: Math.min(...event.ys.map((y) => Math.abs(y - accessory.y))) })) .sort(({ d: d1 }, { d: d2 }) => d1 - d2) .map(({ event }) => event)[0]; } //console.log("relatedEvents:", accessory, owner); let direction = accessory.y > Math.max(...owner.ys) ? AccessoryDirection.Down : AccessoryDirection.Up; if (TokenDirectionless.includes(accessory.type)) direction = null; owner.accessories.push({ type: accessory.type, id: accessory.id, direction, x: accessory.x - owner.left, }); } //else // console.debug("alone accessory:", accessory.type); }); // arpeggio const sortEvents = [...events]; sortEvents.sort((e1, e2) => e1.left - e2.left); const arpeggios = this.tokens.filter((token) => token.type === TokenType.ScriptArpeggio); arpeggios.forEach((arpeggio) => { const owner = sortEvents.find((event) => arpeggio.x < event.left && event.ys.some((y) => y < arpeggio.y + 0.25) && event.ys.some((y) => y > arpeggio.y)); //const owner = sortEvents.find(event => event.left - leftMost.left < 2 && event.ys.some(y => Math.abs(y - arpeggio.y + 0.25) < 0.5)); if (owner) { owner.accessories.push({ type: TokenType.ScriptArpeggio, id: arpeggio.id, x: arpeggio.x - owner.left, }); } //else // console.debug("alone arpeggio:", arpeggio); }); // grace noteheads const graceNhs = this.tokens.filter((token) => token.type === TokenType.GraceNotehead); graceNhs.forEach((grace) => { const event = events.find((event) => grace.x > event.left && grace.x < event.right && event.ys.some((y) => Math.abs(grace.y - y) < 0.4)); if (event) event.grace = GraceType.Grace; }); // tremolos const tremolsLs = this.tokens.filter((token) => token.type === TokenType.TremoloLeft); const tremolsRs = this.tokens.filter((token) => token.type === TokenType.TremoloRight); const tremolsMs = this.tokens.filter((token) => token.type === TokenType.TremoloMiddle); const tevents = events .filter((event) => !event.rest) .map((event) => { const ys = [...event.ys]; if (event.tip) ys.push(event.tip.y); else { ys.push(event.ys[0] + 2); ys.push(event.ys[event.ys.length - 1] - 2); } const stemL = event.tip ? event.tip.x : event.left; const stemR = event.tip ? event.tip.x : event.right; return { event, top: Math.min(...ys), bottom: Math.max(...ys), stemL, stemR, }; }); tremolsMs.forEach((tm) => { const te = tevents.find((te) => { if (te.event.tip) return tm.y > te.top && tm.y < te.bottom && Math.abs(tm.x - te.event.tip.x) < 0.3; return false; }); if (te) { te.event.tremolo = te.event.tremolo || 2; ++te.event.tremolo; } }); tremolsLs.forEach((tl) => { const te = tevents.find((te) => tl.y > te.top && tl.y < te.bottom && tl.x > te.stemR && tl.x < te.stemR + 1.6); if (te) { te.event.tremolo = te.event.tremolo || 2; ++te.event.tremolo; te.event.tremoloLink = TremoloLink.Pitcher; } }); tremolsRs.forEach((tr) => { const te = tevents.find((te) => tr.y > te.top && tr.y < te.bottom && tr.x < te.stemL && tr.x > te.stemL - 1.6); if (te) { te.event.tremolo = te.event.tremolo || 2; ++te.event.tremolo; te.event.tremoloLink = TremoloLink.Catcher; } }); } assignFeaturesOnEvents(events, semantics) { const points = semantics.filter((point) => point.x > this.left && point.x < this.right); const rests = points.filter((point) => REST_SEMANTICS.includes(point.semantic)); const flags = points.filter((point) => point.semantic === SemanticType.Flag3); const dotPs = points.filter((point) => point.semantic === SemanticType.Dot); const beamLs = points.filter((points) => points.semantic === SemanticType.BeamLeft); const beamMs = points.filter((points) => points.semantic === SemanticType.BeamContinue); const beamRs = points.filter((points) => points.semantic === SemanticType.BeamRight); const gracePs = points.filter((point) => point.semantic === SemanticType.GraceNotehead); const tremoloRs = points.filter((point) => point.semantic === SemanticType.TremoloRight); const stems = points.filter((point) => point.semantic === SemanticType.vline_Stem); const s0 = points.filter((point) => point.semantic === SemanticType.NoteheadS0); const s1 = points.filter((point) => point.semantic === SemanticType.NoteheadS1); const s2 = points.filter((point) => point.semantic === SemanticType.NoteheadS2); events.forEach((event) => { const cx = event.tip ? event.tip.x : (event.left + event.right) / 2; const top = event.tip ? Math.min(event.tip.y, event.ys[event.ys.length - 1]) : event.ys[event.ys.length - 1]; const bottom = event.tip ? Math.max(event.tip.y, event.ys[0]) : event.ys[0]; const stemL = event.tip ? event.tip.x : event.left; const divisions = [0, 0, 0, 0, 0, 0, 0]; if (event.rest) { const i_rests = rests.filter((point) => distance2D(point, { x: cx, y: event.ys[0] }) < 0.5); i_rests.forEach((r) => { const d = REST_SEMANTICS.indexOf(r.semantic); divisions[d] = Math.max(divisions[d], r.confidence); }); } else { const nhs = [s0, s1, s2] .map((ss) => ss.filter((nh) => nh.x > event.left && nh.x < event.right && nh.y > top - 0.25 && nh.y < bottom + 0.25)) .map((ss) => Math.max(0, ...ss.map((nh) => nh.confidence))); const i_flags = flags.filter((flag) => flag.y > top - 0.2 && flag.y < bottom + 0.2 && Math.abs(flag.x - cx) < 0.2); i_flags.sort((f1, f2) => f2.confidence - f1.confidence); divisions[0] = nhs[0]; divisions[1] = nhs[1]; divisions[2] = nhs[2]; Array(divisions.length - 3) .fill(0) .forEach((_, i) => (divisions[3 + i] = i_flags[i] ? i_flags[i].confidence : 0)); } const i_dots = dotPs.filter((dot) => dot.x > cx && dot.x < event.right + 2.6); const dots2 = i_dots.filter((dot) => i_dots.some((d) => dot.x > d.x && Math.abs(dot.y - d.y) < 0.2)); const dots = [Math.max(0, ...i_dots.map((dot) => dot.confidence)), Math.max(0, ...dots2.map((dot) => dot.confidence))]; const beams = [beamLs, beamMs, beamRs] .map((bs) => bs.filter((b) => Math.abs(b.x - cx) < 0.2 && b.y > top - 0.2 && b.y < bottom + 0.2)) .map((bs) => Math.max(0, ...bs.map((b) => b.confidence))); const u_stems = stems.filter((stem) => distance2D({ x: cx, y: event.ys[0] }, { x: stem.x, y: stem.extension.y2 }) < 0.5); const d_stems = stems.filter((stem) => distance2D({ x: cx, y: event.ys[event.ys.length - 1] }, { x: stem.x, y: stem.extension.y1 }) < 0.5); const stemDirections = [Math.max(0, ...u_stems.map((stem) => stem.confidence)), Math.max(0, ...d_stems.map((stem) => stem.confidence))]; const graces = gracePs.filter((grace) => Math.abs(grace.x - cx) < 0.6 && event.ys.some((y) => Math.abs(grace.y - y) < 0.4)); const grace = Math.max(0, ...graces.map((grace) => grace.confidence)); const tremolos = event.division === 0 ? tremoloRs.filter((tremolo) => tremolo.x > event.left - 2 && tremolo.x < event.right) : tremoloRs.filter((tremolo) => tremolo.y > top - 0.04 && tremolo.y < bottom + 0.04 && tremolo.x > stemL - 2 && tremolo.x < stemL); const tremoloCatcher = Math.max(0, ...tremolos.map((tremolo) => tremolo.confidence)); event.feature = { divisions, dots, beams, stemDirections, grace, tremoloCatcher, }; }); } } Measure.className = 'Measure'; Measure.blackKeys = ['tokens', 'antiTokens']; class Staff extends SimpleClass { constructor({ measureCount = null, measureBars = null, ...data } = {}) { super(); super.assign(data); this.height = this.height || 10; this.staffY = this.staffY || 5; if (measureBars) { let left = 0; this.measures = measureBars.map((endX) => { const measure = new Measure({ left, width: endX - left, height: this.height }); left = endX; return measure; }); } else if (measureCount) this.measures = Array(measureCount) .fill(null) .map(() => new Measure()); else this.measures = []; } // relative to staffY get noteRange() { const noteheads = [].concat(...this.measures.map((measure) => measure.noteheads)); const ys = noteheads.map((note) => note.y); const top = Math.min(-2, ...ys); const bottom = Math.max(2, ...ys); return { top, bottom }; } get additionalLines() { return [].concat(...this.measures.map((measure) => measure.additionalLines)); } rearrangeMeasures(measureBars) { if (!measureBars.length) { console.warn('rearrangeMeasures error, measureBars are empty.'); return; } const tokens = this.measures?.map((measure) => measure.tokens).flat(1) || []; let left = 0; this.measures = measureBars.map((endX) => { const measure = new Measure({ left, width: endX - left, height: this.height }); left = endX; return measure; }); this.reassignTokens(tokens); } reassignTokens(tokens = null) { if (!tokens) tokens = [].concat(...this.measures.map((measure) => measure.tokens)); this.measures.forEach((measure) => (measure.tokens = [])); tokens.forEach((token) => { for (const measure of this.measures) { if (token.x < measure.right) { measure.tokens.push(token); break; } } }); } assignSemantics(graph) { this.semantics = graph.getStaffPoints(); } // generate tokens from semantics assemble(threshold, system, logger = new DummyLogger()) { if (!this.semantics) return; let points = system.qualifiedSemantics(this.semantics, threshold); points = solveOverlapping(points); // tempo noteheads const tempoNhs = points.filter((point) => point.semantic === SemanticType.TempoNotehead); tempoNhs.forEach((tempoNh) => { const index = points.findIndex((point) => /^Notehead/.test(point.semantic) && distance2D(tempoNh, point) < 0.3); //console.log("temponh:", tempoNh, index, points[index]); if (index >= 0) points.splice(index, 1); // TODO: construct tempo term }); const antiP = (id) => { if (system.displacementSemantics?.[id]) return this.semantics.find((p) => p.id === id); return null; }; points.filter((point) => MEASURE_SEMANTICS.includes(point.semantic)).forEach((point) => this.appendPoint(point, { points })); // noteheads with stem from noteheads & stems const stems = points .filter((point) => point.semantic === SemanticType.vline_Stem) .filter((stem) => stem.extension.y2 - stem.extension.y1 > 1.5) // exclude too short stems .map((p) => ({ x: p.x, y1: p.extension.y1, y2: p.extension.y2, direction: null, })); const noteheads = points.filter((point) => NOTEHEAD_FOR_STEM_SEMANTICS.includes(point.semantic) && point.y > this.semanticTop && point.y < this.semanticBottom); const rootNhs = new Set(); // for 2nd degree chord notes const nhOffsetX = (nh, stem, down) => { if ((down ? 1 : 0) ^ (nh.x < stem.x ? 1 : 0)) return 0; const offset = NOTEHEAD_WIDTHS[nh.semantic]; return down ? -offset : offset; }; // find root noteheads on stem stems.forEach((stem) => { const attachedHeads = noteheads.filter((nh) => Math.abs(nh.x - stem.x) - NOTEHEAD_WIDTHS[nh.semantic] / 2 < 0.28 && Math.abs(nh.x - stem.x) - NOTEHEAD_WIDTHS[nh.semantic] / 2 > -0.44 && // for grace noteheads, more close to their stem nh.y > stem.y1 - 0.5 && nh.y < stem.y2 + 0.5 && !(nh.x > stem.x && nh.y > stem.y2) && !(nh.x < stem.x && nh.y < stem.y1)); //if (stem.x===102.0625 && stem.y2===1.875) // debugger; if (attachedHeads.length) { attachedHeads.sort((n1, n2) => n1.y - n2.y); const topDist = Math.min(...attachedHeads.map((nh) => nh.y - stem.y1)); const bottomDist = Math.min(...attachedHeads.map((nh) => stem.y2 - nh.y)); if (Math.min(topDist, bottomDist) > 0.5) return; // no root notehead on this stem const down = topDist < bottomDist; stem.direction = down ? 'd' : 'u'; if (!down) attachedHeads.reverse(); const root = attachedHeads[0]; const semantic = down ? NOTEHEAD_TABLE[root.semantic].down : NOTEHEAD_TABLE[root.semantic].up; this.appendPoint({ id: root.id, semantic, x: stem.x + nhOffsetX(root, stem, down), y: root.y, pivotX: root.x, confidence: root.confidence, }, { tip: { x: stem.x, y: down ? stem.y2 : stem.y1 }, antiPoint: antiP(root.id), points, }); rootNhs.add(root.id); } }); // non-root noteheads noteheads .filter((nh) => !rootNhs.has(nh.id)) .forEach((nh) => { const nearStems = stems .filter((stem) => Math.abs(stem.x - nh.x) < 2 && nh.y > stem.y1 && nh.y < stem.y2) .sort((s1, s2) => Math.abs(s1.x - nh.x) - Math.abs(s2.x - nh.x)); const stem = nearStems[0]; if (stem) { const down = stem.direction === 'd'; const semantic = down ? NOTEHEAD_TABLE[nh.semantic].down : NOTEHEAD_TABLE[nh.semantic].up; this.appendPoint({ id: nh.id, semantic, x: stem.x + nhOffsetX(nh, stem, down), y: nh.y, pivotX: nh.x, confidence: nh.confidence, }, { tip: { x: stem.x, y: down ? stem.y2 : stem.y1 }, antiPoint: antiP(nh.id), points, }); } else logger.debug('isolated notehead:', system.index, this.index, nh); }); // group flags const flags = points.filter((point) => point.semantic === SemanticType.Flag3); flags.sort((f1, f2) => f1.x - f2.x); this.appendFlags(flags, stems); // group dots const dots = points .filter((point) => point.semantic === SemanticType.Dot) .map((dot) => { const y = roundNumber(dot.y, 0.5); return { x: dot.x, y }; }); const dotLines = dots.reduce((table, dot) => { table[dot.y] = table[dot.y] || []; table[dot.y].push(dot); return table; }, {}); Object.entries(dotLines).forEach(([sy, line]) => { const y = Number(sy); if (line.length > 1) { line.sort((d1, d2) => d1.x - d2.x); for (let i = 0; i < line.length - 1; i++) { const dot = line[i]; if (line.find((d) => d.x > dot.x && d.x - dot.x < 1.2)) { this.appendPoint({ id: dot.id, x: dot.x, y, confidence: dot.confidence, }, { type: TokenType.DotDot, antiPoint: antiP(dot.id), points }); } } } }); // pair key accidentals const keyaccs = points.filter((point) => point.semantic === SemanticType.KeyAcc); const accs = points.filter((point) => KEYACC_CANDIDATE_SEMANTICS[point.semantic]); accs.forEach((acc) => { if (keyaccs.some((key) => Math.abs(acc.x - key.x) < 0.5 && Math.abs(acc.y - key.y) < 1)) { this.appendPoint({ id: acc.id, x: acc.x, y: acc.y, confidence: acc.confidence, }, { type: KEYACC_CANDIDATE_SEMANTICS[acc.semantic], points }); } }); // octave shift heads const octs = points.filter((point) => point.semantic === SemanticType.OctaveShift8); octs.forEach((oct) => { const type = oct.y < 0 ? TokenType.OctaveShift8va : TokenType.OctaveShift8vb; this.appendPoint({ id: oct.id, x: oct.x, y: oct.y, confidence: oct.confidence, }, { type, points }); }); // group volta dots const voltaDots = this.semantics.filter((point) => [SemanticType.VoltaLeft, SemanticType.VoltaRight].includes(point.semantic)); voltaDots.sort((d1, d2) => d1.x - d2.x); const voltaGroups = voltaDots.reduce((groups, dot) => { const group = groups[dot.semantic]; const xs = Array.from(Object.keys(group)).map(Number); const x = xs.find((x) => dot.x < x + 0.2) || dot.x; group[x] = groups[dot.semantic][x] || []; group[x].push(dot); return groups; }, { [SemanticType.VoltaLeft]: {}, [SemanticType.VoltaRight]: {} }); for (const [type, group] of Object.entries(voltaGroups)) { Object.values(group).forEach((dots) => { if (dots.length > 1) { const confidence = dots.reduce((sum, dot) => sum + dot.confidence, 0); if (dots[0].y * dots[1].y < 0 && confidence >= threshold * 2) this.appendPoint(dots[0], { type: TokenType[type] }); } }); } } appendPoint(point, { type, points = null, antiPoint, ...fields } = {}) { //console.log("appendPoint.0:", point, point.x, point.y); const x = point.x; const measure = this.measures.find((measure) => x < measure.left + measure.width); if (!measure) // drop tokens out of measures range return; // lined or interval let lined = false; let interval = false; if (STAFF_LINED_SEMANTICS.includes(point.semantic)) { console.assert(points, 'argument of points for this semantic is required:', point.semantic); const signs = points.filter((p) => LINED_INTERVAL_SEMANTICS.includes(p.semantic) && Math.abs(p.y - point.y) < 0.2 && Math.abs(p.x - point.x) < 1.2); if (signs.some((s) => s.semantic === SemanticType.SignLined)) lined = true; else if (signs.some((s) => s.semantic === SemanticType.SignInterval)) interval = true; } type = type || TokenType[point.semantic]; const fixedY = TOKEN_Y_FIXED[type]; let roundY = TOKEN_Y_ROUND[type]; if (lined || interval) roundY = Math.max(roundY, 1); let y = point.y; if (Number.isFinite(fixedY)) y = fixedY; else if (roundY) { if (interval) y = roundNumber(y + 0.5, roundY) - 0.5; else y = roundNumber(y, roundY); } //if (lined || interval) // console.log("round sign:", point.semantic, y, lined, interval); const holder = measure.tokens.find((token) => token.type === type && Math.abs(token.x - x) < 0.1 && Math.abs(token.y - y) < 0.1); if (holder) { if (Number.isFinite(holder.confidence) && holder.confidence < point.confidence) { holder.x = x; holder.y = y; holder.confidence = point.confidence; } return; } // exlude clef out of pitch range if (TokenClefs.includes(type)) { if (Math.abs(y) > 3) return; } // TODO: exclude overlapped pair by a token prior table measure.tokens.push(new Token({ id: point.id, type, x, y, pivotX: point.pivotX, confidence: point.confidence, ...fields, })); if (antiPoint) { measure.antiTokens.push(new Token({ id: antiPoint.id, type, x, y: antiPoint.y, confidence: antiPoint.confidence, })); } } appendFlags(flags, stems) { //console.log("flags:", flags); const stemGroups = stems .map((stem) => ({ ...stem, flags: flags.filter((flag) => Math.abs(flag.x - stem.x) < 0.3 && flag.y > stem.y1 - 0.5 && flag.y < stem.y2 + 0.5), })) .filter((group) => group.flags.length); stemGroups.forEach((group) => { const mainFlag = group.flags.reduce((main, flag) => (main && main.confidence > flag.confidence ? main : flag), null); //const upDistance = mainFlag.y - group.y1; //const downDistance = group.y2 - mainFlag.y; //const downward = downDistance < upDistance; const downward = group.direction === 'd'; const tailY = downward ? Math.min(group.y2, group.y1 + STEM_LENGTH_MAX) : Math.max(group.y1, group.y2 - STEM_LENGTH_MAX); const flagTips = group.flags.map((flag) => ({ tip: (tailY - flag.y) * (downward ? 1 : -1), confidence: flag.confidence, })); const count = flagTips.filter((f) => f.tip < 2 || f.confidence > mainFlag.confidence * 0.7).length; const type = TokenFlags[count - 1]; if (type) { this.appendPoint({ id: group.flags[0].id, x: group.x, y: tailY, confidence: Math.min(...group.flags.map((flag) => flag.confidence)), }, { type }); //console.log("flag:", type); } }); } clearTokens() { this.measures.forEach((measure) => (measure.tokens = [])); this.semantics = []; } clearPredictedTokens() { this.measures.forEach((measure) => (measure.tokens = measure.tokens.filter((token) => !token.isPredicted))); } } Staff.className = 'Staff'; Staff.blackKeys = ['index', 'semanticTop', 'semanticBttom']; class System extends SimpleClass { constructor({ stavesCount, ...fields }) { super(); super.assign(fields); if (!this.measureBars) { const HEAD_WIDTH = 5; const segmentLength = (this.width - HEAD_WIDTH) / this.measureCount; this.measureBars = Array(this.measureCount) .fill(0) .map((_, i) => HEAD_WIDTH + segmentLength * (i + 1)); } if (!fields.staves && stavesCount) this.staves = Array(stavesCount) .fill(null) .map(() => new Staff({ measureBars: this.measureBars })); this.arrangePosition(); this.measureCount = this.measureCount || this.measureBars.length; this.sidBlackList = this.sidBlackList || []; this.sidWhiteList = this.sidWhiteList || []; } get noteRange() { if (!this.staves.length) return null; const staffTop = this.staves[0]; const staffBottom = this.staves[this.staves.length - 1]; return { top: staffTop.top + staffTop.staffY + staffTop.noteRange.top, bottom: staffBottom.top + staffBottom.staffY + staffBottom.noteRange.bottom, }; } get staffPositions() { return this.staves.map((staff) => ({ y: staff.top + staff.staffY, radius: 2, })); } get staffMask() { if (this.staffMaskChanged) return this.staffMaskChanged; if (this.prev && this.staves.length === this.prev.staves.length) return this.prev.staffMask; return 2 ** this.staves.length - 1; } get staffTop() { const positions = this.staffPositions; return positions.length ? positions[0].y - positions[0].radius : 0; } get staffBottom() { const positions = this.staffPositions; return positions.length ? positions[positions.length - 1].y + positions[positions.length - 1].radius : 0; } arrangePosition() { let y = 0; for (const staff of this.staves) { if (Number.isFinite(staff.top)) break; staff.top = y; y += staff.height; } } tidyMeasureBars() { this.measureBars = this.measureBars.filter((x) => x > 1); this.measureBars.sort((b1, b2) => b1 - b2); const restWidth = this.width - this.measureBars[this.measureBars.length - 1]; if (restWidth > 12) this.measureBars.push(this.width); else if (restWidth < 2) this.measureBars[this.measureBars.length - 1] = this.width; this.measureBars = this.measureBars.filter((x, i) => i < 1 || x - this.measureBars[i - 1] > 4); } rearrangeMeasures() { this.measureCount = this.measureBars.length; this.staves.forEach((staff) => staff.rearrangeMeasures(this.measureBars)); } get height() { return this.staves.reduce((height, staff) => height + staff.height, 0); } get connectionLine() { const staffHead = this.staves[0]; const staffTail = this.staves[this.staves.length - 1]; return (staffHead && { top: staffHead.top + staffHead.staffY - 2, bottom: staffTail.top + staffTail.staffY + 2, }); } get middleY() { if (!this.staves.length) return 0; const sum = this.staves.reduce((sum, staff) => sum + staff.top + staff.staffY, 0); return sum / this.staves.length; } get timeSignatureOnHead() { return this.staves.some((staff) => staff.measures[0]?.tokens.some((token) => TokenTimesigs.includes(token.type))); } // an array staff or null on every position of full staff layout getStaffArray(stavesCount) { let si = 0; return Array(stavesCount) .fill(null) .map((_, i) => { const on = this.staffMask & (1 << i); const staff = on ? this.staves[si++] : null; console.assert(!on || staff, 'system staves count is less than staff mask declared:', this.staves.length, this.staffMask.toString(2)); return staff; }); } // measureIndex: the local measure index getMarksInMeasure(measureIndex) { console.assert(measureIndex < this.measureBars.length, 'measure index out of range:', measureIndex, this.measureBars.length); const left = measureIndex > 0 ? this.measureBars[measureIndex - 1] : 0; const right = this.measureBars[measureIndex]; const tempoTokens = (this.tokens ?? []).filter((token) => token.x >= left && token.x < right && token instanceof TextToken && token.textType === TextType.TempoNumeral); return [...tempoTokens.map((token) => TempoTerm.fromNumeralText(token.text)).filter(Boolean)]; } getEvents(stavesCount) { console.assert(Number.isInteger(this.headMeasureIndex), 'invalid headMeasureIndex:', this.headMeasureIndex); // Empty system (no measureBars / no staves with measures): return empty result if (!this.measureBars?.length && this.staves.every((s) => !s.measures?.length)) { return { staffMask: this.staffMask, columns: [] }; } const staves = this.getStaffArray(stavesCount); // [staff, measure] const rows = staves.map((staff) => { if (!staff) { return Array(this.measureCount) .fill(null) .map(() => ({ events: [], contexts: [], voltaBegin: false, voltaEnd: false, alternative: false, barTypes: {}, })); } return staff.measures.map((measure) => { const events = measure.getEvents(); measure.assignAccessoriesOnEvents(events); measure.assignFeaturesOnEvents(events, staff.semantics); return { events: events.map((event) => new EventTerm({ staff: staff.index, system: this.index, ...event, rest: event.rest ? 'r' : null, })), contexts: measure.getContexts({ staff: staff.index }), voltaBegin: measure.tokens.some((token) => token.type === TokenType.VoltaLeft), voltaEnd: measure.tokens.some((token) => token.type === TokenType.VoltaRight), alternative: measure.alternative, barTypes: measure.barTypes, }; }); }); // supplement time signatures for empty staves for (let mi = 0; mi < this.measureCount; ++mi) { const tsRows = rows.map((row) => row[mi]?.contexts?.filter((term) => [ContextType.TimeSignatureC, ContextType.TimeSignatureN].includes(term.type))); const timeSigs = tsRows.find((row) => row?.length); if (timeSigs) { rows.forEach((row) => { if (row[mi] && !row[mi].contexts.length && !row[mi].events.length) row[mi].contexts.push(...timeSigs); }); } } //const measureWidths = this.staves[0].measures.map(measure => measure.width); //onst measureStartXs = [0, ...this.measureBars]; const columns = Array(this.measureCount) .fill(null) .map((_, i) => ({ measureIndex: this.headMeasureIndex + i, //startX: measureStartXs[i], //width: measureWidths[i], rows: rows.map((row) => row[i]), marks: this.getMarksInMeasure(i), //xToTick: {}, duration: 0, voltaBegin: rows.some((row) => row[i]?.voltaBegin), voltaEnd: rows.some((row) => row[i]?.voltaEnd), alternative: rows.some((row) => row[i]?.alternative), barTypes: rows.reduce((ts, row) => ({ ...ts, ...row[i]?.barTypes, }), {}), })); //columns.forEach(computeMeasureTicks); // assign id on column events columns.forEach((column) => { const events = [].concat(...column.rows.filter(Boolean).map((row) => row.events)); events.forEach((event, i) => (event.id = i + 1)); }); const lastColumn = columns[columns.length - 1]; if (lastColumn) lastColumn.break = true; return { staffMask: this.staffMask, columns, }; } getEventsFunctional(stavesCount, ev, processors = [], { useXMap = false } = {}) { const staves = this.getStaffArray(stavesCount); // [staff, measure] const rows = staves.map((staff, si) => { if (!staff) { return Array(this.measureCount) .fill(null) .map(() => ({ events: [], contexts: [], voltaBegin: false, voltaEnd: false, alternative: false, barTypes: {}, })); } return staff.measures.map((measure, mi) => { const events = ev(si, mi); return (events && { events: events.map((event) => new EventTerm({ system: this.index, ...event, rest: event.rest ? 'r' : null, })), contexts: measure.getContexts({ staff: si }), voltaBegin: measure.tokens.some((token) => token.type === TokenType.VoltaLeft), voltaEnd: measure.tokens.some((token) => token.type === TokenType.VoltaRight), alternative: measure.alternative, barTypes: measure.barTypes, }); }); }); //const measureWidths = this.staves[0].measures.map(measure => measure.width); //const measureStartXs = [0, ...this.measureBars]; // [measure, staff] const columns = Array(this.measureCount) .fill(null) .map((_, mi) => { const localRows = rows.map((row) => row[mi]); if (localRows.some((row) => !row)) return null; let xMap = null; if (useXMap) { const events = [].concat(...localRows.map((row) => row.events)); const groupMap = events.reduce((map, event) => { if (Number.isFinite(event.tickGroup)) map[event.tickGroup] = map[event.tickGroup] || []; map[event.tickGroup].push(event); return map; }, {}); xMap = Object.values(groupMap).reduce((map, events) => { const x = Math.min(...events.map((event) => (event.left + event.right) / 2)); map.set(x, events); return map; }, new Map()); } return { measureIndex: this.headMeasureIndex + mi, //startX: measureStartXs[mi], //width: measureWidths[mi], rows: localRows, marks: this.getMarksInMeasure(mi), //xToTick: {}, duration: 0, xMap, voltaBegin: localRows.some((row) => row.voltaBegin), voltaEnd: localRows.some((row) => row.voltaEnd), alternative: localRows.some((row) => row.alternative), barTypes: localRows.reduce((ts, row) => ({ ...ts, ...row.barTypes, }), {}), }; }); processors.forEach((proc) => columns.forEach(proc)); return { staffMask: this.staffMask, columns, }; } // get EventSystem contains only contexted terms getContexts(stavesCount) { const staves = this.getStaffArray(stavesCount); // [staff, measure] const rows = staves.map((staff) => { if (!staff) { return Array(this.measureCount) .fill(null) .map(() => ({ events: null, contexts: [], voltaBegin: false, voltaEnd: false, alternative: false, barTypes: {}, })); } return staff.measures.map((measure) => ({ events: null, contexts: measure.getContexts(), voltaBegin: measure.tokens.some((token) => token.type === TokenType.VoltaLeft), voltaEnd: measure.tokens.some((token) => token.type === TokenType.VoltaRight), alternative: rows.some((row) => row.alternative), barTypes: measure.barTypes, })); }); // supplement time signatures for empty staves for (let mi = 0; mi < this.measureCount; ++mi) { const tsRows = rows.map((row) => row[mi]?.contexts.filter((term) => [ContextType.TimeSignatureC, ContextType.TimeSignatureN].includes(term.type))); const timeSigs = tsRows.find((row) => row?.length); if (timeSigs) { rows.forEach((row) => { if (!row[mi].contexts.length) row[mi].contexts.push(...timeSigs); }); } } //const measureWidths = this.staves[0].measures.map(measure => measure.width); //const measureStartXs = [0, ...this.measureBars]; const columns = Array(this.measureCount) .fill(null) .map((_, i) => ({ measureIndex: this.headMeasureIndex + i, //startX: measureStartXs[i], //width: measureWidths[i], rows: rows.map((row) => row[i]), marks: [], //xToTick: {}, duration: 0, voltaBegin: rows.some((row) => row[i].voltaBegin), voltaEnd: rows.some((row) => row[i].voltaEnd), alternative: rows.some((row) => row.alternative), barTypes: rows.reduce((ts, row) => ({ ...ts, ...row[i].barTypes, }), {}), })); return { staffMask: this.staffMask, columns, }; } assignSemantics(staffIndex, graph) { const staff = this.staves[staffIndex]; console.assert(staff, 'staff is null:', staffIndex, this.staves); const oy = staff.top + staff.staffY; graph.getSystemPoints().forEach((point) => { const p = { ...point }; p.y += oy; if (p.extension) { p.extension = { ...p.extension }; if (Number.isFinite(p.extension.y1)) { p.extension.y1 += oy; p.extension.y2 += oy; } } this.semantics.push(p); }); } // generate tokens from semantics assemble(threshold, logger = new DummyLogger()) { //console.log("System.assignSemantics:", graph); this.measureBars = []; if (!this.semantics) return; const graph = SemanticGraph.fromPoints(this.semantics); const bars = graph.getConfidentLayer(SemanticType.vline_BarMeasure, threshold); bars.sort((b1, b2) => b1.x - b2.x); const staffTop = this.staffTop; const staffBottom = this.staffBottom; const MERGE_WINDOW = 0.4; let lastX = 0; const barColumns = bars.reduce((columns, bar) => { const confidence = Number.isFinite(bar.confidence) ? Math.tanh(bar.confidence) : 1; const x = bar.x - lastX > MERGE_WINDOW ? bar.x : lastX; lastX = bar.x; let intensity = columns[x] || 0; intensity += (Math.min(bar.extension.y2, staffBottom) - Math.max(bar.extension.y1, staffTop)) * confidence; if (bar.x !== x) delete columns[x]; columns[bar.x] = intensity; return columns; }, {}); const barXs = Object.entries(barColumns) .filter(([x, intensity]) => (intensity > 3 * this.staves.length)) .map(([x]) => Number(x)); barXs.sort((x1, x2) => x1 - x2); barXs.forEach((x, i) => { if (i <= 0 || x - barXs[i - 1] > 2) this.measureBars.push(x); }); if (!this.measureBars.length) this.measureBars.push(this.width); this.tidyMeasureBars(); this.rearrangeMeasures(); // measure bar type const typeBars = this.semantics.filter((point) => [SemanticType.vline_BarTerminal, SemanticType.vline_BarSegment].includes(point.semantic)); typeBars.forEach((bar) => { const measure = this.staves[0].measures.find((measure) => bar.x > measure.right - 2 && bar.x < measure.right + 1); if (measure) { const type = bar.semantic.replace(/^vline_Bar/, ''); measure.barTypes[type] = measure.barTypes[type] || 0; measure.barTypes[type] += bar.confidence; } }); let staffIndex = 0; const staffMask = this.staffMask; this.staves.forEach((staff, si) => { // staff index while (!(staffMask & (1 << staffIndex))) ++staffIndex; staff.index = staffIndex++; // assign semantic boundaries if (si === 0) staff.semanticTop = -staff.staffY; else { const prevStaff = this.staves[si - 1]; staff.semanticTop = prevStaff.top + prevStaff.staffY + 3 - (staff.top + staff.staffY); } if (si < this.staves.length - 1) { const nextStaff = this.staves[si + 1]; staff.semanticBottom = nextStaff.top + nextStaff.staffY - 3 - (staff.top + staff.staffY); } else staff.semanticBottom = this.height - (staff.top + staff.staffY); if (staff.semantics && staff.semantics.length) { staff.semantics.forEach((point) => hashSemanticPoint(this.index, si, point)); staff.clearPredictedTokens(); staff.assemble(threshold, this, logger); } }); } qualifiedSemantics(semantics, threshold = 1) { return semantics .filter((p) => this.sidWhiteList.includes(p.id) || (!this.sidBlackList.includes(p.id) && (p.confidence >= threshold || !Number.isFinite(p.confidence)))) .map((point) => { // displace semantic point if (this.displacementSemantics && this.displacementSemantics[point.id]) return { ...point, ...this.displacementSemantics[point.id] }; return point; }); } clearTokens() { this.staves.forEach((staff) => staff.clearTokens()); this.semantics = []; } newPoint(staffIndex, data, threshold = 1) { const staff = this.staves[staffIndex]; console.assert(staff, 'staff index out of bound:', staffIndex, this.staves.length); const { semantic, x, y, confidence = 0, extension = null } = data; const point = { semantic, x, y, confidence, extension }; if (!point.extension) delete point.extension; hashSemanticPoint(this.index, staffIndex, point); staff.semantics.push(point); staff.clearPredictedTokens(); staff.assemble(threshold, this); return point; } appendToken(token) { this.tokens.push(token); switch (token.textType) { case TextType.TempoNumeral: { // remove noteheads in text area const staff = this.staves[0]; if (staff) { const oy = staff.top + staff.staffY; staff.measures.forEach((measure) => { measure.tokens = measure.tokens.filter((t) => !TokenNoteheads.includes(t.type) || Math.abs(t.x - token.x) > token.width / 2 || Math.abs(oy + t.y - token.y) > token.fontSize / 2); }); } } break; case TextType.Alternation1: case TextType.Alternation2: //console.log("appendToken:", token, this.staves[0].measures); this.staves[0].measures.forEach((measure) => { const overlap = Math.min(measure.left + measure.width, token.x + token.width / 2) - Math.max(measure.left, token.x - token.width / 2); measure.alternative = measure.alternative || overlap / measure.width > 0.5; }); break; } } } System.className = 'System'; System.blackKeys = ['index', 'pageIndex', 'prev', 'next', 'headMeasureIndex', 'tokens', 'indent']; class Page extends SimpleClass { constructor(data) { super(); super.assign(data); this.systems = this.systems || []; if (this.source) { this.source.matrix = this.source.matrix || [1, 0, 0, 1, 0, 0]; } } get sidBlackList() { const ids = [].concat(...this.systems.map((system) => system.sidBlackList)); return new Set(ids); } get sidWhiteList() { const ids = [].concat(...this.systems.map((system) => system.sidWhiteList)); return new Set(ids); } clearTokens() { this.semantics = null; this.tokens = null; this.systems.forEach((system) => (system.tokens = null)); } assignTexts(areas, [imageHeight, imageWidth]) { const interval = this.source && this.source.interval ? this.source.interval * (imageHeight / this.source.dimensions.height) : imageHeight / this.height; this.semantics = areas.map((area) => { const p = { x: (area.cx - imageWidth / 2) / interval, y: (area.cy - imageHeight / 2) / interval, }; const rp = this.source && this.source.matrix ? trans23(p, this.source.matrix) : p; return { confidence: area.score, x: rp.x + this.width / 2, y: rp.y + this.height / 2, semantic: SemanticType.rect_Text, extension: { text: area.text, type: area.type, width: area.width / interval, height: area.height / interval, theta: area.theta, textFeature: area.feature_dict, }, }; }); } assemble({ textAnnotations = null } = {}, logger = new DummyLogger()) { this.tokens = []; this.systems.forEach((system) => (system.tokens = [])); // compute system indent if (this.systems.length) { const sysXs = this.systems.map((system) => system.left); const middleX = sysXs[Math.floor((sysXs.length - 1) / 2)]; this.systems.forEach((system) => (system.indent = system.left > middleX + INDENT_THRESHOLD)); } if (this.semantics) { const pageName = this.source ? this.source.name : this.index.toString(); this.semantics.forEach((point) => { hashPageSemanticPoint(pageName, point); const fields = { id: point.id, type: TokenType.Text, confidence: point.confidence, textType: TEXT_TYPE_ALIAS[point.extension.type] || point.extension.type, text: (textAnnotations && textAnnotations[point.id]) || point.extension.text, textFeasure: point.extension.textFeature, width: point.extension.width, fontSize: point.extension.height, }; switch (point.semantic) { case SemanticType.rect_Text: switch (fields.textType) { // page tokens case TextType.Title: case TextType.Author: case TextType.PageMargin: case TextType.Other: this.tokens.push(new TextToken({ x: point.x, y: point.y, ...fields, })); break; // tokens on the top of system case TextType.TempoNumeral: case TextType.Chord: case TextType.MeasureNumber: case TextType.Instrument: case TextType.Alternation1: case TextType.Alternation2: { const system = this.systems.find((system) => system.top + system.staffTop > point.y); if (system) { system.appendToken(new TextToken({ x: point.x - system.left, y: point.y - system.top, ...fields, })); } } break; // tokens in staff case TextType.TextualMark: case TextType.Times: { const system = [...this.systems].reverse().find((system) => system.top < point.y); if (system) { const sy = point.y - (system.top + system.staffTop); const sx = point.x - system.left; const staff = system.staves.find((staff) => sy >= staff.top && sy < staff.top + staff.height); if (staff) { const measure = staff.measures.find((measure) => sx >= measure.left && sx < measure.left + measure.width); if (measure) { measure.tokens.push(new TextToken({ x: sx, y: sy, ...fields, })); } } } } break; } break; } }); } } } Page.className = 'Page'; Page.blackKeys = ['index', 'tokens']; var SemanticElementType; (function (SemanticElementType) { SemanticElementType[SemanticElementType["BOS"] = 0] = "BOS"; SemanticElementType[SemanticElementType["PAD"] = 1] = "PAD"; SemanticElementType[SemanticElementType["NoteheadS0"] = 2] = "NoteheadS0"; SemanticElementType[SemanticElementType["NoteheadS1"] = 3] = "NoteheadS1"; SemanticElementType[SemanticElementType["NoteheadS2"] = 4] = "NoteheadS2"; SemanticElementType[SemanticElementType["NoteheadGrace"] = 5] = "NoteheadGrace"; SemanticElementType[SemanticElementType["vline_Stem"] = 6] = "vline_Stem"; SemanticElementType[SemanticElementType["Flag3"] = 7] = "Flag3"; SemanticElementType[SemanticElementType["BeamLeft"] = 8] = "BeamLeft"; SemanticElementType[SemanticElementType["BeamContinue"] = 9] = "BeamContinue"; SemanticElementType[SemanticElementType["BeamRight"] = 10] = "BeamRight"; SemanticElementType[SemanticElementType["Dot"] = 11] = "Dot"; SemanticElementType[SemanticElementType["Rest0"] = 12] = "Rest0"; SemanticElementType[SemanticElementType["Rest1"] = 13] = "Rest1"; SemanticElementType[SemanticElementType["Rest2"] = 14] = "Rest2"; SemanticElementType[SemanticElementType["Rest3"] = 15] = "Rest3"; SemanticElementType[SemanticElementType["Rest4"] = 16] = "Rest4"; SemanticElementType[SemanticElementType["Rest5"] = 17] = "Rest5"; SemanticElementType[SemanticElementType["Rest6"] = 18] = "Rest6"; // measure time signature denominators & numerators SemanticElementType[SemanticElementType["TimeD2"] = 19] = "TimeD2"; SemanticElementType[SemanticElementType["TimeD4"] = 20] = "TimeD4"; SemanticElementType[SemanticElementType["TimeD8"] = 21] = "TimeD8"; SemanticElementType[SemanticElementType["TimeN1"] = 22] = "TimeN1"; SemanticElementType[SemanticElementType["TimeN2"] = 23] = "TimeN2"; SemanticElementType[SemanticElementType["TimeN3"] = 24] = "TimeN3"; SemanticElementType[SemanticElementType["TimeN4"] = 25] = "TimeN4"; SemanticElementType[SemanticElementType["TimeN5"] = 26] = "TimeN5"; SemanticElementType[SemanticElementType["TimeN6"] = 27] = "TimeN6"; SemanticElementType[SemanticElementType["TimeN7"] = 28] = "TimeN7"; SemanticElementType[SemanticElementType["TimeN8"] = 29] = "TimeN8"; SemanticElementType[SemanticElementType["TimeN9"] = 30] = "TimeN9"; SemanticElementType[SemanticElementType["TimeN10"] = 31] = "TimeN10"; SemanticElementType[SemanticElementType["TimeN11"] = 32] = "TimeN11"; SemanticElementType[SemanticElementType["TimeN12"] = 33] = "TimeN12"; })(SemanticElementType || (SemanticElementType = {})); const TIME_SIG_DENOMINATORS = Object.fromEntries([2, 4, 8].map((n) => [n, SemanticElementType[`TimeD${n}`]])); const TIME_SIG_NUMERATORS = Object.fromEntries(Array(12) .fill(null) .map((_, i) => i + 1) .map((n) => [n, SemanticElementType[`TimeN${n}`]])); const et = SemanticElementType; const ELEMENT_TOKEN_NAMES = { [et.BOS]: 'BOS', [et.NoteheadS0]: 'noteheads-s0', [et.NoteheadS1]: 'noteheads-s1', [et.NoteheadS2]: 'noteheads-s2', [et.NoteheadGrace]: 'GraceNotehead', [et.Flag3]: 'flags-u3', [et.BeamLeft]: 'BeamLeft', [et.BeamContinue]: 'BeamContinue', [et.BeamRight]: 'BeamRight', [et.Dot]: 'dot', [et.Rest0]: 'rests-0o', [et.Rest1]: 'rests-1o', [et.Rest2]: 'rests-2', [et.Rest3]: 'rests-3', [et.Rest4]: 'rests-4', [et.Rest5]: 'rests-5', [et.Rest6]: 'rests-6', }; const NOTEHEAD_BASE_DIVISION = { [et.NoteheadS0]: 0, [et.NoteheadS1]: 1, [et.NoteheadS2]: 2, [et.NoteheadGrace]: 2, }; const NOTEHEAD_ELEMENT_TYPES = [et.NoteheadS0, et.NoteheadS1, et.NoteheadS2, et.NoteheadGrace]; const REST_ELEMENT_TYPES = [et.Rest0, et.Rest1, et.Rest2, et.Rest3, et.Rest4, et.Rest5, et.Rest6]; const BEAM_ELEMENT_TYPES = [et.BeamLeft, et.BeamContinue, et.BeamRight]; const NOTE_ELEMENT_TYPES = [...NOTEHEAD_ELEMENT_TYPES, ...REST_ELEMENT_TYPES]; const SOURCE_ELEMENT_TYPES = [...NOTEHEAD_ELEMENT_TYPES, ...REST_ELEMENT_TYPES, et.vline_Stem]; const TARGET_ELEMENT_TYPES = [et.BOS, et.NoteheadS0, et.vline_Stem, ...REST_ELEMENT_TYPES]; const ROOT_NOTE_ELEMENT_TYPES = [...NOTE_ELEMENT_TYPES, et.vline_Stem]; const ELEMENT_TO_STEMBEAM = { [et.BeamLeft]: 'Open', [et.BeamRight]: 'Close', }; const metaElem = (type) => ({ type, staff: -1, x: 0, y1: 0, y2: 0, }); const BOS_ELEMENT = metaElem(SemanticElementType.BOS); const fractionToElems = (fraction) => [ metaElem(TIME_SIG_NUMERATORS[fraction.numerator]), metaElem(TIME_SIG_DENOMINATORS[fraction.denominator]), ]; const argmax = (data, mask) => { const values = data.filter((_, i) => mask[i]); const max = Math.max(...values); return data.findIndex((x) => x === max); }; class SemanticCluster extends SimpleClass { static elementToJSON(elem) { const result = { type: elem.type, staff: elem.staff, x: elem.x, y1: elem.y1, y2: elem.y2, }; if (elem.id) result.id = elem.id; return result; } constructor(data) { super(); super.assign(data); } get sourceMask() { return this.elements.map((elem) => SOURCE_ELEMENT_TYPES.includes(elem.type)); } get targetMask() { return this.elements.map((elem) => TARGET_ELEMENT_TYPES.includes(elem.type)); } get vMask() { return this.elements.map((elem) => ROOT_NOTE_ELEMENT_TYPES.includes(elem.type)); } get compactMatrixH() { if (!this.matrixH) return null; const sourceMask = this.sourceMask; const targetMask = this.targetMask; return this.matrixH.filter((_, i) => sourceMask[i]).map((row) => row.filter((_, j) => targetMask[j])); } set compactMatrixH(value) { this.matrixH = expandMatrixByMasks([].concat(...value), [this.sourceMask, this.targetMask]); } get compactMatrixV() { if (!this._matrixV) return null; const vMask = this.vMask; const matrix = this._matrixV.filter((_, i) => vMask[i]).map((row) => row.filter((_, j) => vMask[j])); return [].concat(...matrix.map((row, i) => row.slice(0, i))); } set compactMatrixV(value) { this.matrixV = value && expandMatrixByMaskTriu(value, this.vMask); } get matrixV() { return this.groupsV && matrixFromGroups(this.elements.length, this.groupsV); } set matrixV(value) { if (!value) { this.groupsV = null; this._matrixV = value; return; } const THRESHOLD = 0.5; const groups = []; const vMask = value.map((row, i) => row.some(Number.isFinite) || value.some((row) => Number.isFinite(row[i]))); value.forEach((row, i) => { if (vMask[i]) { let found = false; for (let j = 0; j < i; ++j) { const cell = row[j]; if (cell >= THRESHOLD) { const g = groups.findIndex((group) => group.includes(j)); groups[g].push(i); found = true; break; } } if (!found) groups.push([i]); } }); this.groupsV = groups; this._matrixV = value; } toJSON() { return { __prototype: 'SemanticCluster', index: this.index, elements: this.elements.map(SemanticCluster.elementToJSON), compactMatrixH: this.compactMatrixH, compactMatrixV: this.compactMatrixV, //groupsV: this.groupsV, }; } static mapMatrix(matrix, x2i, i2x) { const rows = x2i.reduce((rows, i, x) => { if (rows[i]) rows[i] = rows[i].map((v, xi) => (v + matrix[x][xi] ? 1 : 0)); else rows[i] = matrix[x]; return rows; }, []); return rows.map((row) => i2x.map((x) => row[x])); } mergeOverlapping() { const overlaps = this.overlappedNoteheads(); if (overlaps.length) { const x2i = this.elements.map((_, index) => { const pair = overlaps.find((ij) => index === ij[1]); const i = pair ? pair[0] : index; return i - overlaps.filter((ij) => ij[1] < i).length; }); const i2x = Array(this.elements.length - overlaps.length) .fill(null) .map((_, i) => x2i.findIndex((ii) => ii === i)); this.elements = i2x.map((x) => this.elements[x]); console.assert(this.elements.every(Boolean), 'null element found:', this, x2i, i2x); this.matrixH = SemanticCluster.mapMatrix(this.matrixH, x2i, i2x); this.groupsV = this.groupsV.map((group) => Array.from(new Set(group.map((x) => x2i[x])))); } } overlappedNoteheads() { const indices = []; const noteheads = this.elements.filter((elem) => NOTEHEAD_ELEMENT_TYPES.includes(elem.type)); for (let i = 0; i < noteheads.length; ++i) { const nh1 = noteheads[i]; for (let j = i + 1; j < noteheads.length; ++j) { const nh2 = noteheads[j]; if ((nh1.x - nh2.x) * (nh1.x - nh2.x) + (nh1.y1 - nh2.y1) * (nh1.y1 - nh2.y1) < 0.2 ** 2) indices.push([nh1.index, nh2.index]); } } return indices; } getEvents() { console.assert(this.matrixH, '[SemanticCluster.getEvents] matrixH is null.'); const NOTE_STEM_CONFIDENCE = 0.5; const ids = Array(this.elements.length) .fill(null) .map((_, index) => index); const targetMask = this.masks ? this.masks[1] : ids.map((id) => TARGET_ELEMENT_TYPES.includes(this.elements[id].type)); //const stems = ids.filter(i => this.elements[i].type === et.vline_Stem); const stemMasks = ids.map((id) => this.elements[id].type === et.vline_Stem && this.elements[id].y2 - this.elements[id].y1 > 2); // TODO: sift out too short stems by rectification model const stemNotes = ids.filter((i) => [et.NoteheadS1, et.NoteheadS2, et.NoteheadGrace].includes(this.elements[i].type)); const s0s = ids.filter((i) => this.elements[i].type === et.NoteheadS0); const subS0Masks = ids.map(() => false); // root elements: top NoteheadS0, Rests, stem with noteheads const stemMap = {}; stemNotes.forEach((id) => { const note = this.elements[id]; const stems = ids .filter((i) => stemMasks[i]) .filter((stemId) => this.elements[stemId].y1 - 0.5 < note.y1 && this.elements[stemId].y2 + 0.5 > note.y1) // filter by stem Y range .sort((i1, i2) => this.matrixH[id][i2] - this.matrixH[id][i1]) // sort by confidence .slice(0, 2) .filter((i, ii) => ii === 0 || this.matrixH[id][i] >= NOTE_STEM_CONFIDENCE); stems.forEach((stem) => { stemMap[stem] = stemMap[stem] || []; stemMap[stem].push(id); }); }); s0s.forEach((id) => { const s0 = this.elements[id]; const prevId = argmax(this.matrixH[id], targetMask); const prev = this.elements[prevId]; if (prev.type === et.NoteheadS0 && Math.abs(s0.x - prev.x) < 2.6) { subS0Masks[id] = true; stemMap[prevId] = stemMap[prevId] || [prevId]; stemMap[prevId].push(id); } else stemMap[id] = stemMap[id] || [id]; }); // setup linkings const linkings = {}; const roots = ids.filter((id) => stemMap[id] || REST_ELEMENT_TYPES.includes(this.elements[id].type)); roots.sort((i1, i2) => this.elements[i1].x - this.elements[i2].x); // traverse roots from left to right later const parentMasks = ids.map((id) => id === et.BOS); roots.forEach((id) => { const parentId = argmax(this.matrixH[id], parentMasks); linkings[id] = parentId; if (parentId && !REST_ELEMENT_TYPES.includes(this.elements[parentId].type)) parentMasks[parentId] = false; parentMasks[id] = true; }); //console.log("topology:", stemMap, linkings); const dots = this.elements.filter((elem) => elem.type === et.Dot); const flags = this.elements.filter((elem) => elem.type === et.Flag3); const beams = this.elements.filter((elem) => BEAM_ELEMENT_TYPES.includes(elem.type)); const groupsV = this.groupsV; return roots .map((rootId) => { const root = this.elements[rootId]; const tickGroup = groupsV ? groupsV.findIndex((group) => group.includes(rootId)) : null; if (REST_ELEMENT_TYPES.includes(root.type)) { const nearbyDots = dots.filter((dot) => dot.x > root.x + 0.5 && dot.x < root.x + 0.75 + 1.2 && dot.y1 > root.y1 - 1 && dot.y1 < root.y1); return { left: root.x - 0.75, right: root.x + 0.75, pivotX: root.x, rest: true, ys: [root.y1], noteIds: [root.id], dots: nearbyDots.length, division: root.type - et.Rest0, stemDirection: null, id: rootId, prevId: linkings[rootId], staff: root.staff, tickGroup, }; } else if (stemMap[rootId]) { const subNotes = stemMap[rootId].map((id) => this.elements[id]); const left = Math.min(...subNotes.map((n) => n.x - 0.7)); const right = Math.max(...subNotes.map((n) => n.x + 0.7)); subNotes.sort((n1, n2) => n2.y1 - n1.y1); const ys = subNotes.map((note) => note.y1); const noteIds = subNotes.map((note) => note.id); const top = ys[0]; const bottom = ys[ys.length - 1]; const nearbyDots = dots.filter((dot) => dot.x > right && dot.x < right + 1.2 && dot.y1 > top - 1 && dot.y1 < bottom + 0.5); const dotGroups = nearbyDots.reduce((groups, dot) => { const y = roundNumber(dot.y1, 0.5); groups[y] = groups[y] || []; groups[y].push(dot); return groups; }, {}); const dotValue = Math.max(...Object.values(dotGroups).map((group) => group.length), 0); let division = NOTEHEAD_BASE_DIVISION[subNotes[0].type]; let stemDirection = null; let beam = null; let tip = null; if (root.type === et.vline_Stem) { const topTip = top - root.y1; const bottomTip = root.y2 - bottom; stemDirection = topTip > bottomTip ? 'u' : 'd'; tip = { x: root.x, y: stemDirection === 'u' ? root.y1 : root.y2 }; if (division === 2) { const flagRange = stemDirection === 'u' ? [root.y1 - 0.4, root.y2 - 1] : [root.y1 + 1, root.y2 + 0.4]; const nearbyFlags = flags.filter((flag) => Math.abs(flag.x - root.x) < 0.2 && flag.y1 > flagRange[0] && flag.y1 < flagRange[1]); division += nearbyFlags.length; } //const tipY = stemDirection === "u" ? root.y1 : root.y2; const tipRange = stemDirection === 'u' ? [root.y1 - 0.2, root.y1 + 0.9] : [root.y2 - 0.9, root.y2 + 0.2]; const beamElem = beams.find((beam) => Math.abs(beam.x - root.x) < 0.2 && beam.y1 > tipRange[0] && beam.y1 < tipRange[1]); beam = beamElem ? ELEMENT_TO_STEMBEAM[beamElem.type] : null; } const grace = subNotes[0].type === SemanticElementType.NoteheadGrace ? GraceType.Grace : null; return { left, right, pivotX: root.x, ys, tip, noteIds, division, dots: dotValue, rest: false, stemDirection, beam, id: rootId, prevId: linkings[rootId], staff: subNotes[0].staff, grace, tickGroup, }; } }) .filter(Boolean); } } class SemanticClusterSet { constructor(data) { if (data) { this.clusters = data.clusters; // upgrade vocab if (data.vocab) { const converts = data.vocab .map((name, i) => [i, SemanticElementType[name]]) .filter(([x, y]) => x !== y) .reduce((table, [x, y]) => ((table[x] = y), table), {}); this.clusters.forEach((connection) => connection.elements.forEach((elem) => { if (Number.isFinite(converts[elem.type])) elem.type = converts[elem.type]; })); } } } toJSON() { const vocab = Object.entries(SemanticElementType) .filter((entry) => Number.isFinite(entry[1])) .map((entry) => entry[0]); return { __prototype: 'SemanticClusterSet', vocab, clusters: this.clusters.map((c) => c.toJSON()), }; } } const expandMatrixByMasks = (matrix, masks) => { const gen = function* () { for (const x of matrix) yield x; }; const iter = gen(); const [maskSrc, maskTar] = masks; return maskSrc.map((src) => maskTar.map((tar) => (src && tar ? iter.next().value : null))); }; const expandMatrixByMaskTriu = (matrix, mask) => { const gen = function* () { for (const x of matrix) yield x; }; const iter = gen(); return mask.map((row, i) => mask.map((column, j) => (row && column && j < i ? iter.next().value : null))); }; const matrixFromGroups = (len, groups) => { const groupIds = Array(len) .fill(null) .map((_, i) => groups.findIndex((group) => group.includes(i))); return Array(len) .fill(null) .map((_, i) => Array(len) .fill(null) .map((_, j) => { if (j >= i) return null; const id1 = groupIds[i]; const id2 = groupIds[j]; if (id1 < 0 || id2 < 0) return null; return id1 === id2 ? 1 : 0; })); }; // implicit note (from expressive marks) types var ImplicitType; (function (ImplicitType) { ImplicitType[ImplicitType["None"] = 0] = "None"; ImplicitType["Mordent"] = "mordent"; ImplicitType["Prall"] = "prall"; ImplicitType["Turn"] = "turn"; ImplicitType["Trill"] = "trill"; ImplicitType["Tremolo"] = "tremolo"; ImplicitType["Arpeggio"] = "arpeggio"; })(ImplicitType || (ImplicitType = {})); /* Wrapper for accessing buffer through sequential reads */ var stream = class Stream { constructor (buffer) { this.array = new Uint8Array(buffer); this.position = 0; } eof () { return this.position >= this.array.length; } read (length) { const result = this.array.slice(this.position, this.position + length); this.position += length; return result; } readString (length) { const data = Array.from(this.read(length)); return data.map(c => String.fromCharCode(c)).join(""); } // read a big-endian 32-bit integer readInt32 () { const result = ( (this.array[this.position] << 24) + (this.array[this.position + 1] << 16) + (this.array[this.position + 2] << 8) + this.array[this.position + 3]); this.position += 4; return result; } // read a big-endian 16-bit integer readInt16 () { const result = ( (this.array[this.position] << 8) + this.array[this.position + 1]); this.position += 2; return result; } // read an 8-bit integer readInt8 (signed) { let result = this.array[this.position]; if (signed && result > 127) result -= 256; this.position += 1; return result; } /* read a MIDI-style variable-length integer (big-endian value in groups of 7 bits, with top bit set to signify that another byte follows) */ readVarInt () { let result = 0; while (true) { const b = this.readInt8(); if (b & 0x80) { result += (b & 0x7f); result <<= 7; } else { // b is the last byte return result + b; } } } }; /* class to parse the .mid file format (depends on stream.js) */ const Stream = stream; var midifile = function MidiFile (data) { function readChunk (stream) { const id = stream.readString(4); const length = stream.readInt32(); return { id, length, data: stream.read(length), }; } let lastEventTypeByte; function readEvent (stream) { const event = {}; event.deltaTime = stream.readVarInt(); let eventTypeByte = stream.readInt8(); if ((eventTypeByte & 0xf0) === 0xf0) { // system / meta event if (eventTypeByte === 0xff) { // meta event event.type = "meta"; const subtypeByte = stream.readInt8(); const length = stream.readVarInt(); switch (subtypeByte) { case 0x00: event.subtype = "sequenceNumber"; if (length !== 2) throw new Error("Expected length for sequenceNumber event is 2, got " + length); event.number = stream.readInt16(); return event; case 0x01: event.subtype = "text"; event.text = stream.readString(length); return event; case 0x02: event.subtype = "copyrightNotice"; event.text = stream.readString(length); return event; case 0x03: event.subtype = "trackName"; event.text = stream.readString(length); return event; case 0x04: event.subtype = "instrumentName"; event.text = stream.readString(length); return event; case 0x05: event.subtype = "lyrics"; event.text = stream.readString(length); return event; case 0x06: event.subtype = "marker"; event.text = stream.readString(length); return event; case 0x07: event.subtype = "cuePoint"; event.text = stream.readString(length); return event; case 0x20: event.subtype = "midiChannelPrefix"; if (length !== 1) throw new Error("Expected length for midiChannelPrefix event is 1, got " + length); event.channel = stream.readInt8(); return event; case 0x2f: event.subtype = "endOfTrack"; if (length !== 0) throw new Error("Expected length for endOfTrack event is 0, got " + length); return event; case 0x51: event.subtype = "setTempo"; if (length !== 3) throw new Error("Expected length for setTempo event is 3, got " + length); event.microsecondsPerBeat = ( (stream.readInt8() << 16) + (stream.readInt8() << 8) + stream.readInt8() ); return event; case 0x54: event.subtype = "smpteOffset"; if (length !== 5) throw new Error("Expected length for smpteOffset event is 5, got " + length); const hourByte = stream.readInt8(); event.frameRate = { 0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30, }[hourByte & 0x60]; event.hour = hourByte & 0x1f; event.min = stream.readInt8(); event.sec = stream.readInt8(); event.frame = stream.readInt8(); event.subframe = stream.readInt8(); return event; case 0x58: event.subtype = "timeSignature"; if (length !== 4) throw new Error("Expected length for timeSignature event is 4, got " + length); event.numerator = stream.readInt8(); event.denominator = Math.pow(2, stream.readInt8()); event.metronome = stream.readInt8(); event.thirtyseconds = stream.readInt8(); return event; case 0x59: event.subtype = "keySignature"; if (length !== 2) throw new Error("Expected length for keySignature event is 2, got " + length); event.key = stream.readInt8(true); event.scale = stream.readInt8(); return event; case 0x7f: event.subtype = "sequencerSpecific"; event.data = stream.readString(length); return event; default: // console.log("Unrecognised meta event subtype: " + subtypeByte); event.subtype = "unknown"; event.data = stream.readString(length); return event; } //event.data = stream.readString(length); //return event; } else if (eventTypeByte === 0xf0) { event.type = "sysEx"; const length = stream.readVarInt(); event.data = stream.readString(length); return event; } else if (eventTypeByte === 0xf7) { event.type = "dividedSysEx"; const length = stream.readVarInt(); event.data = stream.readString(length); return event; } else throw new Error("Unrecognised MIDI event type byte: " + eventTypeByte); } else { /* channel event */ let param1; if ((eventTypeByte & 0x80) === 0) { /* running status - reuse lastEventTypeByte as the event type. eventTypeByte is actually the first parameter */ param1 = eventTypeByte; eventTypeByte = lastEventTypeByte; } else { param1 = stream.readInt8(); lastEventTypeByte = eventTypeByte; } const eventType = eventTypeByte >> 4; event.channel = eventTypeByte & 0x0f; event.type = "channel"; switch (eventType) { case 0x08: event.subtype = "noteOff"; event.noteNumber = param1; event.velocity = stream.readInt8(); return event; case 0x09: event.noteNumber = param1; event.velocity = stream.readInt8(); if (event.velocity === 0) event.subtype = "noteOff"; else event.subtype = "noteOn"; return event; case 0x0a: event.subtype = "noteAftertouch"; event.noteNumber = param1; event.amount = stream.readInt8(); return event; case 0x0b: event.subtype = "controller"; event.controllerType = param1; event.value = stream.readInt8(); return event; case 0x0c: event.subtype = "programChange"; event.programNumber = param1; return event; case 0x0d: event.subtype = "channelAftertouch"; event.amount = param1; return event; case 0x0e: event.subtype = "pitchBend"; event.value = param1 + (stream.readInt8() << 7); return event; default: throw new Error("Unrecognised MIDI event type: " + eventType); /* console.log("Unrecognised MIDI event type: " + eventType); stream.readInt8(); event.subtype = 'unknown'; return event; */ } } } let source = data; if (typeof data === "string") source = data.split("").map(c => c.charCodeAt(0)); const stream = new Stream(source); const headerChunk = readChunk(stream); if (headerChunk.id !== "MThd" || headerChunk.length !== 6) throw new Error("Bad .mid file - header not found"); const headerStream = new Stream(headerChunk.data); const formatType = headerStream.readInt16(); const trackCount = headerStream.readInt16(); const timeDivision = headerStream.readInt16(); let ticksPerBeat; if (timeDivision & 0x8000) throw new Error("Expressing time division in SMTPE frames is not supported yet"); else ticksPerBeat = timeDivision; const header = { formatType, trackCount, ticksPerBeat, }; const tracks = []; for (let i = 0; i < header.trackCount; i++) { tracks[i] = []; const trackChunk = readChunk(stream); if (trackChunk.id !== "MTrk") throw new Error("Unexpected chunk - expected MTrk, got " + trackChunk.id); const trackStream = new Stream(trackChunk.data); while (!trackStream.eof()) { const event = readEvent(trackStream); tracks[i].push(event); } } return { header, tracks, }; }; /* Wrapper for accessing strings through sequential writes */ var streamEx = class OStream { constructor () { this.buffer = ""; } write (str) { this.buffer += str; } /* write a big-endian 32-bit integer */ writeInt32 (i) { this.buffer += String.fromCharCode((i >> 24) & 0xff) + String.fromCharCode((i >> 16) & 0xff) + String.fromCharCode((i >> 8) & 0xff) + String.fromCharCode(i & 0xff); } /* write a big-endian 16-bit integer */ writeInt16 (i) { this.buffer += String.fromCharCode((i >> 8) & 0xff) + String.fromCharCode(i & 0xff); } /* write an 8-bit integer */ writeInt8 (i) { this.buffer += String.fromCharCode(i & 0xff); } /* write a MIDI-style variable-length integer (big-endian value in groups of 7 bits, with top bit set to signify that another byte follows) */ writeVarInt (i) { if (i < 0) throw new Error("OStream.writeVarInt minus number: " + i); const b = i & 0x7f; i >>= 7; let str = String.fromCharCode(b); while (i) { const b = i & 0x7f; i >>= 7; str = String.fromCharCode(b | 0x80) + str; } this.buffer += str; } getBuffer () { return this.buffer; } getArrayBuffer () { return Uint8Array.from(this.buffer.split("").map(c => c.charCodeAt(0))).buffer; } }; /* class to encode the .mid file format (depends on streamEx.js) */ const OStream = streamEx; var midifileEx = function OMidiFile ({ header, tracks }) { function writeChunk (stream, id, data) { console.assert(id.length === 4, "chunk id must be 4 byte"); stream.write(id); stream.writeInt32(data.length); stream.write(data); } function writeEvent (stream, event) { if (event.subtype === "unknown") return; stream.writeVarInt(event.deltaTime); switch (event.type) { case "meta": stream.writeInt8(0xff); switch (event.subtype) { case "sequenceNumber": stream.writeInt8(0x00); stream.writeVarInt(2); stream.writeInt16(event.number); break; case "text": stream.writeInt8(0x01); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "copyrightNotice": stream.writeInt8(0x02); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "trackName": stream.writeInt8(0x03); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "instrumentName": stream.writeInt8(0x04); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "lyrics": stream.writeInt8(0x05); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "marker": stream.writeInt8(0x06); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "cuePoint": stream.writeInt8(0x07); stream.writeVarInt(event.text.length); stream.write(event.text); break; case "midiChannelPrefix": stream.writeInt8(0x20); stream.writeVarInt(1); stream.writeInt8(event.channel); break; case "endOfTrack": stream.writeInt8(0x2f); stream.writeVarInt(0); break; case "setTempo": stream.writeInt8(0x51); stream.writeVarInt(3); stream.writeInt8((event.microsecondsPerBeat >> 16) & 0xff); stream.writeInt8((event.microsecondsPerBeat >> 8) & 0xff); stream.writeInt8(event.microsecondsPerBeat & 0xff); break; case "smpteOffset": stream.writeInt8(0x54); stream.writeVarInt(5); var frameByte = { 24: 0x00, 25: 0x20, 29: 0x40, 30: 0x60 }[event.frameRate]; stream.writeInt8(event.hour | frameByte); stream.writeInt8(event.min); stream.writeInt8(event.sec); stream.writeInt8(event.frame); stream.writeInt8(event.subframe); break; case "timeSignature": stream.writeInt8(0x58); stream.writeVarInt(4); stream.writeInt8(event.numerator); stream.writeInt8(Math.log2(event.denominator)); stream.writeInt8(event.metronome); stream.writeInt8(event.thirtyseconds); break; case "keySignature": stream.writeInt8(0x59); stream.writeVarInt(2); stream.writeInt8(event.key); stream.writeInt8(event.scale); break; case "sequencerSpecific": stream.writeInt8(0x7f); stream.writeVarInt(event.data.length); stream.write(event.data); break; default: throw new Error("unhandled event subtype:" + event.subtype); } break; case "sysEx": stream.writeInt8(0xf0); stream.writeVarInt(event.data.length); stream.write(event.data); break; case "dividedSysEx": stream.writeInt8(0xf7); stream.writeVarInt(event.data.length); stream.write(event.data); break; case "channel": switch (event.subtype) { case "noteOn": stream.writeInt8(0x90 | event.channel); stream.writeInt8(event.noteNumber); stream.writeInt8(event.velocity); break; case "noteOff": stream.writeInt8(0x80 | event.channel); stream.writeInt8(event.noteNumber); stream.writeInt8(event.velocity ? event.velocity : 0); break; case "noteAftertouch": stream.writeInt8(0xa0 | event.channel); stream.writeInt8(event.noteNumber); stream.writeInt8(event.amount); break; case "controller": stream.writeInt8(0xb0 | event.channel); stream.writeInt8(event.controllerType); stream.writeInt8(event.value); break; case "programChange": stream.writeInt8(0xc0 | event.channel); stream.writeInt8(event.programNumber); break; case "channelAftertouch": stream.writeInt8(0xd0 | event.channel); stream.writeInt8(event.amount); break; case "pitchBend": stream.writeInt8(0xe0 | event.channel); stream.writeInt8(event.value & 0xff); stream.writeInt8((event.value >> 7) & 0xff); break; default: throw new Error("unhandled event subtype:" + event.subtype); } break; default: throw new Error("unhandled event type:" + event.type); } } const stream = new OStream(); const headerChunk = new OStream(); headerChunk.writeInt16(header.formatType); headerChunk.writeInt16(tracks.length); headerChunk.writeInt16(header.ticksPerBeat); writeChunk(stream, "MThd", headerChunk.getBuffer()); for (let i = 0; i < tracks.length; ++i) { const trackChunk = new OStream(); for (let ei = 0; ei < tracks[i].length; ++ei) writeEvent(trackChunk, tracks[i][ei]); writeChunk(stream, "MTrk", trackChunk.getBuffer()); } return stream.getArrayBuffer(); }; var MIDI$2 = { parseMidiData: midifile, encodeMidiFile: midifileEx, }; const midiToSequence = (midiFile, {timeWarp = 1} = {}) => { const trackStates = []; let beatsPerMinute = 120; const ticksPerBeat = midiFile.header.ticksPerBeat; for (let i = 0; i < midiFile.tracks.length; i++) { trackStates[i] = { nextEventIndex: 0, ticksToNextEvent: ( midiFile.tracks[i].length ? midiFile.tracks[i][0].deltaTime : null ), }; } function getNextEvent () { let ticksToNextEvent = null; let nextEventTrack = null; let nextEventIndex = null; for (let i = 0; i < trackStates.length; i++) { if ( trackStates[i].ticksToNextEvent != null && (ticksToNextEvent == null || trackStates[i].ticksToNextEvent < ticksToNextEvent) ) { ticksToNextEvent = trackStates[i].ticksToNextEvent; nextEventTrack = i; nextEventIndex = trackStates[i].nextEventIndex; } } if (nextEventTrack != null) { /* consume event from that track */ const nextEvent = midiFile.tracks[nextEventTrack][nextEventIndex]; if (midiFile.tracks[nextEventTrack][nextEventIndex + 1]) trackStates[nextEventTrack].ticksToNextEvent += midiFile.tracks[nextEventTrack][nextEventIndex + 1].deltaTime; else trackStates[nextEventTrack].ticksToNextEvent = null; trackStates[nextEventTrack].nextEventIndex += 1; /* advance timings on all tracks by ticksToNextEvent */ for (let i = 0; i < trackStates.length; i++) { if (trackStates[i].ticksToNextEvent != null) trackStates[i].ticksToNextEvent -= ticksToNextEvent; } return { ticksToEvent: ticksToNextEvent, event: nextEvent, track: nextEventTrack, }; } else return null; } // let midiEvent; const events = []; // function processEvents () { function processNext () { let secondsToGenerate = 0; if (midiEvent.ticksToEvent > 0) { const beatsToGenerate = midiEvent.ticksToEvent / ticksPerBeat; secondsToGenerate = beatsToGenerate / (beatsPerMinute / 60); } // beatsPerMinute must be changed after secondsToGenerate calculation if ( midiEvent.event.type == "meta" && midiEvent.event.subtype == "setTempo" ) { // tempo change events can occur anywhere in the middle and affect events that follow beatsPerMinute = 60e+6 / midiEvent.event.microsecondsPerBeat; } const time = (secondsToGenerate * 1000 * timeWarp) || 0; events.push([ midiEvent, time ]); midiEvent = getNextEvent(); } // if (midiEvent = getNextEvent()) { while (midiEvent) processNext(); } } processEvents(); return events; }; const trimSequence = seq => { const status = new Map(); return seq.filter(([{event, ticksToEvent}]) => { if (ticksToEvent > 0) status.clear(); if (event.type !== "channel") return true; const key = `${event.subtype}|${event.channel}|${event.noteNumber}`; if (status.get(key)) { //console.debug("event trimmed:", event, ticksToEvent); return false; } status.set(key, event); return true; }); }; const fixOverlapNotes = seq => { const noteMap = new Map(); const overlapMap = new Map(); const swaps = []; let leapIndex = -1; seq.forEach(([{event, ticksToEvent}], index) => { if (ticksToEvent > 0) leapIndex = index; if (event.type !== "channel") return; const key = `${event.channel}|${event.noteNumber}`; switch (event.subtype) { case "noteOn": if (noteMap.get(key)) overlapMap.set(key, leapIndex); else noteMap.set(key, leapIndex); break; case "noteOff": if (overlapMap.get(key)) { swaps.push([overlapMap.get(key), index]); overlapMap.delete(key); } else noteMap.delete(key); break; } }); // shift overlapped swaps swaps.forEach((swap, i) => { for (let ii = i - 1; ii >= 0; --ii) { const pre = swaps[ii]; if (pre[1] < swap[0]) break; if (swap[0] > pre[0]) ++swap[0]; } }); //console.debug("swaps:", swaps); swaps.forEach(([front, back]) => { if (back >= seq.length - 1 || front < 0) return; const offEvent = seq[back]; const nextEvent = seq[back + 1]; const leapEvent = seq[front]; if (!leapEvent[0].ticksToEvent) { console.warn("invalid front index:", front, back, leapEvent); return; } // ms per tick const tempo = leapEvent[1] / leapEvent[0].ticksToEvent; nextEvent[1] += offEvent[1]; nextEvent[0].ticksToEvent += offEvent[0].ticksToEvent; offEvent[0].ticksToEvent = leapEvent[0].ticksToEvent - 1; leapEvent[0].ticksToEvent = 1; offEvent[1] = offEvent[0].ticksToEvent * tempo; leapEvent[1] = leapEvent[0].ticksToEvent * tempo; //console.debug("swap:", [front, back], offEvent, nextEvent, leapEvent); seq.splice(back, 1); seq.splice(front, 0, offEvent); }); return seq; }; var MidiSequence$1 = { midiToSequence, trimSequence, fixOverlapNotes, }; const MidiSequence = MidiSequence$1; const PedalControllerTypes = { 64: "Sustain", 65: "Portamento", 66: "Sostenuto", 67: "Soft", }; class Notation$1 { static parseMidi (data, {fixOverlap = true} = {}) { const channelStatus = []; const pedalStatus = {}; const pedals = {}; const channels = []; const bars = []; let time = 0; let millisecondsPerBeat = 600000 / 120; let beats = 0; let numerator = 4; let barIndex = 0; const keyRange = {}; let rawTicks = 0; let ticks = 0; let correspondences; const tempos = []; const ticksPerBeat = data.header.ticksPerBeat; let rawEvents = MidiSequence.midiToSequence(data); if (fixOverlap) rawEvents = MidiSequence.trimSequence(MidiSequence.fixOverlapNotes(rawEvents)); const events = rawEvents.map(d => ({ data: d[0].event, track: d[0].track, deltaTime: d[1], deltaTicks: d[0].ticksToEvent, })); let index = 0; const ticksNormal = 1; for (const ev of events) { rawTicks += ev.deltaTicks; ticks = Math.round(rawTicks * ticksNormal); if (ev.deltaTicks > 0) { // append bars const deltaBeats = ev.deltaTicks / ticksPerBeat; for (let b = Math.ceil(beats); b < beats + deltaBeats; ++b) { const t = time + (b - beats) * millisecondsPerBeat; bars.push({time: t, index: barIndex % numerator}); ++barIndex; } beats += deltaBeats; } time += ev.deltaTime; //const ticksTime = beats * millisecondsPerBeat; //console.log("time:", time, ticksTime, ticksTime - time); ev.time = time; ev.ticks = ticks; const event = ev.data; switch (event.type) { case "channel": //channelStatus[event.channel] = channelStatus[event.channel] || []; switch (event.subtype) { case "noteOn": { const pitch = event.noteNumber; //channelStatus[event.channel][pitch] = { channelStatus.push({ channel: event.channel, pitch, startTick: ticks, start: time, velocity: event.velocity, beats: beats, track: ev.track, }); keyRange.low = Math.min(keyRange.low || pitch, pitch); ev.index = index; ++index; } break; case "noteOff": { const pitch = event.noteNumber; channels[event.channel] = channels[event.channel] || []; const statusIndex = channelStatus.findIndex(status => status.channel == event.channel && status.pitch == pitch); if (statusIndex >= 0) { const status = channelStatus.splice(statusIndex, 1)[0]; channels[event.channel].push({ channel: event.channel, startTick: status.startTick, endTick: ticks, pitch, start: status.start, duration: time - status.start, velocity: status.velocity, beats: status.beats, track: status.track, finger: status.finger, }); } else console.debug("unexpected noteOff: ", time, event); keyRange.high = Math.max(keyRange.high || pitch, pitch); } break; case "controller": switch (event.controllerType) { // pedal controllers case 64: case 65: case 66: case 67: const pedalType = PedalControllerTypes[event.controllerType]; pedalStatus[event.channel] = pedalStatus[event.channel] || {}; pedals[event.channel] = pedals[event.channel] || []; const status = pedalStatus[event.channel][pedalType]; if (status) pedals[event.channel].push({type: pedalType, start: status.start, duration: time - status.start, value: status.value}); pedalStatus[event.channel][pedalType] = {start: time, value: event.value}; break; } break; } break; case "meta": switch (event.subtype) { case "setTempo": millisecondsPerBeat = event.microsecondsPerBeat / 1000; //beats = Math.round(beats); //console.assert(Number.isFinite(time), "invalid time:", time); tempos.push({tempo: event.microsecondsPerBeat, tick: ticks, time}); break; case "timeSignature": numerator = event.numerator; barIndex = 0; break; case "text": if (!correspondences && /^find-corres:/.test(event.text)) { const captures = event.text.match(/:([\d\,-]+)/); const str = captures && captures[1] || ""; correspondences = str.split(",").map(s => Number(s)); } else if (/fingering\(.*\)/.test(event.text)) { const [_, fingers] = event.text.match(/\((.+)\)/); const finger = Number(fingers); if (!Number.isNaN(finger)) { const status = channelStatus[channelStatus.length - 1]; if (status) status.finger = finger; const event = events.find(e => e.index == index - 1); if (event) event.data.finger = finger; } } break; case "copyrightNotice": console.log("MIDI copyright:", event.text); break; } break; } } channelStatus.forEach(status => { console.debug("unclosed noteOn event at", status.startTick, status); channels[status.channel].push({ startTick: status.startTick, endTick: ticks, pitch: status.pitch, start: status.start, duration: time - status.start, velocity: status.velocity, beats: status.beats, track: status.track, finger: status.finger, }); }); return new Notation$1({ channels, keyRange, pedals, bars, endTime: time, endTick: ticks, correspondences, events, tempos, ticksPerBeat, meta: {}, }); } constructor (fields) { Object.assign(this, fields); // channels to notes this.notes = []; for (const channel of this.channels) { if (channel) { for (const note of channel) this.notes.push(note); } } this.notes.sort(function (n1, n2) { return n1.start - n2.start; }); for (const i in this.notes) this.notes[i].index = Number(i); // duration this.duration = this.notes.length > 0 ? (this.endTime - this.notes[0].start) : 0, //this.endSoftIndex = this.notes.length ? this.notes[this.notes.length - 1].softIndex : 0; // pitch map this.pitchMap = []; for (const c in this.channels) { for (const n in this.channels[c]) { const pitch = this.channels[c][n].pitch; this.pitchMap[pitch] = this.pitchMap[pitch] || []; this.pitchMap[pitch].push(this.channels[c][n]); } } this.pitchMap.forEach(notes => notes.sort((n1, n2) => n1.start - n2.start)); /*// setup measure notes index if (this.measures) { const measure_list = []; let last_measure = null; const measure_entries = Object.entries(this.measures).sort((e1, e2) => Number(e1[0]) - Number(e2[0])); for (const [t, measure] of measure_entries) { //console.log("measure time:", Number(t)); measure.startTick = Number(t); measure.notes = []; if (last_measure) last_measure.endTick = measure.startTick; const m = measure.measure; measure_list[m] = measure_list[m] || []; measure_list[m].push(measure); last_measure = measure; } if (last_measure) last_measure.endTick = this.notes[this.notes.length - 1].endTick; for (const i in this.notes) { const note = this.notes[i]; for (const t in this.measures) { const measure = this.measures[t]; if (note.startTick >= measure.startTick && note.startTick < measure.endTick || note.endTick > measure.startTick && note.endTick <= measure.endTick) measure.notes.push(note); } } this.measure_list = measure_list; }*/ // prepare beats info if (this.meta.beatInfos) { for (let i = 0; i < this.meta.beatInfos.length; ++i) { const info = this.meta.beatInfos[i]; if (i > 0) { const lastInfo = this.meta.beatInfos[i - 1]; info.beatIndex = lastInfo.beatIndex + Math.ceil((info.tick - lastInfo.tick) / this.ticksPerBeat); } else info.beatIndex = 0; } } // compute tempos tick -> time { let time = 0; let ticks = 0; let tempo = 500000; for (const entry of this.tempos) { const deltaTicks = entry.tick - ticks; time += (tempo / 1000) * deltaTicks / this.ticksPerBeat; ticks = entry.tick; tempo = entry.tempo; entry.time = time; } } } findChordBySoftindex (softIndex, radius = 0.8) { return this.notes.filter(note => Math.abs(note.softIndex - softIndex) < radius); } averageTempo (tickRange) { tickRange = tickRange || {from: 0, to: this.endtick}; console.assert(this.tempos, "no tempos."); console.assert(tickRange.to > tickRange.from, "range is invalid:", tickRange); const span = index => { const from = Math.max(tickRange.from, this.tempos[index].tick); const to = (index < this.tempos.length - 1) ? Math.min(this.tempos[index + 1].tick, tickRange.to) : tickRange.to; return Math.max(0, to - from); }; const tempo_sum = this.tempos.reduce((sum, tempo, index) => sum + tempo.tempo * span(index), 0); const average = tempo_sum / (tickRange.to - tickRange.from); // convert microseconds per beat to beats per minute return 60e+6 / average; } ticksToTime (tick) { console.assert(Number.isFinite(tick), "invalid tick value:", tick); console.assert(this.tempos && this.tempos.length, "no tempos."); const next_tempo_index = this.tempos.findIndex(tempo => tempo.tick > tick); const tempo_index = next_tempo_index < 0 ? this.tempos.length - 1 : Math.max(next_tempo_index - 1, 0); const tempo = this.tempos[tempo_index]; return tempo.time + (tick - tempo.tick) * tempo.tempo * 1e-3 / this.ticksPerBeat; } timeToTicks (time) { console.assert(Number.isFinite(time), "invalid time value:", time); console.assert(this.tempos && this.tempos.length, "no tempos."); const next_tempo_index = this.tempos.findIndex(tempo => tempo.time > time); const tempo_index = next_tempo_index < 0 ? this.tempos.length - 1 : Math.max(next_tempo_index - 1, 0); const tempo = this.tempos[tempo_index]; return tempo.tick + (time - tempo.time) * this.ticksPerBeat / (tempo.tempo * 1e-3); } tickRangeToTimeRange (tickRange) { console.assert(tickRange.to >= tickRange.from, "invalid tick range:", tickRange); return { from: this.ticksToTime(tickRange.from), to: this.ticksToTime(tickRange.to), }; } /*getMeasureRange (measureRange) { console.assert(Number.isInteger(measureRange.start) && Number.isInteger(measureRange.end), "invalid measure range:", measureRange); console.assert(this.measure_list && this.measure_list[measureRange.start] && this.measure_list[measureRange.end], "no measure data for specific index:", this.measure_list, measureRange); const startMeasure = this.measure_list[measureRange.start][0]; let endMeasure = null; for (const measure of this.measure_list[measureRange.end]) { if (measure.endTick > startMeasure.startTick) { endMeasure = measure; break; } } // there no path between start measure and end measure. if (!endMeasure) return null; const tickRange = {from: startMeasure.startTick, to: endMeasure.endTick, duration: endMeasure.endTick - startMeasure.startTick}; const timeRange = this.tickRangeToTimeRange(tickRange); timeRange.duration = timeRange.to - timeRange.from; return { tickRange, timeRange, }; }*/ scaleTempo ({factor, headTempo}) { console.assert(this.tempos && this.tempos.length, "[Notation.scaleTempo] tempos is empty."); if (headTempo) factor = headTempo / this.tempos[0].tempo; console.assert(Number.isFinite(factor) && factor > 0, "[Notation.scaleTempo] invalid factor:", factor); this.tempos.forEach(tempo => { tempo.tempo *= factor; tempo.time *= factor; }); this.events.forEach(event => { event.deltaTime *= factor; event.time *= factor; }); this.notes.forEach(note => { note.start *= factor; note.duration *= factor; }); this.endTime *= factor; } } var MusicNotation$1 = { Notation: Notation$1, }; const { Notation } = MusicNotation$1; //const msDelay = ms => new Promise(resolve => setTimeout(resolve, ms)); const animationDelay = () => new Promise(resolve => requestAnimationFrame(resolve)); class MidiPlayer$1 { constructor (midiData, {cacheSpan = 600, onMidi, onPlayFinish, onTurnCursor} = {}) { this.cacheSpan = cacheSpan; this.onMidi = onMidi; this.onPlayFinish = onPlayFinish; this.onTurnCursor = onTurnCursor; let notation; if (midiData.notes && Number.isFinite(midiData.endTime)) notation = midiData; else notation = Notation.parseMidi(midiData); this.notation = notation; this.events = notation.events; //console.log("events:", this.events); this.isPlaying = false; this.progressTime = 0; this.startTime = performance.now(); this.duration = notation.endTime; this.cursorTurnDelta = 0; console.assert(notation.tempos && notation.tempos.length, "[MidiPlayer] invalid notation, tempos is empty."); } dispose () { this.isPlaying = false; this.progressTime = 0; } get progressTicks () { return this.notation.timeToTicks(this.progressTime); } set progressTicks (value) { this.progressTime = this.notation.ticksToTime(value); if (this.onTurnCursor) this.onTurnCursor(this.progressTime); } async play ({nextFrame = animationDelay} = {}) { if (this.progressTime >= this.duration) this.progressTime = 0; let now = performance.now(); this.startTime = now - this.progressTime; this.isPlaying = true; let currentEventIndex = this.events.findIndex(event => event.time >= now - this.startTime); while (this.isPlaying) { for (; currentEventIndex < this.events.length; ++currentEventIndex) { const event = this.events[currentEventIndex]; //console.log("play event:", currentEventIndex, event.time, this.progressTime + this.cacheSpan); if (!event || event.time > this.progressTime + this.cacheSpan) break; if (event.data.type === "channel" && this.startTime + event.time >= now) if (this.onMidi) this.onMidi(event.data, this.startTime + event.time); } await nextFrame(); if (!this.isPlaying) break; if (this.cursorTurnDelta !== 0) { const backturn = this.cursorTurnDelta < 0; this.startTime -= this.cursorTurnDelta; this.cursorTurnDelta = 0; if (backturn) { for (; currentEventIndex > 0; --currentEventIndex) { const eventTime = this.events[currentEventIndex].time; if (this.startTime + eventTime < now) break; } } } now = performance.now(); this.progressTime = now - this.startTime; if (this.progressTime > this.duration) { this.isPlaying = false; if (this.onPlayFinish) this.onPlayFinish(); } } } pause () { this.isPlaying = false; } turnCursor (time) { //console.log("onTurnCursor:", time, oldTime); if (this.isPlaying) this.cursorTurnDelta += time - this.progressTime; else this.progressTime = time; if (this.onTurnCursor) this.onTurnCursor(time); } } var MidiPlayer_1 = MidiPlayer$1; var config = { CostStepAttenuation: 0.6, SkipDeep: 3, PriorDistanceSigmoidFactor: 0.1, PriorValueSigmoidFactor: 0.12, SkipCost: 0.5, LagOffsetCost: 1, LeadOffsetCost: 1.6, ZeroOffsetCost: 0.58, RelocationThreshold: 6, }; const {pick} = require$$0__default["default"]; const Config$1 = config; class Node$2 { constructor (s_note, c_note) { this.s_note = s_note; this.c_note = c_note; console.assert(this.s_note.softIndex != null, "s_note softIndex is null"); this.offset = this.s_note.softIndex - this.c_note.softIndex; this._prev = null; this._totalCost = 0; this._value = 0; this.cacheDirty = true; //this.evaluatePrev(Node.Zero); } get prev () { return this._prev; } set prev (value) { if (value != this._prev) { this._prev = value; this.cacheDirty = true; } } get si () { return this.s_note.index; } get ci () { return this.c_note.index; } get root () { return this.prev.root || this; } get rootSi () { return !this.prev.zero ? this.prev.rootSi : this.si; } get id () { return `${this.s_note.index},${this.c_note.index}`; } static cost (prev, skip, self) { return prev * Config$1.CostStepAttenuation + Math.tanh(skip * Config$1.SkipCost) + Math.tanh(self * 0.5); } updateCache () { if (this.cacheDirty) { this._totalCost = Node$2.cost(this.prev.totalCost, this.si - this.prev.si - 1, this.selfCost); this._value = this.prev.value + 1 - Math.tanh(this.selfCost * 0.5); this.cacheDirty = false; } } get totalCost () { this.updateCache(); return this._totalCost; } get value () { this.updateCache(); return this._value; } get deep () { return this.prev.deep + 1; } get path () { const path = []; for (let node = this; !node.zero; node = node.prev) { path[node.si] = node.ci; } for (let i = 0; i < path.length; ++i) if (typeof path[i] != "number") path[i] = -1; return path; } dump () { return pick(this, ["id", "si", "ci", "rootSi", "value", "deep", "rootSi", "offset", "prior", "selfCost", "totalCost"]); } evaluatePrev (node) { const cost = this.evaluatePrevCost(node); console.assert(this.si - node.si >= 1, "node index error:", this, node/*, {get [Symbol.toStringTag]() {debugger}}*/); //if (this.si - node.si < 1) // debugger; const totalCost = Node$2.cost(node.totalCost, this.si - node.si - 1, cost); if (!this.prev || totalCost < this.totalCost) { this.prev = node; this.selfCost = cost; return true; } return false; } evaluatePrevCost (node) { let cost = 0; if (node.offset != null) { const bias = this.offset - node.offset; const costCoeff = node.zero ? Config$1.ZeroOffsetCost : (bias > 0 ? Config$1.LagOffsetCost : Config$1.LeadOffsetCost); cost += (bias * costCoeff) ** 2; } return cost; } priorByOffset (offset) { const distance = Math.abs(this.offset - offset) / 1;//(this.s_note.deltaSi + 0.04); return Math.tanh(this.value * Config$1.PriorValueSigmoidFactor) - Math.tanh(distance * Config$1.PriorDistanceSigmoidFactor); //return Math.log(this.value) * Math.tanh(4 / distance); //return this.value - distance; } static zero () { return { zero: true, totalCost: 0, value: 0, si: -1, ci: -1, deep: 0, offset: 0, }; } } var node = Node$2; const Config = config; const Node$1 = node; class Navigator$1 { constructor (criterion, sample, options = {}) { this.criterion = criterion; this.sample = sample; this.getCursorOffset = options.getCursorOffset || (() => null); this.outOfPage = options.outOfPage; this.bestNode = null; this.fineCursor = null; this.breakingSI = sample.notes.length - 1; this.zeroNode = Node$1.zero(); this.zeroNode.offset = this.getCursorOffset() || 0; this.relocationThreshold = options.relocationThreshold || Config.RelocationThreshold; } step (index) { //console.log("step:", this.zeroNode.offset); const note = this.sample.notes[index]; if (note.matches.length > 0) { //console.log("zeroNode.offset:", index, this.zeroNode.offset); note.matches.forEach(node => { node.evaluatePrev(this.zeroNode); //console.log("node:", node, node.evaluatePrevCost(this.zeroNode), node.offset, this.zeroNode.offset); for (let si = index - 1; si >= Math.max(this.breakingSI + 1, index - Config.SkipDeep); --si) { //const skipCost = Config.SkipCost * (index - 1 - si); const prevNote = this.sample.notes[si]; console.assert(prevNote, "prevNote is null:", si, index, this.sample.notes); prevNote.matches.forEach(prevNode => { const bias = node.offset - prevNode.offset; if (/*prevNode.totalCost + skipCost < node.totalCost &&*/ (bias < 2 / Config.LagOffsetCost && bias > -2 / Config.LeadOffsetCost)) node.evaluatePrev(prevNode); }); } node.prior = node.totalCost > 1.99 ? -1 : node.priorByOffset(this.zeroNode.offset); if (node.prior > 0 && this.outOfPage) { const tick = this.criterion.notes[node.ci].startTick; if (this.outOfPage(tick)) node.prior -= 0.7; } }); note.matches.sort((c1, c2) => c2.prior - c1.prior); this.cursors = note.matches; //console.log("navigator cursors:", this.cursors); let fineCursor = null; const nullLength = this.nullSteps(index); const cursor = this.cursors[0]; if (cursor && cursor.totalCost < 1) { //console.log("nullLength:", nullLength, nullLength * Math.log(cursor.value / 4)); if (cursor.prior > 0 || (cursor.totalCost < 0.4 && Math.log(Math.max(nullLength * cursor.value, 1e-3)) > this.relocationThreshold)) { this.zeroNode.offset = cursor.offset; fineCursor = cursor; if (!this.bestNode || cursor.value > this.bestNode.value) this.bestNode = cursor; } } if (fineCursor) this.fineCursor = fineCursor; else { if (!this.resetCursor(index, {breaking: false/*nullLength > Config.SkipDeep*/})) { this.zeroNode.offset += note.deltaSi * Math.tanh(nullLength); console.assert(!Number.isNaN(this.zeroNode.offset), "zeroNode.offset is NaN.", note.deltaSi, nullLength); } } } else this.cursors = []; } path ({fromIndex = 0, toIndex = this.sample.notes.length - 1} = {}) { const path = []; let offset = null; for (let si = toIndex; si >= fromIndex;) { const note = this.sample.notes[si]; if (!note.matches.length || note.matches[0].prior < -0.01 || note.matches[0].totalCost >= 1) { //if (note.matches.length) // console.log("path -1:", si, note.matches[0].prior, note.matches[0].totalCost); path[si] = -1; --si; continue; } // sort nodes by backwards heuristic offset if (offset != null) { note.matches.forEach(node => node.backPrior = (node.totalCost < 1.99 ? node.priorByOffset(offset) : -1)); note.matches.sort((n1, n2) => n2.backPrior - n1.backPrior); } const node = note.matches[0]; node.path.forEach((ci, si) => path[si] = ci); //console.log("node path:", si, node.path); offset = node.root.offset; si = node.rootSi - 1; } console.assert(path.length == toIndex + 1, "path length error:", path, fromIndex, toIndex + 1, this.sample.notes.length, this.sample.notes.length ? this.sample.notes[this.sample.notes.length - 1].index : null); return path; } nullSteps (index) { return index - (this.fineCursor ? this.fineCursor.si : -1) - 1; } resetCursor (index, {breaking = true} = {}) { if (breaking) this.breakingSI = index; const cursorOffset = this.getCursorOffset(); if (cursorOffset != null) { //console.log("cursorOffset:", cursorOffset); this.zeroNode.offset = cursorOffset; //this.breaking = this.nullSteps(index) > Config.SkipDeep; //if (this.breaking) // trivial zero node si resets result in focus path interruption this.zeroNode.si = index; this.fineCursor = null; console.assert(!Number.isNaN(this.zeroNode.offset), "zeroNode.offset is NaN.", cursorOffset); //console.log("cursor offset reset:", cursorOffset); return true; } return false; } get relocationTendency () { const cursor = this.cursors && this.cursors[0]; if (!cursor) return null; const nullLength = this.nullSteps(cursor.si); if (nullLength <= 0) return 0; return Math.log(Math.max(nullLength * cursor.value, 1e-3)) / this.relocationThreshold; } } var navigator = Navigator$1; const Node = node; const Navigator = navigator; const HEART_BEAT = 800; // in ms const SIMULTANEOUS_INTERVAL = HEART_BEAT * 0.24; const normalizeInterval = interval => Math.tanh(interval / SIMULTANEOUS_INTERVAL); // greater softIndexFactor make 'harder' soft index const makeNoteSoftIndex = function (notes, index, {softIndexFactor = 1} = {}) { index = Number(index); const note = notes[index]; // make soft index if (index > 0) { const lastNote = notes[index - 1]; console.assert(note.start != null, "note.start is null", note); console.assert(lastNote.start != null, "lastNote.start is null", lastNote); note.deltaSi = normalizeInterval((note.start - lastNote.start) * softIndexFactor); note.softIndex = lastNote.softIndex + note.deltaSi; console.assert(!Number.isNaN(note.deltaSi), "note.deltaSi is NaN.", note.start, lastNote.start); } else { note.softIndex = 0; note.deltaSi = 0; } }; const makeMatchNodes = function (note, criterion, zeroNode = Node.zero()) { note.matches = []; const targetList = criterion.pitchMap[note.pitch]; if (targetList) { for (const targetNote of targetList) { const node = new Node(note, targetNote); if (zeroNode) node.evaluatePrev(zeroNode); note.matches.push(node); } } }; const genNotationContext = function (notation, {softIndexFactor = 1} = {}) { for (let i = 0; i < notation.notes.length; ++i) makeNoteSoftIndex(notation.notes, i, {softIndexFactor}); }; const runNavigation = async function(criterion, sample, onStep) { const navigator = new Navigator(criterion, sample); navigator.resetCursor(-1); for (let i = 0; i < sample.notes.length; ++i) { navigator.step(i); const next = await (onStep && onStep(i, navigator)); if (next === Symbol.for("end")) { console.log("Navigation interrupted."); return; } } //console.log("Navigation accomplished."); return navigator; }; var Matcher$1 = { normalizeInterval, makeNoteSoftIndex, makeMatchNodes, genNotationContext, runNavigation, Navigator, Node, }; const MIDI$1 = MIDI$2; const trackDeltaToAbs = events => { let tick = 0; events.forEach(event => { tick += event.deltaTime; event.tick = tick; }); }; const trackAbsToDelta = events => { let lastTick = 0; events.sort((e1, e2) => e1.tick - e2.tick).forEach(event => { event.deltaTime = event.tick - lastTick; lastTick = event.tick; }); }; const sliceTrack = (track, startTick, endTick) => { trackDeltaToAbs(track); const events = []; const status = {}; track.forEach(event => { if (event.tick >= startTick && event.tick <= endTick && event.subtype !== "endOfTrack") events.push({ ...event, tick: event.tick - startTick, }); else if (event.tick < startTick) { switch (event.type) { case "meta": status[event.subtype] = event; break; } } }); Object.values(status).forEach(event => events.push({ ...event, tick: 0, })); events.push({ tick: endTick - startTick, type: "meta", subtype: "endOfTrack", }); trackAbsToDelta(events); return events; }; const sliceMidi = (midi, startTick, endTick) => ({ header: midi.header, tracks: midi.tracks.map(track => sliceTrack(track, startTick, endTick)), }); const TICKS_PER_BEATS = 480; const EXCLUDE_MIDI_EVENT_SUBTYPES = [ "endOfTrack", "trackName", "noteOn", "noteOff", ]; function encodeToMIDIData(notation, {startTime, unclosedNoteDuration = 30e+3} = {}) { notation.microsecondsPerBeat = notation.microsecondsPerBeat || 500000; const ticksPerBeat = TICKS_PER_BEATS; const msToTicks = ticksPerBeat * 1000 / notation.microsecondsPerBeat; const header = { formatType: 0, ticksPerBeat }; const track = []; if (!Number.isFinite(startTime)) { if (!notation.notes || !notation.notes[0]) throw new Error("encodeToMidiData: no start time specificed"); startTime = notation.notes[0].start; } track.push({ time: startTime, type: "meta", subtype: "copyrightNotice", text: `Composed by MusicWdigets. BUILT on ${new Date(Number(process.env.VUE_APP_BUILD_TIME)).toDateString()}` }); const containsTempo = notation.events && notation.events.find(event => event.subtype == "setTempo"); if (!containsTempo) { track.push({ time: startTime, type: "meta", subtype: "timeSignature", numerator: 4, denominator: 4, thirtyseconds: 8 }); track.push({ time: startTime, type: "meta", subtype: "setTempo", microsecondsPerBeat: notation.microsecondsPerBeat }); } //if (notation.correspondences) // track.push({ time: startTime, type: "meta", subtype: "text", text: "find-corres:" + notation.correspondences.join(",") }); let endTime = startTime || 0; if (notation.notes) { for (const note of notation.notes) { track.push({ time: note.start, type: "channel", subtype: "noteOn", channel: note.channel || 0, noteNumber: note.pitch, velocity: note.velocity, finger: note.finger, }); endTime = Math.max(endTime, note.start); if (Number.isFinite(unclosedNoteDuration)) note.duration = note.duration || unclosedNoteDuration; if (note.duration) { track.push({ time: note.start + note.duration, type: "channel", subtype: "noteOff", channel: note.channel || 0, noteNumber: note.pitch, velocity: 0, }); endTime = Math.max(endTime, note.start + note.duration); } } } if (notation.events) { const events = notation.events.filter(event => !EXCLUDE_MIDI_EVENT_SUBTYPES.includes(event.data.subtype)); for (const event of events) { track.push({ time: event.time, ...event.data, }); endTime = Math.max(endTime, event.time); } } track.push({ time: endTime + 100, type: "meta", subtype: "endOfTrack" }); track.sort(function (e1, e2) { return e1.time - e2.time; }); // append finger event after every noteOn event track.map((event, index) => ({event, index})) .filter(({event}) => event.subtype == "noteOn" && event.finger != null) .reverse() .forEach(({event, index}) => track.splice(index + 1, 0, { time: event.time, type: "meta", subtype: "text", text: `fingering(${event.finger})`, })); track.forEach(event => event.ticks = Math.round((event.time - startTime) * msToTicks)); track.forEach((event, i) => event.deltaTime = (event.ticks - (i > 0 ? track[i - 1].ticks : 0))); return {header, tracks: [track]}; } function encodeToMIDI(notation, options) { const data = encodeToMIDIData(notation, options); return MIDI$1.encodeMidiFile(data); } var MidiUtils$1 = { sliceMidi, encodeToMIDIData, encodeToMIDI, }; const MIDI = MIDI$2; const MusicNotation = MusicNotation$1; const MidiPlayer = MidiPlayer_1; const Matcher = Matcher$1; const MidiUtils = MidiUtils$1; var musicWidgets = { MIDI, MusicNotation, MidiPlayer, Matcher, MidiUtils, }; const WHOLE_DURATION_MAGNITUDE = 1920; const TICKS_PER_BEAT = WHOLE_DURATION_MAGNITUDE / 4; const EXTRA_NOTE_FIELDS = ['rest', 'tied', 'overlapped', 'implicitType', 'afterGrace', 'contextIndex', 'staffTrack', 'chordPosition', 'division']; const COMMON_NOTE_FIELDS = ['id', 'ids', 'pitch', 'velocity', 'track', 'channel', ...EXTRA_NOTE_FIELDS]; class MetaNotation { static fromAbsoluteNotes(notes, measureHeads, data) { const notation = new MetaNotation(data); notation.measures = Array(measureHeads.length) .fill(null) .map((__, i) => { const tick = measureHeads[i]; const duration = measureHeads[i + 1] ? measureHeads[i + 1] - tick : 0; const mnotes = notes .filter((note) => note.measure === i + 1) .map((note) => ({ tick: note.startTick - tick, duration: note.endTick - note.startTick, ...pick__default["default"](note, COMMON_NOTE_FIELDS), subNotes: [], })); // reduce note data size mnotes.forEach((mn) => ['rest', 'tied', 'implicitType', 'afterGrace'].forEach((field) => { if (!mn[field]) delete mn[field]; })); return { tick, duration, notes: mnotes, }; }); notation.idTrackMap = notes.reduce((map, note) => { if (note.id) map[note.id] = note.track; return map; }, {}); return notation; } static performAbsoluteNotes(abNotes, { withRestTied = false } = {}) { const notes = abNotes .filter((note) => (withRestTied || (!note.rest && !note.tied)) && !note.overlapped) .map((note) => ({ measure: note.measure, channel: note.channel, track: note.track, start: note.start, startTick: note.startTick, endTick: note.endTick, pitch: note.pitch, duration: note.duration, velocity: note.velocity || 127, id: note.id, ids: note.ids, staffTrack: note.staffTrack, contextIndex: note.contextIndex, implicitType: note.implicitType, chordPosition: note.chordPosition, })); const noteMap = notes.reduce((map, note) => { const key = `${note.channel}|${note.start}|${note.pitch}`; const priorNote = map[key]; if (priorNote) priorNote.ids.push(...note.ids); else map[key] = note; return map; }, {}); return Object.values(noteMap); } constructor(data) { this.ripe = false; if (data) Object.assign(this, data); } /*get ordinaryMeasureIndices (): number[] { if (this.measureLayout) return this.measureLayout.serialize(LayoutType.Ordinary); return Array(this.measures.length).fill(null).map((_, i) => i + 1); }*/ // In Lilypond 2.20.0, minus tick value at the head of a track result in MIDI event time bias, // So store the bias values to correct MIDI time from lilyond. get trackTickBias() { const headMeasure = this.measures[0]; return this.trackNames.reduce((map, name, track) => { map[name] = 0; if (headMeasure) { const note = headMeasure.notes.find((note) => note.track === track); if (note) map[name] = Math.min(note.tick, 0); } return map; }, {}); } get idSet() { return this.measures.reduce((set, measure) => (measure.notes.filter((note) => !note.rest).forEach((note) => note.ids.forEach((id) => set.add(id))), set), new Set()); } toJSON() { return { __prototype: 'LilyNotation', //pitchContextGroup: this.pitchContextGroup, //measureLayout: this.measureLayout, measures: this.measures, idTrackMap: this.idTrackMap, trackNames: this.trackNames, ripe: this.ripe, }; } toAbsoluteNotes(measureIndices /*= this.ordinaryMeasureIndices*/) { let measureTick = 0; const measureNotes = measureIndices.map((index) => { const measure = this.measures[index - 1]; console.assert(!!measure, 'invalid measure index:', index, this.measures.length); const notes = measure.notes.map((mnote) => { return { startTick: measureTick + mnote.tick, endTick: measureTick + mnote.tick + mnote.duration, start: measureTick + mnote.tick, duration: mnote.duration, measure: index, ...pick__default["default"](mnote, COMMON_NOTE_FIELDS), }; }); measureTick += measure.duration; return notes; }); return [].concat(...measureNotes); } /*getMeasureIndices (type: LayoutType) { return this.measureLayout.serialize(type); }*/ toPerformingNotation(measureIndices /*= this.ordinaryMeasureIndices*/, options = {}) { //console.debug("toPerformingNotation:", this, measureIndices); const abNotes = this.toAbsoluteNotes(measureIndices); const notes = MetaNotation.performAbsoluteNotes(abNotes, options); //const lastNote = notes[notes.length - 1]; const endTime = Math.max(...notes.map((note) => note.start + note.duration)); const endTick = measureIndices.reduce((tick, index) => tick + this.measures[index - 1].duration, 0); const notation = new musicWidgets.MusicNotation.Notation({ ticksPerBeat: TICKS_PER_BEAT, meta: {}, tempos: [], channels: [notes], endTime, endTick, }); return notation; } toPerformingMIDI(measureIndices, { trackList } = {}) { if (!measureIndices.length) return null; // to avoid begin minus tick const zeroTick = -Math.min(0, ...(this.measures[0]?.events.map((e) => e.ticks) || []), ...(this.measures[0]?.notes.map((note) => note.tick) || [])); let measureTick = zeroTick; const measureEvents = measureIndices.map((index) => { const measure = this.measures[index - 1]; console.assert(!!measure, 'invalid measure index:', index, this.measures.length); const events = measure.events.map((mevent) => ({ ticks: measureTick + mevent.ticks, track: mevent.track, data: { ...mevent.data, measure: index, }, })); measureTick += measure.duration; return events; }); const eventPriority = (event) => event.ticks + (event.subtype === 'noteOff' ? -1e-8 : 0); const tracks = [].concat(...measureEvents).reduce((tracks, mevent) => { tracks[mevent.track] = tracks[mevent.track] || []; tracks[mevent.track].push({ ticks: mevent.ticks, ...mevent.data, }); return tracks; }, []); tracks[0] = tracks[0] || []; /*tracks[0].push({ ticks: 0, type: "meta", subtype: "text", text: `${npmPackage.name} ${npmPackage.version}`, });*/ // append note events measureTick = zeroTick; measureIndices.map((index) => { const measure = this.measures[index - 1]; console.assert(!!measure, 'invalid measure index:', index, this.measures.length); if (!Number.isFinite(measure.duration)) return; measure.notes.forEach((note) => { if (trackList && !trackList[note.track]) return; if (note.rest) return; const tick = measureTick + note.tick; const track = (tracks[note.track] = tracks[note.track] || []); note.subNotes.forEach((subnote) => { track.push({ ticks: tick + subnote.startTick, measure: index, ids: note.ids, type: 'channel', subtype: 'noteOn', channel: note.channel, noteNumber: subnote.pitch, velocity: subnote.velocity, staffTrack: note.staffTrack, staff: note.staff, }); track.push({ ticks: tick + subnote.endTick, measure: index, ids: note.ids, type: 'channel', subtype: 'noteOff', channel: note.channel, noteNumber: subnote.pitch, velocity: 0, staffTrack: note.staffTrack, staff: note.staff, }); }); }); measureTick += measure.duration; }); const finalTick = measureTick; // ensure no empty track for (let t = 0; t < tracks.length; ++t) tracks[t] = tracks[t] || []; // sort & make deltaTime tracks.forEach((events) => { events.sort((e1, e2) => eventPriority(e1) - eventPriority(e2)); let ticks = 0; events.forEach((event) => { event.deltaTime = event.ticks - ticks; if (!Number.isFinite(event.deltaTime)) event.deltaTime = 0; else ticks = event.ticks; }); events.push({ deltaTime: Math.max(finalTick - ticks, 0), type: 'meta', subtype: 'endOfTrack' }); }); return { header: { formatType: 0, ticksPerBeat: TICKS_PER_BEAT, }, tracks, zeroTick, }; } toPerformingNotationWithEvents(measureIndices, options = {}) { if (!measureIndices.length) return null; const { zeroTick, ...midi } = this.toPerformingMIDI(measureIndices, options); const notation = musicWidgets.MusicNotation.Notation.parseMidi(midi); assignNotationNoteDataFromEvents(notation); let tick = zeroTick; notation.measures = measureIndices.map((index) => { const startTick = tick; tick += this.measures[index - 1].duration; return { index, startTick, endTick: tick, }; }); return notation; } // find the MIDI event of setTempo in measures data, and change the value of microsecondsPerBeat setTempo(bpm) { let found = false; for (const measure of this.measures) { for (const event of measure.events) { if (event.data.subtype === 'setTempo') { event.data.microsecondsPerBeat = 60e6 / bpm; found = true; } } } return found; } } const assignNotationNoteDataFromEvents = (midiNotation, fields = ['ids', 'measure', 'staffTrack']) => { const noteId = (channel, pitch, tick) => `${channel}|${pitch}|${tick}`; const noteMap = midiNotation.notes.reduce((map, note) => { map[noteId(note.channel, note.pitch, note.startTick)] = note; return map; }, {}); midiNotation.events.forEach((event) => { if (event.data.subtype === 'noteOn') { const id = noteId(event.data.channel, event.data.noteNumber, event.ticks); const note = noteMap[id]; console.assert(!!note, 'cannot find note of', id); if (note) Object.assign(note, pick__default["default"](event.data, fields)); } }); }; var sha256 = {exports: {}}; var core = {exports: {}}; (function (module, exports) { (function (root, factory) { { // CommonJS module.exports = factory(); } }(this, function () { /*globals window, global, require*/ /** * CryptoJS core components. */ var CryptoJS = CryptoJS || (function (Math, undefined$1) { var crypto; // Native crypto from window (Browser) if (typeof window !== 'undefined' && window.crypto) { crypto = window.crypto; } // Native crypto in web worker (Browser) if (typeof self !== 'undefined' && self.crypto) { crypto = self.crypto; } // Native crypto from worker if (typeof globalThis !== 'undefined' && globalThis.crypto) { crypto = globalThis.crypto; } // Native (experimental IE 11) crypto from window (Browser) if (!crypto && typeof window !== 'undefined' && window.msCrypto) { crypto = window.msCrypto; } // Native crypto from global (NodeJS) if (!crypto && typeof global !== 'undefined' && global.crypto) { crypto = global.crypto; } // Native crypto import via require (NodeJS) if (!crypto && typeof commonjsRequire === 'function') { try { crypto = require('crypto'); } catch (err) {} } /* * Cryptographically secure pseudorandom number generator * * As Math.random() is cryptographically not safe to use */ var cryptoSecureRandomInt = function () { if (crypto) { // Use getRandomValues method (Browser) if (typeof crypto.getRandomValues === 'function') { try { return crypto.getRandomValues(new Uint32Array(1))[0]; } catch (err) {} } // Use randomBytes method (NodeJS) if (typeof crypto.randomBytes === 'function') { try { return crypto.randomBytes(4).readInt32LE(); } catch (err) {} } } throw new Error('Native crypto module could not be used to get secure random number.'); }; /* * Local polyfill of Object.create */ var create = Object.create || (function () { function F() {} return function (obj) { var subtype; F.prototype = obj; subtype = new F(); F.prototype = null; return subtype; }; }()); /** * CryptoJS namespace. */ var C = {}; /** * Library namespace. */ var C_lib = C.lib = {}; /** * Base object for prototypal inheritance. */ var Base = C_lib.Base = (function () { return { /** * Creates a new object that inherits from this object. * * @param {Object} overrides Properties to copy into the new object. * * @return {Object} The new object. * * @static * * @example * * var MyType = CryptoJS.lib.Base.extend({ * field: 'value', * * method: function () { * } * }); */ extend: function (overrides) { // Spawn var subtype = create(this); // Augment if (overrides) { subtype.mixIn(overrides); } // Create default initializer if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { subtype.init = function () { subtype.$super.init.apply(this, arguments); }; } // Initializer's prototype is the subtype object subtype.init.prototype = subtype; // Reference supertype subtype.$super = this; return subtype; }, /** * Extends this object and runs the init method. * Arguments to create() will be passed to init(). * * @return {Object} The new object. * * @static * * @example * * var instance = MyType.create(); */ create: function () { var instance = this.extend(); instance.init.apply(instance, arguments); return instance; }, /** * Initializes a newly created object. * Override this method to add some logic when your objects are created. * * @example * * var MyType = CryptoJS.lib.Base.extend({ * init: function () { * // ... * } * }); */ init: function () { }, /** * Copies properties into this object. * * @param {Object} properties The properties to mix in. * * @example * * MyType.mixIn({ * field: 'value' * }); */ mixIn: function (properties) { for (var propertyName in properties) { if (properties.hasOwnProperty(propertyName)) { this[propertyName] = properties[propertyName]; } } // IE won't copy toString using the loop above if (properties.hasOwnProperty('toString')) { this.toString = properties.toString; } }, /** * Creates a copy of this object. * * @return {Object} The clone. * * @example * * var clone = instance.clone(); */ clone: function () { return this.init.prototype.extend(this); } }; }()); /** * An array of 32-bit words. * * @property {Array} words The array of 32-bit words. * @property {number} sigBytes The number of significant bytes in this word array. */ var WordArray = C_lib.WordArray = Base.extend({ /** * Initializes a newly created word array. * * @param {Array} words (Optional) An array of 32-bit words. * @param {number} sigBytes (Optional) The number of significant bytes in the words. * * @example * * var wordArray = CryptoJS.lib.WordArray.create(); * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); */ init: function (words, sigBytes) { words = this.words = words || []; if (sigBytes != undefined$1) { this.sigBytes = sigBytes; } else { this.sigBytes = words.length * 4; } }, /** * Converts this word array to a string. * * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex * * @return {string} The stringified word array. * * @example * * var string = wordArray + ''; * var string = wordArray.toString(); * var string = wordArray.toString(CryptoJS.enc.Utf8); */ toString: function (encoder) { return (encoder || Hex).stringify(this); }, /** * Concatenates a word array to this word array. * * @param {WordArray} wordArray The word array to append. * * @return {WordArray} This word array. * * @example * * wordArray1.concat(wordArray2); */ concat: function (wordArray) { // Shortcuts var thisWords = this.words; var thatWords = wordArray.words; var thisSigBytes = this.sigBytes; var thatSigBytes = wordArray.sigBytes; // Clamp excess bits this.clamp(); // Concat if (thisSigBytes % 4) { // Copy one byte at a time for (var i = 0; i < thatSigBytes; i++) { var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); } } else { // Copy one word at a time for (var j = 0; j < thatSigBytes; j += 4) { thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2]; } } this.sigBytes += thatSigBytes; // Chainable return this; }, /** * Removes insignificant bits. * * @example * * wordArray.clamp(); */ clamp: function () { // Shortcuts var words = this.words; var sigBytes = this.sigBytes; // Clamp words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); words.length = Math.ceil(sigBytes / 4); }, /** * Creates a copy of this word array. * * @return {WordArray} The clone. * * @example * * var clone = wordArray.clone(); */ clone: function () { var clone = Base.clone.call(this); clone.words = this.words.slice(0); return clone; }, /** * Creates a word array filled with random bytes. * * @param {number} nBytes The number of random bytes to generate. * * @return {WordArray} The random word array. * * @static * * @example * * var wordArray = CryptoJS.lib.WordArray.random(16); */ random: function (nBytes) { var words = []; for (var i = 0; i < nBytes; i += 4) { words.push(cryptoSecureRandomInt()); } return new WordArray.init(words, nBytes); } }); /** * Encoder namespace. */ var C_enc = C.enc = {}; /** * Hex encoding strategy. */ var Hex = C_enc.Hex = { /** * Converts a word array to a hex string. * * @param {WordArray} wordArray The word array. * * @return {string} The hex string. * * @static * * @example * * var hexString = CryptoJS.enc.Hex.stringify(wordArray); */ stringify: function (wordArray) { // Shortcuts var words = wordArray.words; var sigBytes = wordArray.sigBytes; // Convert var hexChars = []; for (var i = 0; i < sigBytes; i++) { var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; hexChars.push((bite >>> 4).toString(16)); hexChars.push((bite & 0x0f).toString(16)); } return hexChars.join(''); }, /** * Converts a hex string to a word array. * * @param {string} hexStr The hex string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Hex.parse(hexString); */ parse: function (hexStr) { // Shortcut var hexStrLength = hexStr.length; // Convert var words = []; for (var i = 0; i < hexStrLength; i += 2) { words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); } return new WordArray.init(words, hexStrLength / 2); } }; /** * Latin1 encoding strategy. */ var Latin1 = C_enc.Latin1 = { /** * Converts a word array to a Latin1 string. * * @param {WordArray} wordArray The word array. * * @return {string} The Latin1 string. * * @static * * @example * * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); */ stringify: function (wordArray) { // Shortcuts var words = wordArray.words; var sigBytes = wordArray.sigBytes; // Convert var latin1Chars = []; for (var i = 0; i < sigBytes; i++) { var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; latin1Chars.push(String.fromCharCode(bite)); } return latin1Chars.join(''); }, /** * Converts a Latin1 string to a word array. * * @param {string} latin1Str The Latin1 string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); */ parse: function (latin1Str) { // Shortcut var latin1StrLength = latin1Str.length; // Convert var words = []; for (var i = 0; i < latin1StrLength; i++) { words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); } return new WordArray.init(words, latin1StrLength); } }; /** * UTF-8 encoding strategy. */ var Utf8 = C_enc.Utf8 = { /** * Converts a word array to a UTF-8 string. * * @param {WordArray} wordArray The word array. * * @return {string} The UTF-8 string. * * @static * * @example * * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); */ stringify: function (wordArray) { try { return decodeURIComponent(escape(Latin1.stringify(wordArray))); } catch (e) { throw new Error('Malformed UTF-8 data'); } }, /** * Converts a UTF-8 string to a word array. * * @param {string} utf8Str The UTF-8 string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); */ parse: function (utf8Str) { return Latin1.parse(unescape(encodeURIComponent(utf8Str))); } }; /** * Abstract buffered block algorithm template. * * The property blockSize must be implemented in a concrete subtype. * * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 */ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ /** * Resets this block algorithm's data buffer to its initial state. * * @example * * bufferedBlockAlgorithm.reset(); */ reset: function () { // Initial values this._data = new WordArray.init(); this._nDataBytes = 0; }, /** * Adds new data to this block algorithm's buffer. * * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. * * @example * * bufferedBlockAlgorithm._append('data'); * bufferedBlockAlgorithm._append(wordArray); */ _append: function (data) { // Convert string to WordArray, else assume WordArray already if (typeof data == 'string') { data = Utf8.parse(data); } // Append this._data.concat(data); this._nDataBytes += data.sigBytes; }, /** * Processes available data blocks. * * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. * * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. * * @return {WordArray} The processed data. * * @example * * var processedData = bufferedBlockAlgorithm._process(); * var processedData = bufferedBlockAlgorithm._process(!!'flush'); */ _process: function (doFlush) { var processedWords; // Shortcuts var data = this._data; var dataWords = data.words; var dataSigBytes = data.sigBytes; var blockSize = this.blockSize; var blockSizeBytes = blockSize * 4; // Count blocks ready var nBlocksReady = dataSigBytes / blockSizeBytes; if (doFlush) { // Round up to include partial blocks nBlocksReady = Math.ceil(nBlocksReady); } else { // Round down to include only full blocks, // less the number of blocks that must remain in the buffer nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); } // Count words ready var nWordsReady = nBlocksReady * blockSize; // Count bytes ready var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); // Process blocks if (nWordsReady) { for (var offset = 0; offset < nWordsReady; offset += blockSize) { // Perform concrete-algorithm logic this._doProcessBlock(dataWords, offset); } // Remove processed words processedWords = dataWords.splice(0, nWordsReady); data.sigBytes -= nBytesReady; } // Return processed words return new WordArray.init(processedWords, nBytesReady); }, /** * Creates a copy of this object. * * @return {Object} The clone. * * @example * * var clone = bufferedBlockAlgorithm.clone(); */ clone: function () { var clone = Base.clone.call(this); clone._data = this._data.clone(); return clone; }, _minBufferSize: 0 }); /** * Abstract hasher template. * * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) */ C_lib.Hasher = BufferedBlockAlgorithm.extend({ /** * Configuration options. */ cfg: Base.extend(), /** * Initializes a newly created hasher. * * @param {Object} cfg (Optional) The configuration options to use for this hash computation. * * @example * * var hasher = CryptoJS.algo.SHA256.create(); */ init: function (cfg) { // Apply config defaults this.cfg = this.cfg.extend(cfg); // Set initial values this.reset(); }, /** * Resets this hasher to its initial state. * * @example * * hasher.reset(); */ reset: function () { // Reset data buffer BufferedBlockAlgorithm.reset.call(this); // Perform concrete-hasher logic this._doReset(); }, /** * Updates this hasher with a message. * * @param {WordArray|string} messageUpdate The message to append. * * @return {Hasher} This hasher. * * @example * * hasher.update('message'); * hasher.update(wordArray); */ update: function (messageUpdate) { // Append this._append(messageUpdate); // Update the hash this._process(); // Chainable return this; }, /** * Finalizes the hash computation. * Note that the finalize operation is effectively a destructive, read-once operation. * * @param {WordArray|string} messageUpdate (Optional) A final message update. * * @return {WordArray} The hash. * * @example * * var hash = hasher.finalize(); * var hash = hasher.finalize('message'); * var hash = hasher.finalize(wordArray); */ finalize: function (messageUpdate) { // Final message update if (messageUpdate) { this._append(messageUpdate); } // Perform concrete-hasher logic var hash = this._doFinalize(); return hash; }, blockSize: 512/32, /** * Creates a shortcut function to a hasher's object interface. * * @param {Hasher} hasher The hasher to create a helper for. * * @return {Function} The shortcut function. * * @static * * @example * * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); */ _createHelper: function (hasher) { return function (message, cfg) { return new hasher.init(cfg).finalize(message); }; }, /** * Creates a shortcut function to the HMAC's object interface. * * @param {Hasher} hasher The hasher to use in this HMAC helper. * * @return {Function} The shortcut function. * * @static * * @example * * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); */ _createHmacHelper: function (hasher) { return function (message, key) { return new C_algo.HMAC.init(hasher, key).finalize(message); }; } }); /** * Algorithm namespace. */ var C_algo = C.algo = {}; return C; }(Math)); return CryptoJS; })); }(core)); (function (module, exports) { (function (root, factory) { { // CommonJS module.exports = factory(core.exports); } }(this, function (CryptoJS) { (function (Math) { // Shortcuts var C = CryptoJS; var C_lib = C.lib; var WordArray = C_lib.WordArray; var Hasher = C_lib.Hasher; var C_algo = C.algo; // Initialization and round constants tables var H = []; var K = []; // Compute constants (function () { function isPrime(n) { var sqrtN = Math.sqrt(n); for (var factor = 2; factor <= sqrtN; factor++) { if (!(n % factor)) { return false; } } return true; } function getFractionalBits(n) { return ((n - (n | 0)) * 0x100000000) | 0; } var n = 2; var nPrime = 0; while (nPrime < 64) { if (isPrime(n)) { if (nPrime < 8) { H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); } K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); nPrime++; } n++; } }()); // Reusable object var W = []; /** * SHA-256 hash algorithm. */ var SHA256 = C_algo.SHA256 = Hasher.extend({ _doReset: function () { this._hash = new WordArray.init(H.slice(0)); }, _doProcessBlock: function (M, offset) { // Shortcut var H = this._hash.words; // Working variables var a = H[0]; var b = H[1]; var c = H[2]; var d = H[3]; var e = H[4]; var f = H[5]; var g = H[6]; var h = H[7]; // Computation for (var i = 0; i < 64; i++) { if (i < 16) { W[i] = M[offset + i] | 0; } else { var gamma0x = W[i - 15]; var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3); var gamma1x = W[i - 2]; var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10); W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; } var ch = (e & f) ^ (~e & g); var maj = (a & b) ^ (a & c) ^ (b & c); var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); var t1 = h + sigma1 + ch + K[i] + W[i]; var t2 = sigma0 + maj; h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; } // Intermediate hash value H[0] = (H[0] + a) | 0; H[1] = (H[1] + b) | 0; H[2] = (H[2] + c) | 0; H[3] = (H[3] + d) | 0; H[4] = (H[4] + e) | 0; H[5] = (H[5] + f) | 0; H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0; }, _doFinalize: function () { // Shortcuts var data = this._data; var dataWords = data.words; var nBitsTotal = this._nDataBytes * 8; var nBitsLeft = data.sigBytes * 8; // Add padding dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; data.sigBytes = dataWords.length * 4; // Hash final blocks this._process(); // Return final computed hash return this._hash; }, clone: function () { var clone = Hasher.clone.call(this); clone._hash = this._hash.clone(); return clone; } }); /** * Shortcut function to the hasher's object interface. * * @param {WordArray|string} message The message to hash. * * @return {WordArray} The hash. * * @static * * @example * * var hash = CryptoJS.SHA256('message'); * var hash = CryptoJS.SHA256(wordArray); */ C.SHA256 = Hasher._createHelper(SHA256); /** * Shortcut function to the HMAC's object interface. * * @param {WordArray|string} message The message to hash. * @param {WordArray|string} key The secret key. * * @return {WordArray} The HMAC. * * @static * * @example * * var hmac = CryptoJS.HmacSHA256(message, key); */ C.HmacSHA256 = Hasher._createHmacHelper(SHA256); }(Math)); return CryptoJS.SHA256; })); }(sha256)); var _SHA256 = sha256.exports; const SHA256 = (source) => { const { words, sigBytes } = _SHA256(source); const uwords = words.map((x) => (x < 0 ? x + 0x100000000 : x)); const word_len = sigBytes / words.length; return new Uint8Array(sigBytes).map((_, i) => (uwords[Math.floor(i / word_len)] >> ((3 - (i % word_len)) * 8)) & 0xff); }; const HASH_LEN = 256; class HashVector { static fromHash(hash) { const fields = []; for (const byte of hash) { for (let b = 0; b < 8; ++b) fields.push((byte >> b) & 1 ? 1 : -1); } return new HashVector(fields); } static fromString(source) { const hash = SHA256(source); return HashVector.fromHash(hash); } static fromWords(words) { const vs = words.map((word) => HashVector.fromString(word)); return vs.reduce((sum, v) => sum.add(v), HashVector.zero); } static concat(...vectors) { const fields = vectors.map((v) => v.fields).flat(1); return new HashVector(fields); } constructor(fields = null) { this.fields = fields || Array(HASH_LEN).fill(0); } get length() { return this.fields.length; } toHash() { return Uint8Array.from(Array(this.length / 8) .fill(0) .map((_, i) => { const bits = this.fields.slice(i * 8, (i + 1) * 8); return bits.reduce((byte, bit, b) => byte | ((bit > 0 ? 1 : 0) << b), 0); })); } add(vec) { this.fields.forEach((value, i) => (this.fields[i] = value + vec.fields[i])); return this; } scale(factor) { this.fields = this.fields.map((value) => value * factor); return this; } sub(crop) { const fields = crop > 0 ? this.fields.slice(0, crop) : this.fields.slice(crop); return new HashVector(fields); } static get zero() { return new HashVector(); } } const odds = (byte) => { let result = 0; for (let b = byte; b > 0; b >>= 1) { if (b % 2) ++result; } return result; }; const ODDS = Array(2 ** 8) .fill(0) .map((_, i) => odds(i)); ODDS.reduce((table, odd, i) => ({ ...table, [('0' + i.toString(16)).slice(-2)]: odd }), {}); const countOnes = (hash) => hash.reduce((sum, byte) => sum + ODDS[byte], 0); const xorHashes = (hash1, hash2) => hash1.map((byte, i) => byte ^ hash2[i]); const cosHashes = (hash1, hash2) => { const len = hash1.length * 8; const xor = xorHashes(hash1, hash2); const ones = countOnes(xor); return (len - ones * 2) / len; }; const i2hex = (i) => ('0' + i.toString(16)).slice(-2); const hashToHex = (hash) => Array.from(hash).map(i2hex).join(''); const hashToBigInt = (hash) => { // __NOT_FOR_BROWSER_ return Array.from(hash).reduce((r, x) => r * 0x100n + BigInt(x), 0n); /* // _NOT_FOR_BROWSER__ throw new Error('BigInt not supported'); //*/ }; var Sylvester = {}; Sylvester.Matrix = function () {}; Sylvester.Matrix.create = function (elements) { var M = new Sylvester.Matrix(); return M.setElements(elements) }; Sylvester.Matrix.I = function (n) { var els = [], i = n, j; while (i--) { j = n; els[i] = []; while (j--) { els[i][j] = i === j ? 1 : 0; } } return Sylvester.Matrix.create(els) }; Sylvester.Matrix.prototype = { dup: function () { return Sylvester.Matrix.create(this.elements) }, isSquare: function () { var cols = this.elements.length === 0 ? 0 : this.elements[0].length; return this.elements.length === cols }, toRightTriangular: function () { if (this.elements.length === 0) return Sylvester.Matrix.create([]) var M = this.dup(), els; var n = this.elements.length, i, j, np = this.elements[0].length, p; for (i = 0; i < n; i++) { if (M.elements[i][i] === 0) { for (j = i + 1; j < n; j++) { if (M.elements[j][i] !== 0) { els = []; for (p = 0; p < np; p++) { els.push(M.elements[i][p] + M.elements[j][p]); } M.elements[i] = els; break } } } if (M.elements[i][i] !== 0) { for (j = i + 1; j < n; j++) { var multiplier = M.elements[j][i] / M.elements[i][i]; els = []; for (p = 0; p < np; p++) { // Elements with column numbers up to an including the number of the // row that we're subtracting can safely be set straight to zero, // since that's the point of this routine and it avoids having to // loop over and correct rounding errors later els.push( p <= i ? 0 : M.elements[j][p] - M.elements[i][p] * multiplier ); } M.elements[j] = els; } } } return M }, determinant: function () { if (this.elements.length === 0) { return 1 } if (!this.isSquare()) { return null } var M = this.toRightTriangular(); var det = M.elements[0][0], n = M.elements.length; for (var i = 1; i < n; i++) { det = det * M.elements[i][i]; } return det }, isSingular: function () { return this.isSquare() && this.determinant() === 0 }, augment: function (matrix) { if (this.elements.length === 0) { return this.dup() } var M = matrix.elements || matrix; if (typeof M[0][0] === 'undefined') { M = Sylvester.Matrix.create(M).elements; } var T = this.dup(), cols = T.elements[0].length; var i = T.elements.length, nj = M[0].length, j; if (i !== M.length) { return null } while (i--) { j = nj; while (j--) { T.elements[i][cols + j] = M[i][j]; } } return T }, inverse: function () { if (this.elements.length === 0) { return null } if (!this.isSquare() || this.isSingular()) { return null } var n = this.elements.length, i = n, j; var M = this.augment(Sylvester.Matrix.I(n)).toRightTriangular(); var np = M.elements[0].length, p, els, divisor; var inverse_elements = [], new_element; // Sylvester.Matrix is non-singular so there will be no zeros on the // diagonal. Cycle through rows from last to first. while (i--) { // First, normalise diagonal elements to 1 els = []; inverse_elements[i] = []; divisor = M.elements[i][i]; for (p = 0; p < np; p++) { new_element = M.elements[i][p] / divisor; els.push(new_element); // Shuffle off the current row of the right hand side into the results // array as it will not be modified by later runs through this loop if (p >= n) { inverse_elements[i].push(new_element); } } M.elements[i] = els; // Then, subtract this row from those above it to give the identity matrix // on the left hand side j = i; while (j--) { els = []; for (p = 0; p < np; p++) { els.push(M.elements[j][p] - M.elements[i][p] * M.elements[j][i]); } M.elements[j] = els; } } return Sylvester.Matrix.create(inverse_elements) }, setElements: function (els) { var i, j, elements = els.elements || els; if (elements[0] && typeof elements[0][0] !== 'undefined') { i = elements.length; this.elements = []; while (i--) { j = elements[i].length; this.elements[i] = []; while (j--) { this.elements[i][j] = elements[i][j]; } } return this } var n = elements.length; this.elements = []; for (i = 0; i < n; i++) { this.elements.push([elements[i]]); } return this }, }; var matrixInverse = function (elements) { const mat = Sylvester.Matrix.create(elements).inverse(); if (mat !== null) { return mat.elements } else { return null } }; const EOM = -1; // end event id of measure //const GREAT_NUMBER = 16 * 9 * 5 * 7 * 11 * 13 * 17 * 19 * 23; const GREAT_NUMBER = 1920; const DURATION_MULTIPLIER = 128 * 3 * 5 * 7 * 11 * 13; const floatToFrac = (x) => { const n = Math.round(x * GREAT_NUMBER); return reducedFraction(n, GREAT_NUMBER); }; const floatToTimeWarp = (x) => { if (x === 1) return null; return floatToFrac(x); }; var ActionType; (function (ActionType) { ActionType[ActionType["PLACE"] = 0] = "PLACE"; ActionType[ActionType["VERTICAL"] = 1] = "VERTICAL"; ActionType[ActionType["HORIZONTAL"] = 2] = "HORIZONTAL"; })(ActionType || (ActionType = {})); class Action { constructor(data) { Object.assign(this, data); } static P(e) { return new Action({ type: ActionType.PLACE, e1: e, }); } static V(e1, e2, order = 1) { return new Action({ type: ActionType.VERTICAL, e1: order > 0 ? e1 : e2, e2: order > 0 ? e2 : e1, }); } static H(e1, e2) { return new Action({ type: ActionType.HORIZONTAL, e1, e2, }); } get id() { switch (this.type) { case ActionType.PLACE: return this.e1.toString(); case ActionType.VERTICAL: return `${this.e1}|${this.e2}`; case ActionType.HORIZONTAL: return `${this.e1}-${this.e2 >= 0 ? this.e2 : '.'}`; } } get events() { return [this.e1, this.e2].filter(Number.isFinite); } } class StageMatrix { static fromNode(node, status) { const matrix = Array(node.stages.length) .fill(null) .map(() => Array(node.stages.length) .fill(null) .map(() => new Set())); node.actions .filter((action) => action.type === ActionType.HORIZONTAL) .forEach((action) => { const stage1 = node.stages.findIndex((stage) => stage.events.includes(action.e1)); const stage2 = node.stages.findIndex((stage) => stage.events.includes(action.e2)); console.assert(stage1 >= 0 && stage2 >= 0, 'invalid stages for H action:', node.id, node.stages, action); matrix[stage1][stage2].add(action.e1); }); matrix[0][node.stages.length - 1].add(0); // the entire measure edge const stagedEvents = node.stagedEvents; const endHs = status.matrixH[status.matrixH.length - 1].filter((_, i) => !stagedEvents.has(i)); const endHP = Math.max(0, Math.max(...endHs) - 0.01); const hActions = node.actions.filter((action) => action.type === ActionType.HORIZONTAL); const pendingHeads = Object.keys(status.eventMap) .map(Number) .filter((eid) => !hActions.find((action) => action.e2 === eid)); // edges to end stage node.stages.forEach((stage) => { stage.events.forEach((eid) => { if (eid > 0) { const act = hActions.find((action) => action.e1 === eid); if (!act && status.matrixH[status.matrixH.length - 1][eid] >= endHP) { if (!pendingHeads.some((id) => status.matrixH[id][eid] > 0)) matrix[stage.index][node.stages.length - 1].add(eid); } } }); }); return new StageMatrix({ matrix }); } constructor(data) { Object.assign(this, data); } pathOf(x, y, target, ei = 0) { if (this.matrix[x][y].size) { const eid = [...this.matrix[x][y]][ei]; if (y === target) return [eid]; for (let yy = y + 1; yy <= target; ++yy) { const sub = this.pathOf(y, yy, target); if (sub) return [eid, ...sub]; } } return null; } findDoublePath(s1, s2) { const paths = []; for (let t = s2; t >= s1 + 1; --t) { for (let ei = 0; ei < this.matrix[s1][t].size; ++ei) { const path = this.pathOf(s1, t, s2, ei); if (path) { paths.push(path); if (paths.length === 2) return [paths[0], paths[1]]; } } } return null; } reducePath(path) { this.matrix.forEach((column) => column.forEach((set) => path.forEach((id) => set.delete(id)))); } toEquations(eventCount) { const equations = []; for (let d = 1; d < this.matrix.length; d++) { for (let s1 = 0; s1 < this.matrix.length - d; s1++) { const s2 = s1 + d; while (true) { // find closed loop from s1 to s2 const paths = this.findDoublePath(s1, s2); if (paths) { const [path1, path2] = paths; const equation = Array(eventCount).fill(0); path1.forEach((eid) => (equation[eid] = 1)); path2.forEach((eid) => (equation[eid] = -1)); equations.push(equation); this.reducePath(path1.length > path2.length ? path1 : path2); } else break; } } } return equations; } } class PathNode { constructor(data) { Object.assign(this, data); console.assert(this.logger, 'logger is null:', data); } get actions() { const last = this.parent ? this.parent.actions : []; return this.action ? [...last, this.action] : last; } get id() { const actionIds = this.actions.map((action) => action.id).sort(); return actionIds.join(' '); } get stagedEvents() { const set = new Set(); if (this.stages) this.stages.forEach((stage) => stage.events.forEach((eid) => eid >= 0 && set.add(eid))); return set; } like(ids) { const actionIds = ids.split(' ').sort(); return actionIds.join(' ') === this.id; } constructStages(status) { this.stages = [{ events: [EOM] }]; for (const action of this.actions) { switch (action.type) { case ActionType.PLACE: this.stages.unshift({ events: [action.e1] }); break; case ActionType.VERTICAL: { const stage1 = this.stages.find((stage) => stage.events.includes(action.e1)); const stage2 = this.stages.find((stage) => stage.events.includes(action.e2)); console.assert(stage1 || stage2, 'invalid V action:', this.stages, action); if (stage1 && stage2) { stage1.events.push(...stage2.events); stage2.events = null; this.stages = this.stages.filter((stage) => stage.events); } else if (!stage1) stage2.events.unshift(action.e1); else if (!stage2) stage1.events.push(action.e2); } break; case ActionType.HORIZONTAL: { const stage1 = this.stages.find((stage) => stage.events.includes(action.e1)); const stage2 = this.stages.find((stage) => stage.events.includes(action.e2)); console.assert(stage1 || stage2, 'invalid H action:', this.stages, action); const newStage = (eid) => { console.assert(status.eventMap[eid], 'invalid event id:', action.id, eid, status.eventMap); const x = status.eventMap[eid].x; const stage = this.stages.find((s) => s.events.some((e) => e > 0 && status.eventMap[e].x <= x) && s.events.some((e) => e > 0 && status.eventMap[e].x >= x)); if (stage) stage.events.push(eid); else { const newStage = { events: [eid] }; const si = this.stages.findIndex((s) => s.events[0] === EOM || status.eventMap[s.events[0]].x >= x); this.stages.splice(si, 0, newStage); } }; if (!stage1) newStage(action.e1); if (!stage2) newStage(action.e2); /*if (this.stages.some((s, si) => si < this.stages.length - 2 && s.events.some(e1 => this.stages[si + 1].events.some(e2 => status.eventMap[e2].x <= status.eventMap[e1].x)))) debugger;*/ } break; } } this.stages.forEach((stage, i) => (stage.index = i)); } constructConstraints(status) { const eventCount = Object.keys(status.eventMap).length; const stageMatrix = StageMatrix.fromNode(this, status); const equations = stageMatrix.toEquations(eventCount); const factors = Array(eventCount) .fill(null) .map((_, id) => status.eventMap[id].duration); this.constraints = equations.map((equation) => equation.map((it, i) => it * factors[i])); } inbalancesConstraints(status) { console.assert(this.constraints, 'constraints not constructed.'); const eventCount = Object.keys(status.eventMap).length; const ones = Array(eventCount).fill(true); const fixed = Array(eventCount).fill(false); const inbalances = []; for (const constraint of this.constraints) { const sum = constraint.reduce((sum, it) => sum + it, 0); if (sum !== 0) { const c = sum < 0 ? constraint.map((it) => -it) : constraint; if (c[0] > 0) continue; // entire measure edge usually is larger than others, no effect inbalances.push(c); // set ones for tight items c.forEach((it, i) => { fixed[i] = fixed[i] || it < 0; if (it) ones[i] = it < 0 || fixed[i]; }); } } // pick out influenced equations this.constraints.forEach((constraint) => { const sum = constraint.reduce((sum, it) => sum + it, 0); if (sum === 0 && !constraint[0]) { if (constraint.some((it, i) => it && !ones[i])) { constraint.forEach((it, i) => it && (ones[i] = false)); inbalances.push(constraint); } } }); return { ones, inbalances }; } solveEquations({ ones, inbalances }) { if (!inbalances.length) return ones.map(() => 1); const xis = ones .map((fixed, i) => ({ fixed, i })) .filter(({ fixed }) => !fixed) .map(({ i }) => i) .filter((i) => inbalances.some((items) => items[i] !== 0)); if (!xis.length) return ones.map(() => 1); const factors = xis.map((i) => Math.abs(inbalances.find((items) => items[i] !== 0)[i])); const equationMap = new Map(); let conflicted = false; const lines = inbalances .map((items) => { const line = items.filter((_, i) => xis.includes(i)); const bias = -items.reduce((sum, it, i) => sum + (xis.includes(i) ? 0 : it), 0); return { line, bias }; // remove duplicated equations }) .filter(({ line, bias }) => { if (line.every((it) => it === 0)) return false; const id = line.join(','); if (equationMap.has(id)) { conflicted = equationMap.get(id) !== bias; return false; } equationMap.set(id, bias); return true; }); if (conflicted) return null; const squareLines = lines.slice(0, xis.length); const restLines = lines.slice(xis.length); if (squareLines.length < xis.length) { const candidateLines = []; for (let i1 = 0; i1 < xis.length - 1; ++i1) { const i2 = i1 + 1; const line = { line: xis.map((_, i) => (i === i1 ? 1 : i === i2 ? -1 : 0)), bias: 0, prior: (factors[i1] + factors[i2]) / DURATION_MULTIPLIER, }; if (squareLines.some((sl) => sl.line[i1] && sl.line[i2])) line.prior -= 10; if (squareLines.some((sl) => sl.line.filter(Number).length === 1 && (sl.line[i1] || sl.line[i2]))) line.prior += 1; candidateLines.push(line); } candidateLines.sort((c1, c2) => c1.prior - c2.prior); squareLines.push(...candidateLines.slice(0, xis.length - squareLines.length)); } //console.assert(squareLines.length, "squareLines is empty.", lines, xis, equationMap, inbalances); const matrix = squareLines.map(({ line }) => line); const bias = squareLines.map(({ bias }) => bias); const invert = matrixInverse(matrix); if (!invert) { this.logger.warn('null invert:', matrix); //debugger; return null; } const solution = invert.map((row) => row.reduce((sum, it, i) => sum + it * bias[i], 0)); //console.log("solution:", matrix, invert, solution); if (restLines.length) { if (restLines.some((line) => Math.abs(line.line.reduce((sum, it, i) => sum + it * solution[i], 0)) > 1e-3)) { //console.debug("rest lines not satisfied:", restLines, solution); return null; } } const result = ones.map(() => 1); xis.forEach((xi, i) => (result[xi] = solution[i])); return result; } optimallySolve(status) { const { ones, inbalances } = this.inbalancesConstraints(status); //if (this.like("2 1-2 9|1 2-3 3-4 9-10 4-5 5-6 6-7 7-8 8-. 12|6 11-12 10-11")) // debugger; const shrinknesses = ones.map((fixed, id) => (fixed ? -1 : roundNumber(status.eventMap[id].shrinkness, 0.01))); const shrinkMap = shrinknesses.reduce((map, shrinkness, id) => { if (shrinkness >= 0) { map[shrinkness] = map[shrinkness] || []; map[shrinkness].push(id); } return map; }, {}); const groups = Object.entries(shrinkMap) .sort((p1, p2) => Number(p2[0]) - Number(p1[0])) .map((pair) => pair[1]); //console.log("groups:", groups, shrinknesses); for (let released = 1; released < groups.length; ++released) { const releasedIds = [].concat(...groups.slice(0, released)); const fixed = ones.map((_, id) => !releasedIds.includes(id)); const warps = this.solveEquations({ ones: fixed, inbalances }); if (warps && warps.every((it, i) => it <= 1 && it > status.eventMap[i].lowWarp)) return warps; } return this.solveEquations({ ones, inbalances }); } isConflicted(status) { const { ones, inbalances } = this.inbalancesConstraints(status); //if (this.like("2 8|2 8-9 3|9 2-3 3-4 10|4 4-5 5|11 11-12 6|12 5-6 10-11 9-10 6-7")) // debugger; for (const c of inbalances) { // sum with low warps const lowSum = c.reduce((sum, it, i) => sum + it * (ones[i] || it <= 0 ? 1 : status.eventMap[i].lowWarp), 0); if (lowSum >= 0) { // mark events' broken tendency c.forEach((it, i) => { if (it) status.eventTendencies[i] += it > 0 ? 1 : -1; }); return true; } } if (!inbalances.length) return false; const timeWarps = this.solveEquations({ ones, inbalances }); if (!timeWarps) return true; return !timeWarps.every((it, i) => it > status.eventMap[i].lowWarp && it <= 1); } getSolution(status) { const actionKey = (action) => status.eventMap[action.e2] ? status.eventMap[action.e2].x + Math.abs(status.eventMap[action.e2].x - status.eventMap[action.e1].x) * 0.06 : status.eventMap[action.e1].x + 1e4; const hacts = this.actions.filter((action) => action.type === ActionType.HORIZONTAL).sort((a1, a2) => actionKey(a1) - actionKey(a2)); const hmap = hacts.reduce((map, act) => ({ ...map, [act.e1]: act.e2 }), {}); const startEs = new Set([...Object.keys(hmap)].map(Number)); hacts.forEach((act) => startEs.delete(act.e2)); this.stages[0].events.forEach((eid) => eid > 0 && startEs.add(eid)); let voices = [...startEs].map((se) => { const voice = [se]; let x = se; while (hmap[x]) { x = hmap[x]; if (x < 0 || voice.includes(x)) break; voice.push(x); } return voice; }); const events = Object.values(status.eventMap) .filter((e) => e.id > 0) .map((e) => ({ id: e.id, tick: null, endTick: null, tickGroup: null, timeWarp: null, })); const eventMap = events .filter((e) => voices.some((voice) => voice.includes(e.id)) || hacts.some((act) => [act.e1, act.e2].includes(e.id))) .reduce((map, e) => ({ ...map, [e.id]: e }), {}); this.stages.forEach((stage, si) => stage.events.forEach((eid) => eventMap[eid] && (eventMap[eid].tickGroup = si))); this.stages[0].tick = 0; this.stages[0].events.forEach((eid) => eventMap[eid] && (eventMap[eid].tick = 0)); // solve time warps const timeWarps = this.optimallySolve(status); events.forEach((e) => (e.timeWarp = floatToTimeWarp(timeWarps[e.id]))); //if (this.like("1 12|1 1-2 9|2 2-3 13|3 3-4 4-5 10|5 14|10 10-11 8-9 14-15 15|6 6-7 7-. 13-14 5-6 12-13 9-10")) // debugger; // solve stage ticks const estages = this.stages.slice(0, this.stages.length - 1); const solveStages = () => { if (estages.every((stage) => Number.isFinite(stage.tick))) return false; let changed = false; // forward hacts.forEach((act) => { const stage1 = this.stages.find((stage) => stage.events.includes(act.e1)); const stage2 = this.stages.find((stage) => stage.events.includes(act.e2)); if (Number.isFinite(stage1.tick) && !Number.isFinite(stage2.tick)) { stage2.tick = stage1.tick + fractionMul(status.eventMap[act.e1].duration, eventMap[act.e1].timeWarp); stage2.events.forEach((eid) => eventMap[eid] && (eventMap[eid].tick = stage2.tick)); changed = true; } }); // backward [...hacts].reverse().forEach((act) => { const stage1 = this.stages.find((stage) => stage.events.includes(act.e1)); const stage2 = this.stages.find((stage) => stage.events.includes(act.e2)); if (!Number.isFinite(stage1.tick) && Number.isFinite(stage2.tick)) { stage1.tick = stage2.tick - fractionMul(status.eventMap[act.e1].duration, eventMap[act.e1].timeWarp); stage1.events.forEach((eid) => eventMap[eid] && (eventMap[eid].tick = stage1.tick)); changed = true; } }); return changed; }; while (solveStages()) ; console.assert(estages.every((stage) => Number.isFinite(stage.tick)), 'stage ticks not all solved:', this.stages, this.id); events .filter((event) => Number.isFinite(event.tick)) .forEach((event) => (event.endTick = event.tick + fractionMul(status.eventMap[event.id].duration, event.timeWarp))); // clip out of bound events const measureDuration = status.eventMap[0].duration; voices.forEach((voice) => { const outEI = voice.findIndex((eid) => eventMap[eid].endTick > measureDuration); if (outEI >= 0) { const es = voice.splice(outEI, voice.length - outEI); es.forEach((eid) => { eventMap[eid].tick = null; eventMap[eid].endTick = null; }); } }); voices = voices.filter((voice) => voice.length); const duration = Math.max(0, ...events.map((e) => e.endTick).filter(Number.isFinite)); //console.log("getSolution:", this); this.logger.debug(String.fromCodePoint(0x1f34e), this.id, timeWarps); return { voices, events, duration, actions: this.actions.map((action) => action.id).join(' '), }; } deduce(status, quota) { if (!this.stages) this.constructStages(status); //console.log("deduce:", status); // increase access counting const access = status.actionAccessing.get(this.id) || { times: 0 }; ++access.times; status.actionAccessing.set(this.id, access); this.constructConstraints(status); //console.log("constraints:", this.id, this.stages, this.constraints); if (this.isConflicted(status)) { access.closed = true; this.logger.info(this.action.id, '\u274c'); return null; } //const newStatus = status; this.logger.group(this.action && this.action.id); if (quota.credits > 0) { --quota.credits; if (!this.children) this.expand(status); this.children = this.children.filter((node) => !status.actionAccessing.get(node.id) || !status.actionAccessing.get(node.id).closed); if (this.children.length) { const p = (node) => node.possibility / ((status.actionAccessing.get(node.id) || { times: 0 }).times + 1); this.children.sort((n1, n2) => p(n2) - p(n1)); for (const child of this.children) { const solution = child.deduce(status, quota); if (solution) { this.logger.groupEnd(); return solution; } if (quota.credits <= 0) break; } } //else // console.debug("got the leaf:", this, status); } else this.logger.debug('quota exhausted.'); this.logger.groupEnd(); access.closed = true; return this.getSolution(status); } expand(status) { //this.action.events.forEach(eid => status.pendingEvents.delete(eid)); this.constructStages(status); const { eventMap, matrixV, matrixH } = status; const stagedEvents = this.stagedEvents; const branches = []; const appendBranch = (branch) => { if (!this.actions.some((a) => a.id === branch.action.id) && !branches.some((b) => b.action.id === branch.action.id)) { const stage1 = this.stages.find((stage) => stage.events.includes(branch.action.e1)); const stage2 = this.stages.find((stage) => stage.events.includes(branch.action.e2)); if (stage1 === stage2 || (stage1 && stage2 && stage1.index >= stage2.index)) return; if (stage1 && stage2) { if (branch.action.type === ActionType.VERTICAL) { if (stage2.index - stage1.index > 1) return; if (this.actions.some((a) => stage1.events.includes(a.e1) && stage2.events.includes(a.e2))) return; } else if (branch.action.type === ActionType.HORIZONTAL) { if (stage1.index > stage2.index) return; } } if (branch.action.type === ActionType.HORIZONTAL && this.actions.some((a) => a.type === ActionType.HORIZONTAL && (a.e1 === branch.action.e1 || a.e2 === branch.action.e2 || (a.e1 === branch.action.e2 && a.e2 === branch.action.e1)))) return; // exclude 2 too far away events by vertical if (branch.action.type === ActionType.VERTICAL) { if (stage1) { branch.possibility = Math.min(branch.possibility, ...stage1.events.map((e) => matrixV[branch.action.e2][e])); if (branch.possibility <= 0) return; } if (stage2) { branch.possibility = Math.min(branch.possibility, ...stage2.events.map((e) => matrixV[e][branch.action.e1])); if (branch.possibility <= 0) return; } } branches.push(branch); } }; for (const eid of stagedEvents) { if (eid < 0) continue; matrixV[eid].forEach((p, id) => { if (p > 0 && eid !== id) appendBranch({ action: Action.V(id, eid), possibility: p }); }); matrixV.forEach((ps, id) => { const p = ps[eid]; if (p > 0) appendBranch({ action: Action.V(eid, id), possibility: p }); }); matrixH[eid].forEach((p, id) => { if (p > 0) appendBranch({ action: Action.H(id, eid), possibility: p }); }); matrixH.forEach((ps, id) => { id = id >= Object.keys(eventMap).length ? -1 : id; const p = ps[eid]; if (p > 0) appendBranch({ action: Action.H(eid, id), possibility: p }); }); } // If branches not contains extending actions, clear it. // Because pure inner vertical action may be harmful if (!branches.some((branch) => [ActionType.HORIZONTAL, ActionType.PLACE].includes(branch.action.type) || !stagedEvents.has(branch.action.e1) || !stagedEvents.has(branch.action.e2))) { this.children = []; return; } //console.table(branches.map(b => [b.action.id, b.possibility])); //console.log("branches:", branches.map(b => b.action.id).join(", "), "\n", this.actions.map(a => a.id).join(", ")); this.children = branches.map((branch) => new PathNode({ logger: this.logger, parent: this, ...branch })); } } class Solver { constructor(env, { quota = 1000, logger = new DummyLogger() } = {}) { this.quota = quota; this.logger = logger; const event0 = { id: 0, x: 0, confidence: 1, shrinkness: env.measureShrinkness, duration: env.expectedDuration, lowWarp: 0, }; this.events = [ event0, ...env.events.map((e) => ({ id: e.id, x: e.x, confidence: e.confidence, shrinkness: e.shrinkness, staff: e.staff, duration: e.duration, lowWarp: 0.5, })), ]; this.eventMap = this.events.reduce((map, e) => ({ ...map, [e.id]: e }), {}); this.matrixH = env.matrixH; this.matrixV = env.matrixV; this.xSpan = env.endX - Math.min(env.endX - 1, ...env.events.map((e) => e.x)); this.actionAccessing = new Map(); } solve() { // construct path root this.pathRoot = new PathNode({ logger: this.logger, action: null, }); this.pathRoot.children = this.events.slice(1).map((event) => new PathNode({ logger: this.logger, parent: this.pathRoot, action: Action.P(event.id), possibility: this.matrixV[event.id].reduce((sum, p) => sum + p, 0), })); let bestSolution = null; this.logger.groupCollapsed('solve'); const eventTendencies = Array(this.events.length).fill(0); const quota = { credits: this.quota, times: 0 }; while (quota.credits > 0) { ++quota.times; const status = { eventMap: this.eventMap, matrixH: this.matrixH, matrixV: this.matrixV, actionAccessing: this.actionAccessing, eventTendencies, }; const solution = this.pathRoot.deduce(status, quota); solution.credits = this.quota - quota.credits; solution.times = quota.times; this.evaluateSolution(solution); this.logger.debug('loss:', solution.loss); bestSolution = !bestSolution || solution.loss < bestSolution.loss ? solution : bestSolution; if (!bestSolution.loss) break; // check if searching tree traversed if (this.actionAccessing.get('').closed) break; } this.logger.groupEnd(); this.logger.debug('solution', bestSolution && bestSolution.loss, bestSolution); this.logger.debug('cost:', this.quota - quota.credits); this.logger.debug('eventTendencies:', eventTendencies.map((t) => t / quota.times)); return bestSolution; } evaluateSolution(solution) { solution.loss = 0; const eventMap = solution.events.reduce((map, e) => ({ ...map, [e.id]: { ...e, ...this.eventMap[e.id] } }), {}); /*// minus tick const minuses = solution.events.filter((e) => e.tick < 0).length; solution.loss += minuses * 1000;*/ // minus tick rates penalty const events = solution.events.filter((event) => Number.isFinite(event.tick)).map((event) => eventMap[event.id]); const sevents = events.reduce((map, event) => { map[event.staff] = map[event.staff] || []; map[event.staff].push(event); return map; }, {}); Object.values(sevents).forEach((es) => { const ses = es.sort((e1, e2) => e1.x - e2.x).slice(0, es.length - 1); ses.forEach((e1, i) => { const e2 = es[i + 1]; if (e2.tick < e1.tick) solution.loss += 1000; }); }); const times = new Map(); solution.events.forEach((event) => { if (!Number.isFinite(event.tick) || solution.voices.every((voice) => !voice.includes(event.id))) solution.loss += 100 * eventMap[event.id].confidence; if (event.timeWarp) { const { numerator, denominator } = event.timeWarp; const shrinkness = eventMap[event.id].shrinkness; times.set(numerator, Math.max(times.get(numerator) || 0, 1 - shrinkness)); times.set(denominator, Math.max(times.get(denominator) || 0, 1 - shrinkness)); } }); // partial measure penalty const partialFrac = reducedFraction(solution.duration, this.eventMap[0].duration); times.set(partialFrac.numerator, Math.max(times.get(partialFrac.numerator) || 0, 1 - this.eventMap[0].shrinkness)); times.set(partialFrac.denominator, Math.max(times.get(partialFrac.denominator) || 0, 1 - this.eventMap[0].shrinkness)); for (const [n, weight] of times.entries()) { if (n > 1) solution.loss += Math.log(n) * weight; } let spaceTime = 0; let staffAlters = 0; solution.voices.forEach((voice) => { console.assert(eventMap[voice[0]], 'invalid voice:', voice, Object.keys(eventMap)); const start = Math.abs(eventMap[voice[0]].tick); // abs: penalty for minus start const end = eventMap[voice[voice.length - 1]].endTick; spaceTime += Math.max(0, start + solution.duration - end); // staff alternation penalty let staff = null; voice.forEach((id) => { const event = eventMap[id]; if (event.staff !== staff) { if (staff !== null) ++staffAlters; staff = event.staff; } }); }); solution.loss += (spaceTime * 10) / DURATION_MULTIPLIER; solution.loss += 5 ** staffAlters - 1; // tick twist const eventsXOrder = [...events].sort((e1, e2) => e1.x - e2.x); const tickTwists = eventsXOrder.slice(1).map((e2, i) => { const e1 = eventsXOrder[i]; const dx = e2.x - e1.x; const dt = e2.tick - e1.tick; if (!dt) return dx / this.xSpan; const rate = Math.atan2(dt / solution.duration, dx / this.xSpan); return ((rate * 4) / Math.PI - 1) ** 2; }); const tickTwist = Math.max(...tickTwists, 0); solution.loss += tickTwist ** 2; console.assert(solution.loss >= 0, 'Invalid solution loss!!!', solution.loss, times, spaceTime, staffAlters); if (solution.loss < 0) solution.loss = Infinity; } } class PatchMeasure extends SimpleClass { constructor(data) { super(); Object.assign(this, data); } get staffN() { return Math.floor(Math.log2(this.staffMask)) + 1; } get basics() { return Array(this.staffN).fill(this.basic); } get duration() { return Math.max(0, ...this.voices.map((ids) => { const events = ids.map((id) => this.events.find((e) => e.id === id)); return events.reduce((duration, event) => duration + event.duration, 0); })); } } PatchMeasure.className = 'PatchMeasure'; var EventElementType; (function (EventElementType) { EventElementType[EventElementType["PAD"] = 0] = "PAD"; EventElementType[EventElementType["BOS"] = 1] = "BOS"; EventElementType[EventElementType["EOS"] = 2] = "EOS"; EventElementType[EventElementType["CHORD"] = 3] = "CHORD"; EventElementType[EventElementType["REST"] = 4] = "REST"; })(EventElementType || (EventElementType = {})); class EventCluster extends SimpleClass { constructor(data) { super(); super.assign(data); } get regular() { return (this.elements.some((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && !elem.fake) && this.elements.every((elem) => [elem.x, elem.y1, elem.y2, elem.tick].every(Number.isFinite)) && this.elements .slice(1) .every((elem, ei) => elem.fake || this.elements[ei].fake || elem.grace || this.elements[ei].grace || elem.fullMeasure || this.elements[ei].fullMeasure || elem.tick <= this.elements[ei].tick || elem.x > this.elements[ei].x)); } get grant() { return this.annotation && this.annotation.grant; } get feature() { return { index: this.index, elements: this.elements, }; } get estimatedDuration() { const endElem = this.elements.find((elem) => elem.type === EventElementType.EOS); const tick = endElem?.predisposition ? endElem.predisposition?.tick : endElem?.tick; return Number.isFinite(tick) ? tick : this.duration; } assignPrediction(prediction) { console.assert(prediction.index === this.index, 'index mismatch:', prediction.index, this.index); this.matrixH = prediction.matrixH; prediction.elements.forEach((pe) => { const { index, ...predisposition } = pe; const elem = this.elements.find((elem) => elem.index === index); console.assert(elem, 'element not found:', index); if (elem) elem.predisposition = predisposition; }); } } EventCluster.className = 'EventCluster'; EventCluster.blackKeys = ['id']; class EventClusterSet extends SimpleClass { constructor(data) { super(); super.assign(data); } trimIrregular() { let ir = 0; this.clusters = this.clusters.filter((cluster) => { const regular = cluster.regular; if (!regular) { console.debug('irregular cluster:', cluster); ++ir; } return regular; }); if (ir) console.debug('Irregular clusters trimmed:', `${ir}/${this.clusters.length + ir}`); else console.debug(`The EventClusterSet (${this.clusters.length}) is fine.`); return ir; } } EventClusterSet.className = 'EventClusterSet'; var SimplePolicy; (function (SimplePolicy) { const constructXMap = (measure) => { const xMap = new Map(); // mark full measure rests measure.rows.forEach((row) => { if (row.events.length === 1) { const event = row.events[0]; if (event.rest && event.division === 0) event.rest = 'R'; } }); measure.events.forEach((event) => { const x = Math.round(event.pivotX * 10) / 10; let key = 0; if (event.fullMeasureRest) key = Math.min(x, ...xMap.keys()); else { key = [...xMap.keys()].find((k) => { // check if the event is aligned with the current chord const es = xMap.get(k); const left = Math.min(...es.map((e) => e.left)); const right = Math.max(...es.map((e) => e.right)); const overlaySize = Math.min(right, event.right) - Math.max(left, event.left); return overlaySize > NOTEHEAD_WIDTHS.NoteheadS1 * 0.62; }) || x; } event.roundX = key; const es = xMap.get(key) || []; xMap.set(key, es); es.push(event); }); return xMap; }; SimplePolicy.computeMeasureTicks = (measure) => { const xMap = constructXMap(measure); let tick = 0; const ts = new Set([tick]); const eventGroups = [...xMap.entries()].sort(([x1], [x2]) => x1 - x2); //.map(entry => entry[1]); for (const [x, events] of eventGroups) { events.forEach((event) => { if (event.predisposition) { event.rest = event.rest && event.predisposition.fullMeasure > 0.5 ? 'R' : event.rest; event.grace = event.predisposition.grace ? GraceType.Grace : null; event.division = argmax$1(event.predisposition.divisionVector); event.dots = argmax$1(event.predisposition.dotsVector); if (event.predisposition.timeWarped > 0.5) event.timeWarp = frac(2, 3); } if (event.fullMeasureRest) event.tick = 0; else { if (event.zeroHolder) tick -= event.duration; if (!event.zeroHolder && event.predisposition && Number.isInteger(event.predisposition.tick)) event.tick = event.predisposition.tick; else event.tick = tick; ts.add(event.tick + event.duration); } //console.log("append tick:", event.tick + event.duration, event); }); ts.delete(tick); //column.xToTick[x] = tick; if (ts.size) tick = Math.min(...ts); } if (Number.isInteger(measure.estimatedDuration)) measure.duration = measure.estimatedDuration; else measure.duration = Math.max(...ts, 0); }; SimplePolicy.computeMeasureVoices = (measure) => { measure.voices = []; for (const row of measure.rows) { const events = row.events.filter((event) => !event.grace && !event.tremoloCatcher && !event.fullMeasureRest && !(event.predisposition && event.predisposition.fake > 0.5)); const eventSet = new Set(events); while (eventSet.size) { let tick = 0; const voice = []; const pushEvent = (e) => { voice.push(e.id); if (!e.zeroHolder) tick += e.duration; eventSet.delete(e); }; const e0 = events.find((e) => eventSet.has(e)); if (e0.alignedTick > 0) { //voice.tickMap[tick] = EventTerm.space({ tick, duration: e0.alignedTick }); tick = e0.alignedTick; } pushEvent(e0); while (true) { // TODO: consider slur pair const e = events.find((e) => eventSet.has(e) && e.alignedTick === tick); if (!e) break; pushEvent(e); } //if (tick < measure.duration) // voice.tickMap[tick] = EventTerm.space({ tick, duration: staff.duration - tick }); measure.voices.push(voice); } } }; })(SimplePolicy || (SimplePolicy = {})); const solveGraceEvents = (measure) => { const graceEvents = measure.events.filter((event) => event.grace /*&& !Number.isFinite(event.tick)*/); if (!graceEvents.length) return; const tickMap = measure.tickMap; const staffMap = [...tickMap.entries()].reduce((smap, [tick, events]) => { events.forEach((event) => { if (!event.grace) { smap[event.staff] = smap[event.staff] || {}; const oldEvent = smap[event.staff][tick]; smap[event.staff][tick] = !oldEvent || oldEvent.x > event.x ? event : oldEvent; } }); return smap; }, {}); const staffPositions = Object.entries(staffMap).reduce((map, [staff, emap]) => { map[staff] = Object.entries(emap) .map(([t, event]) => ({ event, tick: Number(t), preTick: -240, graces: [] })) .sort((p1, p2) => p1.event.x - p2.event.x); map[staff].push({ tick: measure.duration, event: measure.endEvent, preTick: 0, graces: [] }); // terminal bar let tick = 0; map[staff].forEach((position) => { if (position.tick > tick) { position.preTick = tick; tick = position.tick; } }); return map; }, {}); // append grace events into positions graceEvents.forEach((event) => { const staff = staffPositions[event.staff]; if (staff) { const position = staff.find((p) => p.event.x > event.x); if (position) position.graces.push(event); event.roundX = event.x; //if (position.tick >= measure.duration) // event.grace = GraceType.AfterGrace; } }); Object.values(staffPositions).forEach((staff) => staff.forEach((position) => { if (position.graces.length) { position.event.graceIds = position.graces.map((e) => e.id); const totalDuration = position.graces.reduce((t, e) => t + e.duration, 0); const duration = Math.min(totalDuration, position.tick - position.preTick); const warp = duration / totalDuration; let tick = position.tick; [...position.graces].reverse().forEach((event) => { event.tick = Math.round(tick - event.duration * warp); tick = event.tick; }); } })); }; const solveTremoloPairs = (measure) => { const catchers = measure.events.filter((event) => event.tremoloCatcher && !event.grace); const pitchers = measure.events.filter((event) => event.tremoloLink === TremoloLink.Pitcher && !event.grace); catchers.forEach((catcher) => { let candidates = pitchers.filter((event) => event.division === catcher.division && event.x < catcher.x); if (!candidates.length) candidates = measure.events.filter((event) => Number.isFinite(event.tick) && !event.grace && !event.rest && event.division === catcher.division && event.dots === catcher.dots && event.x < catcher.x); candidates.sort((c1, c2) => c2.x - c1.x); if (candidates.length) { const pitcher = candidates[0]; pitcher.catcherId = catcher.id; const tremolo = Math.max(pitcher.tremolo || 3, catcher.tremolo || 3); pitcher.tremolo = tremolo; catcher.tremolo = tremolo; if (!catcher.tick) catcher.tick = pitcher.tick + pitcher.duration / 2; const pi = pitchers.indexOf(pitcher); if (pi >= 0) pitchers.splice(pi, 1); } }); }; var EquationPolicy; (function (EquationPolicy) { const DURATION_MULTIPLIER = 128 * 3 * 5 * 7 * 11 * 13; const CHORDS_SEAM_SIGMA = 0.6; const NEIGHBOR_CHORDS_SIGMA = 1.6; const Y_DECAY_SIGMA = 16; const STAFF_DECAY_FACTOR = 2; const STEM_DIRECTION_DECAY = 0.9; const ILL_BEAMS_PENALTY = 0.2; const INVERT_SQRT2 = 0.7071067811865475; const MATRIX_H_WEIGHT = 3; const FINE_BEAMS = [ [null, null], [null, StemBeam.Open], [StemBeam.Open, StemBeam.Continue], [StemBeam.Open, StemBeam.Close], [StemBeam.Continue, StemBeam.Continue], [StemBeam.Continue, StemBeam.Close], [StemBeam.Close, null], [StemBeam.Close, StemBeam.Open], ].map((bb) => bb.join('-')); const solveStaffGroup = (staffGroup, options) => { if (!staffGroup.events.length) { return { events: [], voices: [], duration: 0, }; } const solver = new Solver(staffGroup, options); return solver.solve(); }; EquationPolicy.estiamteMeasure = (measure) => { const allEvents = measure.events .filter((event) => !event.zeroHolder) .map((event) => ({ id: event.id, staff: event.staff, x: event.x, tickEstimated: event.predisposition && Number.isFinite(event.predisposition.tick) ? event.predisposition.tick : event.x, tipX: event.tipX, y: event.tipY + event.staff * 100, duration: (event.mainDuration * DURATION_MULTIPLIER) / WHOLE_DURATION, division: event.division, dots: event.dots, stemDirection: event.stemDirection, beam: event.beam, rest: event.rest, // the possibility of full measure rest pR: event.rest === 'R' ? 1 : event.rest === 'r' && event.division === 0 ? Math.tanh(event.x - measure.eventStartX) : 0, fakeP: event.predisposition ? event.predisposition.fakeP || 0 : 0, shrinkness: event.predisposition ? event.predisposition.timeWarped : null, })); let expectedDuration = (DURATION_MULTIPLIER * measure.timeSignature.numerator) / measure.timeSignature.denominator; if (Number.isFinite(measure.estimatedDuration)) expectedDuration = Math.max(expectedDuration, roundNumber(measure.estimatedDuration, DURATION_MULTIPLIER / 4)); const staffGroupMap = measure.staffGroups.reduce((map, staves, group) => { staves.forEach((staff) => (map[staff] = group)); return map; }, {}); const ids = [0, ...allEvents.map((e) => e.id)]; // compact ids const ievents = allEvents.map((e) => ({ ...e, id: ids.indexOf(e.id), x: e.x - measure.startX, confidence: (1 - e.pR) * (1 - e.fakeP), shrinkness: Number.isFinite(e.shrinkness) ? e.shrinkness : Math.tanh((e.division - e.dots * 0.1) / 4), staffGroup: staffGroupMap[e.staff], })); // estimate topology matrices const matrixH = Array(ids.length + 1) .fill(null) .map(() => Array(ids.length).fill(0)); const matrixV = Array(ids.length) .fill(null) .map(() => Array(ids.length).fill(0)); //const hp = (dx: number): number => 1 - erf(((dx / NEIGHBOR_CHORDS_SIGMA) ** 0.6) * INVERT_SQRT2); const hp = (dx) => erf__default["default"](dx / NEIGHBOR_CHORDS_SIGMA) * erf__default["default"](NEIGHBOR_CHORDS_SIGMA / dx); for (const e1 of ievents) { for (const e2 of ievents) { matrixV[e1.id][e2.id] = e1 !== e2 && e1.tickEstimated >= e2.tickEstimated ? 1 - erf__default["default"](((e1.tickEstimated - e2.tickEstimated) * INVERT_SQRT2) / CHORDS_SEAM_SIGMA) : 0; if (e1.staffGroup !== e2.staffGroup) matrixH[e1.id][e2.id] = 0; // prohibit voice crossing staff groups else if (e1.x <= e2.x) matrixH[e1.id][e2.id] = 0; else { const staffDecay = Math.exp(-Math.abs(e1.staff - e2.staff) * STAFF_DECAY_FACTOR); const yDecay = e1.staff === e2.staff ? Math.exp(-Math.abs(e1.y - e2.y) / Y_DECAY_SIGMA) : 1; const dx = e1.x - e2.x; const dtx = e1.tipX - e2.tipX; matrixH[e1.id][e2.id] = (staffDecay * yDecay * Math.min(hp(dx), hp(dtx))) ** (1 / MATRIX_H_WEIGHT); } // weaken full measure rest connections const nR = (1 - e1.pR) * (1 - e2.pR); matrixV[e1.id][e2.id] *= nR; matrixH[e1.id][e2.id] *= nR; if (matrixV[e1.id][e2.id] < 1e-2) matrixV[e1.id][e2.id] = 0; // weaken inconsistent stem directions if (e1.stemDirection && e2.stemDirection && e1.stemDirection !== e2.stemDirection) matrixH[e1.id][e2.id] *= STEM_DIRECTION_DECAY; // ill beams penalty if (!e1.rest && !e2.rest && !FINE_BEAMS.includes([e2.beam, e1.beam].join('-'))) matrixH[e1.id][e2.id] *= ILL_BEAMS_PENALTY; } // H possibility of e1 and end of measure matrixH[ids.length][e1.id] = hp(measure.width - e1.x) ** (1 / MATRIX_H_WEIGHT); } return { ids, events: ievents, expectedDuration, measureShrinkness: 0, endX: measure.position.right, matrixH, matrixV, }; }; EquationPolicy.regulateMeasure = async (measure, { solver = null, ...options }) => { const env = EquationPolicy.estiamteMeasure(measure); const { ids, matrixH, matrixV } = env; // copy matrices values from measure topology data if (measure.matrixH) { console.assert(measure.matrixH.length > ids[ids.length - 1] && measure.matrixH[0].length > ids[ids.length - 1], 'matrix shape mismatch:', ids.length, `${measure.matrixH.length}x${measure.matrixH[0].length}`, `${matrixH.length}x${matrixH[0].length}`); for (let i = 0; i < ids.length + 1; i++) { const ii = i < ids.length ? ids[i] : measure.matrixH.length - 1; for (let j = 1; j < ids.length; j++) matrixH[i][j] = measure.matrixH[ii][ids[j]]; } } if (measure.matrixV) { matrixV.forEach((row, i) => row.forEach((_, j) => { const mp = measure.matrixV[ids[i]][ids[j]]; if (Number.isFinite(mp)) matrixV[i][j] = mp; })); } if (Number.isFinite(measure.estimatedDuration)) env.measureShrinkness = Math.tanh(Math.log(Math.min(1, measure.estimatedDuration / measure.duration)) * -3); if (options.logger) options.logger.info('--- MEASURE', measure.measureIndex, '---', env); const solution = solver ? await solver(env, options) : solveStaffGroup(env, options); const resultEvents = solution.events.map((e) => ({ ...e, id: env.ids[e.id], // decode compact ids })); resultEvents.forEach((e) => { const event = measure.events.find((e0) => e0.id === e.id); event.tick = Number.isFinite(e.tick) ? Math.round((e.tick * WHOLE_DURATION) / DURATION_MULTIPLIER) : null; event.tickGroup = e.tickGroup; event.timeWarp = e.timeWarp; }); measure.duration = Math.round((solution.duration * WHOLE_DURATION) / DURATION_MULTIPLIER); measure.voices = solution.voices.map((voice) => voice.map((id) => env.ids[id])); measure.solutionStat = { loss: solution.loss, solverCredits: solution.credits, solverTimes: solution.times, }; // full measure rests measure.events.forEach((event) => { const result = resultEvents.find((e) => e.id === event.id); if (!result) return; else if (!Number.isFinite(result.tick) && event.rest === 'r' && event.division === 0) { event.tick = 0; event.tickGroup = 0; event.rest = 'R'; event.duration = measure.duration; measure.voices.push([event.id]); } else if (event.rest === 'R') { event.tick = 0; event.tickGroup = 0; event.duration = measure.duration; measure.voices.push([event.id]); } }); }; EquationPolicy.regulateMeasureWithRectification = async (measure, rectification, { solver = null, ...options }) => { const allEvents = measure.events .filter((event) => !event.zeroHolder) .map((event) => { const re = rectification.events.find((e) => e && e.id === event.id); const division = Number.isFinite(re?.division) ? re.division : event.division; const dots = Number.isFinite(re?.dots) ? re.dots : event.dots; const duration = DURATION_MULTIPLIER * 2 ** -division * (2 - 2 ** -dots); return { id: event.id, staff: event.staff, x: event.x, tickEstimated: event.predisposition?.tick, y: event.tipY + event.staff * 100, duration, // the possibility of full measure rest pR: event.rest === 'R' ? 1 : event.rest === 'r' && event.division === 0 ? Math.tanh(event.x - measure.eventStartX) : 0, fakeP: event.predisposition ? event.predisposition.fakeP || 0 : 0, shrinkness: event.predisposition?.timeWarped || 0, }; }); let expectedDuration = (DURATION_MULTIPLIER * measure.timeSignature.numerator) / measure.timeSignature.denominator; if (Number.isFinite(measure.estimatedDuration)) expectedDuration = Math.max(expectedDuration, roundNumber(measure.estimatedDuration, DURATION_MULTIPLIER / 4)); const staffGroupMap = measure.staffGroups.reduce((map, staves, group) => { staves.forEach((staff) => (map[staff] = group)); return map; }, {}); const ids = [0, ...allEvents.map((e) => e.id)]; // compact ids const ievents = allEvents.map((e) => ({ ...e, id: ids.indexOf(e.id), x: e.x - measure.startX, confidence: (1 - e.pR) * (1 - e.fakeP), shrinkness: e.shrinkness, staffGroup: staffGroupMap[e.staff], })); // estimate topology matrices const matrixH = Array(ids.length + 1) .fill(null) .map(() => Array(ids.length).fill(0)); const matrixV = Array(ids.length) .fill(null) .map(() => Array(ids.length).fill(0)); for (const e1 of ievents) { for (const e2 of ievents) { matrixV[e1.id][e2.id] = e1 !== e2 && e1.tickEstimated >= e2.tickEstimated ? 1 - erf__default["default"](((e1.tickEstimated - e2.tickEstimated) * INVERT_SQRT2) / CHORDS_SEAM_SIGMA) : 0; // weaken full measure rest connections const nR = (1 - e1.pR) * (1 - e2.pR); matrixV[e1.id][e2.id] *= nR; if (matrixV[e1.id][e2.id] < 1e-2) matrixV[e1.id][e2.id] = 0; } } // copy matrices values from measure topology data console.assert(measure.matrixH && measure.matrixH.length > ids[ids.length - 1] && measure.matrixH[0].length > ids[ids.length - 1], 'matrix shape mismatch:', ids.length, `${measure.matrixH.length}x${measure.matrixH[0].length}`, `${matrixH.length}x${matrixH[0].length}`); for (let i = 0; i < ids.length + 1; i++) { const ii = i < ids.length ? ids[i] : measure.matrixH.length - 1; for (let j = 1; j < ids.length; j++) matrixH[i][j] = measure.matrixH[ii][ids[j]]; } let measureShrinkness = 0; if (Number.isFinite(measure.estimatedDuration)) measureShrinkness = Math.tanh(Math.log(Math.min(1, measure.estimatedDuration / measure.duration)) * -3); const env = { ids, events: ievents, expectedDuration, measureShrinkness, endX: measure.position.right, matrixH, matrixV, }; const solution = solver ? await solver(env, options) : solveStaffGroup(env, options); const priority = -solution.loss; const events = solution.events.map(({ id, tick, tickGroup, timeWarp }) => { const re = rectification.events.find((e) => e && e.id === id); const tickN = Number.isFinite(tick) ? Math.round((tick * WHOLE_DURATION) / DURATION_MULTIPLIER) : tick; return { id, tick: tickN, tickGroup, timeWarp, division: re?.division, dots: re?.dots, }; }); const duration = Math.round((solution.duration * WHOLE_DURATION) / DURATION_MULTIPLIER); return { events, voices: solution.voices, duration, priority, }; }; })(EquationPolicy || (EquationPolicy = {})); class SpartitoMeasure extends SimpleClass { static reorderEvents(events, staffYsFull) { const HALF_NOTEHEAD = 0.7; const ys = []; const es = events.map((e) => ({ id: e.id, staff: e.staff, x: e.x / HALF_NOTEHEAD, rx: 0, ry: staffYsFull[e.staff] + e.tipY, tipY: e.tipY, prior: 0, })); es.sort((e1, e2) => e1.x - e2.x); es.slice(1).forEach((e, i) => { const dx = Math.min(Math.round(e.x - es[i].x), 2); e.rx = es[i].rx + dx; }); es.forEach((e) => { e.prior = e.staff * 1e4 + e.rx + e.tipY * 0.01; if (!ys.includes(e.ry)) ys.push(e.ry); }); es.sort((e1, e2) => e1.prior - e2.prior); ys.sort((y1, y2) => y1 - y2); let yi = 0; const yis = ys.map((y, i) => { if (!i || ys[i] - ys[i - 1] < 0.5) return yi; ++yi; return yi; }); const result = es.map((e) => new EventTerm({ ...events.find((ev) => ev.id === e.id), intX: e.rx, intY: yis[ys.indexOf(e.ry)] })); result.forEach((e, i) => (e.id = i + 1)); return result; } constructor(data) { super(); super.assign(data); if (!this.originalRegulationHash && !this.regulated) this.originalRegulationHash = this.regulationHash; this.barTypes = this.barTypes || {}; // Ensure postRegulate runs for measures deserialized with voices (e.g. from patches/JSON) // to set endEvent and roundX needed for playback cursor positioning. if (this.regulated && this.position) this.postRegulate(); } get timeSignature() { return this.basics && this.basics[0].timeSignature; } get keySignature() { return this.basics && this.basics[0].keySignature; } get timeSignatureChanged() { return this.contexts.filter(Boolean)[0].some((term) => [ContextType.TimeSignatureC, ContextType.TimeSignatureN].includes(term.type)); } get doubtfulTimesig() { return this.basics && this.basics[0].doubtfulTimesig; } get regulated() { return !!this.voices; } get validRegulated() { if (!this.voices) return false; return this.voices.flat(1).every((id) => Number.isFinite(this.events.find((e) => e.id === id)?.tick)); } get rows() { return this.contexts.map((contexts, si) => { const events = this.events.filter((e) => e.staff === si); return { events, contexts, }; }); } get eventStartX() { return this.events.length ? Math.min(...this.events.map((e) => e.x)) : this.startX; } get startX() { return this.position.left; } get width() { return this.position.right - this.position.left; } get tickMap() { return this.events .concat([this.endEvent]) .filter(Boolean) .reduce((map, event) => { if (Number.isFinite(event.tick)) { if (!map.has(event.tick)) map.set(event.tick, []); map.get(event.tick).push(event); } return map; }, new Map()); } get tickToX() { return [...this.tickMap.entries()].reduce((map, [tick, events]) => { events = events.filter((e) => !e.fullMeasureRest && !e.grace); if (events.length) { const x = Math.min(...events.map((e) => e.x)); map[tick] = x; } return map; }, {}); } get tickRates() { const events = this.events.filter((event) => Number.isFinite(event.tick) && !event.fullMeasureRest); events.sort((e1, e2) => e1.x - e2.x); return events.slice(0, events.length - 1).map((e1, i) => { const e2 = events[i + 1]; return (e2.tick - e1.tick) / Math.max(e2.x - e1.x, 1e-3); }); } get tickRatesInStaves() { const events = this.events.filter((event) => Number.isFinite(event.tick) && !event.fullMeasureRest && !event.grace); const sevents = events.reduce((map, event) => { map[event.staff] = map[event.staff] || []; map[event.staff].push(event); return map; }, {}); const rates = Object.values(sevents).map((es) => es .sort((e1, e2) => e1.x - e2.x) .slice(0, es.length - 1) .map((e1, i) => { const e2 = es[i + 1]; return (e2.tick - e1.tick) / Math.max(e2.x - e1.x, 1e-3); })); return [].concat(...rates); } get tickRatesInGroups() { const events = this.events.filter((event) => Number.isFinite(event.tick) && !event.fullMeasureRest); const gevents = events.reduce((map, event) => { const groupIndex = this.staffGroups.findIndex((group) => group.includes(event.staff)); map[groupIndex] = map[groupIndex] || []; map[groupIndex].push(event); return map; }, {}); const rates = Object.values(gevents).map((es) => es .sort((e1, e2) => e1.x - e2.x) .slice(0, es.length - 1) .map((e1, i) => { const e2 = es[i + 1]; return (e2.tick - e1.tick) / Math.max(e2.x - e1.x, 1e-3); })); return [].concat(...rates); } get tickTwist() { if (!this.duration || !this.staffGroups) return undefined; const events = this.events.filter((event) => Number.isFinite(event.tick) && !event.fullMeasureRest && !event.grace && !event.tremoloCatcher && !(event.rest && event.division === 0)); // ignore rest0 const gevents = events.reduce((map, event) => { const groupIndex = this.staffGroups.findIndex((group) => group.includes(event.staff)); map[groupIndex] = map[groupIndex] || []; map[groupIndex].push(event); return map; }, {}); const twists = Object.values(gevents).map((es) => { const eventsXOrder = [...es].sort((e1, e2) => e1.pivotX - e2.pivotX); const xSpan = this.position.right - eventsXOrder[0].x; const tickTwists = eventsXOrder.slice(1).map((e2, i) => { const e1 = eventsXOrder[i]; const dx = e2.pivotX - e1.pivotX; const dt = e2.tick - e1.tick; if (!dt) return dx / xSpan; const rate = Math.atan2(dt / this.duration, dx / xSpan); return ((rate * 4) / Math.PI - 1) ** 2; }); return Math.max(0, ...tickTwists); }); return Math.max(0, ...twists); } get eventMap() { return this.events.reduce((map, event) => { map[event.id] = event; return map; }, {}); } get empty() { return !this.events?.length || !this.voices?.length; } get hasIllEvent() { return this.regulated && this.events.some((event) => !event.zeroHolder && !Number.isFinite(event.tick) && !event.fullMeasureRest); } get brief() { const timesig = `${this.timeSignature.numerator}/${this.timeSignature.denominator}`; const eventBrieves = this.events.map((e) => [ e.staff, e.intX, Math.round(e.tip ? e.tip.y : e.ys?.[0] ?? 0), e.fullMeasureRest ? 0 : e.division, e.fullMeasureRest ? 0 : e.dots, e.rest ? 'r' : '', e.grace || '', e.stemDirection, e.beam || '', ].join('|')); return [timesig, ...eventBrieves].join('\n'); } get regulationHash() { return sha1__default["default"](this.brief); } // prefer use originalRegulationHash get regulationHash0() { return this.originalRegulationHash || this.regulationHash; } get regulationHashes() { return Array.from(new Set([this.originalRegulationHash, this.regulationHash].filter(Boolean))); } get featureWords() { if (!this.regulated || !this.voices || !this.voices.length) return null; const invalid = this.tickRatesInStaves.some((rate) => rate < 0); const mainEvents = this.events.filter((event) => !event.zeroHolder && !event.rest); const ys = mainEvents .map((event) => event.ys) .flat(1) .map((y) => `Y${-y * 2}`); const uys = Array.from(new Set(ys)); if (this.keySignature) uys.push(`K${this.keySignature}`); const voices = this.voices .map((ids) => ids.map((id) => this.events.find((e) => e.id === id)).filter((event) => !event.zeroHolder && !event.rest)) .filter((voice) => voice.length); const melodies = invalid ? [] : voices.map((es) => es.map((e) => e.scaleChord).join('-')); const rhythm = invalid ? [] : voices.map((es) => es.map((e) => e.division).join('')); if (this.timeSignature) rhythm.push(`T${this.timeSignature.numerator}/${this.timeSignature.denominator}`); return [uys, melodies, rhythm]; } get barType() { if (this.voltaEnd) return 'VoltaRight'; const typeEntris = Object.entries(this.barTypes).sort((e1, e2) => e2[1] - e1[1]); if (typeEntris[0] && typeEntris[0][1] >= 1) return typeEntris[0][0]; return null; } get partialDuration() { if (!Number.isFinite(this.duration)) return false; const signatureDuration = fractionMul(WHOLE_DURATION, this.timeSignature); return this.duration < signatureDuration; } postRegulate() { this.endEvent = new EventTerm({ tick: this.duration, x: this.position.right }); this.updateRoundX(); solveGraceEvents(this); solveTremoloPairs(this); this.updateContextTick(); } updateRoundX() { const tickToX = this.tickToX; if (tickToX) this.events.forEach((event) => { const x = tickToX[event.tick]; if (Number.isFinite(x)) event.roundX = x; }); } updateContextTick() { if (!this.staffGroups) return; const contexts = this.contexts.flat(1); this.staffGroups.flat(1).forEach((staffIndex) => { const terms = [...this.events.filter((e) => e.staff === staffIndex), ...contexts.filter((c) => c.staff === staffIndex)]; terms.sort((t1, t2) => t2.x - t1.x); // order by x from right to left let tick = this.duration; terms.forEach((term) => { if (term instanceof EventTerm) { if (!term.fullMeasureRest && !term.zeroHolder) tick = term.tick; } else if (term instanceof ContextedTerm) term.tick = tick; }); }); } asSolution(ref = undefined) { if (!this.regulated) return null; //let timeSignature = undefined; //if (ref && printFraction(ref.timeSignature) !== printFraction(this.timeSignature)) timeSignature = this.timeSignature; return { //timeSignature, events: this.events.map((e) => { const se = { id: e.id, tick: e.tick, tickGroup: e.tickGroup, timeWarp: e.timeWarp, }; if (ref) { const refEvent = ref.events.find((re) => re.id === e.id); if (refEvent) { if (e.division !== refEvent.division) se.division = e.division; if (e.dots !== refEvent.dots) se.dots = e.dots; if (e.grace !== refEvent.grace) se.grace = !!e.grace; if (e.beam !== refEvent.beam) se.beam = e.beam; if (e.fullMeasureRest !== refEvent.fullMeasureRest) se.fullMeasure = e.fullMeasureRest; } } return se; }), voices: this.voices, duration: this.duration, priority: -this.solutionStat?.loss, }; } applySolution(solution) { if (solution.timeSignature) { this.basics.forEach((basic) => { basic.timeSignature = solution.timeSignature; basic.doubtfulTimesig = false; }); } this.voices = solution.voices; this.duration = solution.duration; this.events.forEach((event) => { event.timeWarp = null; event.tick = null; event.tickGroup = null; const se = solution.events?.find((e) => e.id === event.id); if (se) { event.tick = se.tick; event.timeWarp = se.timeWarp; event.tickGroup = se.tickGroup; if (Number.isFinite(se.division)) event.division = se.division; if (Number.isFinite(se.dots)) event.dots = se.dots; if (se.beam) event.beam = se.beam; if (se.grace !== undefined) event.grace = se.grace ? GraceType.Grace : undefined; if (se.fullMeasure) event.rest = 'R'; } }); if (Number.isFinite(solution.priority)) this.solutionStat = { loss: -solution.priority }; this.postRegulate(); } cleanupRegulation() { this.voices = null; this.duration = null; this.events.forEach((event) => { event.tick = null; event.tickGroup = null; event.timeWarp = null; }); } regulateTest() { this.duration = 0; this.voices = this.rows.map((row) => row.events.map((e) => e.id)); this.voices.forEach((ids) => { let tick = 0; const events = ids.map((id) => this.events.find((e) => e.id === id)); events.forEach((e, index) => { e.tickGroup = index; e.tick = tick; tick += e.duration; }); this.duration = Math.max(this.duration, tick); }); } regulateSimple() { SimplePolicy.computeMeasureTicks(this); SimplePolicy.computeMeasureVoices(this); } async regulateEquations(options) { await EquationPolicy.regulateMeasure(this, options); } // compute event.tick, event.tickGroup, event.timeWarp, voices, duration async regulate({ policy = 'advanced', ...options } = {}) { switch (policy) { case 'test': this.regulateTest(); break; case 'equations': case 'advanced': await this.regulateEquations(options); break; case 'simple': default: this.regulateSimple(); } this.postRegulate(); } createPatch() { return new PatchMeasure({ measureIndex: this.measureIndex, staffMask: this.staffMask, basic: this.basics[0], //points: [], events: this.events, contexts: this.contexts, marks: this.marks, voices: this.voices, }); } createClusters() { const trueEventIds = this.voices && new Set(this.voices.flat(1)); return this.staffGroups .filter((idx) => idx.length) .map((staffIndices) => { const staffY0 = this.position.staffYs[0]; const staffYn = (n) => this.position.staffYs[staffIndices.indexOf(n)] - staffY0; const events = this.events.filter((event) => staffIndices.includes(event.staff)); if (!events.length) return null; const elements = events.map((event) => ({ index: event.id, voice: (this.voices || []).findIndex((voice) => voice.includes(event.id)), type: event.rest ? EventElementType.REST : EventElementType.CHORD, staff: staffIndices.indexOf(event.staff), x: event.tipX, pivotX: event.pivotX, y1: staffYn(event.staff) + (event.stemDirection === 'u' ? event.tipY : event.ys[event.ys.length - 1]), y2: staffYn(event.staff) + (event.stemDirection === 'u' ? event.ys[0] : event.tipY), headY: event.stemDirection === 'u' ? event.ys[0] : event.ys[event.ys.length - 1], feature: event.feature, division: event.division, dots: event.dots, beam: event.beam || null, stemDirection: event.stemDirection, grace: !!event.grace, tremoloCatcher: event.tremoloCatcher, timeWarped: !!event.timeWarp, fullMeasure: event.fullMeasureRest, tick: event.tick || 0, fake: !event.fullMeasureRest && !event.grace && this.voices && !trueEventIds.has(event.id), // tremoloCatcher deemed as fake })); if (!elements.some((elem) => !elem.fake)) return null; const signatureDuration = fractionMul(WHOLE_DURATION, this.timeSignature); // BOS & EOS elements.unshift({ index: 0, type: EventElementType.BOS, staff: null, division: null, beam: null, dots: null, stemDirection: null, grace: false, tremoloCatcher: false, fullMeasure: false, x: this.position.left, pivotX: this.position.left, y1: 0, y2: 0, headY: 0, feature: null, timeWarped: this.duration < signatureDuration, tick: 0, fake: false, }); elements.push({ index: -1, type: EventElementType.EOS, staff: null, division: null, beam: null, dots: null, stemDirection: null, grace: false, tremoloCatcher: false, fullMeasure: false, x: this.position.right, pivotX: this.position.right, y1: 0, y2: 0, headY: 0, feature: null, timeWarped: false, tick: this.duration, fake: false, }); let matrixH = null; if (this.voices) { matrixH = elements.map(() => elements.map(() => 0)); this.voices.forEach((voice) => { let tar = 0; voice.forEach((id) => { const src = elements.findIndex((e) => e.index === id); if (src > 0 && tar >= 0) matrixH[src][tar] = 1; tar = src; }); if (tar >= 0) matrixH[elements.length - 1][tar] = 1; }); } const annotation = { ...this.solutionStat, patched: this.patched }; const backgroundImages = this.backgroundImages && this.backgroundImages.map(({ url, position }) => ({ url, position: { ...position, y: position.y - staffY0, }, })); return new EventCluster({ index: this.measureIndex, duration: this.duration, signatureDuration, staffY0, elements, matrixH, annotation, backgroundImages, }); }) .filter(Boolean); } applyClusters(clusters) { const id_max = this.events.reduce((max, event) => Math.max(max, event.id), 0) + 1; this.matrixH = Array(id_max + 1) .fill(null) .map(() => Array(id_max).fill(0)); clusters.forEach((cluster) => { const ids = cluster.elements.map((e) => e.index); console.assert(cluster.matrixH.length === ids.length - 1, 'unexpected matrixH size:', cluster.matrixH.length, ids.length); for (let is = 1; is < ids.length; ++is) { for (let it = 0; it < ids.length - 1; ++it) { const srcId = ids[is] < 0 ? id_max : ids[is]; const tarId = ids[it]; this.matrixH[srcId][tarId] = cluster.matrixH[is - 1][it]; } } // event predisposition cluster.elements.forEach((elem) => { const event = this.events.find((event) => event.id === elem.index); if (event) { event.predisposition = elem.predisposition; if (event.predisposition.grace !== undefined) event.grace = event.predisposition.grace ? GraceType.Grace : null; } }); }); // estimated measure duration this.estimatedDuration = clusters.reduce((sum, cluster) => sum + cluster.estimatedDuration, 0) / clusters.length; } } SpartitoMeasure.className = 'SpartitoMeasure'; SpartitoMeasure.blackKeys = ['staffGroups', 'solutionStat', 'measureNumber', 'deposit']; const emptyVoiceFromStaffMeasure = (staff, chiefVoice = false) => { return { empty: true, duration: staff.duration, tickMap: { [0]: EventTerm.space({ duration: staff.duration, tick: 0 }), }, timeSignature: staff.timeSignature, timeSigNumeric: staff.timeSigNumeric, keySignature: staff.keySignature, contextedTerms: staff.terms.filter((term) => term instanceof ContextedTerm && (!term.staffLevel || chiefVoice)), marks: [], }; }; const removeEmptyMeasuresInVoicesStaves = (staves) => { //console.assert(staves[0] && staves[0].voices[0], 'voices is empty:', staves); if (!(staves[0] && staves[0].voices[0])) { console.warn('empty voices:', staves); return; } const measureCount = staves[0].voices[0].measures.length; const measureEmpties = Array(measureCount) .fill(null) .map((_, m) => { for (const staff of staves) { for (const voice of staff.voices) { const measure = voice.measures[m]; if (!measure.empty) return false; } } return true; }); measureEmpties.forEach((empty, m) => { if (empty) { staves.forEach((staff) => staff.voices.forEach((voice) => { const measure = voice.measures[m]; measure.tickMap = {}; })); } }); }; const markingTiesInVoice = (voice) => { const events = [].concat(...voice.measures.map((m) => Object.values(m.tickMap).filter((event) => event instanceof EventTerm))); //console.log("events:", events); for (let i = 1; i < events.length; ++i) { const event0 = events[i - 1]; const event1 = events[i]; if (!event0.rest && !event1.rest) { if (event0.accessories.some((acc) => acc.type === TokenType.SlurBegin) && event1.accessories.some((acc) => acc.type === TokenType.SlurEnd)) { const pitches = event0.pitches.filter((p0) => event1.pitches.some((p1) => p1.note === p0.note && p1.alter === p0.alter)); if (pitches.length > 0) { event0.tying = true; event1.tied = true; pitches.forEach((p0) => { p0.tying = true; const p1 = event1.pitches.find((p1) => p1.note === p0.note && p1.alter === p0.alter); p1.tied = true; }); // remove slurs from accessories pitches.forEach(() => { const si0 = event0.accessories.findIndex((acc) => acc.type === TokenType.SlurBegin); if (si0 >= 0) event0.accessories.splice(si0, 1); const si1 = event1.accessories.findIndex((acc) => acc.type === TokenType.SlurEnd); if (si1 >= 0) event1.accessories.splice(si1, 1); }); } } } } }; class Spartito extends SimpleClass { constructor(data) { super(); super.assign(data); this.measures.forEach((measure) => (measure.staffGroups = this.staffGroups)); } get regulated() { return this.measures.every((m) => m.regulated); } get solidMeasureCount() { return this.measures.filter((measure) => !measure.empty).length; } get measureIndexMapping() { let n = 0; return this.measures.map((measure) => { return !measure.empty ? n++ : null; }); } get headBPM() { for (const measure of this.measures) { if (measure.marks) { const tempoMark = measure.marks.find((mark) => mark instanceof TempoTerm && mark.isValid()); if (tempoMark) return tempoMark.bpm; } } return null; } get measureLayoutCode() { const ms = this.measures .filter((measure) => !measure.empty) .map((measure, i) => ({ index: i + 1, vb: measure.voltaBegin, ve: measure.voltaEnd, alter: measure.alternative, leftSign: '', rightSign: '', })); ms.forEach((m, i) => { if (m.vb) { const nextI = ms.slice(i + 1).findIndex((mm) => mm.vb); const nextVBI = nextI >= 0 ? i + nextI : ms.length; if (ms.slice(i, nextVBI - 1).some((mm) => mm.ve)) // check if volta range closed m.leftSign = '2*['; } if (m.ve) { const pms = ms.slice(0, i + 1).reverse(); const lastVEI = pms.slice(1).findIndex((mm) => mm.ve); if (lastVEI >= 0) { if (!pms.slice(1, lastVEI + 1).some((mm) => mm.vb)) // ignore unclosed right volta return; } if (m.alter) { const lastMI = pms.findIndex((m) => !m.alter); if (lastMI > 0) { pms[lastMI].rightSign = ']'; pms[lastMI - 1].leftSign = '{['; m.rightSign = '],'; if (ms[i + 1]) ms[i + 1].rightSign = '},'; } } else m.rightSign = '],'; if (!pms.some((m) => m.vb)) ms[0].leftSign = '2*['; } }); return ms .map((m) => m.leftSign + m.index.toString() + m.rightSign + (m.rightSign ? '' : ',')) .join(' ') .replace(/,$/, ''); } get qualityScore() { const measures = this.measures.filter((measure) => !measure.empty); const qss = measures.map(evaluateMeasure).map((e) => e.qualityScore); const sum = qss.reduce((a, b) => a + b, 0); //console.log('qss:', qss); return measures.length ? sum / measures.length : null; } dumpEvaluations() { const es = this.measures.filter((measure) => !measure.empty).map((m) => ({ measureIndex: m.measureIndex, ...evaluateMeasure(m) })); const qss = es.map((e) => e.qualityScore); const sum = qss.reduce((a, b) => a + b, 0); console.log('qualityScore:', sum / es.length); console.table(es); } regulate(options = {}) { this.measures.forEach((m) => m.regulated || m.regulate(options)); } cleanupRegulation() { this.measures.forEach((m) => (m.voices = null)); } // measures' estimatedDuration should be valid rectifyTimeSignatures(logger = new DummyLogger()) { const mis = this.measures .map((measure, index) => ({ measure, index })) .filter(({ measure, index }) => !index || measure.timeSignatureChanged) .map(({ index }) => index); const segments = mis .map((index, si) => this.measures.slice(index, si < mis.length - 1 ? mis[si + 1] : this.measures.length)) .map((ms) => ms.filter((m) => m.estimatedDuration > 0)) .filter((seg) => seg.length >= 3 || seg.some((measure) => measure.doubtfulTimesig)); //console.log("segments:", segments.map(ms => ms.map(m => m.measureIndex))); segments.forEach((measures) => { if (measures[0].patched) { // rectify according to patched head measure const newTimeSignature = measures[0].timeSignature; const measuresToFix = measures .slice(1) .filter((measure) => !measure.patched && printFraction(measure.timeSignature) !== printFraction(newTimeSignature)); if (measuresToFix.length) { const originTimeSignature = measuresToFix[0].timeSignature; measuresToFix.forEach((measure) => measure.basics.forEach((basic) => (basic.timeSignature = newTimeSignature))); logger.info('[rectifyTimeSignatures] timesignator overwrote by patched head:', `${printFraction(originTimeSignature)} -> ${printFraction(newTimeSignature)}`, measuresToFix.map((m) => m.measureIndex)); } return; } const originTimeSignature = measures[0].timeSignature; const regularD = Number.isInteger(Math.log2(originTimeSignature.denominator)); let denominator = regularD ? 4 : 8; if (regularD) denominator = Math.max(denominator, measures[0].timeSignature.denominator); const numerators = measures.map((measure) => Math.round((measure.estimatedDuration * denominator) / WHOLE_DURATION)); const countings = Object.entries(numerators.reduce((c, n) => ((c[n] = (c[n] || 0) + 1), c), {})).sort((p1, p2) => p2[1] - p1[1]); const peakCount = countings[0][1]; const candidateNumerators = countings.filter(([_, c]) => c > peakCount * 0.6); const bestCounting = candidateNumerators.reduce((best, c) => (Number(c[0]) > Number(best[0]) ? c : best)); if (bestCounting[1] > 1) { //console.log("countings:", countings, bestCounting[0]); let numerator = Number(bestCounting[0]); if (!regularD || originTimeSignature.denominator * numerator !== originTimeSignature.numerator * denominator) { if (regularD && denominator !== originTimeSignature.denominator) { const reducedN = (numerator * originTimeSignature.denominator) / denominator; if (Number.isInteger(reducedN)) { numerator = reducedN; denominator = originTimeSignature.denominator; } } const measuresToFix = measures.filter((measure) => !measure.patched); const newTimeSignature = frac(numerator, denominator); measuresToFix.forEach((measure) => measure.basics.forEach((basic) => (basic.timeSignature = newTimeSignature))); logger.info('[rectifyTimeSignatures] timesignator overwrote by estimation:', `${printFraction(originTimeSignature)} -> ${numerator}/${denominator}`, measuresToFix.map((m) => m.measureIndex)); } } }); } makeVoiceStaves() { this.regulate(); const voiceCount = Math.max(...this.measures.map((measure) => measure.voices.length)); if (!voiceCount || !Number.isFinite(voiceCount)) return null; // mark tied pitches for patched measues this.measures .filter((measure) => measure.patched) .forEach((measure) => { measure.events.forEach((event) => { if (event.tied) event.pitches.forEach((pitch) => (pitch.tied = true)); }); }); // [measure, voice] const measures = this.measures.map((measure /*, mi*/) => { console.assert(measure.validRegulated, '[makeVoiceStaves] measure is invalid:', measure); const eventMap = {}; measure.events.forEach((event) => (eventMap[event.id] = event)); const leftStaves = new Set(Array(measure.contexts.length) .fill(null) .map((_, i) => i)); let bar = null; if (measure.barType) { switch (measure.barType) { case 'Segment': bar = '||'; break; case 'Terminal': bar = '|.'; break; } } const voices = measure.voices.map((ids /*, vi*/) => { const events = ids.map((id) => eventMap[id]); events.sort((e1, e2) => e1.tick - e2.tick); const tickMap = {}; let tick = 0; let lastEvent = null; for (const event of events) { if (!Number.isFinite(event?.tick)) { console.warn('invalid event tick:', event); continue; } if (event.tick > tick) tickMap[tick] = EventTerm.space({ tick, duration: event.tick - tick }); else if (!event.grace && event.tick < tick && lastEvent) lastEvent.timeWarp = reducedFraction(event.tick - lastEvent.tick, lastEvent.duration); //console.log("timewarp:", event.tick - lastEvent.tick, lastEvent.duration, lastEvent.timeWarp); tickMap[event.tick] = event; if (!event.zeroHolder) { tick = Math.round(event.tick + event.duration); lastEvent = event; // sub grace events if (event.graceIds) { event.graceIds.forEach((id) => { const grace = measure.eventMap[id]; if (grace) tickMap[grace.tick] = grace; }); } } } if (measure.endEvent && measure.endEvent.graceIds) { measure.endEvent.graceIds.forEach((id) => { const grace = measure.eventMap[id]; if (grace && (!lastEvent || grace.staff === lastEvent.staff)) tickMap[grace.tick] = grace; }); } if (tick < measure.duration) tickMap[tick] = EventTerm.space({ tick, duration: measure.duration - tick }); else if (tick > measure.duration && Number.isFinite(measure.duration)) //console.warn("duration out of range:", tick, column.duration, mi, vi); lastEvent.timeWarp = reducedFraction(measure.duration - lastEvent.tick, lastEvent.duration); console.assert(!lastEvent || !lastEvent.timeWarp || (Number.isInteger(lastEvent.timeWarp.numerator) && Number.isInteger(lastEvent.timeWarp.denominator)), 'invalid time warp:', lastEvent); const staffIndex = events[0] ? events[0].staff : 0; leftStaves.delete(staffIndex); const basic = measure.basics[staffIndex]; //const row = measure.rows[staffIndex]; const contextedTerms = measure.contexts[staffIndex]; const tailEvent = events[events.length - 1]; const tailStaff = tailEvent ? tailEvent.staff : 0; // TODO: modify full measure rests duration return { tickMap, duration: measure.duration, ...basic, // TODO: consider staff altered voice contextedTerms, marks: [], break: measure.break, pageBreak: measure.pageBreak, headStaff: staffIndex, tailStaff, bar, }; }); while (voices.length < voiceCount) { const staffIndex = leftStaves.values().next().value || 0; leftStaves.delete(staffIndex); const basic = measure.basics[staffIndex]; const terms = measure.contexts[staffIndex]; const chiefVoice = voices.every((voice) => voice.headStaff !== staffIndex); const voice = emptyVoiceFromStaffMeasure({ terms, duration: measure.duration, ...basic, break: measure.break, pageBreak: measure.pageBreak, }, chiefVoice); voice.headStaff = staffIndex; voice.tailStaff = staffIndex; voices.push(voice); } return voices; }); //console.log("measures:", measures); // compute traits for voice-measures measures.forEach((voices) => voices.forEach((measure) => { const words = []; if (!measure.empty) { words.push(`s${measure.headStaff}`); words.push(`s${measure.tailStaff}`); } Object.values(measure.tickMap).forEach((event) => { if (event instanceof EventTerm) { words.push(`s${event.staff}`); if (event.stemDirection) { const sd = `st${event.staff}-${event.stemDirection}`; words.push(sd, sd); } if (event.grace) words.push(`gd${event.mainDuration}`); else words.push(`d${event.mainDuration}`); if (event.rest) words.push('r-' + event.rest); else { event.pitches.forEach((pitch) => { words.push(`p1-${pitch.note}`); words.push(`p8-${Math.round(pitch.note / 8)}`); }); } } }); measure.trait = HashVector.fromWords(words); })); //console.log("measure traits:"); //console.table(measures.map(voices => voices.map(measure => hashToHex(measure.trait.toHash())))); const staffToGroup = this.staffGroups .flat(1) .reduce((map, si) => ((map[si] = this.staffGroups.findIndex((group) => group.includes(si))), map), {}); // sort voices to connect voices between neighhoring measures const voiceTraits = Array(voiceCount) .fill(null) .map((_, index) => ({ vector: HashVector.zero, index, weight: 0, headStaff: null })); measures.forEach((voices, mi) => { voiceTraits.sort((v1, v2) => v2.weight - v1.weight); const leftVoices = new Set(voices); voiceTraits.forEach((voiceTrait) => { const vs = [...leftVoices]; let measure = vs[0]; if (mi > 0 && vs.length > 1) { const consistencies = vs.map((measure) => staffToGroup[measure.headStaff] === staffToGroup[voiceTrait.headStaff] ? cosHashes(voiceTrait.vector.toHash(), measure.trait.toHash()) : -1); measure = vs[argmax$1(consistencies)]; } leftVoices.delete(measure); measure.voiceIndex = voiceTrait.index; voiceTrait.vector.scale(0.4).add(measure.trait); voiceTrait.weight = Object.keys(measure.tickMap).length; if (mi === 0) voiceTrait.headStaff = measure.headStaff; }); voices.sort((m1, m2) => m1.voiceIndex - m2.voiceIndex); }); //const staffTraits = Array(this.stavesCount).fill(null).map((_, si) => HashVector.fromString(`s${si}`).toHash()); const staffVoiceIndices = Array(this.stavesCount) .fill(null) .map(() => []); voiceTraits.forEach((trait) => { //const consistencies = staffTraits.map(staff => cosHashes(trait.vector.toHash(), staff)); //staffVoiceIndices[argmax(consistencies)].push(trait.index); staffVoiceIndices[trait.headStaff].push(trait.index); }); const staves = Array(this.stavesCount) .fill(null) .map((_, si) => { if (!measures[0]) { return { voices: [], }; } //const voiceIndicies = measures[0].map((voice, vi) => ({ voice, vi })).filter(({ voice }) => voice.headStaff === si).map(({ vi }) => vi); const voiceIndicies = staffVoiceIndices[si]; const voices = voiceIndicies.map((vi) => { return { mode: 'relative', measures: measures.map((m) => m[vi]), }; }); return { voices }; }); removeEmptyMeasuresInVoicesStaves(staves); staves.forEach((staff) => staff.voices.forEach(markingTiesInVoice)); return staves; } perform() { const staves = this.makeVoiceStaves(); if (!staves) return null; const tokenMap = new Map(); // TODO: store staff channels in score const staffToChannel = Array(this.stavesCount) .fill(null) .reduce((map, _, i) => { map[i] = i; return map; }, {}); const voiceChannels = [].concat(...staves.map((staff, si) => staff.voices.map(() => staffToChannel[si]))); let hasTempo = false; let nextTick = 0; let events0 = null; const measures = this.measures .filter((measure) => !measure.empty) .map((measure) => { const { systemIndex, right: endX } = measure.position; const measureIndex = measure.measureIndex; const voices = [].concat(...staves.map((staff) => staff.voices.map((voice) => voice.measures[measureIndex]))); const voice0 = voices[0]; const tick = nextTick; //const signatureDuration = (WHOLE_DURATION * voice0.timeSignature.numerator) / voice0.timeSignature.denominator; nextTick += voice0.duration; const notes = [].concat(...voices.map((measure, vi) => { const tickFactor = 1; //measure.duration ? signatureDuration / measure.duration : 1; const channel = voiceChannels[vi]; const chords = Object.values(measure.tickMap) .filter((term) => term instanceof EventTerm && !term.rest) .map((term) => { const duration = Math.round(term.duration * tickFactor); console.assert(Number.isFinite(term.tick), 'invalid event term tick:', term); console.assert(Number.isFinite(duration), 'invalid event term duration:', term); if (term.tick >= 0) { // exclude minus tick tokens term.noteIds.forEach((id) => { tokenMap.set(id, { system: systemIndex, measure: measureIndex, x: term.roundX, endX, }); }); } const part = this.staffGroups.findIndex((group) => group.includes(term.staff)); return { tick: Math.round(term.tick * tickFactor), duration, pitches: term.pitches, noteIds: term.noteIds, part, staff: term.staff, }; }); return [].concat(...chords.map((chord) => { // exclude repeated pitches const pitchMap = chord.pitches.reduce((map, pitch) => { map[noteToPitch(pitch)] = pitch; return map; }, {}); const pitches = Object.values(pitchMap).sort((p1, p2) => p1.note - p2.note); return pitches .filter((pitch) => !pitch.tied) .map((pitch, i) => { const pitchValue = noteToPitch(pitch); const id = chord.noteIds && chord.noteIds[i]; return { tick: chord.tick, pitch: pitchValue, duration: chord.duration, chordPosition: { index: i, count: chord.pitches.length, }, tied: pitch.tied, id, ids: [id], track: chord.part, staff: chord.staff, channel, subNotes: [ { startTick: 0, endTick: chord.duration, pitch: pitchValue, velocity: 127, }, ], }; }); })); })); const events = []; events0 = events0 || events; if (measure.marks) measure.marks.forEach((mark) => { if (mark instanceof TempoTerm) { const bpm = mark.bpm; if (mark.isValid()) { const es = hasTempo ? events : events0; // set the first tempo to the beginning of the track const tick = hasTempo ? mark.tick : 0; es.push({ track: 0, ticks: tick, data: { type: 'meta', subtype: 'setTempo', microsecondsPerBeat: Math.round(60e6 / bpm), }, }); hasTempo = true; } } }); const basic = measure.basics[0]; return { tick, duration: measure.duration, notes, events, timeSignature: basic && basic.timeSignature, keySignature: basic && basic.keySignature, }; }); if (!hasTempo) { measures[0].events.push({ track: 0, ticks: 0, data: { type: 'meta', subtype: 'setTempo', microsecondsPerBeat: 0.5e6, // TODO }, }); } const notation = new MetaNotation({ measures }); return { notation, tokenMap, }; } performByEstimation() { const tokenMap = new Map(); let nextTick = 0; const measures = this.measures .filter((measure) => measure.events.some((event) => event.predisposition)) .map((measure) => { const tick = nextTick; const duration = Math.round(measure.estimatedDuration || fractionMul(WHOLE_DURATION, measure.timeSignature)); const basic = measure.basics[0]; nextTick += duration; const { systemIndex, right: endX } = measure.position; const measureIndex = measure.measureIndex; const chords = measure.events.filter((event) => event.predisposition && event.predisposition.fake < 0.5 && !event.rest); const notes = chords .map((chord) => { const noteTick = Math.round(chord.predisposition.tick); chord.noteIds.forEach((id) => { tokenMap.set(id, { system: systemIndex, measure: measureIndex, x: chord.roundX, endX, }); }); return chord.pitches.map((pitch, i) => { const pitchValue = noteToPitch(pitch); const id = chord.noteIds && chord.noteIds[i]; const part = this.staffGroups.findIndex((group) => group.includes(chord.staff)); return { tick: noteTick, pitch: pitchValue, duration: chord.duration, chordPosition: { index: i, count: chord.pitches.length, }, tied: pitch.tied, id, ids: [id], track: part, staff: chord.staff, channel: 0, subNotes: [ { startTick: 0, endTick: chord.duration, pitch: pitchValue, velocity: 127, }, ], }; }); }) .flat(1); return { tick, duration, notes, events: [], timeSignature: basic && basic.timeSignature, keySignature: basic && basic.keySignature, }; }); const notation = new MetaNotation({ measures }); return { notation, tokenMap, }; } featureHash() { const headMeasures = this.measures.slice(0, 16); const measureWords = headMeasures.map((measure) => measure.featureWords); const levels = [1, 4, 16].map((len) => { const meaures = measureWords.slice(0, len).filter(Boolean); const ys = meaures.map((words) => words[0]).flat(1); const melodies = meaures.map((words) => words[1]).flat(1); const rhythm = meaures.map((words) => words[2]).flat(1); const [vecY, vecMelody, vecRhythm] = [ys, melodies, rhythm].map(HashVector.fromWords); return HashVector.concat(vecY, vecMelody.sub(128), vecRhythm.sub(128)); }); return HashVector.concat(...levels).toHash(); } featureHashHex() { return hashToHex(this.featureHash()); } featureHashBigInt() { return hashToBigInt(this.featureHash()); } assignMeasureNumbers() { let n = null; for (const measure of this.measures) { if (!measure.discard && !measure.events.length) continue; if (measure.indent) n = null; if (!Number.isFinite(n)) n = measure.partialDuration ? 0 : 1; measure.measureNumber = n++; } } } Spartito.className = 'Spartito'; const GROUP_N_TO_PITCH = [0, 2, 4, 5, 7, 9, 11]; const MIDDLE_C = 60; const mod7 = (x) => { let y = x % 7; while (y < 0) y += 7; return y; }; const mod12 = (x) => { let y = x % 12; while (y < 0) y += 12; return y; }; const PHONETS = 'CDEFGAB'; const ALTER_NAMES = { [-2]: '\u266D\u266D', [-1]: '\u266D', [0]: '\u266E', [1]: '\u266F', [2]: '\uD834\uDD2A', }; /* Coordinates: note: zero: the middle C line (maybe altered) positive: high (right on piano keyboard) unit: a step in scales of the current staff key staff Y: zero: the third (middle) line among 5 staff lines positive: down unit: a interval between 2 neighbor staff lines */ class StaffContext { constructor() { this.logger = new DummyLogger(); this.clef = -3; this.keyAlters = []; this.octaveShift = 0; this.alters = []; this.timeSignature = { numerator: 4, denominator: 4, }; this.timeSigNumeric = false; this.timeSigNumSet = false; this.timeSigDenSet = false; this.doubtingTimesig = true; } change(term) { switch (term.type) { case ContextType.Clef: this.clef = term.clef; break; case ContextType.KeyAcc: this.keyAlters[mod7(this.yToNote(term.y))] = term.alter; break; case ContextType.Acc: this.alters[this.yToNote(term.y)] = term.alter; break; case ContextType.OctaveShift: this.octaveShift = term.octaveShift; break; case ContextType.TimeSignatureC: this.timeSigNumeric = false; switch (term.tokenType) { case 'timesig-C44': this.timeSignature.numerator = 4; this.timeSignature.denominator = 4; break; case 'timesig-C22': this.timeSignature.numerator = 2; this.timeSignature.denominator = 2; break; } this.doubtingTimesig = this.partialTimeSignature; break; case ContextType.TimeSignatureN: this.timeSigNumeric = true; switch (term.y) { case 1: if (this.timeSigDenSet) this.timeSignature.denominator = this.timeSignature.denominator * 10 + term.number; else this.timeSignature.denominator = term.number; this.timeSigDenSet = true; break; case -1: if (this.timeSigNumSet) this.timeSignature.numerator = this.timeSignature.numerator * 10 + term.number; else this.timeSignature.numerator = term.number; this.timeSigNumSet = true; break; default: this.logger.warn('unexpected time signature Y:', term.y); } this.doubtingTimesig = this.partialTimeSignature; break; } } resetMeasure() { this.alters = []; this.timeSigNumSet = false; this.timeSigDenSet = false; } resetSystem() { this.keyAlters = []; } get keySignature() { return this.keyAlters.filter((a) => Number.isInteger(a)).reduce((sum, a) => sum + a, 0); } get partialTimeSignature() { return !this.timeSigNumSet !== !this.timeSigDenSet; } noteToY(note) { return -note / 2 - this.clef - this.octaveShift * 3.5; } pitchToNote(pitch, { preferredAlter = null } = {}) { if (!preferredAlter) preferredAlter = this.keySignature < 0 ? -1 : 1; const group = Math.floor((pitch - MIDDLE_C) / 12); const gp = mod12(pitch); const alteredGp = GROUP_N_TO_PITCH.includes(gp) ? gp : mod12(gp - preferredAlter); const gn = GROUP_N_TO_PITCH.indexOf(alteredGp); this.logger.assert(gn >= 0, 'invalid preferredAlter:', pitch, preferredAlter, alteredGp); const naturalNote = group * 7 + gn; const alterValue = gp - alteredGp; const keyAlterValue = this.keyAlters[gn] || 0; const onAcc = Number.isInteger(this.alters[naturalNote]); const alter = onAcc ? alterValue : alterValue === keyAlterValue ? null : alterValue; return { note: naturalNote, alter }; } pitchToY(pitch, { preferredAlter = null } = {}) { const { note, alter } = this.pitchToNote(pitch, { preferredAlter }); const y = this.noteToY(note); return { y, alter }; } yToNote(y) { this.logger.assert(Number.isInteger(y * 2), 'invalid y:', y); return (-y - this.octaveShift * 3.5 - this.clef) * 2; } alterOnNote(note) { if (Number.isInteger(this.alters[note])) return this.alters[note]; const gn = mod7(note); if (Number.isInteger(this.keyAlters[gn])) return this.keyAlters[gn]; return 0; } noteToPitch(note) { const group = Math.floor(note / 7); const gn = mod7(note); const pitch = MIDDLE_C + group * 12 + GROUP_N_TO_PITCH[gn] + this.alterOnNote(note); if (!Number.isFinite(pitch)) { this.logger.warn('invalid pitch value:', pitch, note, group, gn); return -1; } return pitch; } yToPitch(y) { return this.noteToPitch(this.yToNote(y)); } yToPitchName(y) { const note = this.yToNote(y); const group = Math.floor(note / 7); const gn = mod7(note); let alter = this.alterOnNote(note); if (!alter && !Number.isInteger(this.alters[note])) alter = null; return `${ALTER_NAMES[alter] ? ALTER_NAMES[alter] : ''}${PHONETS[gn]}${group + 4}`; } } const VERSION = 14; const GRAND_STAFF_LAYOUT = '{-}'; const processStaffContext = (staff, logger = new DummyLogger()) => { const context = new StaffContext(); context.logger = logger; for (const row of staff.rows) { for (const measure of row) { const startEvent = measure.terms.find((term) => term instanceof EventTerm); let tick = startEvent ? Math.min(startEvent.tick, 0) : 0; measure.terms.forEach((term) => { if (term instanceof ContextedTerm) { term.tick = tick; // TODO: not working here because measure not regulated yet context.change(term); } else if (term instanceof EventTerm) { const endTick = term.tick + (term.duration || 0); if (endTick > tick) tick = endTick; if (term.ys) { term.pitches = term.ys.map((y) => { const note = context.yToNote(y); const alter = context.alterOnNote(note); return { note, alter, octaveShift: context.octaveShift }; }); } } }); measure.timeSignature = { ...context.timeSignature }; measure.timeSigNumeric = context.timeSigNumeric; measure.doubtfulTimesig = context.doubtingTimesig || !Number.isInteger(Math.log2(measure.timeSignature.denominator)) || measure.timeSignature.numerator <= measure.timeSignature.denominator / 4; measure.keySignature = context.keySignature; // fill empty measure duration if (measure.duration === 0) measure.duration = (WHOLE_DURATION * measure.timeSignature.numerator) / measure.timeSignature.denominator; context.resetMeasure(); } context.resetSystem(); } }; const upgradeScoreData = (data) => { if (data.version < 3) { const { version, stavesCount, layoutTemplate, ...fields } = data; let staffLayoutCode = stavesCount > 1 ? Array(stavesCount - 1) .fill(',') .join('') : ''; // use graph staff by default for 2 staves score if (stavesCount === 2) staffLayoutCode = '{-}'; data = { version: 3, staffLayoutCode, ...fields, }; } if (data.version < 8) { // upgrade system measure bar semantics data.pages.forEach((page) => { page.systems.forEach((system) => { if (system.semantics) { const bars = system.semantics.filter((point) => point.semantic === SemanticType.vline_BarMeasure); system.semantics = [].concat(...system.staves.map((staff) => { const oy = staff.top + staff.staffY; return bars.map((point) => ({ ...point, y: point.y + oy, extension: { ...point.extension, y1: point.extension.y1 + oy, y2: point.extension.y2 + oy, }, })); })); } }); }); data.version = 8; } if (data.version < 9) { // remove old format spartito data.spartito = null; data.version = 9; } return data; }; const bitsToMask = (bits) => bits.reduce((mask, bit, i) => (bit ? mask | (1 << i) : mask), 0); class Score extends SimpleClass { constructor(data) { super(); this.version = VERSION; super.assign(upgradeScoreData(data)); this.pages = this.pages || []; this.headers = this.headers || {}; this.instrumentDict = this.instrumentDict || {}; this.pageSize = this.pageSize || { // A4 paper width: 794, height: 1122, }; this.unitSize = this.unitSize || null; this.staffLayoutCode = this.staffLayoutCode || (this.maxStavesCount === 2 ? GRAND_STAFF_LAYOUT : Array(this.maxStavesCount).fill('').join(',')); } get systems() { return [].concat(...this.pages.map((page) => page.systems)); } get measureCount() { return this.systems.reduce((sum, system) => sum + (system.measureCount || 0), 0); } get imageKeys() { return [ ...this.pages.map((page) => page.source?.url), ...this.systems.map((system) => system.backgroundImage), ...[].concat(...this.systems.map((system) => [...system.staves.map((staff) => staff.backgroundImage), ...system.staves.map((staff) => staff.maskImage)].filter(Boolean))), ].filter(Boolean); } get breakSystemIndices() { const indices = []; let systemCount = 0; this.pages.forEach((page, i) => { if (i < this.pages.length - 1) { systemCount += page.systems.length; indices.push(systemCount - 1); } }); return indices; } get staffLayout() { return parseCode(this.staffLayoutCode); } get measureLayoutCode() { return this.spartito?.measureLayoutCode; } get maxStavesCount() { return Math.max(...this.systems.map((system) => system.staves.length), 0); } get sidBlackList() { const ids = [].concat(...this.systems.map((system) => system.sidBlackList)); return new Set(ids); } get sidWhiteList() { const ids = [].concat(...this.systems.map((system) => system.sidWhiteList)); return new Set(ids); } get semanticHash() { const ids = [].concat(...this.systems.map((system) => [].concat(...system.staves.map((staff) => (staff.semantics ? system.qualifiedSemantics(staff.semantics).map((s) => s.id) : []))))); return sha1__default["default"](ids.join('')); } eventSystemsToTermStaves(eventSystems, logger = new DummyLogger()) { // [staff] const termStaves = Array(this.maxStavesCount) .fill(null) .map((_, staffIndex) => { return { // [system, measure] rows: eventSystems.map((sys, i) => sys.columns.map((column, ii) => { const measure = column.rows[staffIndex]; console.assert(measure, '[eventSystemsToTermStaves] measure is null:', staffIndex, column.rows); const contexts = measure.contexts; // prepend octave shift 0 at begin of every system if (ii === 0) { if (!contexts.some((term) => term.type === ContextType.OctaveShift)) { contexts.unshift(new ContextedTerm({ staff: staffIndex, x: 0, y: 0, tokenType: TokenType.OctaveShift0, tick: 0, })); } } const terms = [...(measure.events || []), ...contexts].sort((t1, t2) => t1.x - t2.x); const pageBreak = staffIndex === 0 && ii === sys.columns.length - 1 && this.breakSystemIndices.includes(i); return { terms, //xToTick: column.xToTick, duration: column.duration, pageBreak, }; })), }; }); termStaves.forEach((staff) => processStaffContext(staff, logger)); return termStaves; } resetPageLayout(parameters) { const { unitSize = this.unitSize, pageSize = this.pageSize } = parameters; const newCenter = { x: (pageSize.width * 0.5) / unitSize, y: (pageSize.height * 0.5) / unitSize, }; this.pages.forEach((page) => { const offsetX = newCenter.x - page.width / 2; const offsetY = newCenter.y - page.height / 2; page.systems.forEach((system) => { system.left += offsetX; system.top += offsetY; }); if (page.semantics) { page.semantics.forEach((point) => { point.x += offsetX; point.y += offsetY; }); } page.width = pageSize.width / unitSize; page.height = pageSize.height / unitSize; page.assemble({ textAnnotations: this.textAnnotations }); }); this.unitSize = unitSize; this.pageSize = pageSize; } getMeasure(measureIndex) { let index = measureIndex; for (const system of this.systems) { if (index < system.measureCount) { const staff = system.staves[0]; const measure = staff.measures[index]; console.assert(measure, 'measure is null:', system.measureCount, index, staff.measures); const measures = system.getStaffArray(this.maxStavesCount).map((staff) => staff && staff.measures[index]); return { measureIndex, system, localIndex: index, left: measure.left, right: measure.right, measures, }; } index -= system.measureCount; } return null; } getRawCluster(measureIndex, threshold, { timeSignature } = {}) { const position = this.getMeasure(measureIndex); if (!position) return null; const { system, left, right } = position; //console.log("measure:", system, left, right); const elements = [BOS_ELEMENT]; if (timeSignature) elements.push(...fractionToElems(timeSignature)); const systemY0 = system.staves[0].top + system.staves[0].staffY - 2; system.staves.forEach((staff) => { let points = system.qualifiedSemantics(staff.semantics, threshold).filter((point) => point.x > left && point.x < right); points = solveOverlapping(points); // exlude tempo noteheads const tempoNhs = points.filter((point) => point.semantic === SemanticType.TempoNotehead); tempoNhs.forEach((tempoNh) => { const index = points.findIndex((point) => /^Notehead/.test(point.semantic) && distance2D(tempoNh, point) < 0.3); if (index >= 0) points.splice(index, 1); }); const y0 = staff.top + staff.staffY - systemY0; points.forEach((point) => { const type = SemanticElementType[point.semantic]; if (type) { let y1 = point.y; let y2 = point.y; if (type === SemanticElementType.vline_Stem) { y1 = point.extension.y1; y2 = point.extension.y2; } elements.push({ id: point.id, type, staff: staff.index, x: point.x - left, y1: y1 + y0, y2: y2 + y0, }); } }); }); return new SemanticCluster({ index: measureIndex, elements, }); } getRawClusters(threshold = 1) { //const times = this.getMeasuresTime(); return Array(this.measureCount) .fill(null) .map((_, mi) => this.getRawCluster(mi, threshold /*, times[mi]*/)); } makeSpartito(logger = new DummyLogger()) { let eventSystems = this.systems.map((system) => system.getEvents(this.maxStavesCount)); /*if (this.topology) { const clusters = this.topology.clusters; // [measure, staff, event] const eventsColumns: ChordColumn[][][] = clusters .filter((cluster) => cluster.index < this.measureCount) .reduce((columns, cluster) => { const { system, measures } = this.getMeasure(cluster.index); const events = cluster.getEvents(); const systemY0 = system.staves[0].top + system.staves[0].staffY - 2; const x0 = measures.filter(Boolean)[0].left; const staves = system.getStaffArray(this.maxStavesCount); // translate by staff & measure relative offset events.forEach((event) => { const staff = staves[event.staff]; const y0 = staff.top + staff.staffY - systemY0; event.ys = event.ys.map((y) => roundNumber(y - y0, 0.5)); event.left += x0; event.right += x0; }); const column = measures.map((measure, staffIndex) => { if (!measure) return []; //console.log("m:", mi, "s:", staffIndex); const localEvents = events.filter((event) => event.staff === staffIndex); //measure.assignModifiersOnEvents(localEvents); measure.assignAccessoriesOnEvents(localEvents); return localEvents; }); columns[cluster.index] = column; return columns; }, []); const breakSystemIndices = this.breakSystemIndices; const basicEventSystems = eventSystems; eventSystems = []; let measures = 0; for (const system of this.systems) { const esys = system.getEventsFunctional(this.maxStavesCount, (si, mi) => eventsColumns[measures + mi] && eventsColumns[measures + mi][si], [], { useXMap: false, }); const basicSys = basicEventSystems[system.index]; //onst nullN = esys.columns.filter(c => !c).length; //if (nullN) // console.log("null:", nullN, esys.columns.length); esys.columns = esys.columns.map((column, i) => (column ? column : basicSys.columns[i])); const sysIndex = this.systems.indexOf(system); const pageBreak = breakSystemIndices.includes(sysIndex); const lastColumn = esys.columns[esys.columns.length - 1]; if (lastColumn) { lastColumn.break = true; lastColumn.pageBreak = pageBreak; } eventSystems.push(esys); measures += system.measureCount; } }*/ const staves = this.eventSystemsToTermStaves(eventSystems, logger); // assign staff basics for columns eventSystems.forEach((sys, ri) => { sys.columns.forEach((column, mi) => { column.basics = staves.map((staff) => { const { timeSignature, timeSigNumeric, keySignature, doubtfulTimesig } = staff.rows[ri][mi]; return { timeSignature, timeSigNumeric, keySignature, doubtfulTimesig }; }); }); }); const clusters = null; //this.topology && this.topology.clusters; const measures = [].concat(...eventSystems.map((esys) => esys.columns.map((column) => { const measureIndex = column.measureIndex; const { system, localIndex, left, right } = this.getMeasure(measureIndex); const cluster = clusters ; const staffYsFull = []; system.staves.forEach((staff) => (staffYsFull[staff.index] = staff.top + staff.staffY)); const patch = this.patches && this.patches.find((patch) => patch.measureIndex === measureIndex); const events = patch ? patch.events : SpartitoMeasure.reorderEvents([].concat(...column.rows.map((row) => row.events)), staffYsFull); const barTypes = Object.fromEntries(Object.entries(column.barTypes).map(([k, v]) => [k, v / system.staves.length])); const indent = localIndex === 0 && system.indent; return new SpartitoMeasure({ measureIndex, staffMask: esys.staffMask, position: { systemIndex: system.index, localIndex, left, right, staffYs: system.staves.map((staff) => staff.top + staff.staffY), staffYsFull, }, //startX: column.startX, //width: column.width, duration: patch ? patch.duration : column.duration, events, contexts: column.rows.map((row) => row.contexts), marks: column.marks, break: column.break, pageBreak: column.pageBreak, voltaBegin: column.voltaBegin, voltaEnd: column.voltaEnd, alternative: column.alternative, barTypes, indent, basics: patch ? patch.basics : column.basics, matrixH: cluster , matrixV: cluster , voices: patch ? patch.voices : null, }); }))); const staffLayout = this.staffLayout; const staffGroups = staffLayout.standaloneGroups.map((ids) => ids.map((id) => staffLayout.staffIds.indexOf(id))); this.spartito = new Spartito({ stavesCount: this.maxStavesCount, staffGroups, measures, }); return this.spartito; } makeMusicSheet() { const spartito = this.spartito || this.makeSpartito(); if (!spartito.regulated) console.warn('[makeMusicSheet] spartito not regulated.'); const voiceStaves = spartito.makeVoiceStaves(); const { title, pageSize, unitSize, staffLayout, paperOptions, headers, instrumentDict } = this; const measureLayout = this.getMeasureLayout(); return { title, pageSize, unitSize, measureLayout, staffLayout, paperOptions, headers, voiceStaves, instrumentDict, }; } findPoint(sid) { for (const system of this.systems) { for (let si = 0; si < system.staves.length; ++si) { const point = system.staves[si].semantics.find((point) => point.id === sid); if (point) { const pageIndex = this.pages.findIndex((page) => page.systems.includes(system)); return { point, pageIndex, systemIndex: system.index, staffIndex: si, }; } } } return null; } getMeasureSemantics(systemIndex, localIndex) { const system = this.systems[systemIndex]; if (!system) return null; const left = localIndex ? system.measureBars[localIndex - 1] : 0; const right = system.measureBars[localIndex] || system.width; return system.staves .map((staff, si) => { const staffY = staff.top + staff.staffY; return staff.semantics .filter((point) => point.x >= left && point.x < right) .map((point) => { const [y1, y2] = Number.isFinite(point.extension?.y1) ? [point.extension.y1, point.extension.y2] : [point.y, point.y]; return { ...point, staff: si, sy1: y1 + staffY, sy2: y2 + staffY, }; }); }) .flat(1); } makeTimewiseGraph({ store = false } = {}) { if (!this.spartito) return null; const measures = this.spartito.measures .filter((measure) => measure.events.length > 0) .map((measure) => { const points = this.getMeasureSemantics(measure.position.systemIndex, measure.position.localIndex); const graph = { measureIndex: measure.measureIndex, left: measure.position.left, right: measure.position.right, points, }; if (store) measure.graph = graph; return graph; }); return { measures }; } getTokenMap() { const map = new Map(); this.systems.forEach((system) => system.staves.forEach((staff) => staff.measures.forEach((measure) => measure.tokens.forEach((token) => map.set(token.id, token))))); return map; } assemble(confidenceThreshold = 1, logger = new DummyLogger()) { const ids = new Map(); const append = (systemIndex, staffIndex, point) => { const id = hashSemanticPoint(systemIndex, staffIndex, point); logger.assert(!ids.has(id), 'semantic point hash conflicted:', id, point, ids.get(id)); ids.set(id, point); }; this.pages.forEach((page, index) => (page.index = index)); let measureIndex = 0; this.systems.forEach((system, systemIndex) => { system.index = systemIndex; system.headMeasureIndex = measureIndex; system.prev = this.systems[systemIndex - 1] || null; system.next = this.systems[systemIndex + 1] || null; if (system.semantics && system.semantics.length) system.semantics.forEach((point) => append(systemIndex, null, point)); system.assemble(confidenceThreshold, logger); measureIndex += system.measureCount; }); this.pages.forEach((page, i) => { page.systems.forEach((system) => (system.pageIndex = i)); page.assemble({ textAnnotations: this.textAnnotations }, logger); }); } assembleSystem(system, confidenceThreshold = 1) { this.systems.forEach((system, si) => (system.index = si)); const systemIndex = system.index; if (system.semantics && system.semantics.length) { system.semantics.forEach((point) => hashSemanticPoint(systemIndex, null, point)); system.assemble(confidenceThreshold); } } markVoices(staves) { const tokenMap = this.getTokenMap(); for (const token of tokenMap.values()) token.voice = 0; const vis = [] .concat(...staves.map((staff, s) => (staff.voices || []).map((_, v) => [s, v]))) .sort(([s1, v1], [s2, v2]) => v1 - v2 || s1 - s2) .map(([s, v]) => `${s}|${v}`); staves.forEach((staff, si) => (staff.voices || []).forEach((voice, vi) => voice.measures.forEach((measure) => { const voiceIndex = vis.indexOf(`${si}|${vi}`); const events = Object.values(measure.tickMap).filter((event) => event instanceof EventTerm); events.forEach((event) => { const notes = event.noteIds ? event.noteIds.map((id) => tokenMap.get(id)).filter(Boolean) : []; const accessories = event.accessories ? event.accessories.map((acc) => tokenMap.get(acc.id)).filter(Boolean) : []; //console.log("notes:", si, vi, mi, event.noteIds, notes, accessories); [...notes, ...accessories].forEach((token) => (token.voice |= 1 << voiceIndex)); if (event.timeWarp) notes.forEach((note) => (note.timeWarped = true)); }); }))); } async replaceImageKeys(proc) { await Promise.all([ ...this.pages.map(async (page) => { if (page.source) page.source.url = await proc(page.source.url); }), ...this.systems.map((system) => Promise.all([ proc(system.backgroundImage).then((key) => (system.backgroundImage = key)), ...system.staves.map(async (staff) => { staff.backgroundImage = await proc(staff.backgroundImage); staff.maskImage = await proc(staff.maskImage); }), ])), ]); } inferenceStaffLayout() { // inference the complete layout const staffTotal = Math.max(...this.systems.map((system) => system.staves.length), 0); this.staffLayoutCode = Array(staffTotal).fill('').join(','); const completeSystems = this.systems.filter((system) => system.staves.length === staffTotal && system.bracketsAppearance); if (!completeSystems.length) return; // no enough evidence const candidateCodes = completeSystems .map((system) => { try { const layout = parseCode(system.bracketsAppearance); if (layout.staffIds.length !== system.staves.length) return null; return system.bracketsAppearance; } catch (_) { return null; } }) .filter(Boolean); if (!candidateCodes.length) return; // no valid layout const codeCounting = candidateCodes.reduce((acc, code) => { const count = acc[code] || 0; acc[code] = count + 1; return acc; }, {}); const maxCount = Math.max(...Object.values(codeCounting)); const code = Object.entries(codeCounting).find(([_, count]) => count === maxCount)[0]; // added connection lines between braces {-} const connectedCode = code.replace(/\{,*\}/g, (match) => match.replace(/,/g, '-')); const layout = parseCode(connectedCode); this.staffLayoutCode = connectedCode; //console.log("complete code:", code); // inference systems' mask let lastSys = null; for (const system of this.systems) { if (lastSys && system.staves.length === lastSys.staves.length && system.bracketsAppearance === lastSys.bracketsAppearance) { system.staffMaskChanged = null; continue; } if (system.staves.length < staffTotal && system.bracketsAppearance) { // validate the system brackets code try { if (!parseCode(system.bracketsAppearance)) continue; } catch (_) { continue; } const search = (bits) => { if (bits.length > layout.staffIds.length) return null; if (bits.reduce((sum, bit) => sum + bit, 0) === system.staves.length) return bitsToMask(bits); for (const bit of [1, 0]) { const bb = [...bits, bit]; const code1 = layout.partialMaskCode(bb); if (code1 === system.bracketsAppearance) return bitsToMask(bb); else if (system.bracketsAppearance.startsWith(code1)) { const result = search(bb); if (result) return result; } } return null; }; const mask = search([]); //console.log("mask:", system.bracketsAppearance, mask.toString(2)); system.staffMaskChanged = !lastSys || mask !== lastSys.staffMask ? mask : null; } lastSys = system; } } assignBackgroundForMeasure(measure) { measure.backgroundImages = []; const system = this.systems[measure.position.systemIndex]; if (system.backgroundImage) { measure.backgroundImages.push({ url: system.backgroundImage, position: system.imagePosition, original: true, }); } system.staves.forEach((staff) => { if (!system.backgroundImage && staff.backgroundImage) measure.backgroundImages.push({ url: staff.backgroundImage.toString(), position: { ...staff.imagePosition, y: staff.imagePosition.y + staff.top, }, original: true, }); if (staff.maskImage) { measure.backgroundImages.push({ url: staff.maskImage.toString(), position: { ...staff.imagePosition, y: staff.imagePosition.y + staff.top, }, }); } }); } blackoutFakeNotes(scope = 'patched') { if (!this.spartito) return; let inScope = (_) => true; switch (scope) { case 'patched': inScope = (measure) => measure.patched; break; case 'perfect': inScope = (measure) => measure.patched || (measure.regulated && evaluateMeasure(measure).perfect); break; } const measures = this.spartito.measures.filter(inScope); const fakeIds = measures.reduce((ids, measure) => { if (!measure.regulated) return; const voicedIds = measure.voices.flat(1); const fakeChords = measure.events.filter((event) => !event.rest && !event.grace && !voicedIds.includes(event.id)); fakeChords.forEach((event) => event.noteIds && ids.push(...event.noteIds)); return ids; }, []); const fakeIdSet = new Set(fakeIds); this.systems.forEach((system) => system.staves.forEach((staff) => { const blackIds = staff.semantics.filter((point) => fakeIdSet.has(point.id)).map((point) => point.id); system.sidBlackList.push(...blackIds); })); return fakeIds; } getMeasureLayout() { const code = this.spartito && this.spartito.measureLayoutCode; if (code) { try { return parseCode$1(code); } catch (err) { console.debug('invalid measure layout code:', err); } } return null; } *splitToSingleScoresGen() { this.assemble(); const startSysIndices = this.systems.filter((system) => system.index > 0 && system.indent && system.timeSignatureOnHead).map((system) => system.index); if (!startSysIndices.length) { yield this.deepCopy(); return; } const templateScore = new Score({ ...this, pages: [], topology: undefined, spartito: undefined, patches: undefined }); // clear temporary objects before deep dopy this.pages.forEach((page) => { delete page.tokens; page.systems.forEach((system) => { delete system.tokens; system.staves.forEach((staff) => { staff.measures = []; }); }); }); let startSysIndex = 0; for (const endSysIndex of [...startSysIndices, this.systems.length]) { const sysInRange = (system) => system.index >= startSysIndex && system.index < endSysIndex; const pages = this.pages .filter((page) => page.systems.some(sysInRange)) .map((page) => { const { systems, ...fields } = page; return new Page({ ...fields, systems: systems.filter(sysInRange).map((system) => new System({ ...system })) }); }); const newScore = templateScore.deepCopy(); newScore.headers.SubScoreSystem = `${startSysIndex}-${endSysIndex - 1}`; newScore.headers.SubScorePage = `${pages[0].index}-${pages[pages.length - 1].index}`; //newScore.pages = pages.map((page) => page.deepCopy()); newScore.pages = pages; newScore.assemble(); newScore.inferenceStaffLayout(); startSysIndex = endSysIndex; yield newScore; } } splitToSingleScores() { return [...this.splitToSingleScoresGen()]; } } Score.className = 'Score'; class EditableEvent extends EventTerm { constructor(data) { super(data); } get agent() { return new Proxy(this, { get(target, key) { const self = target; switch (key) { case 'id': case 'tick': case 'duration': case 'rest': case 'division': case 'dots': case 'stemDirection': case 'beam': case 'tremolo': case 'tremoloLink': case 'arpeggioStyle': { const value = self[key]; return value === undefined ? null : value; } case 'tying': case 'tied': case 'glissando': { const value = self[key]; return value === undefined ? false : value; } case 'grace': return !!self.grace; case 'timeWarp': return self.timeWarp ? `${self.timeWarp.numerator}/${self.timeWarp.denominator}` : null; case 'pitches': return self.pitches; } return undefined; }, set: (target, key, value) => { const self = target; switch (key) { case 'tick': case 'duration': case 'rest': case 'division': case 'dots': case 'stemDirection': case 'tying': case 'tied': case 'beam': case 'tremolo': case 'tremoloLink': case 'glissando': case 'arpeggioStyle': self[key] = value; return true; case 'grace': self.grace = value ? GraceType.Grace : null; return true; case 'timeWarp': self.timeWarp = null; if (value && typeof value === 'string') { const captures = value.match(/^(\d+)\/(\d+)/); if (captures) { self.timeWarp = { numerator: parseInt(captures[1]), denominator: parseInt(captures[2]), }; } } return true; case 'id': case 'pitches': return true; } return false; }, ownKeys: () => [ 'id', 'duration', 'rest', 'division', 'dots', 'stemDirection', 'tying', 'tied', 'beam', 'timeWarp', 'tremolo', 'tremoloLink', 'glissando', 'arpeggioStyle', 'tick', 'grace', 'pitches', ], getOwnPropertyDescriptor() { return { enumerable: true, configurable: true }; }, }); } } class EditableMeasure extends SpartitoMeasure { constructor(data) { super(data); this.events = null; this.events = data.events; if (this.events?.some((event) => !(event instanceof EditableEvent))) this.events = this.events.map((event) => new EditableEvent(event)); if (this.voices) this.syncVoiceToEvents(); } syncVoiceToEvents() { this.events.forEach((event) => (event.voice = -1)); this.voices.forEach((voice, voiceIndex) => { voice.forEach((id) => { const event = this.events.find((event) => event.id === id); if (event) event.voice = voiceIndex; else console.warn('no event with id:', id, this.events.length); }); }); } syncVoiceFromEvents() { const voices = []; this.events.forEach((event) => { if (event?.voice >= 0) { voices[event.voice] = voices[event.voice] || []; voices[event.voice].push(event); } }); voices.forEach((voice) => voice.sort((e1, e2) => e1.tick - e2.tick)); this.voices = voices.map((voice) => voice.map((event) => event.id)); } get agent() { return new Proxy(this, { get: (target, key) => { const self = target; switch (key) { case 'measureIndex': case 'duration': return self[key]; case 'voices': return self.voices?.map((voice) => voice.join(',')) || null; case 'timeSignature': case 'keySignature': case 'doubtfulTimesig': return self.basics[0][key]; //case 'events': // return self.events.map(eventUIAgent); case 'toJSON': return () => ({ measureIndex: self.measureIndex, voices: self.voices, duration: self.duration, timeSignature: self.basics[0].timeSignature, keySignature: self.basics[0].keySignature, }); } return undefined; }, set: (target, key, value) => { //console.log('set:', key, value); const self = target; switch (key) { case 'timeSignature': case 'keySignature': case 'doubtfulTimesig': self.basics[0][key] = value; self.basics = self.basics.map(() => self.basics[0]); return true; case 'duration': self.duration = value; return true; case 'measureIndex': case 'voices': return true; } return false; }, ownKeys: () => ['measureIndex', 'timeSignature', 'doubtfulTimesig', 'keySignature', 'duration', 'voices'], getOwnPropertyDescriptor() { return { enumerable: true, configurable: true }; }, }); } makeMIDI(bpm = 120) { if (!this.regulated) return null; const microsecondsPerBeat = 60e6 / bpm; const header = { formatType: 0, ticksPerBeat: 480 }; const tracks = this.voices.map((ids, vi) => { const events = ids .map((id) => { const event = this.events.find((event) => event.id === id); if (event) { const subEvents = event.graceIds ? event.graceIds.map((id) => this.events.find((event) => event.id === id)) : []; return [...subEvents, event]; } return []; }) .flat(1); const startTime = 0; const midiEvents = events .filter((event) => !event.rest && Number.isFinite(event.tick) && event.tick >= 0 && Number.isFinite(event.duration)) .map((event) => event.pitches.map((pitch) => [ { id: event.id, time: event.tick, type: 'channel', subtype: 'noteOn', channel: event.staff, noteNumber: noteToPitch(pitch), velocity: 96, }, { id: event.id, time: event.tick + event.duration, type: 'channel', subtype: 'noteOff', channel: event.staff, noteNumber: noteToPitch(pitch), }, ])) .flat(2); midiEvents.sort(function (e1, e2) { return e1.time - e2.time; }); if (vi === 0) { midiEvents.unshift({ time: startTime, type: 'meta', subtype: 'timeSignature', numerator: this.timeSignature.numerator, denominator: this.timeSignature.denominator, thirtyseconds: 8, }, { time: startTime, type: 'meta', subtype: 'setTempo', microsecondsPerBeat }); } midiEvents.forEach((event) => { event.ticks = Math.round(event.time - startTime); }); midiEvents.forEach((event, i) => { event.deltaTime = event.ticks - (i > 0 ? midiEvents[i - 1].ticks : 0); }); midiEvents.push({ deltaTime: 0, type: 'meta', subtype: 'endOfTrack' }); return midiEvents; }); return { header, tracks, }; } } EditableMeasure.className = 'EditableMeasure'; EditableMeasure.blackKeys = []; var BeadType; (function (BeadType) { BeadType["Pass"] = "i"; BeadType["Division"] = "d"; BeadType["Dots"] = "o"; })(BeadType || (BeadType = {})); const DIVISION_NAMES = ['whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirtysecond', 'sixtyfourth', '128th', '256th']; const RESIDUE_LOSS_WEIGHT = 0.2; const VOICEN_LOSS_WEIGHT = 0.002; const SPACE_LOSS_WEIGHT = 0.4 / WHOLE_DURATION; const PRETENTIOUSNESS_LOSS_WEIGHT = 0.02; const POSSIBILITY_LOW_BOUNDARY = 1e-12; const PRETENTIOUSNESS_CLIP = 100; const STEM_DIRECTION_OPTIONS = [undefined, 'u', 'd']; const BEAM_OPTIONS = [undefined, StemBeam.Open, StemBeam.Continue, StemBeam.Close]; const saveClusterState = (cluster) => ({ elements: cluster.elements.map((elem) => ({ tick: elem.tick, division: elem.division, dots: elem.dots, beam: elem.beam, stemDirection: elem.stemDirection, grace: elem.grace, timeWarped: elem.timeWarped, fullMeasure: elem.fullMeasure, fake: elem.fake, order: elem.order, predisposition: elem.predisposition, })), }); const restoreClusterState = (cluster, state) => cluster.elements.forEach((elem, i) => Object.assign(elem, state.elements[i])); const measurePretentious = (p) => Math.min(PRETENTIOUSNESS_CLIP, -Math.log(p)); class BeadNode { constructor(data) { Object.assign(this, data); //this.possibilities = this.possibilities.map((x, i) => (this.type === BeadType.Pass && !i) ? 0 : Math.max(POSSIBILITY_LOW_BOUNDARY, x)); this.children = {}; this.accessCount = 0; } nextBranch() { const ps = this.possibilities.map((p, i) => p / (this.children[i] ? this.children[i].accessCount + 1 : 1)); //const ps = this.possibilities.map((p, i) => p * (this.children[i] ? (2 ** -this.children[i].accessCount) : 1)); if (ps.every((p) => !p)) { this.accessCount = Infinity; return null; } return argmax$1(ps); } get currentElem() { return this.cluster.elements[this.elemIndex]; } branchID(ni) { switch (this.type) { case BeadType.Pass: return `i_${ni}`; case BeadType.Division: return DIVISION_NAMES[ni]; case BeadType.Dots: return 'o' + '.'.repeat(ni); } return ''; } async deduce({ picker, logger, ptFactor }, deep = 0) { ++this.accessCount; const ni = this.nextBranch(); logger.debug(String.fromCodePoint(0x1f349) + ' '.repeat(deep), this.branchID(ni), this.accessCount > 1 ? `[${this.accessCount}]` : ''); if (!Number.isInteger(ni) || ni < 0) { this.accessCount = Infinity; return evaluateCluster(this.cluster, this.currentElem.order + 1, this.pretentiousness); } this.pretentiousness += measurePretentious(this.possibilities[ni]); if (this.pretentiousness > PRETENTIOUSNESS_CLIP * ptFactor) { this.accessCount = Infinity; return evaluateCluster(this.cluster, this.currentElem.order + 1, this.pretentiousness); } let selfEval = null; switch (this.type) { case BeadType.Pass: { const tip = this.currentElem.order + 1; const element = this.cluster.elements[ni]; console.assert(element, 'null element:', ni, this.cluster.elements.length); if (element.type === EventElementType.EOS) { selfEval = evaluateCluster(this.cluster, tip, this.pretentiousness); if (!selfEval.residue || selfEval.fatalError) { this.accessCount = Infinity; return selfEval; } this.cluster.elements[0].order = tip; if (!this.children[ni]) { if (!picker.quota) return selfEval; const possibilities = (await picker.predictCluster(this.cluster, tip + 1)).map((x, i) => this.cluster.elements[i].order < tip + 1 || i === this.cluster.elements.length - 1 ? 0 : Math.max(POSSIBILITY_LOW_BOUNDARY, x)); this.children[ni] = new BeadNode({ cluster: this.cluster, elemIndex: 0, type: BeadType.Pass, possibilities, pretentiousness: this.pretentiousness, }); } } else { element.order = tip; if (!this.children[ni]) { console.assert(element.predisposition, 'no predisposition:', ni, this.possibilities); const possibilities = element.predisposition.divisionVector.map((x) => Math.max(POSSIBILITY_LOW_BOUNDARY, x)); this.children[ni] = new BeadNode({ cluster: this.cluster, elemIndex: ni, type: BeadType.Division, possibilities, pretentiousness: this.pretentiousness, }); } } } break; case BeadType.Division: { this.currentElem.division = ni; if (!this.children[ni]) { const possibilities = this.currentElem.predisposition.dotsVector.map((x) => Math.max(POSSIBILITY_LOW_BOUNDARY, x)); this.children[ni] = new BeadNode({ cluster: this.cluster, elemIndex: this.elemIndex, type: BeadType.Dots, possibilities, pretentiousness: this.pretentiousness, }); } } break; case BeadType.Dots: { this.currentElem.dots = ni; selfEval = evaluateCluster(this.cluster, this.currentElem.order + 1, this.pretentiousness); if (!selfEval.residue || selfEval.fatalError) { this.accessCount = Infinity; return selfEval; } if (!this.children[ni]) { if (!picker.quota) return selfEval; const tip = this.currentElem.order + 1; const possibilities = (await picker.predictCluster(this.cluster, tip)).map((x, i) => this.cluster.elements[i].order < tip + 1 ? 0 : Math.max(POSSIBILITY_LOW_BOUNDARY, x)); this.children[ni] = new BeadNode({ cluster: this.cluster, elemIndex: this.elemIndex, type: BeadType.Pass, possibilities, pretentiousness: this.pretentiousness, }); } } break; } const evaluation = await this.children[ni].deduce({ picker, logger, ptFactor }, deep + 1); if (selfEval && evaluation.fatalError) { const tip = this.currentElem.order; this.cluster.elements.forEach((elem) => { if (elem.order > tip) elem.order = undefined; }); // clear children data this.cluster.elements.forEach((elem) => (elem.order = elem.order > this.currentElem.order ? undefined : elem.order)); this.cluster.elements[this.cluster.elements.length - 1].tick = selfEval.endTick; return selfEval; } return evaluation; } } const estimateElementDuration = (elem) => WHOLE_DURATION * 2 ** -elem.division * (2 - 2 ** -elem.dots); const evaluateCluster = (cluster, tip, pretentiousness) => { const events = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && Number.isInteger(elem.order) && elem.order < tip); events.sort((e1, e2) => e1.order - e2.order); const eos = cluster.elements[cluster.elements.length - 1]; let tick = 0; let lastOrder = 0; let endTick = 0; let voiceN = 1; // [x, tick, estimated tick] const scales = [[eos.x, cluster.signatureDuration, cluster.signatureDuration]]; let totalDuration = 0; // assign tick for events events.forEach((event) => { if (event.order > lastOrder + 1) { tick = 0; ++voiceN; } const referenceScale = scales.find((s) => s[1] >= tick); if (referenceScale && event.x > referenceScale[0] + 3) { const nearScale = scales.reduce((n, s) => (Math.abs(event.predisposition.tick - s[2]) < Math.abs(event.predisposition.tick - n[2]) ? s : n)); if (Math.abs(nearScale[0] - event.x) < 2) tick = Math.max(tick, nearScale[1]); } event.tick = tick; const si = Math.max(0, scales.findIndex((s) => s[0] > event.x)); scales.splice(si, 0, [event.x, event.tick, event.predisposition.tick]); //let duration = WHOLE_DURATION * (2 ** -event.division!) * (2 - 2 ** -event.dots!); let duration = estimateElementDuration(event); if (event.predisposition.timeWarped > 0.5) duration = (duration * 2) / 3; tick += duration; totalDuration += duration; endTick = Math.max(endTick, tick); lastOrder = event.order; }); /*const pretentiousness = events.reduce((p, event) => p + measurePretentious(event.predisposition!.divisionVector![event.division!]) + measurePretentious(event.predisposition!.dotsVector![event.dots!]), 0);*/ if (endTick > 0) cluster.elements[cluster.elements.length - 1].tick = endTick; const xSpan = cluster.elements[cluster.elements.length - 1].pivotX - cluster.elements[1].pivotX; const tickSpan = Math.max(...events.map((e) => e.tick), endTick); // tick twist loss const eventsXOrder = [...events].sort((e1, e2) => e1.pivotX - e2.pivotX); const tickTwists = eventsXOrder.slice(1).map((e2, i) => { const e1 = eventsXOrder[i]; const dx = e2.pivotX - e1.pivotX; const dt = e2.tick - e1.tick; if (!dt) return dx / xSpan; const rate = Math.atan2(dt / tickSpan, dx / xSpan); //if (dt < 0) // console.log("minus dt:", dt, dx, rate); return ((rate * 4) / Math.PI - 1) ** 2; }); //console.debug("tickTwists:", tickTwists, eventsXOrder); const twist = Math.max(...tickTwists, 0); const tickMSE = events.map((event) => (event.tick - event.predisposition.tick) ** 2); //console.debug("tickMSE:", tickMSE.map(Math.sqrt)); const tickErr = tickMSE.length ? Math.sqrt(tickMSE.reduce((sum, mse) => sum + mse, 0) / tickMSE.length) : 0; //console.debug("tick/twist:", tickErr / WHOLE_DURATION, twist); const residueElements = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && !(Number.isInteger(elem.order) && elem.order < tip) && !(elem.predisposition && elem.predisposition.fakeP > 0.5)); const residue = residueElements.length; const fatalError = twist >= 1 || endTick > cluster.signatureDuration; //const spaceDuration = Math.max(0, cluster.signatureDuration - endTick); const spaceDuration = Math.max(0, cluster.signatureDuration - totalDuration / voiceN); const loss = tickErr / WHOLE_DURATION + twist + residue * RESIDUE_LOSS_WEIGHT + voiceN * VOICEN_LOSS_WEIGHT + spaceDuration * SPACE_LOSS_WEIGHT + pretentiousness * PRETENTIOUSNESS_LOSS_WEIGHT; return { tickErr, twist, residue, endTick, fatalError, voiceN, spaceDuration, pretentiousness, loss, }; }; const solveCluster = async (cluster, picker, logger, quota = 200, stopLoss = 0, ptFactor = 1) => { cluster.elements.forEach((elem, i) => (elem.order = i ? undefined : 0)); const suc0 = await picker.predictCluster(cluster, 1); const root = new BeadNode({ cluster, elemIndex: 0, pretentiousness: 0, type: BeadType.Pass, possibilities: suc0 }); let bestEvaluation = null; let bestState = null; picker.quota = quota; while (picker.quota) { cluster.elements.forEach((elem, i) => (elem.order = i ? undefined : 0)); const evaluation = await root.deduce({ picker, logger, ptFactor }); logger.debug('loss:', evaluation); if (!bestEvaluation || evaluation.loss < bestEvaluation.loss) { bestEvaluation = evaluation; cluster.duration = bestEvaluation.endTick; bestState = saveClusterState(cluster); if (Number.isFinite(stopLoss) && bestEvaluation.loss <= stopLoss) break; } if (!Number.isFinite(root.accessCount)) break; } logger.debug('bestEvaluation:', bestEvaluation); restoreClusterState(cluster, bestState); // solve residue elements const fixedEvents = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && Number.isInteger(elem.order)); const pendingEvents = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && !Number.isInteger(elem.order)); if (fixedEvents.length) { pendingEvents.forEach((event) => { // exclude fake events (includes grace, fullMeasure) from voices event.tick = undefined; if (event.predisposition.fakeP < 0.5) { //const near = fixedEvents.reduce((n, e) => Math.abs(e.predisposition!.tick - event.predisposition!.tick) < Math.abs(n.predisposition!.tick - event.predisposition!.tick) ? e : n); const duration = estimateElementDuration(event); const candidates = fixedEvents.filter((e) => e.tick + duration <= bestEvaluation.endTick); if (candidates.length) { const near = candidates.reduce((n, e) => (Math.abs(e.x - event.x) < Math.abs(n.x - event.x) ? e : n)); event.tick = near.tick; } } }); } fixedEvents.sort((e1, e2) => e1.order - e2.order); // properties [...fixedEvents, ...pendingEvents].forEach((event) => { event.grace = !Number.isFinite(event.tick) && event.predisposition.grace; event.timeWarped = event.predisposition.timeWarped > 0.5; event.fullMeasure = event.predisposition.fullMeasure > 0.5; event.stemDirection = STEM_DIRECTION_OPTIONS[argmax$1(event.predisposition.stemDirectionVector)]; event.beam = BEAM_OPTIONS[argmax$1(event.predisposition.beamVector)]; }); // construct matrixH const ids = cluster.elements.map((e) => e.index); const idx = (id) => ids.indexOf(id); cluster.matrixH = cluster.elements.map(() => Array(cluster.elements.length).fill(0)); fixedEvents.forEach((event, i) => { const lastEvent = fixedEvents[i - 1]; if (!lastEvent || lastEvent.order < event.order - 1) { cluster.matrixH[idx(event.index)][0] = 1; if (lastEvent) cluster.matrixH[cluster.elements.length - 1][idx(lastEvent.index)] = 1; } else { console.assert(cluster.matrixH[idx(event.index)] && Number.isFinite(cluster.matrixH[idx(event.index)][idx(lastEvent.index)]), 'matrixH out of range:', event.index, lastEvent.index, cluster.matrixH.length); cluster.matrixH[idx(event.index)][idx(lastEvent.index)] = 1; } }); if (!pendingEvents.length && fixedEvents.length) cluster.matrixH[cluster.elements.length - 1][idx(fixedEvents[fixedEvents.length - 1].index)] = 1; return bestEvaluation; }; const solveMeasure = async (measure, options) => { const { stopLoss = 0.09, quotaMax = 1000, quotaFactor = 5, ptFactor = 1, logger = new DummyLogger() } = options; let worstLoss = 0; const clusters = measure.createClusters(); for (const cluster of clusters) { const quota = Math.min(quotaMax, Math.ceil(cluster.elements.length * quotaFactor)); logger.info(`[measure-${measure.measureIndex}]`, quota); const { loss } = await solveCluster(cluster, options.picker, logger, quota, stopLoss, ptFactor); worstLoss = Math.max(worstLoss, loss); } const voices = []; const durations = []; const solutionEvents = []; clusters.forEach((cluster) => { const events = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && Number.isInteger(elem.order)); events.sort((e1, e2) => e1.order - e2.order); if (!events.length) return; let voice = []; voices.push(voice); let lastOrder = 0; events.forEach((event) => { if (event.fullMeasure || event.grace || event.tremoloCatcher) return; if (event.order > lastOrder + 1) { voice = [event.index]; voices.push(voice); } else voice.push(event.index); lastOrder = event.order; }); let tipElem = events[events.length - 1]; // complete voices from pending events const pendingEvents = cluster.elements.filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type) && Number.isFinite(elem.tick) && !Number.isInteger(elem.order)); while (pendingEvents.length) { const ei = pendingEvents.findIndex((e) => e.tick >= tipElem.tick + estimateElementDuration(tipElem)); if (ei >= 0) voice.push(pendingEvents.splice(ei, 1)[0].index); else { tipElem = pendingEvents.splice(0, 1)[0]; voice = [tipElem.index]; voices.push(voice); } } if (events.some((elem) => !elem.fullMeasure && Number.isInteger(elem.order))) { const eos = cluster.elements.find((elem) => elem.type === EventElementType.EOS); durations.push(eos.tick); } const eventMap = measure.eventMap; const tickSet = cluster.elements.reduce((set, elem) => { if (Number.isFinite(elem.tick)) set.add(elem.tick); return set; }, new Set()); const ticks = Array.from(tickSet).sort((t1, t2) => t1 - t2); // fill solutionEvents events.forEach((elem) => { const event = eventMap[elem.index]; if (event) { solutionEvents.push({ id: event.id, tick: elem.tick, tickGroup: ticks.indexOf(elem.tick), division: elem.division !== event.division ? elem.division : undefined, dots: elem.dots !== event.dots ? elem.dots : undefined, timeWarp: elem.timeWarped ? frac(2, 3) : undefined, beam: elem.beam !== event.beam ? elem.beam : undefined, grace: elem.grace !== !!event.grace ? elem.grace : undefined, fullMeasure: elem.fullMeasure || undefined, }); } }); }); const estimatedDuration = Math.max(...clusters.map((c) => c.estimatedDuration)); return { voices: voices.filter((voice) => voice.length), duration: Math.max(...durations), events: solutionEvents, priority: -worstLoss, estimatedDuration, }; }; const glimpseMeasure = async (measure, { picker, resetSignatureForDoubtfulOnly }) => { const clusters = measure.createClusters(); const eventMap = measure.eventMap; for (const cluster of clusters) { if (!resetSignatureForDoubtfulOnly || measure.doubtfulTimesig) cluster.signatureDuration = 0; // re-estimate measure duration cluster.elements.forEach((elem, i) => (elem.order = i ? undefined : 0)); await picker.predictCluster(cluster, 1); cluster.elements .filter((elem) => [EventElementType.CHORD, EventElementType.REST].includes(elem.type)) .forEach((elem) => { const event = eventMap[elem.index]; event.predisposition = elem.predisposition; }); } measure.estimatedDuration = Math.max(...clusters.map((c) => c.estimatedDuration)); }; const estimateMeasure = async (measure, picker) => glimpseMeasure(measure, { picker, resetSignatureForDoubtfulOnly: true }); var beadSolver = /*#__PURE__*/Object.freeze({ __proto__: null, solveCluster: solveCluster, solveMeasure: solveMeasure, estimateMeasure: estimateMeasure, glimpseMeasure: glimpseMeasure }); var index = /*#__PURE__*/Object.freeze({ __proto__: null, beadSolver: beadSolver, get PageLayoutMethod () { return PageLayoutMethod; }, get TextType () { return TextType; }, TokenTypes: TokenTypes, TokenClefs: TokenClefs, TokenTimesigs: TokenTimesigs, TokenTimesigsC: TokenTimesigsC, TokenTimesigsN: TokenTimesigsN, TokenOctshifts: TokenOctshifts, TokenNumbers: TokenNumbers, TokenAccidentals: TokenAccidentals, TokenNoteheads: TokenNoteheads, TokenBareNoteheads: TokenBareNoteheads, TokenDirectionalNoteheads: TokenDirectionalNoteheads, TokenRests: TokenRests, TokenFlags: TokenFlags, TokenVolta: TokenVolta, TokenDynamics: TokenDynamics, TokenScripts: TokenScripts, TokenPedals: TokenPedals, TokenDots: TokenDots, TokenArcs: TokenArcs, TokenBeams: TokenBeams, TokenWedges: TokenWedges, TokenAccessories: TokenAccessories, TokenDirectionless: TokenDirectionless, TokenGlyphs: TokenGlyphs, get TokenType () { return TokenType; }, Token: Token, TextToken: TextToken, TOKEN_Y_ROUND: TOKEN_Y_ROUND, TOKEN_Y_FIXED: TOKEN_Y_FIXED, VERSION: VERSION, Score: Score, Page: Page, System: System, Staff: Staff, Measure: Measure, emptyVoiceFromStaffMeasure: emptyVoiceFromStaffMeasure, SpartitoMeasure: SpartitoMeasure, Spartito: Spartito, EditableEvent: EditableEvent, EditableMeasure: EditableMeasure, Term: Term, EventTerm: EventTerm, ContextedTerm: ContextedTerm, MarkTerm: MarkTerm, TempoTerm: TempoTerm, GlyphTerm: GlyphTerm, TextTerm: TextTerm, LyricTerm: LyricTerm, CommandTerm: CommandTerm, ChordmodeTerm: ChordmodeTerm, get ContextType () { return ContextType; }, get GraceType () { return GraceType; }, get GlissandoStyle () { return GlissandoStyle; }, get ArpeggioStyle () { return ArpeggioStyle; }, get AccessoryDirection () { return AccessoryDirection; }, WHOLE_DURATION: WHOLE_DURATION, get StemBeam () { return StemBeam; }, get TremoloLink () { return TremoloLink; }, mod7: mod7, get SemanticType () { return SemanticType; }, glyphSemanticMapping: glyphSemanticMapping, semanticPriorities: semanticPriorities, NOTEHEAD_WIDTHS: NOTEHEAD_WIDTHS, glyphCenters: glyphCenters, ONE_D_SEMANTICS: ONE_D_SEMANTICS, SYSTEM_SEMANTIC_TYPES: SYSTEM_SEMANTIC_TYPES, CONFLICTION_GROUPS: CONFLICTION_GROUPS, STAMP_SEMANTICS: STAMP_SEMANTICS, STAMP_RECTS: STAMP_RECTS, hashSemanticPoint: hashSemanticPoint, hashPageSemanticPoint: hashPageSemanticPoint, SemanticGraph: SemanticGraph, get SemanticElementType () { return SemanticElementType; }, SemanticCluster: SemanticCluster, SemanticClusterSet: SemanticClusterSet, ELEMENT_TOKEN_NAMES: ELEMENT_TOKEN_NAMES, NOTEHEAD_ELEMENT_TYPES: NOTEHEAD_ELEMENT_TYPES, NOTE_ELEMENT_TYPES: NOTE_ELEMENT_TYPES, BOS_ELEMENT: BOS_ELEMENT, fractionToElems: fractionToElems, expandMatrixByMasks: expandMatrixByMasks, expandMatrixByMaskTriu: expandMatrixByMaskTriu, matrixFromGroups: matrixFromGroups, get EventElementType () { return EventElementType; }, EventCluster: EventCluster, EventClusterSet: EventClusterSet, recoverJSON: recoverJSON, SimpleClass: SimpleClass, PatchMeasure: PatchMeasure, evaluateMeasure: evaluateMeasure }); // Here suppose sum of pvals equal to 1. const multinomial_1 = (pvals) => { const n = Math.random(); let s = 0; for (let i = 0; i < pvals.length; ++i) { s += pvals[i]; if (s > n) return i; } return pvals.length - 1; }; const looseVector = (ns, factor = 0.9) => { const logits = ns.map((n) => Math.log(n) * factor); const n2 = logits.map(Math.exp); const sum = n2.reduce((sum, x) => sum + x, 0); return n2.map((x) => x / sum); }; const looseEvent = (event) => { if (!event.predisposition?.divisionVector && !event.predisposition?.dotsVector) return event; const divisionVector = event.predisposition?.divisionVector ? looseVector(event.predisposition.divisionVector) : null; const dotsVector = event.predisposition?.dotsVector ? looseVector(event.predisposition.dotsVector) : null; return new EventTerm({ ...event, predisposition: { ...event.predisposition, divisionVector, dotsVector, }, }); }; class MeasureRectification { constructor(data) { Object.assign(this, data); } toString() { return this.events .map((event) => { if (!event) return ''; const { division = '', dots = '' } = event; return `${division}|${dots}`; }) .join(','); } static default(events) { return new MeasureRectification({ events: events.map((event) => { if (!event.predisposition?.divisionVector && !event.predisposition?.dotsVector) return null; const division = event.predisposition.divisionVector ? event.division : undefined; const dots = event.predisposition.dotsVector ? event.dots : undefined; return { id: event.id, division, dots }; }), }); } static roll(events) { return new MeasureRectification({ events: events.map((event) => { if (!event.predisposition?.divisionVector && !event.predisposition?.dotsVector) return null; let division = undefined; let dots = undefined; if (event.predisposition.divisionVector) division = multinomial_1(event.predisposition.divisionVector); if (event.predisposition.dotsVector) dots = multinomial_1(event.predisposition.dotsVector); return { id: event.id, division, dots }; }), }); } } const genMeasureRectifications = function* (measure) { const keys = new Set(); const origin = MeasureRectification.default(measure.events); keys.add(origin.toString()); yield origin; let stale = 0; let events = measure.events; while (stale < 100) { if (stale && stale % 10 === 0) events = events.map(looseEvent); const rectification = MeasureRectification.roll(events); const key = rectification.toString(); if (keys.has(key)) { ++stale; continue; } stale = 0; keys.add(key); yield rectification; } }; const lruCache = new weakLruCache.WeakLRUCache(); // 默认store const DefaultSolutionStore = { async get(key) { return lruCache.getValue(key); }, async set(key, val) { lruCache.setValue(key, val); }, async batchGet(keys) { return keys.map((key) => lruCache.getValue(key)); }, }; var PendingCondition; (function (PendingCondition) { PendingCondition[PendingCondition["ErrorOnly"] = 0] = "ErrorOnly"; PendingCondition[PendingCondition["NotFine"] = 1] = "NotFine"; PendingCondition[PendingCondition["Imperfect"] = 2] = "Imperfect"; })(PendingCondition || (PendingCondition = {})); const isPending = (evaluation, condition) => { switch (condition) { case PendingCondition.ErrorOnly: return evaluation.error; case PendingCondition.Imperfect: return !evaluation.perfect; } return !evaluation.fine; }; const solveMeasureRecords = async (records, onUpdate, stdout, options, pendingCondition = PendingCondition.NotFine, pass = 0, onProgress) => { const pendingRecords = records.filter(({ evaluation }) => !evaluation || isPending(evaluation, pendingCondition)); stdout?.write('.'.repeat(pendingRecords.length)); stdout?.write('\b'.repeat(pendingRecords.length)); const total = pendingRecords.length; let done = 0; for (const record of pendingRecords) { const measure = record.current.deepCopy(); measure.staffGroups = record.current.staffGroups; const solution = await solveMeasure(measure, { picker: record.picker, ...options }); measure.applySolution(solution); const evaluation = evaluateMeasure(measure); const better = !record.evaluation || evaluation.fine > record.evaluation.fine || (evaluation.qualityScore > record.evaluation.qualityScore && evaluation.fine === record.evaluation.fine); if (better) { record.evaluation = evaluation; Object.assign(record.current, measure); } onUpdate(record.current, evaluation, better); done++; onProgress?.(record.current, evaluation, better, { pass, remaining: total - done, total }); } if (pendingRecords.length) stdout?.write('\n'); return pendingRecords.length; }; const regulateWithBeadSolver = async (score, { logger, pickers, solutionStore = DefaultSolutionStore, ignoreCache, freshOnly, onSaveIssueMeasure, onProgress, onPassStart }) => { score.spartito = undefined; score.assemble(); const spartito = score.makeSpartito(); spartito.measures.forEach((measure) => score.assignBackgroundForMeasure(measure)); const t0 = Date.now(); logger?.info(`[regulateWithBeadSolver] begin, measure total: ${spartito.measures.length}.`, ignoreCache ? 'ignoreCache' : '', freshOnly ? 'freshOnly' : ''); const records = spartito.measures .filter((measure) => measure.events?.length && !measure.patched) .map((measure) => ({ origin: measure.deepCopy(), current: measure, evaluation: undefined, baseQuality: 0, })); // rectify time signature for (const measure of spartito.measures.filter((measure) => measure.events?.length)) { const picker = pickers.find((picker) => picker.n_seq > measure.events.length + 1); if (picker) await estimateMeasure(measure, picker); } spartito.rectifyTimeSignatures(logger); // zero pickers' cost pickers.forEach((picker) => (picker.cost = 0)); const counting = { cached: 0, simple: 0, computed: 0, tryTimes: 0, solved: 0, issue: 0, fatal: 0, }; logger?.info(`[regulateWithBeadSolver] measures estimation finished.`); // apply solutions if (solutionStore && !ignoreCache) for (const record of records) { const solution = await solutionStore.get(record.origin.regulationHash0); if (solution) { record.current.applySolution(solution); ++counting.cached; record.evaluation = evaluateMeasure(record.current); record.baseQuality = record.evaluation.qualityScore; } } logger?.info('[regulateWithBeadSolver]', `${counting.cached}/${records.length}`, 'solutions loaded.'); const stdout = logger ? null : process.stdout; if (counting.cached) stdout?.write(`${counting.cached}c`); records.forEach((record) => { const picker = pickers.find((picker) => picker.n_seq > record.current.events.length + 1); if (!picker) { logger?.info(`[regulateWithBeadSolver] measure[${record.current.measureIndex}] size out of range:`, record.current.events.length); } else record.picker = picker; }); const pendingRecords = records.filter((record) => record.picker && (!record.evaluation || (!record.evaluation.fine && !freshOnly))); // solve by simple policy pendingRecords.forEach((record) => { const measure = record.current.deepCopy(); measure.staffGroups = record.current.staffGroups; measure.regulate({ policy: 'simple' }); const evaluation = evaluateMeasure(measure); const better = !record.evaluation || evaluation.qualityScore > record.evaluation.qualityScore; if (better) { record.evaluation = evaluation; Object.assign(record.current, measure); if (evaluation.perfect) { logger?.info(`[regulateWithBeadSolver] measure[${record.current.measureIndex}] regulated by simple policy.`); ++counting.simple; } } }); counting.computed = pendingRecords.length - counting.simple; if (counting.simple) stdout?.write(`${counting.simple}s`); const onUpdate = (measure, evaluation, better) => { logger?.info(`[regulateWithBeadSolver] measure[${measure.measureIndex}/${spartito.measures.length}] regulated${better ? '+' : '-'}: ${evaluation.qualityScore.toFixed(3)}, ${evaluation.fine ? 'solved' : evaluation.error ? 'error' : 'issue'}, ${measure.regulationHash}`); stdout?.write(`\x1b[${evaluation.fine ? '32' : evaluation.error ? '31' : '33'}m${better ? '+' : '-'}\x1b[0m`); }; // Global progress: total = all measures, remaining = non-fine measures across all passes const totalMeasures = spartito.measures.length; const computeRemaining = () => pendingRecords.filter((r) => !r.evaluation?.fine).length; const wrappedOnProgress = onProgress ? (measure, evaluation, better, progress) => { onProgress(measure, evaluation, better, { pass: progress.pass, remaining: computeRemaining(), total: totalMeasures }); } : undefined; onPassStart?.(1, 'Imperfect', computeRemaining()); counting.tryTimes += await solveMeasureRecords(pendingRecords, onUpdate, stdout, { stopLoss: 0.05, quotaMax: 200, quotaFactor: 3, ptFactor: 1 }, PendingCondition.Imperfect, 1, wrappedOnProgress); onPassStart?.(2, 'NotFine', computeRemaining()); counting.tryTimes += await solveMeasureRecords(pendingRecords, onUpdate, stdout, { stopLoss: 0.08, quotaMax: 1000, quotaFactor: 20, ptFactor: 1.6 }, PendingCondition.NotFine, 2, wrappedOnProgress); onPassStart?.(3, 'ErrorOnly', computeRemaining()); counting.tryTimes += await solveMeasureRecords(pendingRecords, onUpdate, stdout, { stopLoss: 0.08, quotaMax: 1000, quotaFactor: 40, ptFactor: 3 }, PendingCondition.ErrorOnly, 3, wrappedOnProgress); pendingRecords.forEach(({ evaluation, baseQuality, current, origin }) => { if (evaluation.fine) ++counting.solved; else if (evaluation.error) ++counting.fatal; else ++counting.issue; if (evaluation.qualityScore > baseQuality || !baseQuality) { solutionStore.set(origin.regulationHash0, { ...current.asSolution(origin), priority: -current?.solutionStat?.loss }); if (current.regulationHash !== origin.regulationHash0) solutionStore.set(current.regulationHash, { ...current.asSolution(), priority: -current?.solutionStat?.loss }); //console.log('better:', current.measureIndex, evaluation.qualityScore, baseQuality); } if (!evaluation.fine) { onSaveIssueMeasure?.({ measureIndex: current.measureIndex, measure: new EditableMeasure(current), status: evaluation.error ? 2 /* MeasureStatus.Fatal */ : 1 /* MeasureStatus.Issue */, }); } }); const t1 = Date.now(); const pickerCost = pickers.reduce((cost, picker) => cost + picker.cost, 0); const qualityScore = spartito.qualityScore; const totalCost = t1 - t0; logger?.info('[regulateWithBeadSolver] done in ', totalCost, 'ms, qualityScore:', qualityScore); // zero 'cached' statistics for freshOnly mode if (freshOnly) counting.cached = 0; return { totalCost: t1 - t0, pickerCost, measures: counting, qualityScore, }; }; const abstractRegulationBeadStats = (stats) => { const { totalCost, pickerCost, measureN, timeN } = stats.reduce((sum, stat) => ({ totalCost: sum.totalCost + stat.totalCost, pickerCost: sum.pickerCost + stat.pickerCost, measureN: sum.measureN + stat.measures.computed, timeN: sum.timeN + stat.measures.tryTimes, }), { totalCost: 0, pickerCost: 0, measureN: 0, timeN: 0, }); const costPerMeasure = measureN > 0 ? totalCost / measureN : null; const costPerTime = timeN > 0 ? totalCost / timeN : null; const { cached, simple, computed, tryTimes, solved, issue, fatal } = stats.reduce((sum, stat) => ({ cached: sum.cached + stat.measures.cached, simple: sum.simple + stat.measures.simple, computed: sum.computed + stat.measures.computed, tryTimes: sum.tryTimes + stat.measures.tryTimes, solved: sum.solved + stat.measures.solved, issue: sum.issue + stat.measures.issue, fatal: sum.fatal + stat.measures.fatal, }), { cached: 0, simple: 0, computed: 0, tryTimes: 0, solved: 0, issue: 0, fatal: 0 }); return { scoreN: stats.length, totalCost, pickerCost, costPerMeasure, costPerTime, cached, simple, computed, tryTimes, solved, issue, fatal, }; }; globalThis.btoa = globalThis.btoa || ((str) => Buffer.from(str, 'binary').toString('base64')); const RECTIFICATION_SEARCH_ITERATIONS = parseInt(process.env.RECTIFICATION_SEARCH_ITERATIONS || '30'); const BASE_QUOTA_FACTOR = parseInt(process.env.BASE_QUOTA_FACTOR || '40'); const RECTIFICATION_QUOTA_FACTOR = parseInt(process.env.RECTIFICATION_QUOTA_FACTOR || '80'); const MATRIXH_INTERPOLATION_K = 0.9; const computeQuota = (n, factor, limit) => Math.min(Math.ceil((n + 1) * factor * Math.log(n + 2)), Math.ceil(limit * Math.min(1, (24 / (n + 1)) ** 2))); async function solveMeasures(measures, { solver, quotaMax = 1000, quotaFactor = BASE_QUOTA_FACTOR, solutionStore = DefaultSolutionStore, ignoreCache = false, logger } = {}) { let cached = 0; let solved = 0; logger?.info(`[solveMeasures] begin, measure total: ${measures.length}.`); await Promise.all(measures.map(async (measure) => { if (!ignoreCache) { const solution = await solutionStore.get(measure.regulationHash); if (solution) { measure.applySolution(solution); ++cached; return; } } const quota = computeQuota(measure.events.length, quotaFactor, quotaMax); await measure.regulate({ policy: 'equations', quota, solver, }); const stat = evaluateMeasure(measure); if (!stat.error) solutionStore.set(measure.regulationHash0, { ...measure.asSolution(), priority: -measure?.solutionStat?.loss }); if (stat.perfect) ++solved; logger?.info(`[solveMeasures] measure[${measure.measureIndex}/${measures.length}] regulated: ${stat.perfect ? 'solved' : stat.error ? 'error' : 'issue'}, ${measure.regulationHash}`); })); logger?.info(`[solveMeasures] ${cached}/${measures.length} cache hit, ${solved} solved.`); return { cached, computed: measures.length - cached, solved, }; } const solveMeasuresWithRectifications = async (measure, { solver, quotaMax = 4000 }) => { let best = evaluateMeasure(measure); let bestSolution = measure.asSolution(); const quota = computeQuota(measure.events.length, RECTIFICATION_QUOTA_FACTOR, quotaMax); let n_rec = 0; // @ts-ignore for (const rec of genMeasureRectifications(measure)) { const solution = await EquationPolicy.regulateMeasureWithRectification(measure, rec, { solver, quota }); const testMeasure = measure.deepCopy(); testMeasure.applySolution(solution); const result = evaluateMeasure(testMeasure); if (result.perfect > best.perfect || result.error < best.error || (!result.error && result.perfect >= best.perfect && solution.priority > bestSolution.priority)) { best = result; bestSolution = solution; } if (result.perfect) break; ++n_rec; if (n_rec > RECTIFICATION_SEARCH_ITERATIONS) break; } return bestSolution; }; async function doRegulateWithTopo(score, { pyClients, solver, solutionStore = DefaultSolutionStore, onSaveIssueMeasure }) { pyClients.logger.info(`[RegulateWithTopo] regulate score: ${score.title}, measures: ${score.spartito.measures.length}`); const issueMeasures = score.spartito.measures.filter((measure) => { const stat = evaluateMeasure(measure); return !stat.perfect; }); pyClients.logger.info(`[RegulateWithTopo] basic issues: ${issueMeasures.length}`); if (issueMeasures.length === 0) { return { solved: 0, issue: 0, fatal: 0, }; } const clusters = [].concat(...issueMeasures.map((measure) => measure.createClusters())); const results = await pyClients.predictScoreImages('topo', { clusters }); console.assert(results.length === clusters.length, 'prediction number mismatch:', clusters.length, results.length); clusters.forEach((cluster, index) => { const result = results[index]; console.assert(result, 'no result for cluster:', cluster.index); cluster.assignPrediction(result); }); issueMeasures.forEach((measure) => { const cs = clusters.filter((c) => c.index === measure.measureIndex); measure.applyClusters(cs); // intepolate matrixH const { matrixH } = EquationPolicy.estiamteMeasure(measure); matrixH.forEach((row, i) => row.forEach((v, j) => { measure.matrixH[i][j] = measure.matrixH[i][j] * MATRIXH_INTERPOLATION_K + v * (1 - MATRIXH_INTERPOLATION_K); })); }); const solvedIndices = []; const errorIndices = []; // rectification search await Promise.all(issueMeasures.map(async (measure) => { const hash = measure.regulationHash0; const solution = await solveMeasuresWithRectifications(measure, { solver }); if (solution) { measure.applySolution(solution); solutionStore.set(hash, solution); solutionStore.set(measure.regulationHash, measure.asSolution()); pyClients.logger.info(`[RegulateWithTopo] solutionStore set: ${measure.measureIndex}, ${hash}, ${measure.regulationHash}`); } const stat = evaluateMeasure(measure); onSaveIssueMeasure?.({ measureIndex: measure.measureIndex, measure: new EditableMeasure(measure), status: stat.error ? 2 : 1, }); if (stat.perfect) solvedIndices.push(measure.measureIndex); else if (stat.error) errorIndices.push(measure.measureIndex); })); const n_issues = issueMeasures.length - solvedIndices.length - errorIndices.length; pyClients.logger.info(`[RegulateWithTopo] score: ${score.title}, solved/issue/fatal: ${solvedIndices.length}/${n_issues}/${errorIndices.length}`); if (solvedIndices.length) pyClients.logger.info(`[RegulateWithTopo] solved measures: ${solvedIndices.join(', ')}`); if (errorIndices.length) pyClients.logger.info(`[RegulateWithTopo] error measures: ${errorIndices.join(', ')}`); return { solved: solvedIndices.length, issue: n_issues, fatal: errorIndices.length, }; } const doRegulate = async (score, { pyClients, solver, solutionStore = DefaultSolutionStore, onSaveIssueMeasure }) => { pyClients?.logger?.info(`[doRegulate] score: ${score.title}`); score.spartito = undefined; score.assemble(); const spartito = score.makeSpartito(); spartito.measures.forEach((measure) => score.assignBackgroundForMeasure(measure)); const t0 = Date.now(); const baseMeasures = await solveMeasures(spartito.measures, { solver, quotaMax: 1000, solutionStore, logger: pyClients?.logger }); const t1 = Date.now(); const topoMeasures = pyClients ? await doRegulateWithTopo(score, { pyClients, solver, solutionStore, onSaveIssueMeasure }) : undefined; const t2 = Date.now(); return { baseCost: t1 - t0, topoCost: t2 - t1, baseMeasures, topoMeasures, qualityScore: spartito.qualityScore, }; }; const doSimpleRegulate = async (score, { solver, solutionStore = DefaultSolutionStore, logger, quotaMax = 240, quotaFactor = 16 }) => { score.assemble(); const spartito = score.spartito || score.makeSpartito(); const measures = spartito.measures.filter((measure) => !measure.regulated); await solveMeasures(measures, { solver, quotaMax, quotaFactor, solutionStore, logger }); console.assert(score.spartito?.regulated, 'doSimpleRegulate: regulation incomplete:', spartito.measures.filter((measure) => !measure.regulated).length); }; const evaluateScoreQuality = async (score, options) => { if (!score.spartito?.regulated) await doSimpleRegulate(score, options); return score.spartito.regulated ? score.spartito.qualityScore : null; }; const abstractRegulationStats = (stats) => { const { baseCostTotal, topoCostTotal, baseMeasures, topoMeasures } = stats.reduce((sum, stat) => ({ baseCostTotal: sum.baseCostTotal + stat.baseCost, topoCostTotal: sum.topoCostTotal + stat.topoCost, baseMeasures: sum.baseMeasures + stat.baseMeasures.computed, topoMeasures: sum.topoMeasures + (stat.topoMeasures.solved + stat.topoMeasures.issue + stat.topoMeasures.fatal), }), { baseCostTotal: 0, topoCostTotal: 0, baseMeasures: 0, topoMeasures: 0, }); const baseCostPerMeasure = baseMeasures > 0 ? baseCostTotal / baseMeasures : null; const topoCostPerMeasure = topoMeasures > 0 ? topoCostTotal / topoMeasures : null; const { cached, baseComputed, baseSolved, topoSolved, topoIssue, topoFatal } = stats.reduce((sum, stat) => ({ cached: sum.cached + stat.baseMeasures.cached, baseComputed: sum.baseComputed + stat.baseMeasures.computed, baseSolved: sum.baseSolved + stat.baseMeasures.solved, topoSolved: sum.topoSolved + stat.topoMeasures.solved, topoIssue: sum.topoIssue + stat.topoMeasures.issue, topoFatal: sum.topoFatal + stat.topoMeasures.fatal, }), { cached: 0, baseComputed: 0, baseSolved: 0, topoSolved: 0, topoIssue: 0, topoFatal: 0 }); return { scoreN: stats.length, baseCostTotal, topoCostTotal, baseCostPerMeasure, topoCostPerMeasure, cached, baseComputed, baseSolved, topoSolved, topoIssue, topoFatal, }; }; const SYSTEM_MARGIN = 4; const constructSystem = ({ page, backgroundImage, detection, imageSize, position }) => { const systemWidth = (detection.phi2 - detection.phi1) / detection.interval; const systemHeight = imageSize.height / detection.interval; const lastSystem = page.systems[page.systems.length - 1]; const top = position ? position.y : (lastSystem ? lastSystem.top + lastSystem.height : 0) + SYSTEM_MARGIN; const left = position ? position.x : SYSTEM_MARGIN; const stavesTops = [ 0, ...Array(detection.middleRhos.length - 1) .fill(0) .map((_, i) => (detection.middleRhos[i] + detection.middleRhos[i + 1]) / 2 / detection.interval), ]; const measureBars = [systemWidth]; const staves = stavesTops.map((top, i) => new Staff({ top, height: (stavesTops[i + 1] || systemHeight) - top, staffY: detection.middleRhos[i] / detection.interval - top, measureBars, })); //console.log("detection:", detection, options, stavesTops); const imagePosition = { x: -detection.phi1 / detection.interval, y: 0, width: imageSize.width / detection.interval, height: imageSize.height / detection.interval, }; return new System({ staves, left, top, width: systemWidth, backgroundImage, imagePosition, measureBars, }); }; const toBuffer = async (url) => { if (typeof url === 'string') { if (/^https?:\/\//.test(url)) { return (await got__default["default"](url, { responseType: 'buffer', decompress: true, https: { rejectUnauthorized: false } })).body; } if (/^data:image\//.test(url)) { return Buffer.from(url.split(',')[1], 'base64'); } return Buffer.from(url); } return url; }; /** * 转换图片格式,默认webp、最大高度1080,高度小于1080自动不做尺寸变换 * @param url * @param format * @param maxHeight * @param quality */ async function convertImage(url, { format = 'webp', maxHeight = 1080, quality = 80 } = {}) { let buf = await toBuffer(url); const webpBuffer = await new Promise((resolve) => { sharp__default["default"](buf) .resize({ width: maxHeight, height: maxHeight, fit: 'inside', withoutEnlargement: true, }) .toFormat(format, { quality }) .toBuffer((err, buf) => { resolve(buf); }); }); const md5 = SparkMD5__default["default"].ArrayBuffer.hash(webpBuffer); return { buffer: webpBuffer, filename: `${md5}.${format}`, }; } /** * 替换scoreJson图片地址 * @param scoreJson * @param onReplaceImage */ const replaceScoreJsonImages = (scoreJson, onReplaceImage = (src) => src) => { const json = JSON.parse(JSON.stringify(scoreJson)); json.pages.forEach((page) => { page?.src && (page.src = onReplaceImage(page?.src)); }); json.lines.forEach((system) => { system.lineStaves.forEach((line) => { line.imgs.forEach((staff) => { staff?.src && (staff.src = onReplaceImage(staff.src)); }); }); }); return json; }; /** * 获取scoreJson图片资源列表 * @param scoreJson */ const getScoreJsonImages = (scoreJson) => { return [ ...scoreJson.pages.map((page) => page?.src), ...scoreJson.lines .map((system) => system.lineStaves.map((staff) => staff.imgs)) .flat(2) .map((staff) => staff?.src) .filter(Boolean), ]; }; const updateScorePatches = (score, measures, options = {}) => { console.assert(measures.every((measure) => measure.validRegulated), '[updateScorePatches] some measures not valid regulated:', measures.filter((measure) => !measure.validRegulated)); score.patches = measures.map((measure) => measure.createPatch()); if (options?.solutionStore) { score.assemble(); const spartito = score.makeSpartito(); measures.forEach((measure) => { options.solutionStore.set(measure.regulationHash, { ...measure.asSolution(), priority: 1 }); if (measure.regulationHash0 !== measure.regulationHash) { const originMeasure = spartito.measures.find((m) => m.measureIndex === measure.measureIndex); options.solutionStore.set(measure.regulationHash0, { ...measure.asSolution(originMeasure), priority: 1 }); } }); } }; const saveEditableMeasures = async (score, measureIndices, saveMeasure, { status = 2, solutionStore } = {}) => { score.assemble(); const spartito = score.spartito || score.makeSpartito(); const measures = measureIndices .map((index) => spartito.measures.find((measure) => measure.measureIndex === index)) .filter(Boolean); if (solutionStore) { const solutions = await solutionStore.batchGet(measures.map((measure) => measure.regulationHash0)); measures.forEach((measure, i) => { const solution = solutions[i]; if (solution) measure.applySolution(solution); }); } measures.forEach((measure) => { saveMeasure({ measureIndex: measure.measureIndex, measure: new EditableMeasure(measure), status, }); }); }; console.info(`%cstarry-omr%c v1.0.0 2026-02-20T12:54:09.091Z`, 'color:#fff; background-color: #555;padding: 5px;border-radius: 3px 0 0 3px;', 'color: #fff; background-color: #007dc6;padding: 5px;border-radius: 0 3px 3px 0;'); exports.PyClients = PyClients; exports.abstractRegulationBeadStats = abstractRegulationBeadStats; exports.abstractRegulationStats = abstractRegulationStats; exports.constructSystem = constructSystem; exports.convertImage = convertImage; exports.doRegulate = doRegulate; exports.doSimpleRegulate = doSimpleRegulate; exports.evaluateScoreQuality = evaluateScoreQuality; exports.getScoreJsonImages = getScoreJsonImages; exports.regulateWithBeadSolver = regulateWithBeadSolver; exports.replaceScoreJsonImages = replaceScoreJsonImages; exports.saveEditableMeasures = saveEditableMeasures; exports.starry = index; exports.updateScorePatches = updateScorePatches; //# sourceMappingURL=regulator.js.map