@logic-pad/core
Version:
232 lines (231 loc) • 8.81 kB
JavaScript
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);