import { computed , observable } from 'mobx' ; import { range } from 'lodash' ; import { makeNoteScale , makeNoteScaleForKey } from './tonal-utils' ; import * as _ from 'lodash' ; import generator from './generator' ; import logging , { Events } from './logging' ; import masks from './masks' ; import { Note , Source , Voice } from './note' ; import { NoteSequence } from './note-sequence' ; import undo , { undoable } from './undo' ; import { DEFAULT_NOTES , TOTAL_SIXTEENTHS , MAX_PITCH , MIN_PITCH , } from './constants' ; import featureFlags , { FeatureFlags } from './feature-flags' ; export const enum EditorTool { DRAW = 'DRAW' , MASK = 'MASK' , ERASE = 'ERASE' } export interface ScaleValue { pitch : number ; name : string ; octave : number ; } class Editor { @ observable private mainSequence = new NoteSequence ( ) ; @ observable private mutedVoices = new Set < number > ( ) ; @ computed get mainNotes ( ) { return [ ... this . mainSequence . notes ] ; } @ computed get allNotes ( ) { return [ ... this . mainSequence . notes , ... this . candidateNoteSequence . notes ] ; } @ computed get candidateNoteSequence ( ) { const selectedSequence = generator . selectedCandidateSequence ; return selectedSequence ? selectedSequence : NoteSequence . empty ( ) ; } @ computed get unmutedNotes ( ) { return this . allNotes . filter ( note => ! this . isVoiceMuted ( note . voice ) ) ; } @ computed get userNotes ( ) { return this . allNotes . filter ( note => note . source === Source . USER ) ; } @ computed get agentNotes ( ) { return this . allNotes . filter ( note => note . source === Source . AGENT ) ; } @ observable noteBeingDrawn : Note | null = null ; beginDrawingNote ( note ) { undo . beginUndoable ( 'editor.drawNote' ) ; this . noteBeingDrawn = note ; this . _addNote ( note ) ; } trimNoteBeingDrawnSequence ( ) { if ( ! this . noteBeingDrawn ) return ; if ( ! this . candidateNoteSequence . isEmpty ( ) ) { this . candidateNoteSequence . trimOverlappingVoices ( this . noteBeingDrawn ) ; } else { this . mainSequence . trimOverlappingVoices ( this . noteBeingDrawn ) ; } } endDrawingNote ( ) { if ( this . noteBeingDrawn ) { logging . logEvent ( Events . DRAW_NOTE , undo . getUndoStep ( ) ) ; this . noteBeingDrawn = null ; undo . completeUndoable ( 'editor.drawNote' ) ; } } @ observable private _scale = makeNoteScale ( MAX_PITCH , MIN_PITCH ) ; @ computed get scale ( ) { return this . _scale ; } @ observable private _key = 'C' ; @ computed get key ( ) { return this . _key ; } selectKey ( key ) { logging . logEvent ( Events . SELECT_KEY , key ) ; this . _key = key ; } @ observable _mode = 'major' ; @ computed get mode ( ) { return this . _mode ; } selectMode ( mode ) { logging . logEvent ( Events . SELECT_MODE , mode ) ; this . _mode = mode ; } @ observable private _constrainToKey = true ; @ computed get constrainToKey ( ) { return this . _constrainToKey ; } setConstrainToKey ( constrainToKey ) { logging . logEvent ( Events . SET_CONSTRAIN_TO_KEY , constrainToKey ) ; this . _constrainToKey = constrainToKey ; } @ observable private _noteHoverName = '' ; @ computed get noteHoverName ( ) { return this . _noteHoverName ; } setNoteHoverName ( scaleValue ) { if ( scaleValue === null ) { this . _noteHoverName = '' ; } else { const { name , octave } = scaleValue ; this . _noteHoverName = `${ name }${ octave }` ; } } @ computed get scaleMax ( ) { return this . scale [ 0 ] . pitch ; } @ computed get keyScale ( ) { return makeNoteScaleForKey ( this . key , this . mode ) ; } @ computed get keyPitchSet ( ) { return new Set < number > ( this . keyScale . map ( scaleValue => scaleValue . pitch ) ) ; } isPitchInScale ( pitch ) { return this . keyPitchSet . has ( pitch ) ; } @ observable private _activeNoteValue : number | null = null ; @ computed get activeNoteValue ( ) { return this . _activeNoteValue ; } setActiveNoteValue ( noteValue ) { this . _activeNoteValue = noteValue ; } @ observable readonly totalSixteenths = TOTAL_SIXTEENTHS ; @ observable private _quantizeStep = 2 ; @ computed get quantizeStep ( ) { return this . _quantizeStep ; } selectQuantization ( quantizeStep ) { logging . logEvent ( Events . SELECT_QUANTIZATION , quantizeStep ) ; this . _quantizeStep = quantizeStep ; } @ computed get nDivisions ( ) { return this . totalSixteenths / this . quantizeStep ; } @ computed get divisions ( ) { const divisions = range ( this . nDivisions ) ; return divisions ; } @ observable private _selectedTool : EditorTool = EditorTool . DRAW ; @ computed get selectedTool ( ) { return this . _selectedTool ; } selectTool ( tool , shouldLog = true ) { shouldLog ? logging . logEvent ( Events . SELECT_TOOL , tool ) : null ; this . _selectedTool = tool ; } @ observable private _selectedVoice : Voice = Voice . SOPRANO ; @ computed get selectedVoice ( ) { return this . _selectedVoice ; } selectVoice ( voice ) { logging . logEvent ( Events . SELECT_VOICE , voice ) ; this . _selectedVoice = voice ; } constructor ( ) { let position = 0 ; DEFAULT_NOTES . forEach ( ( [ pitch , duration ] , i ) => { const note = new Note ( pitch , position , duration ) ; position += duration ; this . mainSequence . addNote ( note ) ; } ) ; this . currentlySelectedNotes . clear ( ) ; } @ observable private currentlySelectedNotes = new Set < Note > ( ) ; isNoteSelected = ( note ) => { return this . currentlySelectedNotes . has ( note ) ; } ; @ observable private currentlyPlayingNotes = new Set < Note > ( ) ; private setCurrentlyPlaying ( note ) { note . isPlaying = true ; this . currentlyPlayingNotes . add ( note ) ; } clearPlayingNotes ( ) { this . currentlyPlayingNotes . clear ( ) ; this . allNotes . forEach ( note => { note . isPlaying = false ; } ) ; } private getNoteByPitchPosition ( pitch , position ) { return this . allNotes . find ( note => { return note . pitch === pitch && note . position === position ; } ) ; } private getTempNoteByPitchPosition ( pitch , position ) { return this . candidateNoteSequence . notes . find ( note => { return note . pitch === pitch && note . position === position ; } ) ; } setNotePlaying ( pitch , position ) { const note = this . getNoteByPitchPosition ( pitch , position ) ; if ( note ) this . setCurrentlyPlaying ( note ) ; const tempNote = this . getTempNoteByPitchPosition ( pitch , position ) ; if ( tempNote ) this . setCurrentlyPlaying ( tempNote ) ; for ( const playingNote of this . currentlyPlayingNotes . values ( ) ) { if ( playingNote . position + playingNote . duration <= position ) { playingNote . isPlaying = false ; this . currentlyPlayingNotes . delete ( playingNote ) ; } } } @ undoable ( 'editor.addNote' ) addNote ( note , shouldSelect = false ) { this . _addNote ( note ) ; if ( shouldSelect ) { this . currentlySelectedNotes . clear ( ) ; this . currentlySelectedNotes . add ( note ) ; } } private _addNote ( note ) { const isMasked = masks . isNoteMasked ( note ) ; const isCandidateSequenceSelected = generator . selectedCandidateSequence !== null ; if ( isMasked && isCandidateSequenceSelected && ! featureFlags . baseline ) { generator . addNoteToSelected ( note ) ; } else { this . mainSequence . addNote ( note ) ; } } @ undoable ( 'editor.removeNote' ) removeNote ( note ) { this . _removeNote ( note ) ; logging . logEvent ( Events . DELETE_NOTE , undo . getUndoStep ( ) ) ; } private _removeNote ( note ) { generator . removeNoteFromSelected ( note ) ; this . mainSequence . removeNote ( note ) ; } removeNotes ( notes ) { notes . forEach ( note => this . _removeNote ( note ) ) ; } @ undoable ( 'editor.removeMaskedNotes' ) removeMaskedNotes ( ) { const maskedNotes = masks . maskedNotes ; this . removeNotes ( maskedNotes ) ; logging . logEvent ( Events . DELETE_MASKED_NOTES , undo . getUndoStep ( ) ) ; } getPitchFromScaleIndex ( scaleIndex ) { return this . scale [ scaleIndex ] . pitch ; } addGeneratedSequence ( sequence ) { sequence . notes . forEach ( note => { this . mainSequence . addNote ( note ) ; } ) ; } startNoteDrag ( ) { undo . beginUndoable ( 'editor.dragNote' ) ; } endNoteDrag ( note ) { for ( let other of this . allNotes ) { if ( other === note ) continue ; if ( other . position === note . position && other . pitch === note . pitch ) { this . mainSequence . removeNote ( note ) ; } } undo . completeUndoable ( 'editor.dragNote' ) ; logging . logEvent ( Events . MOVE_NOTE , undo . getUndoStep ( ) ) ; } replaceAllNotes ( notes ) { this . mainSequence . clearNotes ( ) ; for ( const note of notes ) { this . mainSequence . addNote ( note ) ; } } getNotesInRange ( positionRange , pitchRange ) { const notesInRange = [ ] ; for ( const note of this . allNotes ) { if ( NoteSequence . overlaps ( note , positionRange , pitchRange ) ) { notesInRange . push ( note ) ; } } return notesInRange ; } toggleVoiceMute ( voiceIndex ) { if ( this . mutedVoices . has ( voiceIndex ) ) { logging . logEvent ( Events . UNMUTE_VOICE , voiceIndex ) ; this . mutedVoices . delete ( voiceIndex ) ; } else { logging . logEvent ( Events . MUTE_VOICE , voiceIndex ) ; this . mutedVoices . add ( voiceIndex ) ; } } isVoiceMuted ( voiceIndex ) { return this . mutedVoices . has ( voiceIndex ) ; } } export default new Editor ( ) ;