Skip to content

Commit

Permalink
add event for last note added
Browse files Browse the repository at this point in the history
  • Loading branch information
fa-sharp committed Oct 23, 2024
1 parent 459d89b commit 6b0bf8e
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 27 deletions.
44 changes: 37 additions & 7 deletions packages/core/src/state/EditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import type { AbcjsNote } from "../types/abcjs";
import { type Measure, parseMeasuresFromAbcjs } from "../parsing/measures";
import { Accidental, Clef, Rhythm } from "../types/constants";
import { getAbcRhythm, getRhythmFromAbcDuration } from "../utils/rhythm";
import { getAbcNoteFromMidiNum, getAbcNoteFromNoteName } from "../utils/notes";
import {
getAbcNoteFromMidiNum,
getAbcNoteFromNoteName,
getLastAccidentalInMeasure,
getMidiNumFromAbcNote,
} from "../utils/notes";
import {
type KeySignatureType,
type TimeSignatureType,
Expand Down Expand Up @@ -55,6 +60,8 @@ export default class EditorState {
};
} | null = null;

lastAddedMidiNum?: number;

constructor(
initialAbc?: string,
options?: { chordTemplate?: string; ending?: EditorState["ending"] },
Expand Down Expand Up @@ -128,6 +135,7 @@ export default class EditorState {
);
}
}
this.lastAddedMidiNum = undefined;
}

addNote(
Expand Down Expand Up @@ -178,16 +186,15 @@ export default class EditorState {
)}`;
if (options?.tied) abcToAdd += "-";

// Add barline if we're at the end of the measure
if (currentMeasure) {
// Add barline if we're at the end of the measure
const measureTotalDuration = getMeasureDurationFromTimeSig(this.timeSig);
const durationWithAddedNote =
currentMeasure.duration +
(1 / rhythm) *
(options?.dotted ? 3 / 2 : 1) *
(options?.triplet ? 2 / 3 : 1);

// Are we at end of measure?
if (durationWithAddedNote >= measureTotalDuration - 0.001) {
if (
this.ending?.lastMeasure &&
Expand All @@ -209,6 +216,18 @@ export default class EditorState {
);
if (chordToAdd) abcToAdd += ` "^${chordToAdd.name}"`;
}

// Set last added note
const midiNum =
typeof note === "number" ? note : getMidiNumFromAbcNote(abcNote);
if (options?.accidental && options.accidental !== Accidental.None)
this.lastAddedMidiNum = midiNum;
else if (midiNum !== undefined) {
const lastAcc = getLastAccidentalInMeasure(abcNote, currentMeasure);
if (lastAcc === "sharp") this.lastAddedMidiNum = midiNum + 1;
else if (lastAcc === "flat") this.lastAddedMidiNum = midiNum - 1;
else this.lastAddedMidiNum = midiNum;
}
}

// Add the note to the ABC score
Expand Down Expand Up @@ -308,10 +327,9 @@ export default class EditorState {
tied?: boolean;
}) {
if (!this.selected) return;
const existingNote = this.measures
.at(this.selected?.measureIdx)
?.notes.at(this.selected.noteIdx);
if (!existingNote) return;
const measure = this.measures.at(this.selected.measureIdx);
const existingNote = measure?.notes.at(this.selected.noteIdx);
if (!measure || !existingNote) return;

let newAbc = "";
if (existingNote.startTriplet) newAbc += "(3";
Expand All @@ -336,6 +354,18 @@ export default class EditorState {
const startIdx = existingNote.startChar + startIdxOfNoteWithoutChord;
const endIdx = existingNote.startChar + endIdxOfNoteWithoutSpaces;
this.abc = this.abc.slice(0, startIdx) + newAbc + this.abc.slice(endIdx);

// Set last added note
const [accidental, note, octave] = AbcNotation.tokenize(data.note);
if (accidental) this.lastAddedMidiNum = getMidiNumFromAbcNote(data.note);
else {
const midiNum = getMidiNumFromAbcNote(note + octave);
if (!midiNum) return;
const lastAcc = getLastAccidentalInMeasure(note + octave, measure);
if (lastAcc === "sharp") this.lastAddedMidiNum = midiNum + 1;
else if (lastAcc === "flat") this.lastAddedMidiNum = midiNum - 1;
else this.lastAddedMidiNum = midiNum;
}
}

moveNote(step: number) {
Expand Down
23 changes: 19 additions & 4 deletions packages/core/src/utils/notes.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { AbcNotation, Midi, Note } from "tonal";
import { Accidental } from "../types/constants";
import type { Measure } from "~src/parsing/measures";

/** Get the note in ABC notation from the given MIDI pitch (0-127) */
export function getAbcNoteFromMidiNum(
midiNum: number,
accidental: Accidental = Accidental.None
accidental: Accidental = Accidental.None,
) {
if (accidental === Accidental.Natural)
return "=" + Midi.midiToNoteName(midiNum);
return AbcNotation.scientificToAbcNotation(
Midi.midiToNoteName(midiNum, {
sharps: accidental === Accidental.Sharp,
})
}),
);
}

/** Get the note in ABC notation from the given note */
export function getAbcNoteFromNoteName(
noteName: string,
accidental: Accidental = Accidental.None
accidental: Accidental = Accidental.None,
) {
if (accidental === Accidental.None)
return AbcNotation.scientificToAbcNotation(noteName);
Expand All @@ -29,7 +30,21 @@ export function getAbcNoteFromNoteName(
note.letter + (accidental === Accidental.Sharp ? "#" : "b");
const noteNameWithAccidental = Note.enharmonic(
Note.transpose(noteName, accidental === Accidental.Sharp ? "m2" : "m-2"),
wantedPitchClass
wantedPitchClass,
);
return AbcNotation.scientificToAbcNotation(noteNameWithAccidental);
}

export function getMidiNumFromAbcNote(abcNote: string) {
return Midi.toMidi(AbcNotation.abcToScientificNotation(abcNote)) ?? undefined;
}

export function getLastAccidentalInMeasure(abcNote: string, measure: Measure) {
return measure.notes
.flatMap((note) => note.pitches)
.findLast((pitch) => {
if (!pitch?.accidental) return false;
const [, note, octave] = AbcNotation.tokenize(pitch.name);
return abcNote === note + octave;
})?.accidental;
}
3 changes: 3 additions & 0 deletions packages/react/src/components/dev/BasicExample.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from "react";
import Editor from "../editor/Editor";

const onNoteAdded = (midiNum: number) => console.log({ noteAdded: midiNum });

export default function BasicExample() {
const [abc, setAbc] = useState("");
return (
Expand All @@ -16,6 +18,7 @@ export default function BasicExample() {
ending={{ lastBarline: "thin-thin", lastMeasure: 8 }}
lineBreaks={[3]}
jazzChords
onNoteAdded={onNoteAdded}
/>
<br />
Generated ABC:
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type EditorProps = {
* on all the class names that can be used to select lines/notes/etc.
*/
onChange?: EditorChangeHandler;
/** Fired after a note is added to the score. Can be used for immediate playback of the added note or other effects. */
onNoteAdded?: (midiNum: number) => void;
};

/** The main ABC notation editor with a built-in toolbar. */
Expand All @@ -73,6 +75,7 @@ export default function Editor({
scale = 1,
enableKbdShortcuts = false,
onChange = () => {},
onNoteAdded,
}: EditorProps) {
//@ts-expect-error FIXME wrong typing for `lineBreaks` - double array and 0-based
const abcjsOptions: AbcVisualParams = useMemo(
Expand Down Expand Up @@ -117,6 +120,7 @@ export default function Editor({
ending,
enableKbdShortcuts,
onChange,
onNoteAdded,
}}
>
<InnerEditor />
Expand Down
51 changes: 35 additions & 16 deletions packages/react/src/context/EditorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface EditorProviderProps {
tuneObject: TuneObject,
renderDiv?: HTMLDivElement,
) => void;
onNoteAdded?: (midiNum: number) => void;
}

const useEditor = ({
Expand All @@ -41,6 +42,7 @@ const useEditor = ({
chordTemplate,
ending,
onChange = () => {},
onNoteAdded,
}: EditorProviderProps) => {
const editorState = useRef<EditorState>(
new EditorState(initialAbc, {
Expand Down Expand Up @@ -106,17 +108,23 @@ const useEditor = ({
tripletRef.current = editorCommands.triplet;
}, [editorCommands.triplet]);

const onAddNote = useCallback((noteName: string | number) => {
editorState.current.addNote(noteName, rhythmRef.current, {
beamed: beamedRef.current,
dotted: dottedRef.current,
rest: restRef.current,
accidental: accidentalRef.current,
triplet: tripletRef.current,
tied: tiedRef.current,
});
setAbc(editorState.current.abc);
}, []);
const onAddNote = useCallback(
(noteName: string | number) => {
editorState.current.addNote(noteName, rhythmRef.current, {
beamed: beamedRef.current,
dotted: dottedRef.current,
rest: restRef.current,
accidental: accidentalRef.current,
triplet: tripletRef.current,
tied: tiedRef.current,
});
onNoteAdded &&
editorState.current.lastAddedMidiNum !== undefined &&
onNoteAdded(editorState.current.lastAddedMidiNum);
setAbc(editorState.current.abc);
},
[onNoteAdded],
);

// Render the ABC notation, update editor state
useEffect(() => {
Expand All @@ -136,7 +144,12 @@ const useEditor = ({
dragging: true,
clickListener: (abcElem, _, _classes, analysis, drag) => {
editorState.current.selectNote(abcElem, analysis, drag);
if (drag.step !== 0) setAbc(editorState.current.abc);
if (drag.step !== 0) {
onNoteAdded &&
editorState.current.lastAddedMidiNum !== undefined &&
onNoteAdded(editorState.current.lastAddedMidiNum);
setAbc(editorState.current.abc);
}
},
},
);
Expand Down Expand Up @@ -173,10 +186,16 @@ const useEditor = ({
if (tiedRef.current === true) dispatchEditorCommand({ type: "toggleTied" });
}, [abc]);

const onMoveNote = useCallback((step: number) => {
editorState.current.moveNote(step);
setAbc(editorState.current.abc);
}, []);
const onMoveNote = useCallback(
(step: number) => {
editorState.current.moveNote(step);
onNoteAdded &&
editorState.current.lastAddedMidiNum !== undefined &&
onNoteAdded(editorState.current.lastAddedMidiNum);
setAbc(editorState.current.abc);
},
[onNoteAdded],
);

const onSelectNextNote = useCallback(() => {
editorState.current.selectNextNote();
Expand Down

0 comments on commit 6b0bf8e

Please sign in to comment.