import Tone from "tone" ; import LoadInstrumentWorker from "@workers/loadInstrument.worker" ; import { EVENT_TYPE } from "@enums/piano" ; import { VISUALIZER_MESSAGES , VISUALIZER_MODE } from "@enums/visualizerMessages" ; import { getDelay , getNotesWithNoteEndEvent , NoteWithIdAndEvent } from "@utils/MidiPlayer/MidiPlayer.utils" ; import { Range } from "@utils/typings/Visualizer" ; import { getInstrumentIdByValue } from "midi-instruments" ; import { MidiSettings } from "@components/TrackList" ; import { OFFSCREEN_2D_CANVAS_SUPPORT } from "@enums/offscreen2dCanvasSupport" ; import { promisifyWorker } from "@utils/promisifyWorker" ; import { Midi } from "@utils/Midi/Midi" ; const loadInstrumentWorker = promisifyWorker ( new LoadInstrumentWorker ( ) ) ; export type IScheduleOptions = MidiSettings ; type IEventCallback = ( notes : number [ ] , trackIndex : number , isLastEvent ? : boolean ) => void ; export interface Sampler { connect : ( master : any ) => void ; triggerAttack : ( note : string , time ? : number , velocity ? : number ) => void ; triggerRelease : ( note : string ) => void ; add : ( key : string , buffer : ArrayBuffer , cb : ( ) => void ) => void ; context : AudioContext ; dispose : ( ) => void ; triggerAttackRelease : ( note : string , duration : number , time : number , velocity ? : number ) => void ; volume : { value : number ; } ; _volume : { mute : boolean ; } ; } export class MidiPlayer { private midi : Midi ; private trackSamplers : Sampler [ ] = [ ] ; private drumSampler : Sampler ; private trackPart = [ ] ; private drumPart = [ ] ; private range ; private mainTrackIndex = 0 ; private is2dOffscreenCanvasSupported = OFFSCREEN_2D_CANVAS_SUPPORT . DETERMINING ; static getTrackSampler = ( audio , volume ) => new Promise ( resolve => { const sampler = new Tone . Sampler ( audio , { onload : ( ) => { sampler . connect ( Tone . Master ) ; resolve ( sampler ) ; } , volume } ) ; } ) ; param midi static getNoteName = ( midi ) => Tone . Frequency ( midi , "midi" ) . toNote ( ) ; private canvasProxy : any ; constructor ( range , midi ? ) { this . range = range ; this . midi = midi ; } public setCanvasProxy = ( canvasProxy ) => { this . canvasProxy = canvasProxy ; } ; public setMidi ( midi ) { this . midi = midi ; } public setRange ( range ) { this . range = range ; this . canvasProxy ( { message : VISUALIZER_MESSAGES . UPDATE_RANGE , range } ) ; } param options public loadInstruments = async ( options ? ) => { const { instrumentIds , drums } = { instrumentIds : options ?. instrumentIds || this . midi . tracks . map ( track => track . instrument && track . instrument . number ) . filter ( Boolean ) , drums : options ? options . drums : this . midi . beats && this . midi . beats . length } ; const data = await loadInstrumentWorker ( { instrumentIds , drums } ) ; if ( data . drums && ! this . drumSampler ) { this . drumSampler = await new Promise ( resolve => { const sampler = new Tone . Sampler ( data . drums , ( ) => { sampler . connect ( Tone . Master ) ; resolve ( sampler ) ; } ) ; } ) ; } const trackInstrumentIds = Object . keys ( data . tracks ) ; const samplers = await Promise . all ( trackInstrumentIds . map ( instrumentId => this . trackSamplers [ instrumentId ] || MidiPlayer . getTrackSampler ( data . tracks [ instrumentId ] , options ?. volume || 0 ) ) ) ; samplers . forEach ( ( sampler , i ) => { const instrumentId = trackInstrumentIds [ i ] ; this . trackSamplers [ instrumentId ] = sampler ; } ) ; } ; public playNote = async ( midi , instrument , velocity = 1 ) => { const instrumentId = getInstrumentIdByValue ( instrument ) ; this . trackSamplers [ instrumentId ] . triggerAttack ( MidiPlayer . getNoteName ( midi ) , undefined , velocity ) ; await this . canvasProxy ( { message : VISUALIZER_MESSAGES . PLAY_NOTE , midi } ) ; } ; param midi param instrument public stopNote = async ( midi , instrument ) => { const instrumentId = getInstrumentIdByValue ( instrument ) ; this . trackSamplers [ instrumentId ] . triggerRelease ( MidiPlayer . getNoteName ( midi ) ) ; await this . canvasProxy ( { message : VISUALIZER_MESSAGES . STOP_NOTE , midi } ) ; } ; private getPitchPlaying = ( set , currentTime ) => [ ... set ] . filter ( id => { const x = id . split ( "_" ) ; const endTime = + x [ 2 ] ; return endTime >= currentTime ; } ) . map ( id => + id . split ( "_" ) [ 0 ] ) ; param currentTrackIndex param cb private playTrack = ( currentTrackIndex , cb ) => { let notesPlaying = new Set < string > ( ) ; const track = this . midi . tracks [ currentTrackIndex ] ; const instrumentNumber = track . instrument . number ; const sampler = this . trackSamplers [ instrumentNumber ] ; sampler . _volume . mute = false ; this . trackPart [ instrumentNumber ] = new Tone . Part ( ( time , note ) => { if ( note . event === EVENT_TYPE . NOTE_START ) { const duration = note . endTime - note . startTime ; sampler . triggerAttackRelease ( Tone . Frequency ( note . pitch , "midi" ) . toNote ( ) , duration , time ) ; notesPlaying . add ( note . id ) ; cb ( this . getPitchPlaying ( notesPlaying , note . startTime ) , currentTrackIndex ) ; Tone . Draw . schedule ( ( ) => { this . midi . redrawStaff ( note ) ; } , time ) ; } if ( note . event === EVENT_TYPE . NOTE_STOP && notesPlaying . has ( note . id ) ) { notesPlaying . delete ( note . id ) ; cb ( this . getPitchPlaying ( notesPlaying , note . startTime ) , currentTrackIndex ) ; } if ( note . event === EVENT_TYPE . PLAYING_COMPLETE ) { cb ( this . getPitchPlaying ( notesPlaying , note . startTime ) , currentTrackIndex , true ) ; } } , getNotesWithNoteEndEvent ( track . notes ) ) . start ( ) ; } ; param _ param currentBeatIndex private playBeat = ( _ , currentBeatIndex ) => { const beat = this . midi . beats [ currentBeatIndex ] ; const beatInstrumentNumber = beat . instrument . number ; this . drumPart [ beatInstrumentNumber ] = new Tone . Part ( time => { this . drumSampler . triggerAttack ( Tone . Frequency ( beatInstrumentNumber , "midi" ) . toNote ( ) , time ) ; } , beat . notes . map ( _beat => ( { time : _beat . startTime } ) ) ) . start ( ) ; } ; /**
     * Starts the visualizer with the main selected track.
     */ private startVisualizer = async ( ) => { const mainTrack = this . midi . tracks [ this . mainTrackIndex ] ; await this . canvasProxy ( { track : mainTrack , range : this . range , message : VISUALIZER_MESSAGES . PLAY_TRACK , delay : getDelay ( ) } ) ; } ; param options param cb public scheduleAndPlay = async ( options , cb ) => { const { selectedTrackIndex = 0 , playBeats , playBackgroundTracks } = options ; this . mainTrackIndex = selectedTrackIndex ; this . midi . tracks . forEach ( ( _track , currentTrackIndex ) => { if ( playBackgroundTracks || selectedTrackIndex === currentTrackIndex ) { this . playTrack ( currentTrackIndex , cb ) ; } } ) ; this . midi . beats . forEach ( this . playBeat ) ; if ( ! playBeats && this . drumSampler ) { this . drumSampler . _volume . mute = true ; } Tone . Transport . start ( `+${ getDelay ( ) }` ) ; if ( this . is2dOffscreenCanvasSupported ) { await this . startVisualizer ( ) ; } } ; public togglePlay = async ( ) => { if ( Tone . Transport . state === "started" ) { Tone . Transport . pause ( ) ; } else { Tone . Transport . start ( ) ; } this . canvasProxy ( { message : VISUALIZER_MESSAGES . TOGGLE } ) ; } ; public toggleBeatsVolume = ( ) => { if ( ! this . drumSampler ) return ; this . drumSampler . _volume . mute = ! this . drumSampler . _volume . mute ; } ; public toggleTracksVolume = ( ) => { const { tracks } = this . midi ; const mainTrackInstrumentIndex = tracks [ this . mainTrackIndex ] ?. instrument . number ; tracks . forEach ( ( track , i ) => { const instrumentIndex = track . instrument . number ; if ( i !== this . mainTrackIndex && instrumentIndex !== mainTrackInstrumentIndex ) { this . trackSamplers [ instrumentIndex ] . _volume . mute = ! this . trackSamplers [ instrumentIndex ] . _volume . mute ; } } ) ; } ; public setVolume = ( volumeValue ) => { Object . values ( this . trackSamplers ) . forEach ( sampler => { sampler . volume . value = volumeValue ; } ) ; } ; /**
     * Cleans everything.
     * 1. disposes all Notes trigger events scheduled in Tone.Transport
     * 2. Clears the visualizer canvas.
     *
     * This needs to be called every time we want to reset everything.
     */ public clear = async ( ) => { await this . canvasProxy ( { message : VISUALIZER_MESSAGES . STOP_TRACK } ) ; [ ... this . trackPart , ... this . drumPart ] . filter ( Boolean ) . forEach ( part => { part . dispose ( ) ; } ) ; Tone . Transport . stop ( ) ; Tone . Transport . cancel ( ) ; this . trackPart = [ ] ; this . drumPart = [ ] ; this . setSpeed ( 1 ) ; } ; public setSpeed = async ( speed ) => { Tone . Transport . bpm . value = speed * 120 ; await this . canvasProxy ( { message : VISUALIZER_MESSAGES . SET_SPEED , speed } ) ; } ; public setMode = async ( mode ) => { await this . canvasProxy ( { message : VISUALIZER_MESSAGES . SET_MODE , mode } ) ; } ; public set2dOffscreenCanvasSupport = ( support ) => { this . is2dOffscreenCanvasSupported = support ; } ; }