@logic-pad/core
Version:
213 lines (212 loc) • 8.75 kB
JavaScript
import { ConfigType } from '../config.js';
import GridData from '../grid.js';
import { resize } from '../dataHelper.js';
import { Color, Instrument, MajorRule, State, } from '../primitives.js';
import CustomIconSymbol from '../symbols/customIconSymbol.js';
import { ControlLine, Row } from './musicControlLine.js';
import Rule from './rule.js';
const DEFAULT_SCALE = [
new Row('C5', Instrument.Piano, null),
new Row('B4', Instrument.Piano, null),
new Row('A4', Instrument.Piano, null),
new Row('G4', Instrument.Piano, null),
new Row('F4', Instrument.Piano, null),
new Row('E4', Instrument.Piano, null),
new Row('D4', Instrument.Piano, null),
new Row('C4', Instrument.Piano, null),
];
export default class MusicGridRule extends Rule {
controlLines;
track;
normalizeVelocity;
title = 'Music Grid';
get configExplanation() {
return 'Solve the grid by listening to the solution being played back.';
}
static EXAMPLE_GRID = Object.freeze(GridData.create(['.']).addSymbol(new CustomIconSymbol('', GridData.create([]), 0, 0, 'MdMusicNote')));
static CONFIGS = Object.freeze([
{
type: ConfigType.ControlLines,
default: [new ControlLine(0, 120, false, false, DEFAULT_SCALE)],
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_SCALE)], null)),
field: 'track',
description: 'Track',
explanation: 'If set, this grid will be played instead of the solution.',
configurable: true,
},
{
type: ConfigType.Boolean,
default: true,
field: 'normalizeVelocity',
description: 'Normalize Velocity',
explanation: 'Whether to adjust note velocities by their pitch such that every note has the same perceived loudness.',
configurable: true,
},
]);
static SEARCH_VARIANTS = [
new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALE)], null).searchVariant(),
];
/**
* **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();
this.controlLines = controlLines;
this.track = track;
this.normalizeVelocity = 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, 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, 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 instrument = lines
.map(l => l.rows[idx]?.instrument)
.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, instrument, 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]);
}
}
export const instance = new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALE)], null);