138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
// Inspired from OSMD example project
|
|
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
|
|
import React, { useEffect, useState } from 'react';
|
|
import { CursorType, Fraction, OpenSheetMusicDisplay as OSMD, IOSMDOptions, Note, Pitch } from 'opensheetmusicdisplay';
|
|
import useColorScheme from '../hooks/colorScheme';
|
|
import { useWindowDimensions } from 'react-native';
|
|
import SoundFont from 'soundfont-player';
|
|
|
|
type PartitionViewProps = {
|
|
// The Buffer of the MusicXML file retreived from the API
|
|
file: string;
|
|
onPartitionReady: () => void;
|
|
onEndReached: () => void;
|
|
// Timestamp of the play session, in milisecond
|
|
timestamp: number;
|
|
}
|
|
|
|
const PartitionView = (props: PartitionViewProps) => {
|
|
const [osmd, setOsmd] = useState<OSMD>();
|
|
const [soundPlayer, setSoundPlayer] = useState<SoundFont.Player>();
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext || false;
|
|
const audioContext = new AudioContext();
|
|
const [wholeNoteLength, setWholeNoteLength] = useState(0); // Length of Whole note, in ms (?)
|
|
const colorScheme = useColorScheme();
|
|
const dimensions = useWindowDimensions();
|
|
const OSMD_DIV_ID = 'osmd-div';
|
|
const options: IOSMDOptions = {
|
|
darkMode: colorScheme == 'dark',
|
|
drawComposer: false,
|
|
drawCredits: false,
|
|
drawLyrics: false,
|
|
drawPartNames: false,
|
|
followCursor: false,
|
|
renderSingleHorizontalStaffline: true,
|
|
cursorsOptions: [{ type: CursorType.Standard, color: 'green', alpha: 0.5, follow: false }],
|
|
autoResize: false,
|
|
}
|
|
// Turns note.Length or timestamp in ms
|
|
const timestampToMs = (timestamp: Fraction) => {
|
|
return timestamp.RealValue * wholeNoteLength;
|
|
}
|
|
const getActualNoteLength = (note: Note) => {
|
|
let duration = timestampToMs(note.Length)
|
|
if (note.NoteTie) {
|
|
const firstNote = note.NoteTie.Notes.at(1)
|
|
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
|
|
duration += timestampToMs(firstNote.Length);
|
|
} else {
|
|
duration = 0;
|
|
}
|
|
}
|
|
return duration;
|
|
}
|
|
|
|
const playNotesUnderCursor = () => {
|
|
osmd!.cursor.NotesUnderCursor()
|
|
.filter((note) => note.isRest() == false)
|
|
.filter((note) => note.Pitch) // Pitch Can be null, avoiding them
|
|
.forEach((note) => {
|
|
// Put your hands together for https://github.com/jimutt/osmd-audio-player/blob/master/src/internals/noteHelpers.ts
|
|
const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0;
|
|
const midiNumber = note.halfTone - fixedKey * 12;
|
|
// console.log('Expecting midi ' + midiNumber);
|
|
let duration = getActualNoteLength(note);
|
|
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
|
|
soundPlayer!.play(midiNumber, audioContext.currentTime, { duration, gain })
|
|
});
|
|
}
|
|
const getShortedNoteUnderCursor = () => {
|
|
return osmd.cursor.NotesUnderCursor().sort((n1, n2) => n1.Length.CompareTo(n2.Length)).at(0);
|
|
}
|
|
|
|
useEffect(() => {
|
|
const _osmd = new OSMD(OSMD_DIV_ID, options);
|
|
Promise.all([
|
|
SoundFont.instrument(audioContext, 'electric_piano_1'),
|
|
_osmd.load(props.file)
|
|
]).then(([player, __]) => {
|
|
setSoundPlayer(player);
|
|
_osmd.render();
|
|
_osmd.cursor.hide();
|
|
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
|
|
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
|
|
setWholeNoteLength(Math.round((60 / bpm) * 4000))
|
|
props.onPartitionReady();
|
|
// Do not show cursor before actuall start
|
|
});
|
|
setOsmd(_osmd);
|
|
}, []);
|
|
|
|
// Re-render manually (otherwise done by 'autoResize' option), to fix disappearing cursor
|
|
useEffect(() => {
|
|
if (osmd && osmd.IsReadyToRender()) {
|
|
osmd.render();
|
|
if (!osmd.cursor.hidden) {
|
|
osmd.cursor.show();
|
|
}
|
|
}
|
|
}, [dimensions])
|
|
|
|
useEffect(() => {
|
|
if (!osmd || !soundPlayer) {
|
|
return;
|
|
}
|
|
if (props.timestamp > 0 && osmd.cursor.hidden && !osmd.cursor.iterator.EndReached) {
|
|
osmd.cursor.show();
|
|
playNotesUnderCursor();
|
|
return;
|
|
}
|
|
let previousCursorPosition = -1;
|
|
let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
|
|
let shortestNote = getShortedNoteUnderCursor();
|
|
while(!osmd.cursor.iterator.EndReached && (shortestNote?.isRest
|
|
? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) +
|
|
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) < props.timestamp
|
|
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) < props.timestamp)
|
|
) {
|
|
previousCursorPosition = currentCursorPosition;
|
|
osmd.cursor.next();
|
|
if (osmd.cursor.iterator.EndReached) {
|
|
osmd.cursor.hide(); // Lousy fix for https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1338
|
|
soundPlayer.stop();
|
|
props.onEndReached();
|
|
} else {
|
|
// Shamelessly stolen from https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL223C7-L224C1
|
|
playNotesUnderCursor();
|
|
currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
|
|
document.getElementById(OSMD_DIV_ID).scrollBy(currentCursorPosition - previousCursorPosition, 0)
|
|
shortestNote = getShortedNoteUnderCursor();
|
|
}
|
|
}
|
|
}, [props.timestamp]);
|
|
|
|
return (<div id={OSMD_DIV_ID} style={{ width: '100%', overflow: 'hidden' }} />);
|
|
}
|
|
|
|
export default PartitionView;
|