Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/elements/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ export class Score {
const timeline = this.getTimelines().find((timeline) => timeline.getPartIndex() === partIndex);
util.assertDefined(timeline);

const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span);
const elementDescriber = playback.ElementDescriber.create(this, { partIndex });
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span, elementDescriber);
const path = new playback.CursorPath(partIndex, frames);
const cursor = playback.Cursor.create(path, this.getScrollContainer());
const cursor = playback.Cursor.create(path, this.getScrollContainer(), elementDescriber);

this.cursors.push(cursor);

Expand Down Expand Up @@ -369,7 +370,8 @@ export class Score {
const timeline = timelines.find((timeline) => timeline.getPartIndex() === partIndex);
util.assertDefined(timeline);
const span = { fromPartIndex: partIndex, toPartIndex: partIndex };
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span);
const elementDescriber = playback.ElementDescriber.create(this, { partIndex });
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span, elementDescriber);
const path = new playback.CursorPath(partIndex, frames);
paths.push(path);
}
Expand Down
18 changes: 13 additions & 5 deletions src/playback/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Duration } from './duration';
import { CursorPath } from './cursorpath';
import { LazyCursorStateHintProvider } from './lazycursorstatehintprovider';
import { EmptyCursorFrame } from './emptycursorframe';
import { ElementDescriber } from './elementdescriber';
import { HintDescriber } from './hintdescriber';

// NOTE: At 2px and below, there is some antialiasing issues on higher resolutions. The cursor will appear to "pulse" as
// it moves. This will happen even when rounding the position.
Expand All @@ -35,18 +37,23 @@ export class Cursor {

private previousFrame: CursorFrame = new EmptyCursorFrame();

private constructor(private path: CursorPath, private locator: CursorFrameLocator, private scroller: Scroller) {}
private constructor(
private path: CursorPath,
private locator: CursorFrameLocator,
private scroller: Scroller,
private elementDescriber: ElementDescriber
) {}

static create(path: CursorPath, scrollContainer: HTMLElement): Cursor {
static create(path: CursorPath, scrollContainer: HTMLElement, elementDescriber: ElementDescriber): Cursor {
const bSearchLocator = new BSearchCursorFrameLocator(path);
const fastLocator = new FastCursorFrameLocator(path, bSearchLocator);
const scroller = new Scroller(scrollContainer);
return new Cursor(path, fastLocator, scroller);
return new Cursor(path, fastLocator, scroller, elementDescriber);
}

iterable(): Iterable<CursorState> {
// Clone the cursor to avoid modifying the index of this instance.
const cursor = new Cursor(this.path, this.locator, this.scroller);
const cursor = new Cursor(this.path, this.locator, this.scroller, this.elementDescriber);
return new CursorIterator(cursor);
}

Expand All @@ -56,7 +63,8 @@ export class Cursor {
const hasPrevious = index > 0;
const frame = this.getCurrentFrame();
const rect = this.getCursorRect(frame, this.alpha);
const hints = new LazyCursorStateHintProvider(frame, this.previousFrame);
const hintDescriber = new HintDescriber(this.elementDescriber);
const hints = new LazyCursorStateHintProvider(frame, this.previousFrame, hintDescriber);

return {
index,
Expand Down
36 changes: 12 additions & 24 deletions src/playback/defaultcursorframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Logger } from '@/debug';
import { DurationRange } from './durationrange';
import { CursorFrame, CursorVerticalSpan, PlaybackElement, TimelineMoment } from './types';
import { Timeline } from './timeline';
import { ElementDescriber } from './elementdescriber';

type TRangeSource = {
moment: TimelineMoment;
Expand Down Expand Up @@ -32,7 +33,8 @@ export class DefaultCursorFrame implements CursorFrame {
log: Logger,
score: elements.Score,
timeline: Timeline,
span: CursorVerticalSpan
span: CursorVerticalSpan,
elementDescriber: ElementDescriber
): DefaultCursorFrame[] {
const partCount = score.getPartCount();
if (partCount === 0) {
Expand All @@ -48,7 +50,7 @@ export class DefaultCursorFrame implements CursorFrame {
throw new Error(`Invalid toPartIndex: ${span.toPartIndex}, must be in [0,${partCount - 1}]`);
}

const factory = new CursorFrameFactory(log, score, timeline, span);
const factory = new CursorFrameFactory(log, score, timeline, span, elementDescriber);
return factory.create();
}

Expand Down Expand Up @@ -92,9 +94,10 @@ class CursorFrameFactory {
private log: Logger,
private score: elements.Score,
private timeline: Timeline,
private span: CursorVerticalSpan
private span: CursorVerticalSpan,
elementDescriber: ElementDescriber
) {
this.describer = CursorFrameDescriber.create(score, timeline.getPartIndex());
this.describer = new CursorFrameDescriber(elementDescriber);
}

create(): DefaultCursorFrame[] {
Expand Down Expand Up @@ -289,22 +292,7 @@ class CursorFrameFactory {
}

class CursorFrameDescriber {
private constructor(private elements: Map<PlaybackElement, number>) {}

static create(score: elements.Score, partIndex: number): CursorFrameDescriber {
const elements = new Map<PlaybackElement, number>();
score
.getMeasures()
.flatMap((measure) => measure.getFragments())
.flatMap((fragment) => fragment.getParts().at(partIndex) ?? [])
.flatMap((part) => part.getStaves())
.flatMap((stave) => stave.getVoices())
.flatMap((voice) => voice.getEntries())
.forEach((element, index) => {
elements.set(element, index);
});
return new CursorFrameDescriber(elements);
}
constructor(private elementDescriber: ElementDescriber) {}

describeTRange(tRangeSources: [TRangeSource, TRangeSource]): string {
return `[${tRangeSources[0].moment.time.ms}ms - ${tRangeSources[1].moment.time.ms}ms]`;
Expand All @@ -321,16 +309,16 @@ class CursorFrameDescriber {
private describeXRangeSource(source: XRangeSource): string {
switch (source.type) {
case 'system':
return `${source.bound}(system(${source.system.getIndex()}))`;
return `${source.bound}(${this.elementDescriber.describe(source.system)})`;
case 'measure':
return `${source.bound}(measure(${source.measure.getAbsoluteMeasureIndex()}))`;
return `${source.bound}(${this.elementDescriber.describe(source.measure)})`;
case 'element':
return `${source.bound}(element(${this.elements.get(source.element)}))`;
return `${source.bound}(${this.elementDescriber.describe(source.element)})`;
}
}

private describeYRangeSource(source: YRangeSource): string {
return `${source.bound}(system(${source.part.getSystemIndex()}), part(${source.part.getIndex()}))`;
return `${source.bound}(system(${source.part.getSystemIndex()}), ${this.elementDescriber.describe(source.part)})`;
}
}

Expand Down
76 changes: 76 additions & 0 deletions src/playback/elementdescriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { PlaybackElement } from './types';
import * as elements from '@/elements';
import * as util from '@/util';

/**
* Describes playback elements in a human readable format.
*/
export class ElementDescriber {
private constructor(private elements: Map<PlaybackElement, number>) {}

static noop(): ElementDescriber {
return new ElementDescriber(new Map());
}

static create(score: elements.Score, { partIndex }: { partIndex: number }): ElementDescriber {
const elements = new Map<PlaybackElement, number>();

score
.getMeasures()
.flatMap((measure) => measure.getFragments())
.flatMap((fragment) => fragment.getParts().at(partIndex) ?? [])
.flatMap((part) => part.getStaves())
.flatMap((stave) => stave.getVoices())
.flatMap((voice) => voice.getEntries())
.forEach((element, index) => {
elements.set(element, index);
});

return new ElementDescriber(elements);
}

describe(element: PlaybackElement | elements.System | elements.Part): string {
switch (element.name) {
case 'part':
return this.describePart(element);
case 'system':
return this.describeSystem(element);
case 'note':
return this.describeNote(element);
case 'rest':
return this.describeRest(element);
case 'fragment':
return this.describeFragment();
case 'measure':
return this.describeMeasure(element);
}
}

private describePart(part: elements.Part): string {
return `part(${part.getIndex()})`;
}

private describeSystem(system: elements.System): string {
return `system(${system.getIndex()})`;
}

private describeMeasure(measure: elements.Measure): string {
return `measure(${measure.getAbsoluteMeasureIndex()})`;
}

private describeFragment(): string {
return 'fragment';
}

private describeRest(rest: elements.Rest): string {
util.assert(this.elements.has(rest), 'Expected element to be indexed');
const index = this.elements.get(rest)!;
return `element(${index})`;
}

private describeNote(note: elements.Note): string {
util.assert(this.elements.has(note), 'Expected element to be indexed');
const index = this.elements.get(note)!;
return `element(${index})`;
}
}
27 changes: 27 additions & 0 deletions src/playback/hintdescriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ElementDescriber } from './elementdescriber';
import { CursorStateHint } from './types';

export class HintDescriber {
constructor(private elementDescriber: ElementDescriber) {}

static noop(): HintDescriber {
return new HintDescriber(ElementDescriber.noop());
}

describe(hint: CursorStateHint): string {
switch (hint.type) {
case 'start':
return `start(${this.elementDescriber.describe(hint.element)})`;
case 'stop':
return `stop(${this.elementDescriber.describe(hint.element)})`;
case 'retrigger':
return `retrigger(${this.elementDescriber.describe(hint.untriggerElement)}, ${this.elementDescriber.describe(
hint.retriggerElement
)})`;
case 'sustain':
return `sustain(${this.elementDescriber.describe(hint.previousElement)}, ${this.elementDescriber.describe(
hint.currentElement
)})`;
}
}
}
2 changes: 2 additions & 0 deletions src/playback/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './types';
export * from './defaultcursorframe';
export * from './lazycursorstatehintprovider';
export * from './emptycursorframe';
export * from './elementdescriber';
export * from './hintdescriber';
21 changes: 15 additions & 6 deletions src/playback/lazycursorstatehintprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import {
StopHint,
SustainHint,
} from './types';
import { HintDescriber } from './hintdescriber';

export class LazyCursorStateHintProvider implements CursorStateHintProvider {
constructor(private currentFrame: CursorFrame, private previousFrame: CursorFrame | undefined) {}
constructor(
private currentFrame: CursorFrame,
private previousFrame: CursorFrame | undefined,
private hintDescriber: HintDescriber
) {}

@util.memoize()
get(): CursorStateHint[] {
Expand All @@ -30,14 +35,18 @@ export class LazyCursorStateHintProvider implements CursorStateHintProvider {
const currentNotes = this.currentFrame.getActiveElements().filter((e) => e.name === 'note');

return [
...this.getStartHints(currentElements, previousElements),
...this.getStopHints(currentElements, previousElements),
...this.getStartHints(currentElements, previousElements),
...this.getRetriggerHints(currentNotes, previousNotes),
...this.getSustainHints(currentNotes, previousNotes),
];
}

private getStartHints(previousElements: Set<PlaybackElement>, currentElements: Set<PlaybackElement>): StartHint[] {
toHumanReadable(): string[] {
return this.get().map((hint) => this.hintDescriber.describe(hint));
}

private getStartHints(currentElements: Set<PlaybackElement>, previousElements: Set<PlaybackElement>): StartHint[] {
const hints = new Array<StartHint>();

for (const element of currentElements) {
Expand All @@ -49,7 +58,7 @@ export class LazyCursorStateHintProvider implements CursorStateHintProvider {
return hints;
}

private getStopHints(previousElements: Set<PlaybackElement>, currentElements: Set<PlaybackElement>): StopHint[] {
private getStopHints(currentElements: Set<PlaybackElement>, previousElements: Set<PlaybackElement>): StopHint[] {
const hints = new Array<StopHint>();

for (const element of previousElements) {
Expand All @@ -66,7 +75,7 @@ export class LazyCursorStateHintProvider implements CursorStateHintProvider {

for (const currentNote of currentNotes) {
const previousNote = previousNotes.find((previousNote) => previousNote.containsEquivalentPitch(currentNote));
if (previousNote && !previousNote.sharesACurveWith(currentNote)) {
if (previousNote && previousNote !== currentNote && !previousNote.sharesACurveWith(currentNote)) {
hints.push({
type: 'retrigger',
untriggerElement: previousNote,
Expand All @@ -83,7 +92,7 @@ export class LazyCursorStateHintProvider implements CursorStateHintProvider {

for (const currentNote of currentNotes) {
const previousNote = previousNotes.find((previousNote) => previousNote.containsEquivalentPitch(currentNote));
if (previousNote && previousNote.sharesACurveWith(currentNote)) {
if (previousNote && previousNote !== currentNote && previousNote.sharesACurveWith(currentNote)) {
hints.push({
type: 'sustain',
previousElement: previousNote,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Jump = { type: 'repeatstart' } | { type: 'repeatend'; times: number } | { t
/**
* A class that iterates over measures in playback order (accounting for repeats and jumps).
*/
export class MeasureSequenceIterator<T extends Measure> implements Iterable<number> {
export class LegacyMeasureSequenceIterator<T extends Measure> implements Iterable<number> {
private measures: T[];

constructor(measures: T[]) {
Expand Down
Loading