UNPKG

gibberwocky

Version:

music sequencing for Live and Max/MSP

363 lines (285 loc) 12.2 kB
const acorn = require( 'acorn' ) const walk = require( 'acorn/dist/walk' ) const Utility = require( './utility.js' ) const $ = Utility.create module.exports = function( Gibber ) { const Marker = { waveform: require( './annotations/waveform.js' )( Gibber ), _patternTypes: [ 'values', 'timings', 'index' ], globalIdentifiers:{}, acorn, walk, // need ecmaVersion 7 for arrow functions to work correctly parsingOptions: { locations:true, ecmaVersion:7 }, __visitors:require( './annotations/visitors.js' ), // pass Marker object to patternMarkupFunctions as a closure init() { for( let key in this.patternMarkupFunctions ) { if( key.includes( '_' ) === true ) { this.patternMarkupFunctions[ key.slice(2) ] = this.patternMarkupFunctions[ key ]( this ) } } this.visitors = this.__visitors( this ) }, clear() { Marker.waveform.clear() }, prepareObject( obj ) { obj.markup = { textMarkers: {}, cssClasses: {} } }, getObj( path, findSeq = false, seqNumber = 0 ) { let obj = window[ path[0] ] const isMessage = path[0] === 'message' if( isMessage ) { return obj( path[1] )[ seqNumber ] } for( let i = 1; i < path.length; i++ ) { let key = path[ i ] if( key !== undefined ) { if( key[0] === "'" || key[0] === '"' ) { key = key.slice(1,-1) } obj = obj[ key ] }else{ break; } } if( findSeq === true ) { if( obj.type !== 'sequence' ) { obj = obj[ seqNumber ] } } return obj }, process( code, position, codemirror, track ) { // store position offset from top of code editor // to use when marking patterns, since acorn will produce // relative offsets Marker.offset = { vertical: position.start.line, horizontal: position.horizontalOffset === undefined ? 0 : position.horizontalOffset } const state = [] state.cm = codemirror state.cm.__state = state const parsed = acorn.parse( code, Marker.parsingOptions ) parsed.body.forEach( node => { state.length = 0 walk.recursive( node, state, Marker.visitors ) }) }, markPatternsForSeq( seq, nodes, state, cb, container, seqNumber = 0 ) { const valuesNode = nodes[0] valuesNode.offset = Marker.offset // XXX We have to markup the timings node first, as there is the potential for // markup on the value node to insert text that will alter the range of the timings node. // If the timings node is already marked up, the mark will simply move with the text addition. // However, if the timing mode is marked up after, the position information provided by the parser // will be off and not valid. if( nodes[1] !== undefined ) { const timingsNode = nodes[1] timingsNode.offset = Marker.offset Marker.patternMarkupFunctions[ timingsNode.type ]( timingsNode, state, seq, 'timings', container, seqNumber ) } Marker.patternMarkupFunctions[ valuesNode.type ]( valuesNode, state, seq, 'values', container, seqNumber ) }, processGen( node, cm, track, patternObject=null, seq=null, lineMod=0 ) { let ch = node.end, line = Marker.offset.vertical + node.loc.start.line, closeParenStart = ch - 1, end = node.end, isAssignment = true // check to see if a given object is a proxy that already has // a widget created; if so, don't make another one! if( node.type === 'AssignmentExpression' ) { const __obj = window[ node.left.name ] if( __obj !== undefined ) { if( __obj.widget !== undefined ) { return } Marker.waveform.createWaveformWidget( line - 1, closeParenStart, ch-1, isAssignment, node, cm, __obj, track, false ) } }else if( node.type === 'CallExpression' ) { const seqExpression = node seqExpression.arguments.forEach( function( seqArgument ) { if( seqArgument.type === 'CallExpression' ) { const idx = Gibber.__gen.ugenNames.indexOf( seqArgument.callee.name ) // not a gen, markup will happen elsewhere if( idx === -1 ) return ch = seqArgument.loc.end.ch || seqArgument.loc.end.column // XXX why don't I need the Marker offset here? line = seqArgument.loc.end.line + lineMod // for some reason arguments to .seq() include the offset, // so we only want to add the offset in if we this is a gen~ // assignment via function call. lineMod will !== 0 if this // is the case. if( lineMod !== 0 ) line += Marker.offset.vertical closeParenStart = ch - 1 isAssignment = false node.processed = true //debugger Marker.waveform.createWaveformWidget( line, closeParenStart, ch, isAssignment, node, cm, patternObject, track, lineMod === 0 ) } else if( seqArgument.type === 'ArrayExpression' ) { //console.log( 'WavePattern array' ) }else if( seqArgument.type === 'Identifier' ) { // handles 'Identifier' when pre-declared variables are passed to methods ch = seqArgument.loc.end.ch || seqArgument.loc.end.column line = seqArgument.loc.end.line + lineMod isAssignment = false node.processsed = true if( lineMod !== 0 ) line += Marker.offset.vertical Marker.waveform.createWaveformWidget( line, closeParenStart, ch, isAssignment, node, cm, patternObject, track, lineMod === 0 ) } }) } }, _createBorderCycleFunction: require( './annotations/update/createBorderCycle.js' ), finalizePatternAnnotation( patternObject, className, seqTarget ) { patternObject.update = Marker._createBorderCycleFunction( className, patternObject ) //Marker._addPatternUpdates( patternObject, className ) Marker._addPatternFilter( patternObject ) patternObject.patternName = className patternObject._onchange = () => { Marker._updatePatternContents( patternObject, className, seqTarget ) } patternObject.clear = () => { patternObject.marker.clear() } }, // Patterns can have *filters* which are functions // that can modify the final output of a pattern and carry out // other miscellaneous tasks. Here we add a filter that schedules // updates for annotations everytime the target pattern outputs // a value. _addPatternFilter( patternObject ) { patternObject.filters.push( args => { const wait = Utility.beatsToMs( patternObject.nextTime + .5, Gibber.Scheduler.bpm ), idx = args[ 2 ], shouldUpdate = patternObject.update.shouldUpdate // delay is used to ensure that timings pattern is processed after values pattern, // because changing the mark of the values pattern messes up the mark of the timings // pattern; reversing their order of execution fixes this. if( patternObject.__delayAnnotations === true ) { Gibber.Environment.animationScheduler.add( () => { if( patternObject.type !== 'Lookup' ) { patternObject.update.currentIndex = idx }else{ patternObject.update.currentIndex = patternObject.update.__currentIndex.shift() } patternObject.update() }, wait + 1 ) }else{ Gibber.Environment.animationScheduler.add( () => { if( patternObject.type !== 'Lookup' ) { patternObject.update.currentIndex = idx }else{ patternObject.update.currentIndex = patternObject.update.__currentIndex.shift() } patternObject.update() }, wait ) } return args }) }, // FunctionExpression and ArrowFunctionExpression are small enough to // include here, as they're simply wrappers for Identifier. All other // pattern markup functions are in their own files. patternMarkupFunctions: { __Literal: require( './annotations/markup/literal.js' ), __Identifier: require( './annotations/markup/identifier.js' ), __UnaryExpression: require( './annotations/markup/unaryExpression.js' ), __BinaryExpression: require( './annotations/markup/binaryExpression.js' ), __ArrayExpression: require( './annotations/markup/arrayExpression.js' ), __CallExpression: require( './annotations/markup/callExpression.js' ), // args[ 0 ] is the pattern node FunctionExpression( ...args ) { if( args[ 0 ].processed === true ) return Marker.patternMarkupFunctions.Identifier( ...args ) }, ArrowFunctionExpression( ...args ) { if( args[ 0 ].processed === true ) return Marker.patternMarkupFunctions.Identifier( ...args ) } }, patternUpdates: { Euclid: require( './annotations/update/euclidAnnotation.js' ), Automata: require( './annotations/update/euclidAnnotation.js' ), Hex: require( './annotations/update/euclidAnnotation.js' ), Lookup: require( './annotations/update/lookupAnnotation.js' ), anonymousFunction: require( './annotations/update/anonymousAnnotation.js' ), }, standalone: { Score: require( './annotations/standalone/scoreAnnotation.js' ), Steps: require( './annotations/standalone/stepsAnnotation.js' ), HexSteps: require( './annotations/standalone/hexStepsAnnotations.js' ) }, _updatePatternContents( pattern, patternClassName, track ) { let marker, pos, newMarker if( pattern.values.length > 1 ) { // array of values for( let i = 0; i < pattern.values.length; i++) { marker = track.markup.textMarkers[ patternClassName ][ i ] pos = marker.find() const itemClass = document.querySelector('.' + marker.className.split(' ')[0] ) itemClass.innerText = pattern.values[ i ] } }else{ // single literal marker = track.markup.textMarkers[ patternClassName ] pos = marker.find() const itemClass = document.querySelector('.' + marker.className.split(' ')[0] ) itemClass.innerText = pattern.values[ 0 ] //marker.doc.replaceRange( '' + pattern.values[ 0 ], pos.from, pos.to ) // newMarker = marker.doc.markText( pos.from, pos.to, { className: patternClassName + ' annotation-border' } ) // track.markup.textMarkers[ patternClassName ] = newMarker } }, _getNamesAndPosition( patternNode, state, patternType, index = 0 ) { let start = patternNode.loc.start, end = patternNode.loc.end, className = state.slice( 0 ), marker className.push( patternType ) className.push( index ) className = className.join( '_' ) let expr = /\[\]/gi className = className.replace( expr, '' ) expr = /\-/gi className = className.replace( expr, '_' ) expr = /\ /gi className = className.replace( expr, '_' ) start.line += patternNode.offset.vertical - 1 end.line += patternNode.offset.vertical - 1 start.ch = start.column + patternNode.offset.horizontal end.ch = end.column + patternNode.offset.horizontal return [ className, start, end ] }, _getCallExpressionHierarchy( expr ) { let callee = expr.callee, obj = callee.object, components = [], index = 0, depth = 0 while( obj !== undefined ) { let pushValue = null if( obj.type === 'ThisExpression' ) { pushValue = 'this' }else if( obj.property && obj.property.name ){ pushValue = obj.property.name }else if( obj.property && obj.property.type === 'Literal' ){ // array index pushValue = obj.property.value // don't fall for tracks[0] etc. if( depth > 1 ) index = obj.property.value }else if( obj.type === 'Identifier' ) { pushValue = obj.name } if( pushValue !== null ) components.push( pushValue ) depth++ obj = obj.object } components.reverse() if( callee.property ) components.push( callee.property.name ) return [ components, depth, index ] }, } return Marker }