UNPKG

@logic-pad/core

Version:
232 lines (231 loc) 8.81 kB
import { ConfigType } from '../config.js'; import GridData from '../grid.js'; import { resize } from '../dataHelper.js'; import { Color, MajorRule, State } from '../primitives.js'; import CustomIconSymbol from '../symbols/customIconSymbol.js'; import { ControlLine, Row } from './musicControlLine.js'; import Rule from './rule.js'; const DEFAULT_SCALLE = [ new Row('C5', null), new Row('B4', null), new Row('A4', null), new Row('G4', null), new Row('F4', null), new Row('E4', null), new Row('D4', null), new Row('C4', null), ]; class MusicGridRule extends Rule { /** * **Music Grid: Listen to the solution** * @param controlLines Denote changes in the playback settings. At least one control line at column 0 should be present to enable playback. * @param track The grid to be played when "listen" is clicked. Set as null to play the solution. * @param normalizeVelocity Whether to normalize the velocity of the notes by their pitch such that lower notes are played softer. */ constructor(controlLines, track, normalizeVelocity = true) { super(); Object.defineProperty(this, "controlLines", { enumerable: true, configurable: true, writable: true, value: controlLines }); Object.defineProperty(this, "track", { enumerable: true, configurable: true, writable: true, value: track }); Object.defineProperty(this, "normalizeVelocity", { enumerable: true, configurable: true, writable: true, value: normalizeVelocity }); this.controlLines = MusicGridRule.deduplicateControlLines(controlLines); this.track = track; this.normalizeVelocity = normalizeVelocity; } get id() { return MajorRule.MusicGrid; } get explanation() { return `*Music Grid:* Listen to the solution`; } get configs() { return MusicGridRule.CONFIGS; } createExampleGrid() { return MusicGridRule.EXAMPLE_GRID; } get searchVariants() { return MusicGridRule.SEARCH_VARIANTS; } validateGrid(_grid) { return { state: State.Incomplete }; } onSetGrid(_oldGrid, newGrid, _solution) { if (newGrid.getTileCount(true, undefined, Color.Gray) === 0) return newGrid; const tiles = newGrid.tiles.map(row => row.map(tile => tile.color === Color.Gray ? tile.withColor(Color.Light) : tile)); return newGrid.copyWith({ tiles }); } onGridChange(newGrid) { if (this.controlLines.length === 0) return this; if (newGrid.height === this.controlLines[0].rows.length && !this.controlLines.some(line => line.column >= newGrid.width)) return this; const controlLines = this.controlLines .filter(line => line.column < newGrid.width) .map(line => line.withRows(resize(line.rows, newGrid.height, () => new Row(null, null)))); return this.copyWith({ controlLines }); } onGridResize(_grid, mode, direction, index) { if (mode === 'insert') { if (direction === 'row') { return this.copyWith({ controlLines: this.controlLines.map(line => { const rows = line.rows.slice(); rows.splice(index, 0, new Row(null, null)); return line.withRows(rows); }), }); } else if (direction === 'column') { return this.copyWith({ controlLines: this.controlLines.map(line => line.column >= index ? line.withColumn(line.column + 1) : line), }); } } else if (mode === 'remove') { if (direction === 'row') { return this.copyWith({ controlLines: this.controlLines.map(line => line.withRows(line.rows.filter((_, idx) => idx !== index))), }); } else if (direction === 'column') { const lines = []; for (const line of this.controlLines) { if (line.column === index) { const nextLine = this.controlLines.find(l => l.column === index + 1); if (nextLine) { lines.push(MusicGridRule.mergeControlLines(line, nextLine)); } else { lines.push(line.withColumn(index)); } } else if (line.column > index) { lines.push(line.withColumn(line.column - 1)); } else { lines.push(line); } } return this.copyWith({ controlLines: lines }); } } return this; } /** * Add or replace a control line. * @param controlLine The control line to set. * @returns A new rule with the control line set. */ setControlLine(controlLine) { const controlLines = this.controlLines.filter(line => line.column !== controlLine.column); return this.copyWith({ controlLines: [...controlLines, controlLine].sort((a, b) => a.column - b.column), }); } withTrack(track) { return this.copyWith({ track }); } copyWith({ controlLines, track, normalizeVelocity, }) { return new MusicGridRule(controlLines ?? this.controlLines, track !== undefined ? track : this.track, normalizeVelocity ?? this.normalizeVelocity); } get validateWithSolution() { return true; } get isSingleton() { return true; } static mergeControlLines(...lines) { const rows = Array.from({ length: Math.max(...lines.map(l => l.rows.length)) }, (_, idx) => { const note = lines .map(l => l.rows[idx]?.note) .reduce((a, b) => b ?? a, null); const velocity = lines .map(l => l.rows[idx]?.velocity) .reduce((a, b) => b ?? a, null); return new Row(note, velocity); }); const bpm = lines.map(l => l.bpm).reduce((a, b) => b ?? a, null); const pedal = lines.map(l => l.pedal).reduce((a, b) => b ?? a, null); const checkpoint = lines.some(l => l.checkpoint); return new ControlLine(lines[0].column, bpm, pedal, checkpoint, rows); } static deduplicateControlLines(lines) { const columns = new Map(); for (const line of lines) { if (!columns.has(line.column)) { columns.set(line.column, [line]); } else { columns.get(line.column).push(line); } } return Array.from(columns.values()).map(lines => lines.length > 1 ? MusicGridRule.mergeControlLines(...lines) : lines[0]); } } Object.defineProperty(MusicGridRule, "EXAMPLE_GRID", { enumerable: true, configurable: true, writable: true, value: Object.freeze(GridData.create(['.']).addSymbol(new CustomIconSymbol('', GridData.create([]), 0, 0, 'MdMusicNote'))) }); Object.defineProperty(MusicGridRule, "CONFIGS", { enumerable: true, configurable: true, writable: true, value: Object.freeze([ { type: ConfigType.ControlLines, default: [new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], field: 'controlLines', description: 'Control Lines', configurable: false, }, { type: ConfigType.NullableGrid, default: null, nonNullDefault: GridData.create([ 'wwwww', 'wwwww', 'wwwww', 'wwwww', ]).addRule(new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null)), field: 'track', description: 'Track', configurable: true, }, { type: ConfigType.Boolean, default: true, field: 'normalizeVelocity', description: 'Normalize Velocity', configurable: true, }, ]) }); Object.defineProperty(MusicGridRule, "SEARCH_VARIANTS", { enumerable: true, configurable: true, writable: true, value: [ new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null).searchVariant(), ] }); export default MusicGridRule; export const instance = new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null);