import {MusicNotation} from "@k-l-lambda/music-widgets"; import {ChordPosition} from "./notation"; declare class StaffToken { system?: number; measure?: number; x: number; endX?: number; }; interface TickItem { tick: number; endTick?: number; system: number; measure: number; x: number; endX: number; }; interface SheetPosition { system: number, x: number, }; interface Note extends MusicNotation.Note { chordPosition?: ChordPosition; measure?: number; outMeasure?: boolean; }; interface Notation { notes: Note[]; endTick: number; }; export default class Scheduler { tickTable: TickItem[]; static createFromNotation (midiNotation: Notation, tokenMap: Map): Scheduler { const tokenTable: {[key: number]: StaffToken[]} = {}; const idSet = new Set(); let measureIndex = 0; midiNotation.notes.filter(note => !note.outMeasure && (!note.chordPosition || note.chordPosition.index === 0)).forEach(note => { if (note.ids) { if (note.measure !== measureIndex) { idSet.clear(); measureIndex = note.measure; } else if (note.ids.some(id => idSet.has(id))) return; note.ids.forEach(idSet.add.bind(idSet)); tokenTable[note.startTick] = tokenTable[note.startTick] || []; const tokens = note.ids.map(id => tokenMap.get(id)).filter(t => t); tokenTable[note.startTick].push(...tokens); } }); //console.log("tokenTable:", tokenTable); return Scheduler.createFromTokenTable(tokenTable, midiNotation.endTick); } static createFromTokenTable (tokenTable: {[key: number]: StaffToken[]}, endTick: number): Scheduler { const tickTable: TickItem[] = Object.entries(tokenTable).map(([tick, tokens]) => { if (!tokens.length) return null; tokens.sort((t1, t2) => t1.system === t2.system ? t1.x - t2.x : t1.system - t2.system); const token = tokens[0]; return { tick: Number(tick), system: token.system, measure: token.measure, x: token.x, endX: token.endX, }; }).filter(Boolean).sort((i1, i2) => i1.tick - i2.tick); //console.log("sequence:", sequence); tickTable.forEach((item, i) => { const nextItem = tickTable[i + 1]; item.endTick = nextItem ? nextItem.tick : endTick; console.assert(item.endTick > item.tick, "invalid tick item, tick span is non-positive:", item, tokenTable[item.tick]); if (nextItem && item.system === nextItem.system && [0, 1].includes(nextItem.measure - item.measure)) item.endX = nextItem.x; }); return new Scheduler({ tickTable, }); } constructor ({tickTable}: {tickTable: TickItem[]}) { console.assert(tickTable.length > 0, "invalid tick table:", tickTable); this.tickTable = tickTable; } get startTick (): number { if (!this.tickTable[0]) return null; return this.tickTable[0].tick; } get endTick (): number { if (!this.tickTable[0]) return null; return this.tickTable[this.tickTable.length - 1].endTick; } lookupPosition (tick: number): SheetPosition { tick = Math.max(Math.min(tick, this.endTick), this.startTick); const item = this.tickTable.find(item => item.tick <= tick && item.endTick > tick) || this.tickTable[this.tickTable.length - 1]; if (!item) { console.warn("cannot find tick item:", tick, this.tickTable); return null; } const x = item.x + (tick - item.tick) * (item.endX - item.x) / (item.endTick - item.tick); return { system: item.system, x, }; } lookupTick (position: SheetPosition): number { const rowItems = this.tickTable.filter(item => item.system === position.system).sort((i1, i2) => i1.x - i2.x); let item = rowItems.find(item => item.x <= position.x && item.endX >= position.x); if (!item) { const firstItem = rowItems[0]; if (firstItem && position.x < firstItem.endX) item = firstItem; else //console.warn("lookup position out of range:", position, this.tickTable); return null; } const tick = item.tick + Math.max(position.x - item.x, 0) * (item.endTick - item.tick) / (item.endX - item.x); return tick; } };