Added the message pinao system reusing a react context for simplicity and emitting note timing messages when scoro gives the result
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import PartitionView from './PartitionView';
|
||||
import PhaserCanvas from './PartitionVisualizer/PhaserCanvas';
|
||||
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
|
||||
import { PianoCursorPosition } from '../models/PianoGame';
|
||||
|
||||
type PartitionCoordProps = {
|
||||
// The Buffer of the MusicXML file retreived from the API
|
||||
@@ -10,9 +10,6 @@ type PartitionCoordProps = {
|
||||
onEndReached: () => void;
|
||||
onResume: () => void;
|
||||
onPause: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
pressedKeys: Map<number, number>;
|
||||
};
|
||||
|
||||
const PartitionCoord = ({
|
||||
@@ -21,8 +18,6 @@ const PartitionCoord = ({
|
||||
onEndReached,
|
||||
onPause,
|
||||
onResume,
|
||||
timestamp,
|
||||
pressedKeys,
|
||||
}: PartitionCoordProps) => {
|
||||
const [partitionData, setPartitionData] = React.useState<
|
||||
[string, PianoCursorPosition[]] | null
|
||||
@@ -40,17 +35,15 @@ const PartitionCoord = ({
|
||||
onEndReached={() => {
|
||||
console.log('osmd end reached');
|
||||
}}
|
||||
timestamp={timestamp}
|
||||
timestamp={0}
|
||||
/>
|
||||
)}
|
||||
{partitionData && (
|
||||
<PhaserCanvas
|
||||
partitionB64={partitionData?.[0]}
|
||||
cursorPositions={partitionData?.[1]}
|
||||
timestamp={timestamp}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
pressedKeys={pressedKeys}
|
||||
onEndReached={() => {
|
||||
onEndReached();
|
||||
}}
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
Note,
|
||||
} from 'opensheetmusicdisplay';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
|
||||
|
||||
import { PianoCursorPosition } from '../models/PianoGame';
|
||||
type PartitionViewProps = {
|
||||
// The Buffer of the MusicXML file retreived from the API
|
||||
file: string;
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// create a simple phaser effect with a canvas that can be easily imported as a react component
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useContext } from 'react';
|
||||
import Phaser from 'phaser';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
import { RootState, useSelector } from '../../state/Store';
|
||||
import { setSoundPlayer as setSPStore } from '../../state/SoundPlayerSlice';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SplendidGrandPiano, CacheStorage } from 'smplr';
|
||||
import { Note } from 'opensheetmusicdisplay';
|
||||
import { handlePianoGameMsg } from './PianoGameUpdateFunctions';
|
||||
import { PianoCC } from '../../views/PlayView';
|
||||
import { PianoCanvasMsg, PianoCursorNote, PianoCursorPosition } from '../../models/PianoGame';
|
||||
|
||||
let globalTimestamp = 0;
|
||||
let globalPressedKeys: Map<number, number> = new Map();
|
||||
// the messages are consummed from the end and new messages should be added at the end
|
||||
let globalMessages: Array<PianoCanvasMsg> = [];
|
||||
const globalStatus: 'playing' | 'paused' | 'stopped' = 'playing';
|
||||
|
||||
const isValidSoundPlayer = (soundPlayer: SplendidGrandPiano | undefined) => {
|
||||
@@ -50,8 +54,7 @@ const getPianoScene = (
|
||||
private partition!: Phaser.GameObjects.Image;
|
||||
private cursor!: Phaser.GameObjects.Rectangle;
|
||||
private emitter!: Phaser.GameObjects.Particles.ParticleEmitter;
|
||||
private emitzone!: Phaser.GameObjects.Particles.Zones.EdgeZone;
|
||||
private nbTextureTolad!: number;
|
||||
private nbTextureToload!: number;
|
||||
create() {
|
||||
this.textures.addBase64(
|
||||
'star',
|
||||
@@ -59,12 +62,13 @@ const getPianoScene = (
|
||||
);
|
||||
this.textures.addBase64('partition', partitionB64);
|
||||
this.cursorPositionsIdx = -1;
|
||||
this.nbTextureTolad = 2;
|
||||
// this is to prevent multiple initialisation of the scene
|
||||
this.nbTextureToload = 2;
|
||||
|
||||
this.cameras.main.setBackgroundColor(colorScheme === 'light' ? '#FFFFFF' : '#000000');
|
||||
this.textures.on('onload', () => {
|
||||
this.nbTextureTolad--;
|
||||
if (this.nbTextureTolad > 0) return;
|
||||
this.nbTextureToload--;
|
||||
if (this.nbTextureToload > 0) return;
|
||||
this.partition = this.add.image(0, 0, 'partition').setOrigin(0, 0);
|
||||
this.cameras.main.setBounds(0, 0, this.partition.width, this.partition.height);
|
||||
|
||||
@@ -103,13 +107,15 @@ const getPianoScene = (
|
||||
return true;
|
||||
}
|
||||
if (globalPressedKeys.size > 0) {
|
||||
// add particles at the position of the cursor
|
||||
this.emitter.start(1);
|
||||
this.cursor.fillAlpha = 0.9;
|
||||
} else if (this.cursor) {
|
||||
this.cursor.fillAlpha = 0.5;
|
||||
}
|
||||
|
||||
if (globalMessages.length > 0) {
|
||||
handlePianoGameMsg(globalMessages, this.emitter);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (cP) {
|
||||
@@ -134,50 +140,28 @@ const getPianoScene = (
|
||||
return PianoScene;
|
||||
};
|
||||
|
||||
type PianoCursorNote = {
|
||||
note: Note;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type PianoCursorPosition = {
|
||||
// offset in pixels
|
||||
x: number;
|
||||
// timestamp in ms
|
||||
timing: number;
|
||||
timestamp: number;
|
||||
notes: PianoCursorNote[];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
currentTimestamp: number;
|
||||
status: 'playing' | 'paused' | 'stopped';
|
||||
};
|
||||
|
||||
export type PhaserCanvasProps = {
|
||||
partitionB64: string;
|
||||
cursorPositions: PianoCursorPosition[];
|
||||
onEndReached: () => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
pressedKeys: Map<number, number>;
|
||||
};
|
||||
|
||||
const PhaserCanvas = ({
|
||||
partitionB64,
|
||||
cursorPositions,
|
||||
onEndReached,
|
||||
timestamp,
|
||||
pressedKeys,
|
||||
}: PhaserCanvasProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const dispatch = useDispatch();
|
||||
const pianoCC = useContext(PianoCC);
|
||||
const soundPlayer = useSelector((state: RootState) => state.soundPlayer.soundPlayer);
|
||||
const [game, setGame] = React.useState<Phaser.Game | null>(null);
|
||||
|
||||
globalTimestamp = timestamp;
|
||||
globalPressedKeys = pressedKeys;
|
||||
globalTimestamp = pianoCC.timestamp;
|
||||
globalPressedKeys = pianoCC.pressedKeys;
|
||||
globalMessages = pianoCC.messages;
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidSoundPlayer(soundPlayer)) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NoteTiming, PianoCanvasMsg } from '../../models/PianoGame';
|
||||
|
||||
const handleNoteTimingMsg = (
|
||||
noteTiming: NoteTiming,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter
|
||||
) => {
|
||||
if (noteTiming === NoteTiming.Perfect) {
|
||||
emitter.particleTint = 0x00ff00;
|
||||
emitter.start(10);
|
||||
} else if (noteTiming === NoteTiming.Great) {
|
||||
emitter.particleTint = 0x00ffff;
|
||||
emitter.start(5);
|
||||
} else if (noteTiming === NoteTiming.Good) {
|
||||
emitter.particleTint = 0xffff00;
|
||||
emitter.start(3);
|
||||
} else if (noteTiming === NoteTiming.Missed) {
|
||||
emitter.particleTint = 0xff0000;
|
||||
emitter.start(1);
|
||||
} else if (noteTiming === NoteTiming.Wrong) {
|
||||
// maybe add some other effect
|
||||
}
|
||||
};
|
||||
|
||||
export const handlePianoGameMsg = (
|
||||
msgs: Array<PianoCanvasMsg>,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter
|
||||
) => {
|
||||
const msg = msgs.shift();
|
||||
if (msg) {
|
||||
if (msg.type === 'noteTiming') {
|
||||
handleNoteTimingMsg(msg.data as NoteTiming, emitter);
|
||||
}
|
||||
}
|
||||
};
|
||||
40
front/models/PianoGame.ts
Normal file
40
front/models/PianoGame.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Note } from 'opensheetmusicdisplay';
|
||||
|
||||
export type PianoCursorNote = {
|
||||
note: Note;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type PianoCursorPosition = {
|
||||
// offset in pixels
|
||||
x: number;
|
||||
// timestamp in ms
|
||||
timing: number;
|
||||
timestamp: number;
|
||||
notes: PianoCursorNote[];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
currentTimestamp: number;
|
||||
status: 'playing' | 'paused' | 'stopped';
|
||||
};
|
||||
|
||||
export enum NoteTiming {
|
||||
Perfect = 'Perfect',
|
||||
Great = 'Great',
|
||||
Good = 'Good',
|
||||
Missed = 'Missed',
|
||||
Wrong = 'Wrong',
|
||||
}
|
||||
|
||||
export type PianoCanvasMsg = {
|
||||
type: 'noteTiming' | 'score' | 'gameUpdate';
|
||||
data: UpdateInfo | NoteTiming | number;
|
||||
};
|
||||
|
||||
export type PianoCanvasContext = {
|
||||
messages: Array<PianoCanvasMsg>;
|
||||
// Timestamp of the play session, in miliseconds
|
||||
timestamp: number;
|
||||
pressedKeys: Map<number, number>;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import { StackActions } from '@react-navigation/native';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, createContext, useReducer } from 'react';
|
||||
import { SafeAreaView, Platform, Animated } from 'react-native';
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
import {
|
||||
@@ -32,6 +32,7 @@ import TextButton from '../components/TextButton';
|
||||
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
|
||||
import * as Linking from 'expo-linking';
|
||||
import url from 'url';
|
||||
import { PianoCanvasContext, PianoCanvasMsg, NoteTiming } from '../models/PianoGame';
|
||||
|
||||
type PlayViewProps = {
|
||||
songId: number;
|
||||
@@ -68,6 +69,13 @@ function parseMidiMessage(message: MIDIMessageEvent) {
|
||||
};
|
||||
}
|
||||
|
||||
//create a context with an array of number
|
||||
export const PianoCC = createContext<PianoCanvasContext>({
|
||||
pressedKeys: new Map(),
|
||||
timestamp: 0,
|
||||
messages: [],
|
||||
});
|
||||
|
||||
const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const accessToken = useSelector((state: RootState) => state.user.accessToken);
|
||||
const navigation = useNavigation();
|
||||
@@ -89,6 +97,13 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
|
||||
// first number is the note, the other is the time when pressed on release the key is removed
|
||||
const [pressedKeys, setPressedKeys] = useState<Map<number, number>>(new Map()); // [note, time]
|
||||
const [pianoMsgs, setPianoMsgs] = useReducer(
|
||||
(state: PianoCanvasMsg[], action: PianoCanvasMsg) => {
|
||||
state.push(action);
|
||||
return state;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onPause = () => {
|
||||
stopwatch.pause();
|
||||
@@ -182,25 +197,45 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
|
||||
if (data.type == 'miss') {
|
||||
formattedMessage = translate('missed');
|
||||
setPianoMsgs({
|
||||
type: 'noteTiming',
|
||||
data: NoteTiming.Missed,
|
||||
});
|
||||
messageColor = 'black';
|
||||
} else if (data.type == 'timing' || data.type == 'duration') {
|
||||
formattedMessage = translate(data[data.type]);
|
||||
switch (data[data.type]) {
|
||||
case 'perfect':
|
||||
messageColor = 'green';
|
||||
setPianoMsgs({
|
||||
type: 'noteTiming',
|
||||
data: NoteTiming.Perfect,
|
||||
});
|
||||
break;
|
||||
case 'great':
|
||||
messageColor = 'blue';
|
||||
setPianoMsgs({
|
||||
type: 'noteTiming',
|
||||
data: NoteTiming.Great,
|
||||
});
|
||||
break;
|
||||
case 'short':
|
||||
case 'long':
|
||||
case 'good':
|
||||
messageColor = 'lightBlue';
|
||||
setPianoMsgs({
|
||||
type: 'noteTiming',
|
||||
data: NoteTiming.Good,
|
||||
});
|
||||
break;
|
||||
case 'too short':
|
||||
case 'too long':
|
||||
case 'wrong':
|
||||
messageColor = 'trueGray';
|
||||
setPianoMsgs({
|
||||
type: 'noteTiming',
|
||||
data: NoteTiming.Wrong,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -302,15 +337,21 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
</Animated.View>
|
||||
</HStack>
|
||||
<View style={{ flexGrow: 1, justifyContent: 'center' }}>
|
||||
<PartitionCoord
|
||||
file={musixml.data}
|
||||
timestamp={time}
|
||||
onEndReached={onEnd}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
pressedKeys={pressedKeys}
|
||||
onPartitionReady={() => setPartitionRendered(true)}
|
||||
/>
|
||||
<PianoCC.Provider
|
||||
value={{
|
||||
pressedKeys: pressedKeys,
|
||||
timestamp: time,
|
||||
messages: pianoMsgs,
|
||||
}}
|
||||
>
|
||||
<PartitionCoord
|
||||
file={musixml.data}
|
||||
onEndReached={onEnd}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
onPartitionReady={() => setPartitionRendered(true)}
|
||||
/>
|
||||
</PianoCC.Provider>
|
||||
{!partitionRendered && <LoadingComponent />}
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user