import React , { useEffect , useRef , useContext , } from 'react' ; import { TrackContext } from './Track' ; import Tone from '../lib/tone' ; import { usePrevious } from '../lib/hooks' ; type NoteType = { name : string ; velocity ? : number ; duration ? : number | string ; /** Use unique key to differentiate from same notes, otherwise it won't play */ key ? : string | number ; } ; export type InstrumentType = 'amSynth' | 'duoSynth' | 'fmSynth' | 'membraneSynth' | 'metalSynth' | 'monoSynth' | 'noiseSynth' | 'pluckSynth' | 'synth' | 'sampler' ; export interface InstrumentProps { type : InstrumentType ; notes ? : NoteType [ ] ; /** Should deprecate */ options ? : any ; polyphony ? : number ; oscillator ? : { type : 'triangle' | 'sine' | 'square' ; } ; envelope ? : { attack ? : number ; decay ? : number ; sustain ? : number ; release ? : number ; } ; samples ? : { [ k ] : string ; } ; mute ? : boolean ; solo ? : boolean ; /** TODO: Type properly and consider loading status */ onLoad ? : ( buffers : any [ ] ) => void ; } interface InstrumentConsumerProps extends InstrumentProps { volume ? : number ; pan ? : number ; effectsChain ? : React . ReactNode [ ] ; onInstrumentsUpdate ? : Function ; } const InstrumentConsumer = ( { type = 'synth' , options , polyphony = 4 , oscillator , envelope , notes = [ ] , samples , onLoad , volume , pan , mute , solo , effectsChain , onInstrumentsUpdate , } ) => { const instrumentRef = useRef < Partial < { curve : number ; release : number ; triggerAttack : Function ; triggerAttackRelease : Function ; triggerRelease : Function ; add : Function ; set : Function ; chain : Function ; dispose : Function ; disconnect : Function ; } > > ( ) ; const trackChannelBase = useRef ( null ) ; const prevNotes = usePrevious ( notes ) ; useEffect ( ( ) => { trackChannelBase . current = new Tone . Channel ( volume , pan ) ; return function cleanup ( ) { if ( trackChannelBase . current ) { trackChannelBase . current . dispose ( ) ; } } ; } , [ ] ) ; const prevType = usePrevious < InstrumentType > ( type ) ; useEffect ( ( ) => { if ( type === 'sampler' ) { instrumentRef . current = new Tone . Sampler ( samples , onLoad ) ; if ( options && options . curve ) { instrumentRef . current . curve = options . curve ; } if ( options && options . release ) { instrumentRef . current . release = options . release ; } } else if ( type === 'membraneSynth' ) { instrumentRef . current = new Tone . MembraneSynth ( buildSynthOptions ( { oscillator , envelope } ) ) ; } else if ( type === 'metalSynth' ) { instrumentRef . current = new Tone . MetalSynth ( ) ; } else if ( type === 'noiseSynth' ) { instrumentRef . current = new Tone . NoiseSynth ( ) ; } else if ( type === 'pluckSynth' ) { instrumentRef . current = new Tone . PluckSynth ( ) ; } else { let synth ; if ( type === 'amSynth' ) { synth = Tone . AMSynth ; } else if ( type === 'duoSynth' ) { synth = Tone . DuoSynth ; } else if ( type === 'fmSynth' ) { synth = Tone . FMSynth ; } else if ( type === 'monoSynth' ) { synth = Tone . MonoSynth ; } else if ( type === 'synth' ) { synth = Tone . Synth ; } else { synth = Tone . Synth ; } /**
             * PolySynth accepts other Synth types as second param, making them
             * polyphonic. As this is a common use case, all Synths will be created
             * via PolySynth. Monophonic synths can easily be created by setting the
             * `polyphony` prop to 1.
             */ instrumentRef . current = new Tone . PolySynth ( polyphony , synth , buildSynthOptions ( { oscillator , envelope } ) ) ; } instrumentRef . current . chain ( ... effectsChain , trackChannelBase . current , Tone . Master ) ; onInstrumentsUpdate ( [ instrumentRef . current ] ) ; return function cleanup ( ) { if ( instrumentRef . current ) { instrumentRef . current . dispose ( ) ; } } ; } , [ type , polyphony ] ) ; useEffect ( ( ) => { if ( type === 'synth' && instrumentRef && instrumentRef . current && oscillator ) { instrumentRef . current . set ( 'oscillator' , oscillator ) ; } } , [ oscillator , type ] ) ; useEffect ( ( ) => { trackChannelBase . current . volume . value = volume ; } , [ volume ] ) ; useEffect ( ( ) => { trackChannelBase . current . pan . value = pan ; } , [ pan ] ) ; useEffect ( ( ) => { trackChannelBase . current . mute = mute ; } , [ mute ] ) ; useEffect ( ( ) => { trackChannelBase . current . solo = solo ; } , [ solo ] ) ; /**
     NOTE: Would prefer to use useLayoutEffect as it is a little faster, but unable to test it right now
     **/ useEffect ( ( ) => { notes && notes . forEach ( ( note ) => { const isPlaying = prevNotes && prevNotes . filter ( ( prevNote ) => { return prevNote . name === note . name && prevNote . key === note . key ; } ) . length > 0 ; if ( ! isPlaying ) { if ( note . duration ) { instrumentRef . current . triggerAttackRelease ( note . name , note . duration , undefined , note . velocity ) ; } else { instrumentRef . current . triggerAttack ( note . name , undefined , note . velocity ) ; } } } ) ; prevNotes && prevNotes . forEach ( ( note ) => { const isPlaying = notes && notes . filter ( ( n ) => n . name === note . name ) . length > 0 ; if ( ! isPlaying ) { instrumentRef . current . triggerRelease ( note . name ) ; } } ) ; } , [ notes , prevNotes ] ) ; useEffect ( ( ) => { instrumentRef . current . disconnect ( ) ; instrumentRef . current . chain ( ... effectsChain , trackChannelBase . current , Tone . Master ) ; } , [ effectsChain ] ) ; const prevSamples = usePrevious ( samples ) ; useEffect ( ( ) => { const isFirstSamplerInit = type === 'sampler' && prevType !== type ; if ( type === 'sampler' && Boolean ( samples ) && ! isFirstSamplerInit ) { const prevSampleKeys = Object . keys ( prevSamples ) ; const sampleKeys = Object . keys ( samples ) ; const addSampleKeys = sampleKeys . filter ( ( key ) => ! prevSampleKeys . includes ( key ) ) ; if ( addSampleKeys . length ) { const loadSamplePromises = addSampleKeys . map ( ( key ) => { return new Promise ( ( resolve ) => { const sample = samples [ key ] ; const prevSample = prevSamples ? ( prevSamples as object ) [ key ] : '' ; if ( sample !== prevSample ) { instrumentRef . current . add ( key , sample , resolve ) ; } else { resolve ( null ) ; } } ) ; } ) ; Promise . all ( loadSamplePromises ) . then ( ( event ) => { if ( typeof onLoad === 'function' ) { onLoad ( event ) ; } } ) ; } } } , [ samples , type ] ) ; return null ; } ; const Instrument = ( { type , options , notes , polyphony , oscillator , envelope , samples , onLoad , } ) => { const { volume , pan , mute , solo , effectsChain , onInstrumentsUpdate , } = useContext ( TrackContext ) ; if ( typeof window === 'undefined' ) { return null ; } return ( < InstrumentConsumer type = { type } options = { options } notes = { notes } polyphony = { polyphony } oscillator = { oscillator } envelope = { envelope } samples = { samples } onLoad = { onLoad } volume = { volume } pan = { pan } mute = { mute } solo = { solo } effectsChain = { effectsChain } onInstrumentsUpdate = { onInstrumentsUpdate } / > ) ; } ; /**
 * Use Instrument's flattened synth props to create options object for Tone JS
 */ const buildSynthOptions = ( { oscillator , envelope } ) => { if ( oscillator || envelope ) { return { ... ( envelope ? { envelope } : { } ) , ... ( oscillator ? { oscillator } : { } ) } ; } return undefined ; } ; export default Instrument ;