├── .gitignore ├── COPYING ├── README.md ├── images └── recurselogo2.png ├── recurse-app ├── Gruntfile.js ├── app │ ├── package.json │ ├── src │ │ ├── compiler │ │ │ ├── Compiler.ts │ │ │ ├── ErrorMessages.ts │ │ │ ├── ICompilerSettings.ts │ │ │ └── compiler.spec.ts │ │ ├── converter │ │ │ ├── IClip.ts │ │ │ └── flatten.ts │ │ ├── core │ │ │ ├── type │ │ │ │ ├── Constants.ts │ │ │ │ ├── IRecurseValue.ts │ │ │ │ ├── Note.ts │ │ │ │ ├── NoteEvent.ts │ │ │ │ ├── RecurseObject.ts │ │ │ │ ├── RecurseResult.ts │ │ │ │ ├── RecurseStatus.ts │ │ │ │ └── Scale.ts │ │ │ └── util │ │ │ │ ├── Helpers.ts │ │ │ │ ├── convertNoteListToRecurseCode.spec.ts │ │ │ │ ├── convertNoteListToRecurseCode.ts │ │ │ │ ├── createLookupTable.ts │ │ │ │ ├── forEachPitch.ts │ │ │ │ ├── forEachSelectedPitch.ts │ │ │ │ └── nodewrapper.js │ │ ├── function │ │ │ ├── IContext.ts │ │ │ ├── SyntaxTree.ts │ │ │ ├── base │ │ │ │ ├── Nested.ts │ │ │ │ ├── Root.ts │ │ │ │ ├── Value.ts │ │ │ │ ├── Variable.ts │ │ │ │ └── VariableReference.ts │ │ │ ├── generator │ │ │ │ ├── NoteSet.ts │ │ │ │ ├── RhythmicMotive.ts │ │ │ │ └── VelocitySet.ts │ │ │ ├── modifier │ │ │ │ ├── Pitch.ts │ │ │ │ └── Transpose.ts │ │ │ ├── operator │ │ │ │ ├── Alternate.ts │ │ │ │ ├── Chain.ts │ │ │ │ ├── Fill.ts │ │ │ │ ├── GenericOperator.ts │ │ │ │ ├── Interpolate.spec.ts │ │ │ │ ├── Interpolate.ts │ │ │ │ ├── Multiply.ts │ │ │ │ ├── Random.ts │ │ │ │ ├── Range.ts │ │ │ │ └── Repeat.ts │ │ │ ├── selection │ │ │ │ ├── EndSelect.ts │ │ │ │ ├── Select.ts │ │ │ │ └── SelectStrategy.ts │ │ │ └── setter │ │ │ │ ├── Loop.ts │ │ │ │ ├── PatternLength.ts │ │ │ │ └── SetScale.ts │ │ ├── interpreter │ │ │ ├── Entity.ts │ │ │ ├── INode.ts │ │ │ ├── ISetting.ts │ │ │ ├── ISyntaxTree.ts │ │ │ ├── IToken.ts │ │ │ ├── Lexer.spec.ts │ │ │ ├── Lexer.ts │ │ │ ├── Parser.spec.ts │ │ │ ├── Parser.ts │ │ │ ├── TokenType.ts │ │ │ └── ValueType.ts │ │ └── recurse-cli.ts │ └── test.rse ├── package.json ├── test-snippets │ ├── repo.rse │ └── test.rse ├── testCode │ ├── invalid │ │ └── unmatchingParentheses.rse │ └── valid │ │ ├── alt.rse │ │ ├── alt.rse.json │ │ ├── altShorthand.rse │ │ ├── altShorthand.rse.json │ │ ├── altWithNested.rse │ │ ├── altWithNested.rse.json │ │ ├── alternateWithRests.rse │ │ ├── alternateWithRests.rse.json │ │ ├── comments.rse │ │ ├── comments.rse.json │ │ ├── cutBleedingNotes.rse │ │ ├── cutBleedingNotes.rse.json │ │ ├── fill.rse │ │ ├── fill.rse.json │ │ ├── fillFirst.rse │ │ ├── fillFirst.rse.json │ │ ├── fillMid.rse │ │ ├── fillMid.rse.json │ │ ├── interpolate.rse │ │ ├── length.rse │ │ ├── length.rse.json │ │ ├── lengthMultipleClips.rse │ │ ├── lengthMultipleClips.rse.json │ │ ├── loopFactor.rse │ │ ├── loopFactor.rse.json │ │ ├── multipleClips.rse │ │ ├── multipleClips.rse.json │ │ ├── multipleTracks.rse │ │ ├── multipleTracks.rse.json │ │ ├── multiply.rse │ │ ├── multiply.rse.json │ │ ├── nested.rse │ │ ├── nested.rse.json │ │ ├── nestedSimple.rse │ │ ├── nestedSimple.rse.json │ │ ├── nestedWithAlts.rse │ │ ├── nestedWithAlts.rse.json │ │ ├── nestedWithInterpolateHead.rse │ │ ├── nestedWithInterpolateHead.rse.json │ │ ├── nestedWithRangeHead.rse │ │ ├── nestedWithRangeHead.rse.json │ │ ├── nestedWithRepeat.rse │ │ ├── nestedWithRepeat.rse.json │ │ ├── noteSetWithScaleDegrees.rse │ │ ├── noteSetWithScaleDegrees.rse.json │ │ ├── patternLengthVisibleInOutput.rse │ │ ├── patternLengthVisibleInOutput.rse.json │ │ ├── pitchEven.rse │ │ ├── pitchEven.rse.json │ │ ├── pitchOdd.rse │ │ ├── pitchOdd.rse.json │ │ ├── pitchPlus.rse │ │ ├── pitchPlus.rse.json │ │ ├── postAndPreRest.rse │ │ ├── postAndPreRest.rse.json │ │ ├── range.rse │ │ ├── range.rse.json │ │ ├── rangeShorthand.rse │ │ ├── rangeShorthand.rse.json │ │ ├── repeatNotes.rse │ │ ├── rest.rse │ │ ├── rest.rse.json │ │ ├── restShouldNotStealNote.rse │ │ ├── restShouldNotStealNote.rse.json │ │ ├── selectAdditive.rse │ │ ├── selectAdditive.rse.json │ │ ├── selectByIndexList.rse │ │ ├── selectByIndexList.rse.json │ │ ├── selectFirst.rse │ │ ├── selectFirst.rse.json │ │ ├── selectLast.rse │ │ ├── selectLast.rse.json │ │ ├── transpose.rse │ │ ├── transpose.rse.json │ │ ├── variables.rse │ │ ├── variables.rse.json │ │ ├── variablesInline.rse │ │ ├── variablesInline.rse.json │ │ ├── variablesWithNested.rse │ │ ├── variablesWithNested.rse.json │ │ ├── vel.rse │ │ ├── vel.rse.json │ │ ├── velNested.rse │ │ └── velNested.rse.json ├── tsconfig.json └── typings │ ├── commander │ └── commander.d.ts │ ├── jquery │ └── jquery.d.ts │ ├── lodash │ └── lodash.d.ts │ ├── node │ └── node.d.ts │ ├── osc-min │ └── osc-min.d.ts │ ├── sprintf-js │ └── sprintf-js.d.ts │ ├── tape │ └── tape.d.ts │ └── tsd.d.ts ├── recurse-connectors └── ableton-live │ ├── RecurseConnector.amxd │ └── externals │ └── parseInput.js └── recurse-sublime └── plugin ├── Default.sublime-commands └── recurse.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .idea/* 3 | *.iml 4 | ./main/vendor/* 5 | ./main/node_modules/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Recurse](https://raw.github.com/carrierdown/recurse/master/images/recurselogo2.png) 2 | 3 | ## `re { 32 | var lexer: Lexer, 33 | tokens: Array, 34 | parseResult: RecurseResult, 35 | compileResult: RecurseResult, 36 | noteEvents: IClip[], 37 | jsonOutput: string; 38 | 39 | lexer = new Lexer(); 40 | tokens = lexer.getTokenSet(code.toString()); 41 | parseResult = Parser.parseTokensToSyntaxTree(tokens); 42 | compileResult = new RecurseResult(parseResult.status, parseResult.error); 43 | 44 | if (parseResult.isOk()) { 45 | if (this.settings.debug) { 46 | Parser.printSyntaxTree(parseResult.result); 47 | } 48 | let generated = parseResult.result.generate(); 49 | //console.log('generated result', generated); 50 | noteEvents = flatten(generated, parseResult.result.rootNodes); 51 | // console.log(JSON.stringify(noteEvents)); 52 | compileResult.result = noteEvents; 53 | jsonOutput = JSON.stringify(noteEvents); 54 | if (this.settings.debug) { 55 | console.log(jsonOutput); 56 | } 57 | if (!this.settings.preview) { 58 | this.sendOscMessage(jsonOutput); 59 | } 60 | } 61 | return compileResult; 62 | } 63 | 64 | public setPreview(state: boolean) { 65 | this.settings.preview = state; 66 | } 67 | 68 | public setDebug(state: boolean) { 69 | this.settings.debug = state; 70 | } 71 | 72 | private sendOscMessage(message: string): void { 73 | var buf: any; 74 | buf = osc.toBuffer({ 75 | address: '/recurse/data', 76 | args: [ 77 | { 78 | type: 'string', 79 | value: message 80 | } 81 | ] 82 | }); 83 | this.udp.send(buf, 0, buf.length, this.outport, 'localhost', (err: any) => { 84 | this.udp.close(); 85 | }); 86 | } 87 | } -------------------------------------------------------------------------------- /recurse-app/app/src/compiler/ErrorMessages.ts: -------------------------------------------------------------------------------- 1 | import sprintf_js = require('sprintf-js'); 2 | 3 | export class ErrorMessages { 4 | public static FILE_READ_ERROR: string = 'Unable to read file %s'; 5 | public static UNEXPECTED_TOKEN_ERROR: string = 'Expected %s following %s, but found %s'; 6 | public static REPEAT_STATEMENT_TOO_MANY_OPERANDS: string = 'Repeat statement can only contain 2 operands'; 7 | public static INVALID_IDENTIFIER: string = 'Invalid identifier: %s'; 8 | public static REPEAT_SHORTHAND_INVALID_IN_ALT_SHORTHAND: string = 'Shorthand repeat statement can not be used inside shorthand alternating statement. Use regular alternating statement alt() instead.'; 9 | public static NUMERIC_ARGUMENT_NOT_ALLOWED: string = "Numeric arguments not allowed in %s"; 10 | public static NOT_IN_CHAIN: string = "Encountered ; (new part) but not currently in chain"; 11 | public static PARENTHESES_DO_NOT_MATCH: string = "Parentheses do not match up. Check that all parentheses start and end correctly."; 12 | 13 | public static getError(errorMsg: string, ...tokens: Array): string { 14 | return sprintf_js.vsprintf(errorMsg, tokens); 15 | } 16 | } -------------------------------------------------------------------------------- /recurse-app/app/src/compiler/ICompilerSettings.ts: -------------------------------------------------------------------------------- 1 | export interface ICompilerSettings { 2 | preview: boolean; 3 | debug: boolean; 4 | } -------------------------------------------------------------------------------- /recurse-app/app/src/compiler/compiler.spec.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import tape = require('tape'); 4 | import fs = require('fs'); 5 | import path = require('path'); 6 | 7 | import {IClip} from "../converter/IClip"; 8 | import {Compiler} from "./Compiler"; 9 | import {RecurseResult} from "../core/type/RecurseResult"; 10 | import {RecurseStatus} from "../core/type/RecurseStatus"; 11 | 12 | function forEachFileInDir(dirname: string, callback: (filename: string, content: string, expectedContent: string) => void, finalize: () => void, soloFile: string = '') { 13 | var filenames:Array = fs.readdirSync(dirname); 14 | 15 | if (soloFile.length > 0 && filenames.indexOf(soloFile) >= 0) { 16 | filenames = [soloFile]; 17 | } 18 | 19 | for (let filename of filenames) { 20 | // Only test files ending in .rse 21 | if (filename.indexOf('.rse') === filename.length - '.rse'.length) { 22 | let expectedFilename = path.join(dirname, `${filename}.json`); 23 | callback(filename, 24 | fs.readFileSync(path.join(dirname, filename), 'utf-8'), 25 | fs.existsSync(expectedFilename) ? fs.readFileSync(expectedFilename, 'utf-8') : '' 26 | ); 27 | } 28 | } 29 | finalize(); 30 | } 31 | 32 | tape('Testing compiler with valid code', (test) => { 33 | var compiler: Compiler = new Compiler(), 34 | compile: RecurseResult, 35 | dirname: string = path.join(__dirname, '../../../testCode/valid/'), 36 | propertiesToTest: string[] = ['start', 'duration', 'pitch', 'velocity']; 37 | 38 | compiler.setDebug(true); 39 | compiler.setPreview(true); 40 | 41 | forEachFileInDir(dirname, (filename, content, expectedContent) => { 42 | compile = compiler.compile(content); 43 | if (compile.status === RecurseStatus.ERROR) { 44 | console.log(`Compiler failed with: ${compile.error}`); 45 | } 46 | test.equal(compile.status, RecurseStatus.OK, `file: ${filename} containing ${content} should compile ok`); 47 | 48 | if (expectedContent.length > 0) { 49 | let expectedResultsSets = JSON.parse(expectedContent) as IClip[]; 50 | // console.log(expectedResultsSets); 51 | for (let i = 0; i < expectedResultsSets.length; i++) { 52 | let expectedResults = expectedResultsSets[i]; 53 | test.equal(expectedResults.notes.length, compile.result[i].notes.length, `Expected output length ${expectedResults.notes.length} and actual output length ${compile.result[i].notes.length} should be the same`); 54 | if (expectedResults.loopLength) { 55 | test.equal(expectedResults.loopLength, compile.result[i].loopLength, `Property loopLength: Expected ${expectedResults.loopLength}, found ${compile.result[i].loopLength}`); 56 | } 57 | for (let x = 0; x < expectedResults.notes.length; x++) { 58 | let expectedResult = expectedResults.notes[x]; 59 | let compiledResult = compile.result[i].notes[x]; 60 | //console.log(compile.result[i], expectedResult); 61 | for (let propertyToTest of propertiesToTest) { 62 | if (expectedResult[propertyToTest]) { 63 | test.equal(expectedResult[propertyToTest], compiledResult[propertyToTest], `Property ${propertyToTest} of expected output (${expectedResult[propertyToTest]}) and actual output (${compiledResult[propertyToTest]}) should be the same (${filename})`); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }, () => { 70 | test.end(); 71 | }/*, 'noteSetWithScaleDegrees.rse'*/); 72 | }); 73 | 74 | tape('Testing compiler with invalid code', (test) => { 75 | var compiler: Compiler = new Compiler(), 76 | result: RecurseResult, 77 | dirname: string = path.join(__dirname, '../../../testCode/invalid/'); 78 | 79 | compiler.setDebug(false); 80 | compiler.setPreview(true); 81 | 82 | forEachFileInDir(dirname, (filename, content) => { 83 | result = compiler.compile(content); 84 | if (result.status === RecurseStatus.ERROR) { 85 | console.log(`Compiler failed with: ${result.error}`); 86 | } 87 | test.equal(result.status, RecurseStatus.ERROR, `file: ${filename} containing ${content} should not compile ok. Error returned was ${result.error}`); 88 | }, () => { 89 | test.end(); 90 | }); 91 | }); 92 | 93 | // Misc tests that need special setup/assertions 94 | 95 | /*tape('Testing random function', (test) => { 96 | var compiler: Compiler = new Compiler(); 97 | 98 | compiler.setDebug(false); 99 | compiler.setPreview(true); 100 | 101 | // rnd invokes are repeated manually rather than being looped so that we avoid issues with events being chopped of at the end which are hard to test. We instead use * to make sure that only "clean" events are produced. 102 | let compiled: RecurseResult = compiler.compile('rm(rnd(4,6,8), rnd(1,2,3), rnd(4,6,8), rnd(1,2,3), rnd(4,6,8), rnd(1,2,3), rnd(4,6,8), rnd(1,2,3), *) ns(c3)'); 103 | 104 | test.equal(compiled.status, RecurseStatus.OK, `Compilation should be successful`); 105 | let i = 0; 106 | let evenValues: number[] = [], 107 | oddValues: number[] = []; 108 | for (let result of compiled.result[0].notes) { 109 | if (i % 2 === 0) { 110 | test.ok(result.duration === 1 || result.duration === 1.5 || result.duration === 2, `Expected 1 or 1.5 or 2, and got ${result.duration}`); 111 | evenValues.push(result.duration); 112 | } else { 113 | test.ok(result.duration === 0.25 || result.duration === 0.5 || result.duration === 0.75, `Expected 0.25 or 0.5 or 0.75, and got ${result.duration}`); 114 | oddValues.push(result.duration); 115 | } 116 | i++; 117 | } 118 | 119 | test.ok(_.toArray(_.countBy(oddValues)).length > 1, `Expected more than one unique result to be returned from random during generation ${_.toArray(_.countBy(oddValues)).length}`); 120 | test.ok(_.toArray(_.countBy(evenValues)).length > 1, `Expected more than one unique result to be returned from random during generation ${_.toArray(_.countBy(evenValues)).length}`); 121 | 122 | test.end(); 123 | });*/ 124 | -------------------------------------------------------------------------------- /recurse-app/app/src/converter/IClip.ts: -------------------------------------------------------------------------------- 1 | import {NoteEvent} from "../core/type/NoteEvent"; 2 | 3 | export interface IClip { 4 | notes: NoteEvent[]; 5 | loopLength: number; 6 | } -------------------------------------------------------------------------------- /recurse-app/app/src/converter/flatten.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import {Constants} from "../core/type/Constants"; 4 | import {IClip} from "./IClip"; 5 | import {RecurseObject} from "../core/type/RecurseObject"; 6 | import {INode} from "../interpreter/INode"; 7 | import {NoteEvent} from "../core/type/NoteEvent"; 8 | 9 | // "flattens" sequence for consumption by a typical sequencer. Flattening makes intervals relative to zero rather than the preceding note event 10 | export function flatten(itemsSets: Array>, clipRootNodes: INode[]): IClip[] { 11 | var currentPos: number = 0, 12 | flattenedResultsSets: IClip[] = [], 13 | flattenedResults: Array = [], 14 | currentRefNode: INode = null; 15 | 16 | // todo: should probably do some sorting according to order-property prior to flattening 17 | for (let i = 0; i < itemsSets.length; i++) { 18 | let items = itemsSets[i]; 19 | for (let item of items) { 20 | if (currentRefNode === null || item.refToGenNode !== currentRefNode) { 21 | currentRefNode = item.refToGenNode; 22 | currentPos = getQuarterNoteValue(currentRefNode.startOffset); // startOffset always specified in sixteenths? Or should it follow global res? Probably the latter... 23 | } 24 | 25 | // todo: current precision level (1/16, 1/64, etc) should probably come into play here... 26 | currentPos = _.round(currentPos + getQuarterNoteValue(item.preRest), 3); 27 | let interval = getQuarterNoteValue(item.interval); 28 | for (let pitch of item.pitches) { 29 | flattenedResults.push(new NoteEvent(currentPos, interval, pitch, item.velocity || Constants.DEFAULT_VELOCITY)); 30 | } 31 | currentPos += interval + getQuarterNoteValue(item.postRest); 32 | } 33 | flattenedResultsSets.push({notes:_.clone(flattenedResults), loopLength: clipRootNodes[i].contextRef.patternLength / 4} as IClip); 34 | flattenedResults = []; 35 | } 36 | return flattenedResultsSets; 37 | } 38 | 39 | function getQuarterNoteValue(value: number, divisor: number = 4): number { 40 | return _.round(value / divisor, 3); 41 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/Constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | public static get DEFAULT_PATTERN_LENGTH() { return 64; }; 3 | public static get DEFAULT_VELOCITY() { return 127; } 4 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/IRecurseValue.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {ValueType} from "../../interpreter/ValueType"; 3 | 4 | export interface IRecurseValue { 5 | value: number; 6 | valueType: ValueType; 7 | refToTargetNode?: INode; 8 | additionalValues?: {value: number, valueType: ValueType}[]; 9 | } 10 | -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/Note.ts: -------------------------------------------------------------------------------- 1 | import {Helpers} from "../util/Helpers"; 2 | 3 | export class Note { 4 | public static noteNames: Array = ['C-2', 'C#-2', 'D-2', 'D#-2', 'E-2', 'F-2', 'F#-2', 'G-2', 'G#-2', 'A-2', 'A#-2', 'B-2', 'C-1', 'C#-1', 'D-1', 'D#-1', 'E-1', 'F-1', 'F#-1', 'G-1', 'G#-1', 'A-1', 'A#-1', 'B-1', 'C0', 'C#0', 'D0', 'D#0', 'E0', 'F0', 'F#0', 'G0', 'G#0', 'A0', 'A#0', 'B0', 'C1', 'C#1', 'D1', 'D#1', 'E1', 'F1', 'F#1', 'G1', 'G#1', 'A1', 'A#1', 'B1', 'C2', 'C#2', 'D2', 'D#2', 'E2', 'F2', 'F#2', 'G2', 'G#2', 'A2', 'A#2', 'B2', 'C3', 'C#3', 'D3', 'D#3', 'E3', 'F3', 'F#3', 'G3', 'G#3', 'A3', 'A#3', 'B3', 'C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4', 'C5', 'C#5', 'D5', 'D#5', 'E5', 'F5', 'F#5', 'G5', 'G#5', 'A5', 'A#5', 'B5', 'C6', 'C#6', 'D6', 'D#6', 'E6', 'F6', 'F#6', 'G6', 'G#6', 'A6', 'A#6', 'B6', 'C7', 'C#7', 'D7', 'D#7', 'E7', 'F7', 'F#7', 'G7', 'G#7', 'A7', 'A#7', 'B7', 'C8', 'C#8', 'D8', 'D#8', 'E8', 'F8', 'F#8', 'G8']; 5 | public static pureNoteNames: Array = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; 6 | private static default = 48; 7 | public value = Note.default; 8 | 9 | constructor(value: number = Note.default) { 10 | this.value = Helpers.ensureRange(value, 0, 127); 11 | } 12 | 13 | setByName(noteName: string): void { 14 | var noteIndex = Note.noteNames.indexOf(noteName); 15 | this.setByNumber(noteIndex >= 0 ? noteIndex : Note.default); 16 | } 17 | 18 | setByNumber(noteNumber: number): void { 19 | this.value = Helpers.ensureRange(noteNumber, 0, 127); 20 | } 21 | 22 | getNoteName(): string { 23 | return Note.noteNames[this.value]; 24 | } 25 | 26 | getNoteNumber(): number { 27 | return this.value; 28 | } 29 | 30 | // returns 0-11 based on c,c#,...,b 31 | static indexFromPureNoteName(pureNoteName: string): number { 32 | return Note.pureNoteNames.indexOf(pureNoteName.toUpperCase()); 33 | } 34 | 35 | static pitchFromNoteName(noteName: string): number { 36 | var noteIndex: number = Note.noteNames.indexOf(noteName.toUpperCase()); 37 | return noteIndex >= 0 ? noteIndex : Note.default; 38 | } 39 | 40 | static getInstanceByNoteName(noteName: string): Note { 41 | var note: Note = new Note(); 42 | note.setByName(noteName); 43 | return note; 44 | } 45 | 46 | static getInstanceByNoteNumber(noteNumber: number): Note { 47 | return new Note(noteNumber); 48 | } 49 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/NoteEvent.ts: -------------------------------------------------------------------------------- 1 | export class NoteEvent { 2 | public start: number; 3 | public duration: number; 4 | public pitch: number; 5 | public velocity: number; 6 | 7 | constructor(start: number, duration: number, pitch: number, velocity: number) { 8 | this.start = start; 9 | this.duration = duration; 10 | this.pitch = pitch; 11 | this.velocity = velocity; 12 | } 13 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/RecurseObject.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | 3 | export class RecurseObject { 4 | public interval: number; 5 | public pitches: number[]; 6 | public order: number; 7 | public velocity: number; // might be useful to have multiple velos for e.g. chords 8 | public duration: number; 9 | public postRest: number; // to simplify subsequent processing of events (i.e. in selections), we merge ROs having isRest=true with nearest left neighbour 10 | public preRest: number; 11 | public refToGenNode: INode; 12 | public level: number; // todo: to be implemented - needed for e.g. selecting on levels, i.e. select level 2 for nested intervals only. 13 | 14 | constructor(interval: number = 0, pitches: number[] = [], order: number = -1, velocity: number = void 0, duration: number = 0.25, preRest: number = 0, postRest: number = 0) { 15 | this.interval = interval; 16 | this.pitches = pitches; 17 | this.order = order; 18 | this.velocity = velocity; 19 | this.duration = duration; 20 | this.preRest = preRest; 21 | this.postRest = postRest; 22 | } 23 | 24 | public static getInstanceFromIntervalWithRests(interval: number, preRest: number, postRest: number): RecurseObject { 25 | return new RecurseObject(interval, [], -1, void 0, 0.25, preRest, postRest); 26 | } 27 | 28 | public static getInstanceFromInterval(interval: number): RecurseObject { 29 | return new RecurseObject(interval); 30 | } 31 | 32 | //static getInstanceFromNoteArray() 33 | 34 | /* 35 | public noteEvents: Array = []; 36 | private cursorPosition: NoteEvent; 37 | private static noteEventsStringTemplate = _.template('<% _.forEach(noteEvents, function(noteEvent) { %><%- noteEvent %>\n<% }); %>'); 38 | 39 | constructor() { 40 | this.cursorPosition = NoteEvent.getDefaultInstance(); 41 | } 42 | 43 | static createNew(): RecurseObject { 44 | return new RecurseObject(); 45 | } 46 | 47 | 48 | private addEvent(event: NoteEvent): void { 49 | this.noteEvents.push(this.cursorPosition.clone()); 50 | this.cursorPosition.add(event); 51 | } 52 | 53 | addInterval(interval: number): RecurseObject { 54 | var noteEvent = NoteEvent.getDefaultInstance(); 55 | noteEvent.addSixteenths(interval); 56 | this.addEvent(noteEvent); 57 | return this; 58 | } 59 | 60 | toString():string { 61 | return RecurseObject.noteEventsStringTemplate(this); 62 | } 63 | */ 64 | 65 | /* private cloneLatestNoteEventOrDefault():NoteEvent { 66 | return this.noteEvents.length > 0 ? this.noteEvents[this.noteEvents.length - 1].clone() : recurse.core.types.NoteEvent.getDefaultInstance(); 67 | } 68 | rep(interval: number, numRepeats: number): RecurseObject { 69 | var noteInterval: NoteInterval = NoteInterval.getDefaultInstance(), 70 | i: number, 71 | noteEvent: NoteEvent; 72 | 73 | noteInterval.addSixteenths(interval); 74 | noteInterval.divideBy(numRepeats); 75 | 76 | for (i = 0; i < numRepeats; i++) { 77 | noteEvent = NoteEvent.getDefaultInstance(); 78 | noteEvent.addFraction(noteInterval.getFractionalTime()); 79 | this.addEvent(noteEvent); 80 | } 81 | return this; 82 | } 83 | 84 | nestedItvl(interval: number, ...nestedIntervals: Array): RecurseObject { 85 | var noteInterval: NoteInterval = NoteInterval.getDefaultInstance(), 86 | noteEvent: NoteEvent; 87 | 88 | if (nestedIntervals.length < 2) { 89 | return this; 90 | } 91 | 92 | noteInterval.addSixteenths(interval); 93 | noteInterval.divideBy(_.sum(nestedIntervals)); 94 | _.forEach(nestedIntervals, (nestedInterval: number) => { 95 | noteEvent = NoteEvent.getDefaultInstance(); 96 | noteEvent.addFraction(noteInterval.getFractionalTime() * nestedInterval); 97 | this.addEvent(noteEvent); 98 | }); 99 | 100 | return this; 101 | } 102 | 103 | // some proof-of-concept for how generation might work with recursive structures as well 104 | 105 | rm(...intervals: Array) { 106 | var flattened = _.flatten(intervals, true); 107 | console.log(flattened); 108 | } 109 | 110 | static interval1(interval: number):Array { 111 | return [1,2,3]; 112 | } 113 | 114 | static interval2(interval: number):Array { 115 | return [4,[2,2],5,6,8,[2,4,2]]; 116 | }*/ 117 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/RecurseResult.ts: -------------------------------------------------------------------------------- 1 | import {RecurseStatus} from "./RecurseStatus"; 2 | 3 | export class RecurseResult { 4 | public error: string; 5 | public status: RecurseStatus; 6 | public result: T; 7 | 8 | constructor(status = RecurseStatus.OK, error = '') { 9 | this.status = status; 10 | this.error = error; 11 | } 12 | 13 | public isOk(): boolean { 14 | return (this.status === RecurseStatus.OK); 15 | } 16 | 17 | public setError(msg: string): RecurseResult { 18 | this.error = msg; 19 | this.status = RecurseStatus.ERROR; 20 | return this; 21 | } 22 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/RecurseStatus.ts: -------------------------------------------------------------------------------- 1 | export enum RecurseStatus { 2 | OK, 3 | ERROR, 4 | WARNING 5 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/type/Scale.ts: -------------------------------------------------------------------------------- 1 | import {Note} from "./Note"; 2 | import {Helpers} from "../util/Helpers"; 3 | 4 | export class Scale { 5 | public indexes: Array; 6 | public name: string; 7 | public tonicOffset: number = 0; 8 | 9 | constructor(name: string = 'chromatic') { 10 | this.indexes = Scale.getScaleIndexesFromName(name); 11 | this.name = name; 12 | // figure out key if applicable 13 | if (['major', 'minor'].indexOf(name.substr(name.length - 5).toLowerCase()) >= 0) { 14 | let tonic = name.substr(0, name.length - 5); 15 | // replace s with # to get proper note name 16 | if (tonic.length > 1 && tonic.substr(1,1) === 's') { 17 | tonic = tonic.substr(0,1) + '#'; 18 | } 19 | let tonicIndex = Note.indexFromPureNoteName(tonic); 20 | if (tonicIndex >= 0) { 21 | this.tonicOffset = tonicIndex; 22 | console.log('tonic was ' + tonic + ' and offset was ' + this.tonicOffset); 23 | } 24 | } 25 | } 26 | 27 | // todo complete 28 | public static get validScaleNames(): Array { 29 | return [ 30 | 'cmajor', 31 | 'csmajor', 32 | 'dmajor', 33 | 'dsmajor', 34 | 'emajor', 35 | 'fmajor', 36 | 'fsmajor', 37 | 'gmajor', 38 | 'gsmajor', 39 | 'amajor', 40 | 'asmajor', 41 | 'bmajor', 42 | 'cminor', 43 | 'csminor', 44 | 'dminor', 45 | 'dsminor', 46 | 'eminor', 47 | 'fminor', 48 | 'fsminor', 49 | 'gminor', 50 | 'gsminor', 51 | 'aminor', 52 | 'asminor', 53 | 'bminor', 54 | 'chromatic' 55 | ]; 56 | } 57 | 58 | public static get majorNoteIndexes(): Array { 59 | return [0,2,4,5,7,9,11]; 60 | } 61 | 62 | public static get minorNoteIndexes(): Array { 63 | return [0,2,3,5,7,8,10]; 64 | } 65 | 66 | public static get chromaticScaleIndexes(): Array { 67 | return [0,1,2,3,4,5,6,7,8,9,10,11]; 68 | } 69 | 70 | public scaleDegreeToPitchRelative(scaleDegree: number): number { 71 | var pitch: number = 0; 72 | if (scaleDegree === 0) { 73 | scaleDegree = 1; 74 | } 75 | var offset = Math.floor(((Math.abs(scaleDegree) - 1) / this.indexes.length)) * 12; 76 | if (scaleDegree > 0) { 77 | pitch += offset + this.indexes[(scaleDegree - 1) % this.indexes.length]; 78 | } else { 79 | let reverseIndex = Math.abs(scaleDegree) % this.indexes.length, 80 | index = 0; 81 | if (reverseIndex > 0) { 82 | index = this.indexes.length - reverseIndex; 83 | } 84 | pitch -= offset + 12 - this.indexes[index] 85 | } 86 | return pitch; 87 | } 88 | 89 | public scaleDegreeToPitch(scaleDegree: number, rootOct: number = 5): number { 90 | return (rootOct * 12) + this.tonicOffset + this.scaleDegreeToPitchRelative(scaleDegree); 91 | } 92 | 93 | public static getScaleFromName(scaleName: string): Scale { 94 | return new Scale(scaleName); 95 | } 96 | 97 | public static getScaleIndexesFromName(scale: string): Array { 98 | if (Scale.isScaleNameValid(scale)) { 99 | if (scale.substr(scale.length - 5).toLowerCase() === 'major') { 100 | return Scale.majorNoteIndexes; 101 | } else if (scale.substr(scale.length - 5).toLowerCase() === 'minor') { 102 | return Scale.minorNoteIndexes; 103 | } else { 104 | return Scale.chromaticScaleIndexes; 105 | } 106 | } 107 | } 108 | 109 | public static isScaleNameValid(scale: string): boolean { 110 | return (Scale.validScaleNames.indexOf(scale.toLowerCase()) >= 0); 111 | } 112 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/Helpers.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | import {INode} from "../../interpreter/INode"; 3 | import {Entity} from "../../interpreter/Entity"; 4 | 5 | export class Helpers { 6 | static ensureRange(val: number, min: number, max: number): number { 7 | if (val < min) return min; 8 | if (val > max) return max; 9 | return val; 10 | } 11 | 12 | static timeStringToSixteenths(timeString: string) { 13 | var timeValues: Array = _.words(timeString, '.'), 14 | sixteenths: number, 15 | timeValuesAsInt: Array; 16 | timeValuesAsInt = _.map(timeValues, function (val) { 17 | return parseInt(val, 10); 18 | }); 19 | if (timeValues.length === 3 && _.every(timeValues, _.isNumber)) { 20 | sixteenths = timeValuesAsInt[0] * 16 + timeValuesAsInt[1] * 4 + timeValuesAsInt[2]; 21 | } 22 | return sixteenths; 23 | } 24 | 25 | public static traverseNodes(nodes: INode[], callback: (node: INode, level: number, i: number, path: number[]) => void, level: number = 0, index: number = 0, path: number[] = []): void { 26 | level++; 27 | for (let i = 0; i < nodes.length; i++) { 28 | let newPath: number[] = _.clone(path); 29 | newPath.push(i); 30 | //console.log('Invoking callback on node ' + Entity[nodes[i].type] + ' level ' + level + ' index ' + i + ' path: ', newPath); 31 | callback(nodes[i], level, i, newPath); 32 | Helpers.traverseNodes(nodes[i].children, callback, level, i, newPath); 33 | } 34 | } 35 | 36 | public static getIndexFromParent(node: INode): number { 37 | let parent: INode = node.parent; 38 | for (let i = 0; i < parent.children.length; i++) { 39 | if (parent.children[i] === node) { 40 | return i; 41 | } 42 | } 43 | return -1; 44 | } 45 | 46 | public static getSiblingWithType(node: INode, type: Entity): INode { 47 | let parent: INode = node.parent; 48 | for (let i = 0; i < parent.children.length; i++) { 49 | if (parent.children[i].type === type) { 50 | return parent.children[i]; 51 | } 52 | } 53 | return null; 54 | } 55 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/convertNoteListToRecurseCode.spec.ts: -------------------------------------------------------------------------------- 1 | import tape = require('tape'); 2 | 3 | import {convertNoteListToRecurseCode} from "./convertNoteListToRecurseCode"; 4 | 5 | tape('convert overlapping notes to separate recurse declarations', (test) => { 6 | // note that this input is not realistic since pitch is the same for the overlapping notes, but it nonetheless tests the relevant code correctly 7 | var testInput: {start: number, duration: number, pitch: number, velocity: number, muted: number}[] = [ 8 | { 9 | start: 1, 10 | duration: 1, 11 | pitch: 60, 12 | velocity: 127, 13 | muted: 0 14 | }, 15 | { 16 | start: 2, 17 | duration: 4, 18 | pitch: 60, 19 | velocity: 127, 20 | muted: 0 21 | }, 22 | { 23 | start: 3, 24 | duration: 1, 25 | pitch: 60, 26 | velocity: 127, 27 | muted: 0 28 | }, 29 | { 30 | start: 6, 31 | duration: 1, 32 | pitch: 60, 33 | velocity: 127, 34 | muted: 0 35 | } 36 | ], 37 | expectedOutput: string = "length(32) rm(_4 4 16 4 _4) ns(c3);\nlength(32) rm(_12 4 _16) ns(c3)", 38 | result: string = convertNoteListToRecurseCode(testInput, 8); 39 | 40 | test.equal(result, expectedOutput, `Expected test input to be transformed to ${expectedOutput}. Actual output: ${result}`); 41 | test.end(); 42 | }); 43 | 44 | tape('convert notes on different pitches to separate declarations when using drum mode', (test) => { 45 | var testInput: {start: number, duration: number, pitch: number, velocity: number, muted: number}[] = [ 46 | { 47 | start: 1, 48 | duration: 1, 49 | pitch: 60, 50 | velocity: 127, 51 | muted: 0 52 | }, 53 | { 54 | start: 2, 55 | duration: 4, 56 | pitch: 62, 57 | velocity: 127, 58 | muted: 0 59 | }, 60 | { 61 | start: 3, 62 | duration: 1, 63 | pitch: 64, 64 | velocity: 127, 65 | muted: 0 66 | }, 67 | { 68 | start: 4, 69 | duration: 1, 70 | pitch: 60, 71 | velocity: 127, 72 | muted: 0 73 | }, 74 | { 75 | start: 6, 76 | duration: 1, 77 | pitch: 61, 78 | velocity: 127, 79 | muted: 0 80 | } 81 | ], 82 | expectedOutput: string = "length(32) rm(_4 4 _8 4 _12) ns(c3);\nlength(32) rm(_8 16 _8) ns(d3);\nlength(32) rm(_12 4 _16) ns(e3);\nlength(32) rm(_24 4 _4) ns(c#3)", 83 | result: string = convertNoteListToRecurseCode(testInput, 8, 1 /* DRUM mode */); 84 | 85 | test.equal(result, expectedOutput, `Expected test input to be transformed to ${expectedOutput}. Actual output: ${result}`); 86 | test.end(); 87 | }); 88 | 89 | tape('convert straight-forward sequence of notes to one recurse declaration', (test) => { 90 | var testInput: {start: number, duration: number, pitch: number, velocity: number, muted: number}[] = [ 91 | { 92 | start: .5, 93 | duration: .25, 94 | pitch: 62, 95 | velocity: 127, 96 | muted: 0 97 | }, 98 | { 99 | start: 1, 100 | duration:.25, 101 | pitch: 64, 102 | velocity: 117, 103 | muted: 0 104 | }, 105 | { 106 | start: 0, 107 | duration: .25, 108 | pitch: 65, 109 | velocity: 127, 110 | muted: 0 111 | }, 112 | { 113 | start: 2.75, 114 | duration:.25, 115 | pitch: 65, 116 | velocity: 127, 117 | muted: 0 118 | } 119 | ], 120 | expectedOutput: string = "length(16) rm(1 _1 1 _1 1 _6 1 _4) ns(f3 d3 e3 f3) vel(127 127 117 127)", 121 | result: string = convertNoteListToRecurseCode(testInput, 4); 122 | 123 | test.equal(result, expectedOutput, `Expected test input to be transformed to ${expectedOutput}. Actual output: ${result}`); 124 | test.end(); 125 | }); 126 | -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/convertNoteListToRecurseCode.ts: -------------------------------------------------------------------------------- 1 | // Intended for use in the recurse connector M4L plugin. Implemented here for easy testing via Tape. 2 | 3 | import {Constants} from "../type/Constants"; 4 | import {Note} from "../type/Note"; 5 | 6 | interface INote { 7 | start: number; 8 | duration: number; 9 | pitch: number; 10 | velocity: number; 11 | muted: number; 12 | } 13 | 14 | interface IResult { 15 | intervals: number[]; 16 | notes: number[]; 17 | velocities: number[]; 18 | } 19 | 20 | const enum ConversionStrategy { 21 | compact, 22 | drum 23 | } 24 | 25 | // Note: This code could be split up a bit more, but I'm keeping everything in one function here since it's targeted as a single function in a M4L patch 26 | export function convertNoteListToRecurseCode(noteList: INote[], clipLength: number = 16, strategy: ConversionStrategy = ConversionStrategy.compact): string { 27 | var results: IResult[] = [{intervals: [], notes: [], velocities: []}], 28 | currentResultIndex: number = 0, 29 | currentStartPos: number, 30 | currentEndPos: number, 31 | backlog: INote[], 32 | output: string = "", 33 | velocitiesNeeded = false; 34 | 35 | noteList.sort((a: INote, b: INote): number => { 36 | if (a.start > b.start) { 37 | return 1; 38 | } 39 | if (a.start < b.start) { 40 | return -1; 41 | } 42 | return 0; 43 | }); 44 | 45 | do { 46 | backlog = []; 47 | currentEndPos = currentStartPos = 0; 48 | let currentPitch = noteList[0].pitch; 49 | 50 | for (let note of noteList) { 51 | if (!results[currentResultIndex]) { 52 | results[currentResultIndex] = {intervals: [], notes: [], velocities: []}; 53 | } 54 | let currentResults = results[currentResultIndex]; 55 | if (currentEndPos <= note.start && (note.pitch === currentPitch || strategy === ConversionStrategy.compact)) { 56 | if (currentEndPos < note.start) { 57 | currentResults.intervals.push(0 - ((note.start - currentEndPos) * 4)); // negative signifies blank intervals 58 | } 59 | currentResults.intervals.push(note.duration * 4); 60 | currentResults.notes.push(note.pitch); 61 | currentResults.velocities.push(note.velocity); 62 | if (note.velocity !== 127 && !velocitiesNeeded) { 63 | velocitiesNeeded = true; 64 | } 65 | currentStartPos = note.start; 66 | currentEndPos = note.start + note.duration; 67 | } else { 68 | backlog.push(note); 69 | } 70 | } 71 | noteList = backlog; 72 | currentResultIndex++; 73 | } while (backlog.length !== 0); 74 | 75 | // returns 1-element array with value if all values are equal, otherwise the unmodified array 76 | var compactArray = (elements: any[]): any[] => { 77 | if (elements.length < 2) { 78 | return elements; 79 | } 80 | for (let i = 1; i < elements.length; i++) { 81 | if (elements[i - 1] !== elements[i]) { 82 | return elements; 83 | } 84 | } 85 | return [elements[0]]; 86 | }; 87 | 88 | for (let r: number = 0; r < results.length; r++) { 89 | let result = results[r]; 90 | result.intervals = compactArray(result.intervals); 91 | result.velocities = compactArray(result.velocities); 92 | result.notes = compactArray(result.notes); 93 | 94 | let totalLength = clipLength * 4; 95 | 96 | if (totalLength !== Constants.DEFAULT_PATTERN_LENGTH) { 97 | output += `length(${totalLength}) `; 98 | } 99 | 100 | let rmOutput = ""; 101 | for (let i: number = 0; i < result.intervals.length; i++) { 102 | let interval = result.intervals[i]; 103 | if (interval > 0) { 104 | rmOutput += interval; 105 | } else { 106 | interval = Math.abs(interval); 107 | rmOutput += `_${interval}`; 108 | } 109 | totalLength -= interval; 110 | if (i < result.intervals.length - 1) { 111 | rmOutput += " "; 112 | } 113 | } 114 | if (totalLength > 0) { 115 | rmOutput += ` _${totalLength}`; 116 | } 117 | output += `rm(${rmOutput}) `; 118 | 119 | let noteOutput = ""; 120 | let noteValueStatic: number = -1; // if >= 0, note value is unchanged throughout the sequence and can be included only once in output. Otherwise note value changes and should be included as is. 121 | for (let i: number = 0; i < result.notes.length; i++) { 122 | let note: number = result.notes[i]; 123 | if (noteValueStatic !== note) { 124 | if (noteValueStatic === -1) { 125 | noteValueStatic = note; 126 | } else { 127 | noteValueStatic = -2; 128 | } 129 | } 130 | noteOutput += Note.noteNames[note].toLowerCase() + (i < result.notes.length - 1 ? " " : ""); 131 | } 132 | if (noteValueStatic >= 0) { 133 | noteOutput = Note.noteNames[noteValueStatic].toLowerCase(); 134 | } 135 | output += `ns(${noteOutput})`; 136 | 137 | if (velocitiesNeeded) { 138 | let velOutput = ""; 139 | for (let i: number = 0; i < result.velocities.length; i++) { 140 | let velocity: number = result.velocities[i]; 141 | velOutput += `${velocity}${(i < result.velocities.length - 1 ? " " : "")}`; 142 | } 143 | output += ` vel(${velOutput})`; 144 | } 145 | 146 | output += `${(r < results.length - 1 ? ";\n" : "")}`; 147 | } 148 | 149 | return output; 150 | } 151 | -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/createLookupTable.ts: -------------------------------------------------------------------------------- 1 | import {TokenType} from "../../interpreter/TokenType"; 2 | 3 | export function createLookupTable(source: Array, lookupKey: string, lookupValue: string): Array { 4 | var expectTable: Array = []; 5 | for (var i = 0; i < source.length; i++) { 6 | expectTable[source[i][lookupKey]] = source[i][lookupValue]; 7 | } 8 | return expectTable; 9 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/forEachPitch.ts: -------------------------------------------------------------------------------- 1 | import {IContext} from "../../function/IContext"; 2 | 3 | export function forEachPitch(context: IContext, process: {(index: number, pitch: number): number}): void { 4 | var ix: number, 5 | pix: number; 6 | 7 | for (ix = 0; ix < context.results.length; ix++) { 8 | for (pix = 0; pix < context.results[ix].pitches.length; pix++) { 9 | context.results[ix].pitches[pix] = process(ix, context.results[ix].pitches[pix]); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/forEachSelectedPitch.ts: -------------------------------------------------------------------------------- 1 | import {IContext} from "../../function/IContext"; 2 | 3 | export function forEachSelectedPitch(context: IContext, process: {(index: number, pitch: number): number}): void { 4 | if (!context.selectionActive) { 5 | return; 6 | } 7 | 8 | for (let ix = 0; ix < context.selectedIndexes.length; ix++) { 9 | for (let pix = 0; pix < context.results[context.selectedIndexes[ix]].pitches.length; pix++) { 10 | context.results[context.selectedIndexes[ix]].pitches[pix] = process(ix, context.results[context.selectedIndexes[ix]].pitches[pix]); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /recurse-app/app/src/core/util/nodewrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name recurse.nodeMock 6 | * @description 7 | * # nodeWrapper 8 | * Wrapper service for NodeJS: Returns actual NodeJS require function if available, otherwise 9 | * a specialized mock is returned. This is useful when developing outside the NodeJS environment. 10 | */ 11 | angular.module('recurse.core.utils') 12 | .factory('nodeWrapper', function nodeWrapper($window) { 13 | if (angular.isUndefined($window.require)) { 14 | // return mock-service if NodeJS is not available 15 | return { 16 | require: function(service) { 17 | switch (service) { 18 | case 'dgram': 19 | return { 20 | createSocket: function() { 21 | return { 22 | send: function() {} 23 | }; 24 | } 25 | }; 26 | case 'osc-min': 27 | return { 28 | toBuffer: function() { 29 | return []; 30 | }, 31 | send: function() {} 32 | }; 33 | } 34 | } 35 | }; 36 | } 37 | return { 38 | require: function(service) { 39 | console.log('called upon to deliver', service); 40 | return $window.require(service); 41 | } 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /recurse-app/app/src/function/IContext.ts: -------------------------------------------------------------------------------- 1 | import {RecurseObject} from "../core/type/RecurseObject"; 2 | import {Scale} from "../core/type/Scale"; 3 | 4 | export interface IContext { 5 | endPosition: number; 6 | loopFactor: number; // number of times.Can also be <1, but need to handle floats first 7 | patternLength: number; 8 | prePhase: boolean; 9 | results: Array; 10 | rootOct: number; 11 | scale: Scale; 12 | selectedIndexes: Array; 13 | selectionActive: boolean; // kan evt droppe selectionActive og heller sjekke selectedIndexes direkte, i.e. tømme denne når den ikke er aktiv, den må uansett reinstansieres ved hver select 14 | startOffset: number; 15 | startPosition: number; 16 | createNewClip: boolean; // if true, generated notes are put into a new buffer 17 | //idSequenceTicker: number; // meant as an internal sequence of id's which can be used for events that need to be tagged and are related, such as 18 | //currentPosition: number; // maybe start using this again - this way morphs could be done in subnode given that they have all context info (length, loop, curpos). However, it would still need to know total length of sequence, which it doesn't have access to yet... 19 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/SyntaxTree.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import {Constants} from "../core/type/Constants"; 4 | import {ISyntaxTree} from "../interpreter/ISyntaxTree"; 5 | import {INode} from "../interpreter/INode"; 6 | import {RecurseObject} from "../core/type/RecurseObject"; 7 | import {IContext} from "./IContext"; 8 | import {Scale} from "../core/type/Scale"; 9 | 10 | export class SyntaxTree implements ISyntaxTree { 11 | public variables: any = {}; // todo: refine 12 | public rootNodes: Array = []; 13 | 14 | public generate(): RecurseObject[][] { 15 | var contexts: IContext[] = []; 16 | for (let rootNode of this.rootNodes) { 17 | contexts.push({ 18 | results: [], 19 | selectedIndexes: [], 20 | selectionActive: false, 21 | patternLength: Constants.DEFAULT_PATTERN_LENGTH, 22 | startOffset: 0, 23 | loopFactor: 1, 24 | startPosition: 0, 25 | endPosition: 0, 26 | prePhase: false, 27 | scale: new Scale(), 28 | rootOct: 5, 29 | createNewClip: false 30 | }); 31 | rootNode.generate(contexts[contexts.length - 1]); 32 | } 33 | 34 | var results: Array> = []; 35 | var resultBuffer: Array = []; 36 | for (let context of contexts) { 37 | if (context.createNewClip) { 38 | results.push(_.clone(resultBuffer)); 39 | resultBuffer = []; 40 | } 41 | resultBuffer = resultBuffer.concat(context.results); 42 | } 43 | results.push(resultBuffer); 44 | return results; 45 | } 46 | 47 | public findVariable(name: string): INode { 48 | return this.variables[name] as INode; 49 | } 50 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/base/Nested.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 3 | import {Parser} from "../../interpreter/Parser"; 4 | import {Helpers} from "../../core/util/Helpers"; 5 | import {INode} from "../../interpreter/INode"; 6 | import {IContext} from "../IContext"; 7 | import {Entity} from "../../interpreter/Entity"; 8 | import {ValueType} from "../../interpreter/ValueType"; 9 | import {Value} from "./Value"; 10 | 11 | export class Nested implements INode { 12 | public type: Entity = Entity.NESTED; 13 | public children: INode[]; 14 | public parent: INode; 15 | public associatedNodes: INode[]; 16 | public head: INode; 17 | 18 | constructor(parent: INode = null, head: INode = null) { 19 | this.parent = parent; 20 | this.children = []; 21 | this.associatedNodes = []; 22 | this.head = head; 23 | } 24 | 25 | public generate(context: IContext): Array { 26 | // Is this node contained by an rm? 27 | // if so - create path to here, counting only structural nodes like nested 28 | var parentRm: INode = Parser.getParentNodeOfType(Entity.RM, this); 29 | if (parentRm !== null && !context.prePhase) { 30 | let path: number[] = [], 31 | node: INode = this; 32 | 33 | while (node.parent !== parentRm.parent) { 34 | let index = Helpers.getIndexFromParent(node); 35 | if (node.parent.type !== Entity.ALT && node.parent.type !== Entity.ALT_SHORTHAND && node.parent.type !== Entity.REPEAT && node.parent.type !== Entity.REPEAT_SHORTHAND) { 36 | //console.log('adding', index, 'node parent', Entity[node.parent.type]); 37 | path.unshift(index); 38 | } 39 | node = node.parent; 40 | } 41 | //console.log("path", path); 42 | // then try matching created path to the closest match in the associated notes and velocities (need special handling for things like alt and maybe also repeat) 43 | 44 | this.addAssociatedNodeOfType(Entity.NS, parentRm, path); 45 | this.addAssociatedNodeOfType(Entity.VEL, parentRm, path); 46 | } 47 | /* 48 | // todo: if we want to support FILL inside nested statements as well... Idea: if * is present in nested, do not rescale values but let head value control length instead of scaling 49 | // If we have a FILL element, figure out its size and set it 50 | let fillNodeIndex = _.findIndex(results, ['valueType', ValueType.FILL]); 51 | if (fillNodeIndex > -1) { 52 | let sum = _.sumBy(results, (result: IRecurseValue) => { 53 | return (_.isNaN(result.value) || result.value === null) ? 0 : result.value; 54 | }); 55 | results[fillNodeIndex].value = context.patternLength - sum; 56 | } 57 | */ 58 | 59 | var totalResults = []; 60 | 61 | if (this.head) { 62 | let headValues = this.head.generate(context); 63 | for (let headValue of headValues) { 64 | // Note: By doing generation for each occurence, we are saying that e.g. 1..3(4'5'6 3) would be equal to 1(4 3) 2(5 3) 3(6 3) 65 | // rather than 1(4 3) 2(4 3) 3(4 3). 66 | totalResults = totalResults.concat(this.generateValues(context, headValue.value)); 67 | } 68 | } else { 69 | totalResults = totalResults.concat(this.generateValues(context, -1)); 70 | } 71 | return totalResults; 72 | } 73 | 74 | private addAssociatedNodeOfType(entity: Entity, parent: INode, path: number[]): void { 75 | let targetNode: INode = Helpers.getSiblingWithType(parent, entity); 76 | 77 | if (!targetNode) { 78 | return; 79 | } 80 | 81 | for (let index of path) { 82 | if (targetNode.children.length > 0) { 83 | targetNode = targetNode.children[index % targetNode.children.length]; 84 | while ((targetNode.type === Entity.ALT || targetNode.type === Entity.ALT_SHORTHAND) && targetNode.children.length > 0) { 85 | let curIx = targetNode['alternationIndex'] || -1; 86 | targetNode = targetNode.children[(curIx + 1) % targetNode.children.length]; 87 | } 88 | } else { 89 | break; 90 | } 91 | } 92 | //console.log('Found target node with type', Entity[targetNode.type], 'and value', targetNode['value']); 93 | 94 | if (targetNode.type !== Entity.NESTED && path.length < 2) { 95 | return; 96 | } 97 | 98 | if (this.associatedNodes.indexOf(targetNode) < 0) { 99 | this.associatedNodes.push(targetNode); 100 | } 101 | } 102 | 103 | private generateValues(context: IContext, value: number) { 104 | var results = []; 105 | let doScaleValues: boolean = (value > 0); 106 | for (let child of this.children) { 107 | results = results.concat(child.generate(context)); 108 | } 109 | 110 | if (results.length > 0 && results[0].valueType === ValueType.INTERVAL || results[0].valueType === ValueType.REST) { 111 | if (doScaleValues) { 112 | let sum: number = 0; 113 | for (let result of results) { 114 | sum += result.value; 115 | } 116 | 117 | for (let i = 0; i < results.length; i++) { 118 | results[i].value = (results[i].value / sum) * value; 119 | } 120 | } 121 | 122 | for (let associatedNode of this.associatedNodes) { 123 | let associatedResults: IRecurseValue[] = [], 124 | i: number = 0; 125 | for (let result of results) { 126 | if (i >= associatedResults.length) { 127 | associatedResults = associatedNode.generate(context); 128 | i = 0; 129 | } 130 | if (!result.additionalValues) { 131 | result.additionalValues = []; 132 | } 133 | let additionalValueOfTypeAlreadyAdded = false; 134 | for (let additionalValue of result.additionalValues) { 135 | if (additionalValue.valueType === associatedResults[i].valueType) { 136 | additionalValueOfTypeAlreadyAdded = true; 137 | break; 138 | } 139 | } 140 | if (!additionalValueOfTypeAlreadyAdded) { 141 | result.additionalValues.push({value: associatedResults[i].value, valueType: associatedResults[i].valueType}); 142 | } 143 | i++; 144 | } 145 | if (associatedNode.reset) { 146 | associatedNode.reset(); 147 | } 148 | } 149 | } 150 | return results; 151 | } 152 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/base/Root.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class Root implements INode { 7 | public type: Entity = Entity.ROOT; 8 | public children: Array; 9 | public parent: INode; 10 | public newClip: boolean; 11 | public contextRef: IContext; 12 | 13 | constructor(newClip: boolean) { 14 | this.parent = null; 15 | this.children = []; 16 | this.newClip = newClip; 17 | } 18 | 19 | // Root serves as a clip/container construct. 20 | public generate(context: IContext): Array { 21 | //var offsetMultiplier: number = 0; 22 | context.createNewClip = this.newClip; 23 | this.contextRef = context; 24 | 25 | for (let child of this.children) { 26 | /* 27 | if (child.type === Entity.RM) { 28 | // if this is a RM, we want to increment our startOffset, starting at 0. In other words, successive rm calls produce output after each other, rather than being stacked on top of each other. 29 | // this should be done on RM itself instead, as this won't work (rms are often wrapped in chains) 30 | context.startOffset = context.patternLength * offsetMultiplier; 31 | offsetMultiplier++; 32 | } 33 | */ 34 | //context.currentPosition = 0; // tl;dr: yep, it should. Should currentPosition be kept track of inside rm instead? Isn't rm only node interested in this property, and should it be shared? 35 | //console.log('Generating for child', child.type); 36 | child.generate(context); 37 | } 38 | //console.log('context is now', context, context.results); 39 | return []; 40 | } 41 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/base/Value.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {ValueType} from "../../interpreter/ValueType"; 3 | import {IContext} from "../IContext"; 4 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 5 | import {Helpers} from "../../core/util/Helpers"; 6 | import {Entity} from "../../interpreter/Entity"; 7 | import {Parser} from "../../interpreter/Parser"; 8 | 9 | // todo: create Interval, Note, PitchOffset and so on.. Interval needs to take into account current res for instance, and 10 | // recalc to keep a consistent res of 1/16 for instance. This is important due to other params like patternLength using this reso. 11 | export class Value implements INode { 12 | public type: Entity = Entity.VALUE; 13 | public valueType: ValueType; 14 | public value: number; 15 | public children: Array; 16 | public parent: INode; 17 | // if more data needed - could use secondaryValue or similar member, usage could be dictated by valuetype 18 | 19 | constructor(value: number = -1, parent: INode = null, type: ValueType) { 20 | this.valueType = type; 21 | this.value = value; 22 | this.children = []; 23 | this.parent = parent; 24 | } 25 | 26 | public generate(context: IContext): Array { 27 | // todo: do any transforms needed here, based on type: INTERVAL, NOTE, REST, etc.. Interval needs to take into account current res for instance, and 28 | // recalc to keep a consistent res of 1/16 for instance. This is important due to other params like patternLength using this reso. 29 | var value = this.value, 30 | currentValueType = this.valueType; 31 | 32 | if (currentValueType === ValueType.VARIABLE) { 33 | currentValueType = Parser.resolveVariableValueType(this); 34 | } 35 | switch (currentValueType) { 36 | case ValueType.VELOCITY: 37 | value = Helpers.ensureRange(value, 0, 127); 38 | break; 39 | case ValueType.SCALE_DEGREE: 40 | value = context.scale.scaleDegreeToPitch(value, context.rootOct); 41 | currentValueType = ValueType.RAW_NOTE; 42 | break; 43 | } 44 | /* 45 | if (this.valueType === ValueType.SCALE_DEGREE) { 46 | console.log(`Turned ${value} into ${context.scale.scaleDegreeToPitch(this.value, context.rootOct)}`); 47 | value = context.scale.scaleDegreeToPitch(this.value, context.rootOct); 48 | } 49 | */ 50 | return [{value: value, valueType: currentValueType}]; 51 | } 52 | 53 | // have a lookup table of different valuetype handlers here, called by generate 54 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/base/Variable.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class Variable implements INode { 7 | public type: Entity = Entity.VARIABLE; 8 | public children: Array; 9 | public parent: INode; 10 | public name: string; 11 | 12 | constructor(name: string, contents: INode[]) { 13 | this.parent = null; 14 | this.children = contents; 15 | this.name = name; 16 | } 17 | 18 | public generate(context: IContext): IRecurseValue[] { 19 | var results: IRecurseValue[] = []; 20 | 21 | for (let child of this.children) { 22 | results = results.concat(child.generate(context)); 23 | } 24 | return results; 25 | } 26 | 27 | public generateVar(context: IContext, parent: INode): IRecurseValue[] { 28 | // Setting parent on generation feels slightly wrong, but since we want to use actual references to our variables 29 | // and not just duplicating everything every time something is referenced, we need to keep track of the ancestor 30 | // tree somehow, otherwise the type system will be confused. 31 | this.parent = parent; 32 | var results: IRecurseValue[] = this.generate(context); 33 | this.parent = null; 34 | return results; 35 | } 36 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/base/VariableReference.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class VariableReference implements INode { 7 | public type: Entity = Entity.VARIABLE_REFERENCE; 8 | public children: Array = []; 9 | public parent: INode; 10 | public variableRef: INode; 11 | public name: string; 12 | 13 | constructor(parent: INode, name: string, variableRef: INode) { 14 | this.parent = parent; 15 | this.name = name; 16 | this.variableRef = variableRef; 17 | } 18 | 19 | public generate(context: IContext): Array { 20 | return this.variableRef.generateVar(context, this.parent); 21 | } 22 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/generator/NoteSet.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {IContext} from "../IContext"; 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | import {ValueType} from "../../interpreter/ValueType"; 6 | 7 | export class NoteSet implements INode { 8 | public type: Entity = Entity.NS; 9 | public children: Array = []; 10 | public parent: INode; 11 | 12 | constructor(parent: INode = null, children: Array = []) { 13 | this.parent = parent; 14 | this.children = children; 15 | } 16 | 17 | public generate(context: IContext): Array { 18 | var results: Array = []; 19 | for (let child of this.children) { 20 | results = results.concat(child.generate(context)); 21 | } 22 | let i = 0; 23 | for (let result of context.results) { 24 | if (result.pitches.length === 0) { 25 | result.pitches.push(results[i % results.length].value); 26 | } 27 | i++; 28 | } 29 | return []; 30 | } 31 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/generator/RhythmicMotive.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 5 | import {ValueType} from "../../interpreter/ValueType"; 6 | import {RecurseObject} from "../../core/type/RecurseObject"; 7 | import {Entity} from "../../interpreter/Entity"; 8 | 9 | export class RhythmicMotive implements INode { 10 | public type: Entity = Entity.RM; 11 | public children: Array = []; 12 | public parent: INode; 13 | public currentPosition: number = 0; 14 | public startOffset: number; 15 | // todo: impl these (if needed) 16 | public reference: INode; // holds reference to another node in the case of statements like stack, +rm, etc. 17 | public clonedContext: IContext; // the referenced node will have a cloned copy of it's context when generation occurred, which can be used by the node referring to it 18 | 19 | constructor(parent: INode = null, children: Array = []) { 20 | this.parent = parent; 21 | this.children = children; 22 | } 23 | 24 | private fillBuffer(context: IContext): Array { 25 | var results: Array = []; 26 | 27 | for (let child of this.children) { 28 | results = results.concat(child.generate(context)); 29 | } 30 | return results; 31 | } 32 | 33 | private resetChildren(child: INode) { 34 | if (child.reset) { 35 | child.reset(); 36 | } 37 | for (let c of child.children) { 38 | this.resetChildren(c); 39 | } 40 | }; 41 | 42 | public generate(context: IContext): Array { 43 | var results: Array = []; 44 | this.startOffset = context.startOffset; 45 | 46 | /* Helpers.traverseNodes(this.children, (node: INode, level: number, index: number, path: number[]) => { 47 | if (node.type === Entity.NESTED) { 48 | console.log('path to nested is', path); 49 | } 50 | });*/ 51 | 52 | // We start by doing a pre-run through all children, prior to actual generation 53 | // This is so that certain operators like Interpolate need to know entire length of sequence before it can generate correct output 54 | // Operators that are not deterministic (i.e. random), cache their results in this phase so that it's the same when actual generation is triggered 55 | // During this phase we also count the number of events the sequence contains, so that actual generation can proceed with less hassle 56 | 57 | // first go through all children and call reset() if available 58 | for (let child of this.children) { 59 | this.resetChildren(child); 60 | } 61 | 62 | context.prePhase = true; 63 | results = this.fillBuffer(context); 64 | 65 | if (results.length === 0) { 66 | return; 67 | } 68 | 69 | // If we have a FILL element, figure out its size and set it 70 | let fillNodeIndex = _.findIndex(results, ['valueType', ValueType.FILL]), 71 | fillNodeValue = 0; 72 | if (fillNodeIndex > -1) { 73 | let sum = _.sumBy(results, (result: IRecurseValue) => { 74 | return (_.isNaN(result.value) || result.value === null) ? 0 : result.value; 75 | }); 76 | fillNodeValue = context.patternLength - sum; 77 | } 78 | 79 | // do a full pre-pass through all children, updating numEvents as we go 80 | let i: number = 0, 81 | numEvents: number = 0; 82 | do { 83 | if (i >= results.length) { 84 | results = this.fillBuffer(context); 85 | i = 0; 86 | } 87 | let result: IRecurseValue = results[i]; 88 | if (result.valueType === ValueType.FILL) { 89 | this.currentPosition += fillNodeValue; 90 | } else { 91 | this.currentPosition += result.value; 92 | } 93 | i++; 94 | numEvents++; 95 | } while (this.currentPosition < context.patternLength); 96 | 97 | // do actual generation-pass through all children 98 | this.currentPosition = 0; 99 | context.prePhase = false; 100 | results = this.fillBuffer(context); 101 | let preRest: number = 0; 102 | for (let i = 0; i < numEvents; i++) { 103 | if (i >= results.length) { 104 | results = results.concat(this.fillBuffer(context)); 105 | } 106 | 107 | let result: IRecurseValue = results[i]; 108 | if (result.valueType !== ValueType.REST && result.valueType !== ValueType.FILL) { 109 | this.currentPosition += preRest; 110 | // cut event if it bleeds outside pattern 111 | if (this.currentPosition < context.patternLength && this.currentPosition + result.value > context.patternLength) { 112 | result.value = context.patternLength - this.currentPosition; 113 | } 114 | this.currentPosition += result.value; 115 | let ro = RecurseObject.getInstanceFromIntervalWithRests(result.value, preRest, 0); 116 | if (result.additionalValues && result.additionalValues.length > 0) { 117 | for (let additionalValue of result.additionalValues) { 118 | if (additionalValue.valueType === ValueType.NOTE || additionalValue.valueType == ValueType.RAW_NOTE) { 119 | if (ro.pitches.length === 0) { 120 | ro.pitches.push(additionalValue.value); 121 | } 122 | } 123 | if (additionalValue.valueType === ValueType.VELOCITY) { 124 | if (!ro.velocity) { 125 | ro.velocity = additionalValue.value; 126 | } 127 | } 128 | } 129 | } 130 | /* 131 | if (result.refToTargetNode) { 132 | let noteResults = result.refToTargetNode.generate() 133 | ro.pitches 134 | } 135 | */ 136 | ro.refToGenNode = this; 137 | context.results.push(ro); 138 | if (preRest > 0) { 139 | preRest = 0; 140 | } 141 | } else { 142 | let restValue = result.valueType === ValueType.FILL ? fillNodeValue : result.value; 143 | if (context.results.length === 0 || context.results[context.results.length - 1].postRest > 0) { // if this is element #0, or previously created RO has postRest already set, set preRest for next RO added 144 | preRest = restValue; 145 | } else { 146 | context.results[context.results.length - 1].postRest = restValue; // ...otherwise set postRest for previously added RO 147 | this.currentPosition += restValue; 148 | } 149 | } 150 | } 151 | context.startOffset += context.patternLength; // startOffset is kept track of in each rm 152 | 153 | return []; 154 | } 155 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/generator/VelocitySet.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {IContext} from "../IContext"; 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class VelocitySet implements INode { 7 | public type: Entity = Entity.VEL; 8 | public children: INode[] = []; 9 | public parent: INode; 10 | 11 | constructor(parent: INode = null, children: INode[] = []) { 12 | this.parent = parent; 13 | this.children = children; 14 | } 15 | 16 | public generate(context: IContext): IRecurseValue[] { 17 | var results: IRecurseValue[] = []; 18 | for (let child of this.children) { 19 | results = results.concat(child.generate(context)); 20 | } 21 | let i = 0; 22 | for (let result of context.results) { 23 | if (!result.velocity) { 24 | result.velocity = results[i % results.length].value; 25 | } 26 | i++; 27 | } 28 | return []; 29 | } 30 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/modifier/Pitch.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 5 | import {forEachSelectedPitch} from "../../core/util/forEachSelectedPitch"; 6 | import {forEachPitch} from "../../core/util/forEachPitch"; 7 | import {Entity} from "../../interpreter/Entity"; 8 | 9 | export class Pitch implements INode { 10 | public type: Entity = Entity.PITCH; 11 | public children: Array = []; 12 | public parent: INode; 13 | private plusMode: boolean; 14 | 15 | constructor(parent: INode = null, children: Array = [], plusMode = false) { 16 | this.parent = parent; 17 | this.children = children; 18 | this.plusMode = plusMode; 19 | } 20 | 21 | public generate(context: IContext): Array { 22 | var results: Array = [], 23 | iterationCounter: number = 1, 24 | doPitch = (index: number, pitch: number): number => { 25 | iterationCounter = Math.floor(index / results.length) + 1; 26 | return pitch + results[index % results.length].value * (this.plusMode ? iterationCounter : 1); 27 | }; 28 | 29 | _.forEach(this.children, (child) => { 30 | results = results.concat(child.generate(context)); 31 | }); 32 | 33 | if (results.length > 0) { 34 | if (context.selectionActive) { 35 | forEachSelectedPitch(context, doPitch); 36 | } 37 | else { 38 | forEachPitch(context, doPitch); 39 | } 40 | } 41 | return []; 42 | } 43 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/modifier/Transpose.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {IContext} from "../IContext"; 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {forEachSelectedPitch} from "../../core/util/forEachSelectedPitch"; 5 | import {forEachPitch} from "../../core/util/forEachPitch"; 6 | import {Entity} from "../../interpreter/Entity"; 7 | 8 | export class Transpose implements INode { 9 | public type: Entity = Entity.TRANSPOSE; 10 | public children: INode[]; 11 | public parent: INode; 12 | 13 | constructor(parent: INode = null, children: INode[] = []) { 14 | this.parent = parent; 15 | this.children = children; 16 | } 17 | 18 | // todo: simplify - we should only do relative pitching according to current mode here, e.g. major, minor, pentatonic, etc 19 | // if we instead want to ensure that all notes fall within current scale then something like quantizeScale should be used instead. 20 | public generate(context: IContext): IRecurseValue[] { 21 | var results: IRecurseValue[] = [], 22 | doTranspose = (index: number, pitch: number): number => { 23 | return pitch + context.scale.scaleDegreeToPitchRelative(results[index].value); 24 | }; 25 | 26 | for (let child of this.children) { 27 | results = results.concat(child.generate(context)); 28 | } 29 | 30 | if (results.length > 0) { 31 | if (context.selectionActive) { 32 | forEachSelectedPitch(context, doTranspose); 33 | } 34 | else { 35 | forEachPitch(context, doTranspose); 36 | } 37 | } 38 | return []; 39 | } 40 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Alternate.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {IContext} from "../IContext"; 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class Alternate implements INode { 7 | public type: Entity = Entity.ALT; 8 | public children: Array; 9 | public parent: INode; 10 | public alternationIndex: number; 11 | public alternationIndexPre: number; 12 | 13 | constructor(parent: INode = null, shorthand: boolean = false) { 14 | if (shorthand) { 15 | this.type = Entity.ALT_SHORTHAND; 16 | } 17 | this.parent = parent; 18 | this.children = []; 19 | this.alternationIndex = -1; 20 | this.alternationIndexPre = -1; 21 | } 22 | 23 | // todo: impl reset if alt should "retrigger" on loop 24 | 25 | public generate(context: IContext): Array { 26 | if (context.prePhase) { 27 | this.alternationIndexPre++; 28 | return this.children[this.alternationIndexPre % this.children.length].generate(context); 29 | } 30 | this.alternationIndex++; 31 | return this.children[this.alternationIndex % this.children.length].generate(context); 32 | } 33 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Chain.ts: -------------------------------------------------------------------------------- 1 | // This construct is similar to a "part" in traditional sequencers like Cubase etc. 2 | // Transforms that need information about all notes in a part should be done here, such as mutate/shuffle/etc. 3 | 4 | import {INode} from "../../interpreter/INode"; 5 | import {ISetting} from "../../interpreter/ISetting"; 6 | import {IContext} from "../IContext"; 7 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 8 | import {Entity} from "../../interpreter/Entity"; 9 | 10 | export class Chain implements INode { 11 | public type: Entity = Entity.CHAIN; 12 | public children: Array; 13 | public parent: INode; 14 | public settings: Array; 15 | 16 | constructor(parent: INode = null, children: Array = []) { 17 | this.parent = parent; 18 | this.children = children; 19 | this.settings = []; 20 | } 21 | 22 | public generate(context: IContext): Array { 23 | // apply any chain-level settings prior to generation 24 | for (let setting of this.settings) { 25 | setting.apply(context); 26 | } 27 | console.log('settings applied', context); 28 | for (let child of this.children) { 29 | child.generate(context); 30 | } 31 | // LoopFactor 32 | // for now, only whole number loop factors are supported - but would be nice to have floating point values as well 33 | if (context.loopFactor < 1) { 34 | return []; 35 | } 36 | for (let i = 0; i < context.loopFactor - 1; i++) { 37 | context.results = context.results.concat(context.results); 38 | } 39 | return []; 40 | } 41 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Fill.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {ValueType} from "../../interpreter/ValueType"; 5 | import {Entity} from "../../interpreter/Entity"; 6 | 7 | export class Fill implements INode { 8 | public type: Entity = Entity.FILL; 9 | public children: Array; 10 | public parent: INode; 11 | 12 | constructor(parent: INode = null, children: Array = []) { 13 | this.parent = parent; 14 | this.children = children; 15 | } 16 | 17 | public generate(context: IContext): Array { 18 | return [{value: 0, valueType: ValueType.FILL}]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/GenericOperator.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {TokenType} from "../../interpreter/TokenType"; 4 | import {IContext} from "../IContext"; 5 | import {ISyntaxTree} from "../../interpreter/ISyntaxTree"; 6 | import {Alternate} from "./Alternate"; 7 | import {Variable} from "../base/Variable"; 8 | import {VariableReference} from "../base/VariableReference"; 9 | import {Repeat} from "./Repeat"; 10 | import {Interpolate} from "./Interpolate"; 11 | import {Range} from "./Range"; 12 | import {Entity} from "../../interpreter/Entity"; 13 | import {Multiply} from "./Multiply"; 14 | 15 | export class GenericOperator implements INode { 16 | public type: Entity; 17 | public children: INode[] = []; 18 | 19 | constructor(public operatorToken: TokenType) { 20 | this.type = Entity.GENERIC_OPERATOR; 21 | } 22 | 23 | public generate(context:IContext): IRecurseValue[] { 24 | return []; 25 | } 26 | 27 | public transform(parent: INode, node1: INode, node2: INode, syntaxTree: ISyntaxTree): INode { 28 | var newNode; 29 | switch (this.operatorToken) { 30 | case TokenType.SINGLE_QUOTE: 31 | if (node1.type === Entity.ALT_SHORTHAND) { 32 | // previous node is an alternating shorthand statement as well, meaning that we should merge into that node instead. 33 | newNode = node1; 34 | node2.parent = newNode; 35 | newNode.children.push(node2); 36 | return newNode; 37 | } else { 38 | newNode = new Alternate(parent, true); 39 | } 40 | break; 41 | case TokenType.EQUALS: 42 | // assignment - empty variable is already created when variable name is encountered for the first time, so retrieve it and populate it. 43 | if (node2.type !== Entity.NESTED) { 44 | throw new Error(`Expected nested block following assignment, but found ${Entity[node2.type]}`); 45 | } 46 | if (node1.type !== Entity.VARIABLE_REFERENCE) { 47 | throw new Error(`Expected variable name preceding = operator, but found ${Entity[node1.type]}`); 48 | } 49 | let varNode = syntaxTree.variables[node1.name]; 50 | for (let child of node2.children) { 51 | child.parent = varNode; 52 | } 53 | varNode.children = node2.children; //newNode = new VariableReference(parent, node1.name, varNode); 54 | return null; 55 | case TokenType.REPEAT: 56 | newNode = new Repeat(parent, true); 57 | break; 58 | case TokenType.MULTIPLY: 59 | if (node1.type === Entity.VALUE && node2.type === Entity.NESTED) { 60 | return this.mergeWithHead(node1, node2, Entity.MULTIPLY); 61 | } 62 | newNode = new Multiply(parent); 63 | break; 64 | case TokenType.DOUBLE_PERIOD: 65 | if (node1.type === Entity.VALUE && node2.type === Entity.NESTED) { 66 | return this.mergeWithHead(node1, node2, Entity.RANGE_SHORTHAND); 67 | } 68 | newNode = new Range(parent, true); 69 | break; 70 | case TokenType.RIGHT_ANGLE: 71 | if (node1.type === Entity.VALUE && node2.type === Entity.NESTED) { 72 | return this.mergeWithHead(node1, node2, Entity.INTERPOLATE); 73 | } 74 | newNode = new Interpolate(parent); 75 | break; 76 | default: 77 | throw new Error(`Transform: Found no matches for ${TokenType[this.operatorToken]}`); 78 | } 79 | node1.parent = node2.parent = newNode; 80 | 81 | newNode.children = [node1, node2]; 82 | return newNode; 83 | } 84 | 85 | private mergeWithHead(node1: INode, node2: INode, entity: Entity): INode { 86 | if (!node2.hasOwnProperty('head')) { 87 | throw new Error(`${Entity[entity]} operator can not be used with anonymous nested block`); 88 | } 89 | var result = node2, 90 | currentHeadNode = node2['head']; 91 | var newHeadNode; 92 | switch (entity) { 93 | case Entity.RANGE_SHORTHAND: 94 | newHeadNode = new Range(result, true); 95 | break; 96 | case Entity.INTERPOLATE: 97 | newHeadNode = new Interpolate(result); 98 | break; 99 | case Entity.MULTIPLY: 100 | newHeadNode = new Multiply(result); 101 | break; 102 | default: 103 | throw new Error(`GenericOperator.mergeWithHead failed to find match for ${Entity[entity]}`); 104 | } 105 | newHeadNode.children = [node1, currentHeadNode]; 106 | node1.parent = currentHeadNode.parent = newHeadNode; 107 | result['head'] = newHeadNode; 108 | return result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Interpolate.spec.ts: -------------------------------------------------------------------------------- 1 | import tape = require('tape'); 2 | import {Interpolate} from "./Interpolate"; 3 | 4 | tape('Testing sum', (test) => { 5 | let interpolate = new Interpolate(); 6 | //interpolate.children.push(new Value(1, interpolate, ValueType.INTERVAL)); 7 | //interpolate.children.push(new Value(4, interpolate, ValueType.INTERVAL)); 8 | 9 | test.equal(interpolate.sum(1, 4, 4), 10, 'Sum of 1 to 4 over 4 steps should equal 10'); 10 | console.log('1>4 0'); 11 | interpolate.sum(1,4,0); 12 | console.log('1>4 1'); 13 | interpolate.sum(1,4,1); 14 | console.log('1>4 2'); 15 | interpolate.sum(1,4,2); 16 | console.log('1>4 3'); 17 | interpolate.sum(1,4,3); 18 | test.end(); 19 | }); -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Interpolate.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {ValueType} from "../../interpreter/ValueType"; 4 | import {IContext} from "../IContext"; 5 | import {Entity} from "../../interpreter/Entity"; 6 | 7 | export class Interpolate implements INode { 8 | public type: Entity = Entity.INTERPOLATE; 9 | public children: Array; 10 | public parent: INode; 11 | public totalSteps: number = 0; 12 | public currentStep: number = 0; 13 | public startValue: number = 0; 14 | public endValue: number = 0; 15 | public valueType: ValueType; 16 | 17 | constructor(parent: INode = null) { 18 | this.parent = parent; 19 | this.children = []; 20 | } 21 | 22 | public reset(): void { 23 | this.totalSteps = 0; 24 | this.currentStep = 0; 25 | //console.log('reset was called'); 26 | } 27 | 28 | public generate(context: IContext): Array { 29 | var generatedValue: number = 0; 30 | 31 | if (this.children.length !== 2) { // todo also check start+end values prolly 32 | console.log('TEMP ERROR: invalid params for Interpolate'); 33 | return; 34 | } 35 | if (this.totalSteps === 0) { 36 | this.startValue = this.children[0].generate(context)[0].value; 37 | this.endValue = this.children[1].generate(context)[0].value; 38 | this.valueType = this.children[0].generate(context)[0].valueType; 39 | } 40 | 41 | let currentValue: number = 0; 42 | 43 | if (context.prePhase) { 44 | let prevSum = this.sum(this.startValue, this.endValue, this.totalSteps); 45 | this.totalSteps++; 46 | let currentSum = this.sum(this.startValue, this.endValue, this.totalSteps); 47 | generatedValue = currentSum - prevSum; 48 | } else { 49 | if (this.currentStep <= this.totalSteps) { 50 | generatedValue = this.getDelta(this.startValue, this.endValue, this.currentStep, this.totalSteps); 51 | this.currentStep++; 52 | } else { 53 | console.log('TEMP error - interpolate: currentStep is bigger than totalSteps'); 54 | } 55 | } 56 | return [{value: generatedValue, valueType: this.valueType}]; 57 | } 58 | 59 | private getDelta(start: number, end: number, currentStep: number, numSteps: number): number { 60 | if (numSteps === 0) { 61 | return 0; 62 | } 63 | if (numSteps === 1) { 64 | return start; 65 | } 66 | if (currentStep === 0) { 67 | return start; 68 | } 69 | if (currentStep === numSteps - 1) { 70 | return end; 71 | } 72 | 73 | let delta: number = (end - start) / (numSteps - 1.0); 74 | return start + (delta * currentStep); 75 | } 76 | 77 | // given start, end and numSteps: calculates the sum of interpolating from start to end 78 | public sum(start: number, end: number, numSteps: number): number { 79 | let sum: number = 0; 80 | for (let i = 0; i < numSteps; i++) { 81 | sum += this.getDelta(start, end, i, numSteps); 82 | //console.log(`Sum is ${sum}, getDelta is ${this.getDelta(start, end, i, numSteps)}`); 83 | } 84 | return sum; 85 | } 86 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Multiply.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "../../interpreter/INode"; 2 | import {Entity} from "../../interpreter/Entity"; 3 | import {IContext} from "../IContext"; 4 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 5 | 6 | export class Multiply implements INode { 7 | public type: Entity = Entity.MULTIPLY; 8 | public children: Array; 9 | public parent: INode; 10 | 11 | constructor(parent: INode = null, children: Array = []) { 12 | this.parent = parent; 13 | this.children = children; 14 | } 15 | 16 | public generate(context: IContext): Array { 17 | if (this.children.length !== 2) { 18 | throw new Error(`Multiply expected 2 parameters, but found ${this.children.length}`); 19 | } 20 | if (this.children[0].type !== Entity.VALUE || this.children[1].type !== Entity.VALUE) { 21 | throw new Error(`Multiply expected 2 value parameters but found parameter 1 of type ${Entity[this.children[0].type]} and parameter 2 of type ${Entity[this.children[1].type]}`); 22 | } 23 | var result: IRecurseValue = this.children[0].generate(context)[0]; 24 | result.value *= this.children[1].generate(context)[0].value; 25 | return [result]; 26 | } 27 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Random.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {INode} from "../../interpreter/INode"; 5 | import {IContext} from "../IContext"; 6 | import {Entity} from "../../interpreter/Entity"; 7 | 8 | export class Random implements INode { 9 | public type: Entity = Entity.RND; 10 | public children: Array; 11 | public parent: INode; 12 | public cache: Array = []; 13 | public invokeCount: number = 0; 14 | 15 | constructor(parent: INode = null, children: Array = []) { 16 | this.parent = parent; 17 | this.children = children; 18 | } 19 | 20 | public reset() { 21 | this.cache = []; 22 | this.invokeCount = 0; 23 | } 24 | 25 | // todo: for loop to work properly, we need to take extra measures in several generators to reset internal params if needed. 26 | // random and interp will both need this. alt won't unless one wants to retrigger alternation for each loop iteration (probably not) 27 | public generate(context: IContext): Array { 28 | if (this.children.length === 0) { 29 | return []; 30 | } 31 | if (context.prePhase) { // todo: prePhase not yet impl 32 | this.cache.push(Math.floor(Math.random() * this.children.length)); 33 | return this.children[this.cache[this.cache.length - 1]].generate(context); 34 | } else { 35 | var index: number = this.cache[this.invokeCount]; 36 | this.invokeCount++; 37 | return this.children[index].generate(context); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Range.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {ValueType} from "../../interpreter/ValueType"; 5 | import {Entity} from "../../interpreter/Entity"; 6 | 7 | export class Range implements INode { 8 | public type: Entity = Entity.RANGE; 9 | public children: Array; 10 | public parent: INode; 11 | 12 | constructor(parent: INode = null, shorthand: boolean = false) { 13 | if (shorthand) { 14 | this.type = Entity.RANGE_SHORTHAND; 15 | } 16 | this.parent = parent; 17 | this.children = []; 18 | } 19 | 20 | // todo: support automatic scaling of values according to number of notes if used on e.g. velocities? 21 | public generate(context: IContext): Array { 22 | if (this.children.length !== 2) { 23 | console.log('WARN: Range produced no output due to incorrect number of params. Expected 2 and got ' + this.children.length); 24 | return; 25 | } 26 | var start: IRecurseValue = this.children[0].generate(context)[0], 27 | end: IRecurseValue = this.children[1].generate(context)[0], 28 | startValue = start.value, 29 | endValue = end.value, 30 | results: Array = []; 31 | 32 | if (start.valueType !== start.valueType) { 33 | console.log(`WARN: Range expected arguments of same type, but received ${ValueType[start.valueType]} and ${ValueType[end.valueType]}`); 34 | } 35 | if (startValue > endValue) { 36 | for (let i = startValue; i >= endValue; i--) { 37 | results.push({value: i, valueType: start.valueType}); 38 | } 39 | } else { 40 | for (let i = startValue; i <= endValue; i++) { 41 | results.push({value: i, valueType: start.valueType}); 42 | } 43 | } 44 | return results; 45 | } 46 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/operator/Repeat.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {INode} from "../../interpreter/INode"; 3 | import {IContext} from "../IContext"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class Repeat implements INode { 7 | public type: Entity = Entity.REPEAT; 8 | public children: Array; 9 | public parent: INode; 10 | 11 | constructor(parent: INode = null, shorthand: boolean = false) { 12 | if (shorthand) { 13 | this.type = Entity.REPEAT_SHORTHAND; 14 | } 15 | this.parent = parent; 16 | this.children = []; 17 | } 18 | 19 | public generate(context: IContext): Array { 20 | if (this.children.length !== 2) { 21 | console.log('WARN: Repeat produced no output due to incorrect params'); 22 | return; 23 | } 24 | 25 | // Swap arguments if they are obviously in the wrong order 26 | if (this.children[0].type === Entity.VALUE && this.children[1].type === Entity.NESTED) { 27 | [this.children[0], this.children[1]] = [this.children[1], this.children[0]]; 28 | } 29 | 30 | var numRepeats: number = this.children[1].generate(context)[0].value, 31 | results: IRecurseValue[] = []; 32 | 33 | for (let i = 0; i < numRepeats; i++) { 34 | results = results.concat(this.children[0].generate(context)); 35 | } 36 | return results; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /recurse-app/app/src/function/selection/EndSelect.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 4 | import {INode} from "../../interpreter/INode"; 5 | import {IContext} from "../IContext"; 6 | import {Entity} from "../../interpreter/Entity"; 7 | 8 | export class EndSelect implements INode { 9 | public type: Entity = Entity.END_SELECT; 10 | public children: Array = []; 11 | public parent: INode; 12 | 13 | constructor(parent: INode = null, children: Array = []) { 14 | this.parent = parent; 15 | this.children = children; 16 | } 17 | 18 | public generate(context: IContext): Array { 19 | context.selectionActive = false; 20 | context.selectedIndexes = []; 21 | 22 | return []; 23 | } 24 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/selection/Select.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | import { SelectStrategy, SelectStrategyTable } from './SelectStrategy'; 4 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 5 | import {INode} from "../../interpreter/INode"; 6 | import {IContext} from "../IContext"; 7 | import {Entity} from "../../interpreter/Entity"; 8 | 9 | export class Select implements INode { 10 | public type: Entity = Entity.SELECT; 11 | public children: Array = []; 12 | public parent: INode; 13 | public strategy: SelectStrategy; 14 | 15 | constructor(parent: INode = null, strategy: SelectStrategy = SelectStrategy.indexList, children: Array = []) { 16 | this.parent = parent; 17 | this.children = children; 18 | this.strategy = strategy; 19 | } 20 | 21 | public generate(context: IContext): Array { 22 | var results: Array = [], 23 | params: Array = []; 24 | 25 | for (let child of this.children) { 26 | results = results.concat(child.generate(context)); 27 | } 28 | for (let result of results) { // argument list, e.g. selection index 29 | params.push(result.value); 30 | } 31 | 32 | var selectionIndices: number[] = []; 33 | if (context.selectionActive && context.selectedIndexes.length > 0) { 34 | selectionIndices = context.selectedIndexes; 35 | } else { 36 | for (let i = 0; i < context.results.length; i++) { 37 | selectionIndices[i] = i; 38 | } 39 | } 40 | context.selectedIndexes = SelectStrategyTable[this.strategy](selectionIndices, params); 41 | 42 | if (context.selectedIndexes.length > 0) { 43 | context.selectionActive = true; 44 | } 45 | return []; 46 | } 47 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/selection/SelectStrategy.ts: -------------------------------------------------------------------------------- 1 | import {createLookupTable} from "../../core/util/createLookupTable"; 2 | 3 | export enum SelectStrategy { 4 | even, 5 | odd, 6 | first, 7 | last, 8 | every, 9 | indexList 10 | } 11 | 12 | var selectStrategies = [ 13 | { 14 | strategy: SelectStrategy.even, 15 | implementation: selectEven 16 | }, 17 | { 18 | strategy: SelectStrategy.odd, 19 | implementation: selectOdd 20 | }, 21 | { 22 | strategy: SelectStrategy.first, 23 | implementation: selectFirst 24 | }, 25 | { 26 | strategy: SelectStrategy.last, 27 | implementation: selectLast 28 | }, 29 | { 30 | strategy: SelectStrategy.every, 31 | implementation: selectEvery 32 | }, 33 | { 34 | strategy: SelectStrategy.indexList, 35 | implementation: selectByIndexList 36 | }, 37 | ]; 38 | 39 | export var SelectStrategyTable: Array<(selectionIndices: number[], indexList: number[]) => number[]> = 40 | createLookupTable<(selectionIndices: number[], indexList: number[]) => number[]>(selectStrategies, 'strategy', 'implementation'); 41 | 42 | function selectEven(selectionIndices: number[], indexList: number[] = []): number[] { 43 | var n: number = Math.floor(selectionIndices.length / 2), 44 | result: number[] = []; 45 | 46 | for (var i = 0; i < n; i++) { 47 | let ix = i * 2 + 1; 48 | if (ix >= selectionIndices.length) { 49 | throw new Error("Selection index is outside of range"); 50 | } 51 | result.push(selectionIndices[ix]); 52 | } 53 | return result; 54 | } 55 | 56 | function selectOdd(selectionIndices: number[], indexList: number[] = []): number[] { 57 | var n: number = Math.floor(selectionIndices.length / 2), 58 | rem: number = selectionIndices.length % 2, 59 | result: number[] = []; 60 | 61 | for (var i = 0; i < n + rem; i++) { 62 | let ix = i * 2; 63 | if (ix >= selectionIndices.length) { 64 | throw new Error("Selection index is outside of range"); 65 | } 66 | result.push(selectionIndices[ix]); 67 | } 68 | return result; 69 | } 70 | 71 | function selectFirst(selectionIndices: number[], indexList: number[] = []): number[] { 72 | return (selectionIndices.length > 0 ? [selectionIndices[0]] : []); 73 | } 74 | 75 | function selectLast(selectionIndices: number[], indexList: number[] = []): number[] { 76 | return (selectionIndices.length > 0 ? [selectionIndices[selectionIndices.length - 1]] : []); 77 | } 78 | 79 | function selectEvery(selectionIndices: number[], indexList: number[] = []) { 80 | //todo: impl 81 | 82 | } 83 | 84 | function selectByIndexList(selectionIndices: number[], indexList: number[] = []) { 85 | var results: number[] = []; 86 | 87 | if (indexList.length === 0) return; 88 | 89 | for (let index of indexList) { 90 | if (index > 0) { 91 | index--; // convert to zero-based index 92 | } else { 93 | index = 0; // sanity 94 | } 95 | // only allow unique indexes within range 96 | if (index < selectionIndices.length && results.indexOf(selectionIndices[index]) < 0) { 97 | results.push(selectionIndices[index]); 98 | } 99 | } 100 | return results; 101 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/setter/Loop.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {ISetting} from "../../interpreter/ISetting"; 3 | import {IContext} from "../IContext"; 4 | import {Entity} from "../../interpreter/Entity"; 5 | 6 | export class Loop implements ISetting { 7 | value: number; 8 | type: Entity = Entity.LOOP_FACTOR; 9 | 10 | constructor(value: number = 1) { 11 | this.value = value; 12 | } 13 | 14 | public apply(context: IContext): void { 15 | context.loopFactor = this.value; 16 | } 17 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/setter/PatternLength.ts: -------------------------------------------------------------------------------- 1 | import {ISetting} from "../../interpreter/ISetting"; 2 | import {IContext} from "../IContext"; 3 | import {Entity} from "../../interpreter/Entity"; 4 | 5 | export class PatternLength implements ISetting { 6 | value: number; 7 | type: Entity = Entity.PATTERN_LENGTH; 8 | 9 | constructor(value: number = 64) { 10 | this.value = value; 11 | } 12 | 13 | public apply(context: IContext): void { 14 | context.patternLength = this.value; 15 | } 16 | } -------------------------------------------------------------------------------- /recurse-app/app/src/function/setter/SetScale.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../../core/type/IRecurseValue"; 2 | import {ISetting} from "../../interpreter/ISetting"; 3 | import {IContext} from "../IContext"; 4 | import {Scale} from "../../core/type/Scale"; 5 | import {Entity} from "../../interpreter/Entity"; 6 | 7 | export class SetScale implements ISetting { 8 | value: string; 9 | type: Entity = Entity.SET_SCALE; 10 | 11 | constructor(value: string) { 12 | this.value = value; 13 | } 14 | 15 | public apply(context: IContext): void { 16 | if (Scale.isScaleNameValid(this.value)) { 17 | context.scale = Scale.getScaleFromName(this.value); 18 | } else { 19 | console.log('Warning: attempted to set unrecognized scale ' + this.value); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/Entity.ts: -------------------------------------------------------------------------------- 1 | export enum Entity { 2 | _KEYWORDS_BEGIN, 3 | RM, 4 | NS, 5 | ALT, 6 | END_SELECT, 7 | EVEN, 8 | FIRST, 9 | LAST, 10 | PATTERN_LENGTH, 11 | PITCH, 12 | PITCH_PLUS, 13 | RANGE, 14 | REPEAT, 15 | REST, 16 | RND, 17 | TRANSPOSE, 18 | SELECT, 19 | VEL, 20 | VELOCITY, 21 | _KEYWORDS_END, 22 | 23 | _OPERATORS_BEGIN, 24 | REPEAT_SHORTHAND, 25 | INTERPOLATE, 26 | RANGE_SHORTHAND, 27 | ALT_SHORTHAND, 28 | MULTIPLY, 29 | _OPERATORS_END, 30 | 31 | VARIABLE, 32 | VARIABLE_REFERENCE, 33 | VARIABLE_VALUE, 34 | CHAIN, 35 | CONTAINER, 36 | EMPTY, 37 | EVENT_RESOLUTION, 38 | FILL, 39 | GENERIC_OPERATOR, 40 | INTERVAL, 41 | INVALID, 42 | LOOP_FACTOR, 43 | NESTED, 44 | NOTE, 45 | ODD, 46 | PITCH_OFFSET, 47 | RAW_NOTE, 48 | SCALE_DEGREE, 49 | REST_SHORTHAND, 50 | ROOT, 51 | SELECT_INDEX, 52 | SET_SCALE, 53 | VALUE, 54 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/INode.ts: -------------------------------------------------------------------------------- 1 | import {IRecurseValue} from "../core/type/IRecurseValue"; 2 | import {Entity} from "./Entity"; 3 | import {IContext} from "../function/IContext"; 4 | import {ISetting} from "./ISetting"; 5 | import {ISyntaxTree} from "./ISyntaxTree"; 6 | 7 | export interface INode { 8 | type: Entity; 9 | children: Array; 10 | parent?: INode; 11 | reference?: INode; // Not implemented. holds reference to another node in the case of statements like stack, +rm, etc. 12 | clonedContext?: IContext; // the referenced node will have a cloned copy of it's context when generation occurred, which can be used by the node referring to it 13 | contextRef?: IContext; // all root nodes hold a copy of the context they receive, which can be used to extract settings such as lenght etc. 14 | generate: (context: IContext) => IRecurseValue[]; 15 | startOffset?: number; 16 | reset?: () => void; 17 | settings?: Array; 18 | transform?: (parent: INode, node1: INode, node2: INode, syntaxTree: ISyntaxTree) => INode; 19 | name?: string; 20 | generateVar?: (context: IContext, parent: INode) => IRecurseValue[]; // Used from a variable reference to generate its contents 21 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/ISetting.ts: -------------------------------------------------------------------------------- 1 | import {Entity} from "./Entity"; 2 | import {IContext} from "../function/IContext"; 3 | 4 | export interface ISetting { 5 | type: Entity; 6 | value: any; 7 | apply: (context: IContext) => void; 8 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/ISyntaxTree.ts: -------------------------------------------------------------------------------- 1 | import {INode} from "./INode"; 2 | import {RecurseObject} from "../core/type/RecurseObject"; 3 | 4 | export interface ISyntaxTree { 5 | rootNodes: INode[]; 6 | variables: any; 7 | generate: () => RecurseObject[][]; 8 | findVariable: (name: string) => INode; 9 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/IToken.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from "./TokenType"; 2 | 3 | export interface IToken { 4 | type: TokenType; 5 | value: any; 6 | pos: number; 7 | originalValue?: any; 8 | originalType?: TokenType; 9 | isolatedLeft?: boolean; // if this token is preceded by a space, isolatedLeft = true 10 | isolatedRight?: boolean; // similarly for whether it is followed by a space 11 | } -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/Lexer.spec.ts: -------------------------------------------------------------------------------- 1 | import tape = require('tape'); 2 | 3 | import {TokenType} from "./TokenType"; 4 | import {Lexer} from "./Lexer"; 5 | import {IToken} from "./IToken"; 6 | 7 | tape('return tokenized version of input', (test) => { 8 | var input: string = "rm(8 , 6,4,4 * 4,13(8,10;8;6 (5,5),8)):{length:64,loop:32}", // old syntax but lexer should give correct output anyways 9 | lexer: Lexer = new Lexer(), 10 | expectedTokens: TokenType[] = [ 11 | TokenType.IDENTIFIER, 12 | TokenType.LEFT_PAREN, 13 | TokenType.NUMBER, 14 | TokenType.COMMA, 15 | TokenType.NUMBER, 16 | TokenType.COMMA, 17 | TokenType.NUMBER, 18 | TokenType.COMMA, 19 | TokenType.NUMBER, 20 | TokenType.MULTIPLY, 21 | TokenType.NUMBER, 22 | TokenType.COMMA, 23 | TokenType.NUMBER, 24 | TokenType.LEFT_PAREN, 25 | TokenType.NUMBER, 26 | TokenType.COMMA, 27 | TokenType.NUMBER, 28 | TokenType.SEMI, 29 | TokenType.NUMBER, 30 | TokenType.SEMI, 31 | TokenType.NUMBER, 32 | TokenType.LEFT_PAREN, 33 | TokenType.NUMBER, 34 | TokenType.COMMA, 35 | TokenType.NUMBER, 36 | TokenType.RIGHT_PAREN, 37 | TokenType.COMMA, 38 | TokenType.NUMBER, 39 | TokenType.RIGHT_PAREN, 40 | TokenType.RIGHT_PAREN, 41 | TokenType.COLON, 42 | TokenType.LEFT_BRACE, 43 | TokenType.IDENTIFIER, 44 | TokenType.COLON, 45 | TokenType.NUMBER, 46 | TokenType.COMMA, 47 | TokenType.IDENTIFIER, 48 | TokenType.COLON, 49 | TokenType.NUMBER, 50 | TokenType.RIGHT_BRACE 51 | ], 52 | token: IToken, 53 | i: number = 0; 54 | lexer.setBuffer(input); 55 | while (token = lexer.token()) { 56 | test.equal(token.type, expectedTokens[i], `Token with value ${token.value} should translate to ${TokenType[expectedTokens[i]]}`); 57 | i++; 58 | } 59 | test.end(); 60 | }); 61 | 62 | tape('handle decimal numbers properly', (test) => { 63 | var input: string = "rm(8.5,1,5.02,4,2.2,1.500)", 64 | lexer: Lexer = new Lexer(), 65 | expectedTokens: TokenType[] = [ 66 | TokenType.IDENTIFIER, 67 | TokenType.LEFT_PAREN, 68 | TokenType.NUMBER, 69 | TokenType.COMMA, 70 | TokenType.NUMBER, 71 | TokenType.COMMA, 72 | TokenType.NUMBER, 73 | TokenType.COMMA, 74 | TokenType.NUMBER, 75 | TokenType.COMMA, 76 | TokenType.NUMBER, 77 | TokenType.COMMA, 78 | TokenType.NUMBER, 79 | TokenType.RIGHT_PAREN 80 | ], 81 | token: IToken, 82 | i: number = 0; 83 | lexer.setBuffer(input); 84 | while (token = lexer.token()) { 85 | test.equal(token.type, expectedTokens[i], `Token with value ${token.value} should translate to ${TokenType[expectedTokens[i]]}`); 86 | i++; 87 | } 88 | test.end(); 89 | }); 90 | 91 | tape('handle negative numbers properly', (test) => { 92 | var input: string = "rm(4 -4 -1 0)", 93 | lexer: Lexer = new Lexer(), 94 | expectedTokens: TokenType[] = [ 95 | TokenType.IDENTIFIER, 96 | TokenType.LEFT_PAREN, 97 | TokenType.NUMBER, 98 | TokenType.NUMBER, 99 | TokenType.NUMBER, 100 | TokenType.NUMBER, 101 | TokenType.RIGHT_PAREN 102 | ], 103 | token: IToken, 104 | i: number = 0; 105 | lexer.setBuffer(input); 106 | while (token = lexer.token()) { 107 | test.equal(token.type, expectedTokens[i], `Token with value ${token.value} should translate to ${TokenType[expectedTokens[i]]}`); 108 | i++; 109 | } 110 | test.end(); 111 | }); 112 | 113 | tape('handle comments properly', (test) => { 114 | var input: string = `// hei 115 | //hoppsan sveisann 116 | rm(16 48)//ekstrahopp 117 | //en siste topp 118 | `, 119 | lexer: Lexer = new Lexer(), 120 | expectedTokens: TokenType[] = [ 121 | TokenType.IDENTIFIER, 122 | TokenType.LEFT_PAREN, 123 | TokenType.NUMBER, 124 | TokenType.NUMBER, 125 | TokenType.RIGHT_PAREN 126 | ], 127 | token: IToken, 128 | i: number = 0; 129 | lexer.setBuffer(input); 130 | while (token = lexer.token()) { 131 | test.equal(token.type, expectedTokens[i], `Token with value ${token.value} should translate to ${TokenType[expectedTokens[i]]}`); 132 | i++; 133 | } 134 | test.end(); 135 | }); -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/Lexer.ts: -------------------------------------------------------------------------------- 1 | import {TokenType} from "./TokenType"; 2 | import {IToken} from "./IToken"; 3 | 4 | // todo: should rewrite from scratch 5 | // should probably differentiate between keywords (maybe with separate classes for functions, scales, and so on) and identifiers (or maybe scales belong here, together with variables and so on) 6 | 7 | export class Lexer { 8 | private position: number = 0; 9 | private buffer: string = null; 10 | private bufferLength: number; 11 | 12 | private operatorTable: any = { 13 | '\\': TokenType.BACKSLASH, 14 | // '+': TokenType.PLUS, 15 | //'-': TokenType.MINUS, 16 | '*': TokenType.MULTIPLY, 17 | '.': TokenType.PERIOD, 18 | ':': TokenType.COLON, 19 | '%': TokenType.PERCENT, 20 | '|': TokenType.PIPE, 21 | '!': TokenType.EXCLAMATION, 22 | '?': TokenType.QUESTION, 23 | '#': TokenType.POUND, 24 | '&': TokenType.AMPERSAND, 25 | ';': TokenType.SEMI, 26 | ',': TokenType.COMMA, 27 | '(': TokenType.LEFT_PAREN, 28 | ')': TokenType.RIGHT_PAREN, 29 | '<': TokenType.LEFT_ANGLE, 30 | '>': TokenType.RIGHT_ANGLE, 31 | '{': TokenType.LEFT_BRACE, 32 | '}': TokenType.RIGHT_BRACE, 33 | '[': TokenType.LEFT_BRACKET, 34 | ']': TokenType.RIGHT_BRACKET, 35 | '=': TokenType.EQUALS, 36 | '\'': TokenType.SINGLE_QUOTE 37 | }; 38 | 39 | 40 | // Initialize the Lexer's buffer. This resets the lexer's internal 41 | // state and subsequent tokens will be returned starting with the 42 | // beginning of the new buffer. 43 | setBuffer(buffer) { 44 | this.position = 0; 45 | this.buffer = buffer; 46 | this.bufferLength = buffer.length; 47 | } 48 | 49 | getTokenSet(inputBuffer: string): Array { 50 | var token: IToken, 51 | tokenSet: Array = []; 52 | 53 | this.setBuffer(inputBuffer); 54 | while (token = this.token()) { 55 | tokenSet.push(token); 56 | } 57 | return tokenSet; 58 | } 59 | 60 | // Get the next token from the current buffer. A token is an object with 61 | // the following properties: 62 | // - name: name of the pattern that this token matched (taken from rules). 63 | // - value: actual string value of the token. 64 | // - pos: offset in the current buffer where the token starts. 65 | // 66 | // If there are no more tokens in the buffer, returns null. In case of 67 | // an error throws Error. 68 | 69 | token(): IToken { 70 | let token; 71 | do { 72 | token = this.nextToken(); 73 | } while (token !== null && token.type === TokenType.COMMENT); 74 | return token; 75 | } 76 | 77 | nextToken(): IToken { 78 | this.skipNonTokens(); 79 | if (this.position >= this.bufferLength) { 80 | return null; 81 | } 82 | 83 | // The char at this.pos is part of a real token. Figure out which. 84 | var char = this.buffer.charAt(this.position); 85 | 86 | // '/' is treated specially, because it starts a comment if followed by 87 | // another '/'. If not followed by another '/', it's the DIVIDE 88 | // operator. 89 | var nextChar = this.buffer.charAt(this.position + 1); 90 | 91 | if (char === '/' && nextChar === '/') { 92 | return this.processComment(); 93 | } 94 | if (char === '/') { 95 | return {type: TokenType.DIVIDE, value: '/', pos: this.position++}; 96 | } 97 | if (char === ';' && nextChar === ';') { 98 | return {type: TokenType.DOUBLE_SEMI, value: ';;', pos: this.position += 2}; 99 | } 100 | if (char === '.' && nextChar === '.') { 101 | return {type: TokenType.DOUBLE_PERIOD, value: '..', pos: this.position += 2}; 102 | } 103 | if (char === '_' && nextChar === '*') { 104 | return {type: TokenType.FILL, value: '_*', pos: this.position += 2}; 105 | } 106 | // Look it up in the table of operators 107 | var op = this.operatorTable[char]; 108 | // special case: x can also be part of an identifier, but if alone it is the repeat token 109 | if (op !== undefined) { 110 | let token:IToken = { 111 | type: op, value: char, pos: this.position, isolatedLeft: (this.getChar(this.position - 1) === ' '), 112 | isolatedRight: (this.getChar(this.position + 1) === ' ') 113 | }; 114 | this.position++; 115 | return token; 116 | } 117 | // Not an operator - so it's the beginning of another token. 118 | if (Lexer.isNote(char, nextChar)) { 119 | return this.processNote(); 120 | } 121 | if (Lexer.isAlpha(char) && Lexer.isAlpha(nextChar)) { // note that this makes one-letter identifiers impossible unless specifically detected below 122 | if (char === '$') { 123 | return this.processVariableName(); 124 | } 125 | return this.processIdentifier(); 126 | } 127 | if (Lexer.isAlpha(char)) { 128 | // check for one-letter alphabetic tokens (a-z, A-Z, _, $) 129 | if (char === 'x') { 130 | return {type: TokenType.REPEAT, value: char, pos: this.position++}; 131 | } 132 | if (char === '_') { 133 | return {type: TokenType.UNDERSCORE, value: char, pos: this.position++}; 134 | } 135 | } 136 | if (Lexer.isDigit(char) || char === '-') { 137 | return this.processNumber(); 138 | } 139 | if (char === '"') { 140 | return this.processQuote(); 141 | } 142 | throw Error('Token error at ' + this.position + ' ' + this.buffer.charAt(this.position)); 143 | } 144 | 145 | private getChar(pos): string { 146 | if (pos >= 0 && pos < this.buffer.length) { 147 | return this.buffer.charAt(pos); 148 | } 149 | return ""; 150 | } 151 | 152 | static isNewline(c): boolean { 153 | return c === '\r' || c === '\n'; 154 | } 155 | 156 | static isDigit(c): boolean { 157 | return c >= '0' && c <= '9'; 158 | } 159 | 160 | static isAlpha(c): boolean { 161 | return (c >= 'a' && c <= 'z') || 162 | (c >= 'A' && c <= 'Z') || 163 | c === '_' || c === '$' || c === '+'; 164 | } 165 | 166 | static isValidVariableCharacter(c): boolean { 167 | return (c >= 'a' && c <= 'z') || 168 | (c >= 'A' && c <= 'Z') || 169 | c === '_' || c === '$' || 170 | (c >= '0' && c <= '9'); 171 | } 172 | 173 | static isNote(c1, c2): boolean { 174 | return ((c1 >= 'a' && c1 <= 'g') || 175 | (c1 >= 'A' && c1 <= 'G')) && 176 | ((c2 >= '0' && c2 <= '9') || c2 === '#' || c2 === '-'); 177 | } 178 | 179 | /* static isAlphaNumeric(c): boolean { 180 | return (c >= 'a' && c <= 'z') || 181 | (c >= 'A' && c <= 'Z') || 182 | (c >= '0' && c <= '9') || 183 | c === '_' || c === '$'; 184 | }*/ 185 | 186 | static isNoteCharacter(c): boolean { 187 | return (c >= 'a' && c <= 'g') || 188 | (c >= 'A' && c <= 'G') || 189 | (c >= '0' && c <= '9') || 190 | c === '#' || c === '-'; 191 | } 192 | 193 | private processNote(): IToken { 194 | var endpos = this.position + 1; 195 | while (endpos < this.bufferLength && 196 | Lexer.isNoteCharacter(this.buffer.charAt(endpos))) { 197 | endpos++; 198 | } 199 | 200 | var token = { 201 | type: TokenType.NOTE, 202 | value: this.buffer.substring(this.position, endpos), 203 | pos: this.position 204 | }; 205 | this.position = endpos; 206 | return token; 207 | } 208 | 209 | private processNumber(): IToken { 210 | var endpos = this.position + 1; 211 | while (endpos < this.bufferLength && (Lexer.isDigit(this.buffer.charAt(endpos)) 212 | || (this.buffer.charAt(endpos) === '.') && (this.buffer.charAt(endpos + 1) !== '.') )) // avoid parsing shorthand ranges as numbers, e.g. 1..4 213 | { 214 | endpos++; 215 | } 216 | 217 | var token: IToken = { 218 | type: TokenType.NUMBER, 219 | value: this.buffer.substring(this.position, endpos), 220 | pos: this.position 221 | }; 222 | token.value = parseFloat(token.value); 223 | if (isNaN(token.value)) { 224 | // invalid number, throw error. fix this when rewriting lexer 225 | throw new Error('WARN: Lexer detected invalid number ' + token.value); 226 | } 227 | 228 | this.position = endpos; 229 | return token; 230 | } 231 | 232 | private processComment(): IToken { 233 | var endpos = this.position + 2; 234 | // Skip until the end of the line 235 | var c = this.buffer.charAt(this.position + 2); 236 | while (endpos < this.bufferLength && !Lexer.isNewline(this.buffer.charAt(endpos))) { 237 | endpos++; 238 | } 239 | 240 | var token: IToken = { 241 | type: TokenType.COMMENT, 242 | value: this.buffer.substring(this.position, endpos), 243 | pos: this.position 244 | }; 245 | this.position = endpos + 1; 246 | return token; 247 | } 248 | 249 | private processIdentifier(): IToken { 250 | var endpos = this.position + 1; 251 | while (endpos < this.bufferLength && 252 | Lexer.isAlpha(this.buffer.charAt(endpos))) { 253 | endpos++; 254 | } 255 | 256 | var token = { 257 | type: TokenType.IDENTIFIER, 258 | value: this.buffer.substring(this.position, endpos), 259 | pos: this.position 260 | }; 261 | this.position = endpos; 262 | return token; 263 | } 264 | 265 | private processVariableName(): IToken { 266 | var endpos = this.position + 1; 267 | while (endpos < this.bufferLength && 268 | Lexer.isValidVariableCharacter(this.buffer.charAt(endpos))) { 269 | endpos++; 270 | } 271 | 272 | var token = { 273 | type: TokenType.VARIABLE_NAME, 274 | value: this.buffer.substring(this.position, endpos), 275 | pos: this.position 276 | }; 277 | this.position = endpos; 278 | return token; 279 | } 280 | 281 | private processQuote(): IToken { 282 | // this.pos points at the opening quote. Find the ending quote. 283 | var end_index = this.buffer.indexOf('"', this.position + 1); 284 | 285 | if (end_index === -1) { 286 | throw Error('Unterminated quote at ' + this.position); 287 | } else { 288 | var token = { 289 | type: TokenType.QUOTE, 290 | value: this.buffer.substring(this.position, end_index + 1), 291 | pos: this.position 292 | }; 293 | this.position = end_index + 1; 294 | return token; 295 | } 296 | } 297 | 298 | private skipNonTokens(): void { 299 | while (this.position < this.bufferLength) { 300 | var c = this.buffer.charAt(this.position); 301 | if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { 302 | this.position++; 303 | } else { 304 | break; 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/Parser.spec.ts: -------------------------------------------------------------------------------- 1 | import tape = require('tape'); 2 | import _ = require('lodash'); 3 | 4 | import {TokenType} from "./TokenType"; 5 | import {INode} from "./INode"; 6 | import {Entity} from "./Entity"; 7 | import {Lexer} from "./Lexer"; 8 | import {RecurseResult} from "../core/type/RecurseResult"; 9 | import {SyntaxTree} from "../function/SyntaxTree"; 10 | import {Parser} from "./Parser"; 11 | 12 | function getSyntaxTreeEntities(nodes: INode[], entities: Entity[] = []): Entity[] { 13 | for (let node of nodes) { 14 | entities.push(node.type); 15 | getSyntaxTreeEntities(node.children, entities); 16 | } 17 | return entities; 18 | } 19 | 20 | function parseAndCheckAgainst(input: string, expectedEntities: Entity[], test) { 21 | var lexer:Lexer = new Lexer(), 22 | parsed:RecurseResult = Parser.parseTokensToSyntaxTree(lexer.getTokenSet(input)); 23 | Parser.printSyntaxTree(parsed.result); 24 | 25 | if (!parsed.isOk()) { 26 | test.fail(parsed.error); 27 | return; 28 | } 29 | var entities: Entity[] = []; 30 | getSyntaxTreeEntities(parsed.result.rootNodes, entities); 31 | 32 | for (let i = 0; i < expectedEntities.length; i++) { 33 | test.equal(entities[i], expectedEntities[i], `Entity ${Entity[entities[i]]}${_.repeat("_", 20 - Entity[entities[i]].length)} should be equal to ${Entity[expectedEntities[i]]}`); 34 | } 35 | } 36 | 37 | // consider doing validation after syntax tree has been created (basic syntax checking still needs to be done by parser) 38 | // for instance we could have rules that are run on each node of the tree, e.g. onlyTopLevel(rm, ns), and so on. 39 | 40 | tape('Parser should handle anonymous nested blocks', (test) => { 41 | parseAndCheckAgainst("rm(1 (2 4)) ns(c4 (c5 c6))", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.VALUE, Entity.NESTED, Entity.VALUE, Entity.VALUE, Entity.NS, Entity.VALUE, Entity.NESTED, Entity.VALUE, Entity.VALUE], test); 42 | test.end(); 43 | }); 44 | 45 | tape('Parser should handle nested blocks', (test) => { 46 | parseAndCheckAgainst("rm(1(2 4)) ns(c4(c5 c6))", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.NESTED, Entity.VALUE, Entity.VALUE, Entity.NS, Entity.NESTED, Entity.VALUE, Entity.VALUE], test); 47 | test.end(); 48 | }); 49 | 50 | tape('Should be possible to do a repeat with an anonymous nested block', (test) => { 51 | parseAndCheckAgainst("rm(3x(2 4))", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.REPEAT_SHORTHAND, Entity.VALUE, Entity.NESTED], test); 52 | parseAndCheckAgainst("rm((2 4)x3)", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.REPEAT_SHORTHAND, Entity.NESTED, Entity.VALUE], test); 53 | test.end(); 54 | }); 55 | 56 | tape('Should be possible to assign a block to a variable', (test) => { 57 | parseAndCheckAgainst("rm($test=(2 4) $test)", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.VARIABLE_REFERENCE], test); 58 | test.end(); 59 | }); 60 | 61 | tape('Should be possible to use a shorthand expression as the head of a nested block', (test) => { 62 | parseAndCheckAgainst("rm(2..4(1x2))", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.NESTED, Entity.REPEAT_SHORTHAND], test); 63 | test.end(); 64 | }); 65 | 66 | // rm(2..4(1x2)) ns(c3) // expecting intervals: 1 1 1.5 1.5 2 2 67 | 68 | // todo: Nested should contain a sub node so that it supports more complex values than simply numbers which is the case today. This means an interpolate statement could be the sum of a nested expr. 69 | /* 70 | tape('Interpolate statements should be supported as the sum of a nested block', (test) => { 71 | parseAndCheckAgainst("rm(1>2(2 4))", [Entity.ROOT, Entity.CHAIN, Entity.RM, Entity.NESTED/!* Containing Entity.INTERPOLATE *!/, Entity.VALUE, Entity.VALUE], test); 72 | }); 73 | 74 | */ 75 | 76 | -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/TokenType.ts: -------------------------------------------------------------------------------- 1 | import {createLookupTable} from "../core/util/createLookupTable"; 2 | 3 | export enum TokenType { 4 | AMPERSAND, 5 | BACKSLASH, 6 | COLON, 7 | COMMA, 8 | COMMENT, 9 | DIVIDE, 10 | DOUBLE_PERIOD, 11 | DOUBLE_SEMI, 12 | END, 13 | EQUALS, 14 | EXCLAMATION, 15 | FILL, 16 | IDENTIFIER, 17 | LEFT_ANGLE, 18 | LEFT_BRACE, 19 | LEFT_BRACKET, 20 | LEFT_PAREN, 21 | MINUS, 22 | MULTIPLY, 23 | NOTE, 24 | NUMBER, 25 | PERCENT, 26 | PERIOD, 27 | PIPE, 28 | PLUS, 29 | POUND, 30 | QUESTION, 31 | QUOTE, 32 | REPEAT, 33 | RIGHT_ANGLE, 34 | RIGHT_BRACE, 35 | RIGHT_BRACKET, 36 | RIGHT_PAREN, 37 | SEMI, 38 | SINGLE_QUOTE, 39 | START, 40 | UNDERSCORE, 41 | VARIABLE_NAME, 42 | } 43 | 44 | // todo: this will get trickier and trickier to maintain as complexity increases. Remove this and replace with per node validation instead (each node validates its contents). 45 | var expects = [ 46 | { 47 | token: TokenType.START, 48 | expects: [TokenType.IDENTIFIER] 49 | }, 50 | { 51 | token: TokenType.PIPE, 52 | expects: [TokenType.IDENTIFIER] 53 | }, 54 | { 55 | token: TokenType.SEMI, 56 | expects: [TokenType.IDENTIFIER] 57 | }, 58 | { 59 | token: TokenType.DOUBLE_SEMI, 60 | expects: [TokenType.IDENTIFIER] 61 | }, 62 | { 63 | token: TokenType.DOUBLE_PERIOD, 64 | expects: [TokenType.NUMBER] 65 | }, 66 | { 67 | token: TokenType.IDENTIFIER, 68 | expects: [TokenType.IDENTIFIER, TokenType.LEFT_PAREN, TokenType.SEMI, TokenType.DOUBLE_SEMI, TokenType.PIPE] 69 | }, 70 | { 71 | token: TokenType.LEFT_PAREN, 72 | expects: [TokenType.NUMBER, TokenType.NOTE, TokenType.IDENTIFIER, TokenType.UNDERSCORE, TokenType.MULTIPLY, TokenType.LEFT_PAREN] 73 | }, 74 | { 75 | token: TokenType.RIGHT_PAREN, 76 | expects: [TokenType.IDENTIFIER, TokenType.RIGHT_PAREN, TokenType.COMMA, TokenType.SEMI, TokenType.DOUBLE_SEMI, TokenType.PIPE, TokenType.REPEAT, TokenType.RIGHT_ANGLE] 77 | }, 78 | { 79 | token: TokenType.NUMBER, 80 | expects: [TokenType.COMMA, TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN, TokenType.SINGLE_QUOTE, TokenType.REPEAT, TokenType.DOUBLE_PERIOD, TokenType.RIGHT_ANGLE] 81 | }, 82 | { 83 | token: TokenType.COMMA, 84 | expects: [TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.UNDERSCORE, TokenType.MULTIPLY, TokenType.NOTE] 85 | } 86 | ]; 87 | 88 | export var TokenTypeExpect = createLookupTable>(expects, 'token', 'expects'); 89 | 90 | export function getExpectsString(expects: Array): string { 91 | var result: string = ''; 92 | for (var i = 0; i < expects.length; i++) { 93 | result += TokenType[expects[i]] + (i !== expects.length - 1 ? ', ' : ''); 94 | } 95 | return result; 96 | } 97 | -------------------------------------------------------------------------------- /recurse-app/app/src/interpreter/ValueType.ts: -------------------------------------------------------------------------------- 1 | export enum ValueType { 2 | FILL, 3 | INTERVAL, 4 | NOTE, 5 | PITCH_OFFSET, 6 | SCALE_DEGREE, 7 | RAW_NOTE, // MIDI pitch numbers 0-127. Currently not in use - replaced by scale_degree 8 | REST, 9 | SELECT_INDEX, 10 | VARIABLE,// special type for variable contents - so that same sequence can mean different things, e.g. intervals in rm-context and pitch offset inside a pitch declaration. 11 | VELOCITY 12 | } -------------------------------------------------------------------------------- /recurse-app/app/src/recurse-cli.ts: -------------------------------------------------------------------------------- 1 | import program = require('commander'); 2 | import fs = require('fs'); 3 | var watch = require('node-watch'); 4 | 5 | import {IClip} from "./converter/IClip"; 6 | import {Compiler} from "./compiler/Compiler"; 7 | import {RecurseResult} from "./core/type/RecurseResult"; 8 | import {RecurseStatus} from "./core/type/RecurseStatus"; 9 | import {ErrorMessages} from "./compiler/ErrorMessages"; 10 | 11 | program 12 | .version('0.0.1') 13 | .usage('[options] [file]') 14 | .option('-p, --preview', 'Compile and output to console only') 15 | .option('-d, --debug', 'Log debug info to console') 16 | .option('-w, --watch', 'Watch source file and recompile on changes') 17 | .parse(process.argv); 18 | 19 | if (program['args'].length > 0) { 20 | var file: string = program['args'][0], 21 | preview: boolean = program['preview'] || false, 22 | debug: boolean = program['debug'] || false; 23 | 24 | console.log('compiling', file); 25 | 26 | doCompile(file, preview, debug); 27 | 28 | if (program['watch']) { 29 | watch(file, function(filename) { 30 | console.log(filename, ' changed.'); 31 | doCompile(filename, preview, debug); 32 | }); 33 | } 34 | } else { 35 | console.log('No arguments given, exiting'); 36 | } 37 | 38 | function doCompile(filename, preview, debug) { 39 | var compiler = new Compiler(), 40 | result: RecurseResult = new RecurseResult(); 41 | 42 | compiler.setPreview(preview); 43 | compiler.setDebug(debug); 44 | 45 | fs.readFile(filename, (err, code) => { 46 | if (!err) { 47 | result = compiler.compile(code.toString()); 48 | } else { 49 | result.status = RecurseStatus.ERROR; 50 | result.error = ErrorMessages.getError(ErrorMessages.FILE_READ_ERROR, file); 51 | } 52 | console.log(result); 53 | }); 54 | } -------------------------------------------------------------------------------- /recurse-app/app/test.rse: -------------------------------------------------------------------------------- 1 | scale(cmajor) rm(6) ns(c3) transpose(0 1 2 3 4 5 6 7 8 9 10) -------------------------------------------------------------------------------- /recurse-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecode", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "^0.4.5", 7 | "grunt-ts": "5.x" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /recurse-app/test-snippets/repo.rse: -------------------------------------------------------------------------------- 1 | 2 | not actually runnable, just a repo of various sequences 3 | 4 | rm(4,4(1x4),_12,4,8(1x6)) ns(c1);; 5 | rm(_16,12(1x4),4(_1,3)) ns(d1);; 6 | rm(2x6,16(4x11),4(1x6)) ns(f#1);; 7 | rm(_4,12(1x11),8(1x6)) ns(a1) 8 | 9 | 10 | rm(4,4(1x4),_12,4,8(1x6)) ns(c1);; 11 | rm(_16,12(1x4),4(_1,3)) ns(d1);; 12 | rm(_8,4,6,8,4,6,rnd(2,4,6)) ns(f1);; 13 | rm(_3,3,_3,5,_7,1) ns(g#1) 14 | 15 | 16 | rm(3,1x2,4,5,2,1x2,*) ns(g#1) length(32) loop(3);; 17 | rm(_6,6'8,8'6,12(10x3,9x5,8x4,7x3,6x2)) ns(d1) length(128) 18 | 19 | 20 | rm(8(1'2,2,3,4),8,8(4,4'4'2,2,1'2'4),8) ns(c1) length(128);; 21 | rm(8(4,3'4,2'1,2),8,8(2'3,2'1,3,4),8) ns(b1) length(128);; 22 | rm(_8,4,4,4) ns(a#1) loop(2);; 23 | rm(8,_8) ns(a1) loop(2);; 24 | rm(2x4,_4) ns(f1) loop(2) 25 | 26 | 27 | rm(8,4(1x4),4(1x2)) length(48) ns(c1,c#1,c#1,c#1,d1,e1,a1); 28 | rm(4(1,2,3,4,5),4(1,2,3,4,5,6),4(1,2,3,4)) ns(c1,c1,c1,c1,c1,c1,d1) length(16) 29 | 30 | 31 | -------------------------------------------------------------------------------- /recurse-app/test-snippets/test.rse: -------------------------------------------------------------------------------- 1 | rm(4,4,3,5,3) ns(c1) length(32);; 2 | rm(12(_4,4,_4,_4),6(4(1x3),4(1x1))) ns(c2,d2,g2,c#3,e3) -------------------------------------------------------------------------------- /recurse-app/testCode/invalid/unmatchingParentheses.rse: -------------------------------------------------------------------------------- 1 | rm(alt(1,2,3) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/alt.rse: -------------------------------------------------------------------------------- 1 | rm(alt(8 16 32)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/alt.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 2, 7 | "velocity": 127 8 | }, 9 | { 10 | "start": 2, 11 | "duration": 4, 12 | "velocity": 127 13 | }, 14 | { 15 | "start": 6, 16 | "duration": 8, 17 | "velocity": 127 18 | }, 19 | { 20 | "start": 14, 21 | "duration": 2, 22 | "velocity": 127 23 | } 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/altShorthand.rse: -------------------------------------------------------------------------------- 1 | rm(8'16'32) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/altShorthand.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 2 7 | }, 8 | { 9 | "start": 2, 10 | "duration": 4 11 | }, 12 | { 13 | "start": 6, 14 | "duration": 8 15 | }, 16 | { 17 | "start": 14, 18 | "duration": 2 19 | } 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/altWithNested.rse: -------------------------------------------------------------------------------- 1 | rm(16'16'16'16(1x4)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/altWithNested.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "duration": 4 }, 5 | { "duration": 4 }, 6 | { "duration": 4 }, 7 | { "duration": 1 }, 8 | { "duration": 1 }, 9 | { "duration": 1 }, 10 | { "duration": 1 } 11 | ] 12 | } 13 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/alternateWithRests.rse: -------------------------------------------------------------------------------- 1 | rm(4'_8'4 16 4'_8) ns(c5) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/alternateWithRests.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 1 7 | }, 8 | { 9 | "start": 1, 10 | "duration": 4 11 | }, 12 | { 13 | "start": 5, 14 | "duration": 1 15 | }, 16 | { 17 | "start": 8, 18 | "duration": 4 19 | }, 20 | { 21 | "start": 14, 22 | "duration": 1 23 | }, 24 | { 25 | "start": 15, 26 | "duration": 1 27 | } 28 | ] 29 | } 30 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/comments.rse: -------------------------------------------------------------------------------- 1 | // hei 2 | rm(16 48) ns(c3) loop(2);; 3 | // hei 4 | // hoppsan soppsan 5 | rm(48 16) ns(d3) 6 | // hopp 7 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/comments.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 12, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 16, 16 | "duration": 4, 17 | "pitch": 60 18 | }, 19 | { 20 | "start": 20, 21 | "duration": 12, 22 | "pitch": 60 23 | } 24 | ] 25 | }, 26 | { 27 | "notes": [ 28 | { 29 | "start": 0, 30 | "duration": 12, 31 | "pitch": 62 32 | }, 33 | { 34 | "start": 12, 35 | "duration": 4, 36 | "pitch": 62 37 | } 38 | ] 39 | } 40 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/cutBleedingNotes.rse: -------------------------------------------------------------------------------- 1 | rm(16 32 15) ns(c3); 2 | rm(_16 32 15) ns(c4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/cutBleedingNotes.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 8, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 12, 16 | "duration": 3.75, 17 | "pitch": 60 18 | }, 19 | { 20 | "start": 15.75, 21 | "duration": 0.25, 22 | "pitch": 60 23 | }, 24 | { 25 | "start": 4, 26 | "duration": 8, 27 | "pitch": 72 28 | }, 29 | { 30 | "start": 12, 31 | "duration": 3.75, 32 | "pitch": 72 33 | } 34 | ] 35 | } 36 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fill.rse: -------------------------------------------------------------------------------- 1 | rm(1,2,_*) ns(c4,d4) | rm(4,8,_*) ns(e4,f#4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fill.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 0.25, 7 | "pitch": 72 8 | }, 9 | { 10 | "start": 0.25, 11 | "duration": 0.5, 12 | "pitch": 74 13 | }, 14 | { 15 | "start": 16, 16 | "duration": 1, 17 | "pitch": 76 18 | }, 19 | { 20 | "start": 17, 21 | "duration": 2, 22 | "pitch": 78 23 | } 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fillFirst.rse: -------------------------------------------------------------------------------- 1 | rm(_* 4 2) ns(c4 d4 e4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fillFirst.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 14.5, 6 | "duration": 1, 7 | "pitch": 72 8 | }, 9 | { 10 | "start": 15.5, 11 | "duration": 0.5, 12 | "pitch": 74 13 | } 14 | ] 15 | } 16 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fillMid.rse: -------------------------------------------------------------------------------- 1 | rm(2 _* 4) ns(c4 d4 e4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/fillMid.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 0.5, 7 | "pitch": 72 8 | }, 9 | { 10 | "start": 15, 11 | "duration": 1, 12 | "pitch": 74 13 | } 14 | ] 15 | } 16 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/interpolate.rse: -------------------------------------------------------------------------------- 1 | rm(1>4 6) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/length.rse: -------------------------------------------------------------------------------- 1 | rm(16 32 16) ns(c3) length(70); 2 | rm(16 32 16 8) ns(c4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/length.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 8, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 12, 16 | "duration": 4, 17 | "pitch": 60 18 | }, 19 | { 20 | "start": 16, 21 | "duration": 1.5, 22 | "pitch": 60 23 | }, 24 | { 25 | "start": 0, 26 | "duration": 4, 27 | "pitch": 72 28 | }, 29 | { 30 | "start": 4, 31 | "duration": 8, 32 | "pitch": 72 33 | }, 34 | { 35 | "start": 12, 36 | "duration": 4, 37 | "pitch": 72 38 | } 39 | ] 40 | } 41 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/lengthMultipleClips.rse: -------------------------------------------------------------------------------- 1 | length(64) rm(16 16) ns(c3) 2 | ;; 3 | length(32) rm(16 16) ns(c3) 4 | ;; 5 | length(64) rm(16 16) ns(c3) 6 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/lengthMultipleClips.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "loopLength": 16, 4 | "notes": [ 5 | { "duration": 4 }, 6 | { "duration": 4 }, 7 | { "duration": 4 }, 8 | { "duration": 4 } 9 | ] 10 | }, 11 | { 12 | "loopLength": 8, 13 | "notes": [ 14 | { "duration": 4 }, 15 | { "duration": 4 } 16 | ] 17 | }, 18 | { 19 | "loopLength": 16, 20 | "notes": [ 21 | { "duration": 4 }, 22 | { "duration": 4 }, 23 | { "duration": 4 }, 24 | { "duration": 4 } 25 | ] 26 | } 27 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/loopFactor.rse: -------------------------------------------------------------------------------- 1 | rm(16 48) ns(c3 c4) loop(2) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/loopFactor.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 12, 12 | "pitch": 72 13 | }, 14 | { 15 | "start": 16, 16 | "duration": 4, 17 | "pitch": 60 18 | }, 19 | { 20 | "start": 20, 21 | "duration": 12, 22 | "pitch": 72 23 | } 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multipleClips.rse: -------------------------------------------------------------------------------- 1 | rm(16 48) ns(c3) loop(2);; 2 | rm(48 16) ns(d3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multipleClips.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 12, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 16, 16 | "duration": 4, 17 | "pitch": 60 18 | }, 19 | { 20 | "start": 20, 21 | "duration": 12, 22 | "pitch": 60 23 | } 24 | ] 25 | }, 26 | { 27 | "notes": [ 28 | { 29 | "start": 0, 30 | "duration": 12, 31 | "pitch": 62 32 | }, 33 | { 34 | "start": 12, 35 | "duration": 4, 36 | "pitch": 62 37 | } 38 | ] 39 | } 40 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multipleTracks.rse: -------------------------------------------------------------------------------- 1 | rm(16 48) ns(c3) | 2 | rm(48 16) ns(d3) ; 3 | rm(32 32) ns(c4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multipleTracks.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 12, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 16, 16 | "duration": 12, 17 | "pitch": 62 18 | }, 19 | { 20 | "start": 28, 21 | "duration": 4, 22 | "pitch": 62 23 | }, 24 | { 25 | "start": 0, 26 | "duration": 8, 27 | "pitch": 72 28 | }, 29 | { 30 | "start": 8, 31 | "duration": 8, 32 | "pitch": 72 33 | } 34 | ] 35 | } 36 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multiply.rse: -------------------------------------------------------------------------------- 1 | rm(128*0.5(1x4)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/multiply.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "duration": 4 }, 5 | { "duration": 4 }, 6 | { "duration": 4 }, 7 | { "duration": 4 } 8 | ] 9 | } 10 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nested.rse: -------------------------------------------------------------------------------- 1 | rm(16 16(1 1) 16(1(1) 1(1)) 16(1(1(1)) 1(1(1)))) ns(c3 (d3 e3) c3(c3(f3) c3(g3)) c3(c3(c3(a3)) (c3(c3(b3))))) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nested.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 60 7 | }, 8 | { 9 | "start": 4, 10 | "pitch": 62 11 | }, 12 | { 13 | "start": 6, 14 | "pitch": 64 15 | }, 16 | { 17 | "start": 8, 18 | "pitch": 65 19 | }, 20 | { 21 | "start": 10, 22 | "pitch": 67 23 | }, 24 | { 25 | "start": 12, 26 | "pitch": 69 27 | }, 28 | { 29 | "start": 14, 30 | "pitch": 71 31 | } 32 | ] 33 | } 34 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedSimple.rse: -------------------------------------------------------------------------------- 1 | rm(48(1x3), 16) ns((c3), c4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedSimple.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 60 7 | }, 8 | { 9 | "start": 4, 10 | "pitch": 60 11 | }, 12 | { 13 | "start": 8, 14 | "pitch": 60 15 | }, 16 | { 17 | "start": 12, 18 | "pitch": 72 19 | } 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithAlts.rse: -------------------------------------------------------------------------------- 1 | rm(32(alt(1 1x2)) 32( 1(alt(1x2 1)) 1(alt(1 1x2)) ) ) ns(c3 (alt((c4) c5) d4)) 2 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithAlts.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 60 7 | }, 8 | { 9 | "start": 8, 10 | "pitch": 72 11 | }, 12 | { 13 | "start": 10, 14 | "pitch": 72 15 | }, 16 | { 17 | "start": 12, 18 | "pitch": 74 19 | } 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithInterpolateHead.rse: -------------------------------------------------------------------------------- 1 | length(27) rm(8>10(1x2)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithInterpolateHead.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "duration": 1 }, 5 | { "duration": 1 }, 6 | { "duration": 1.125 }, 7 | { "duration": 1.125 }, 8 | { "duration": 1.25 }, 9 | { "duration": 1.25 } 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithRangeHead.rse: -------------------------------------------------------------------------------- 1 | length(27) rm(8..10(1x2)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithRangeHead.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "duration": 1 }, 5 | { "duration": 1 }, 6 | { "duration": 1.125 }, 7 | { "duration": 1.125 }, 8 | { "duration": 1.25 }, 9 | { "duration": 1.25 } 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithRepeat.rse: -------------------------------------------------------------------------------- 1 | rm(64(1x8)) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/nestedWithRepeat.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 2 7 | }, 8 | { 9 | "start": 2, 10 | "duration": 2 11 | }, 12 | { 13 | "start": 4, 14 | "duration": 2 15 | }, 16 | { 17 | "start": 6, 18 | "duration": 2 19 | }, 20 | { 21 | "start": 8, 22 | "duration": 2 23 | }, 24 | { 25 | "start": 10, 26 | "duration": 2 27 | }, 28 | { 29 | "start": 12, 30 | "duration": 2 31 | }, 32 | { 33 | "start": 14, 34 | "duration": 2 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/noteSetWithScaleDegrees.rse: -------------------------------------------------------------------------------- 1 | scale(cmajor) rm(4) ns(-15 -14 -8 -7 -6 -4 -2 -1 0 1 2 5 6 7 8 9) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/noteSetWithScaleDegrees.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | {"pitch": 35}, 5 | {"pitch": 36}, 6 | {"pitch": 47}, 7 | {"pitch": 48}, 8 | {"pitch": 50}, 9 | {"pitch": 53}, 10 | {"pitch": 57}, 11 | {"pitch": 59}, 12 | {"pitch": 60}, 13 | {"pitch": 60}, 14 | {"pitch": 62}, 15 | {"pitch": 67}, 16 | {"pitch": 69}, 17 | {"pitch": 71}, 18 | {"pitch": 72}, 19 | {"pitch": 74} 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/patternLengthVisibleInOutput.rse: -------------------------------------------------------------------------------- 1 | length(128) rm(64) ns(c3);; 2 | length(64) rm(64) ns(c3);; 3 | length(32) rm(32) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/patternLengthVisibleInOutput.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | {"start":0}, 5 | {"start":16} 6 | ], 7 | "loopLength": 32 8 | }, 9 | { 10 | "notes": [ 11 | {"start":0} 12 | ], 13 | "loopLength": 16 14 | }, 15 | { 16 | "notes": [ 17 | {"start":0} 18 | ], 19 | "loopLength": 8 20 | } 21 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchEven.rse: -------------------------------------------------------------------------------- 1 | rm(8) ns(c3) even pitch(1 2 3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchEven.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 60 7 | }, 8 | { 9 | "start": 2, 10 | "pitch": 61 11 | }, 12 | { 13 | "start": 4, 14 | "pitch": 60 15 | }, 16 | { 17 | "start": 6, 18 | "pitch": 62 19 | }, 20 | { 21 | "start": 8, 22 | "pitch": 60 23 | }, 24 | { 25 | "start": 10, 26 | "pitch": 63 27 | }, 28 | { 29 | "start": 12, 30 | "pitch": 60 31 | }, 32 | { 33 | "start": 14, 34 | "pitch": 61 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchOdd.rse: -------------------------------------------------------------------------------- 1 | rm(8) ns(c1) odd pitch(1 2 3) end pitch(24) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchOdd.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 61 7 | }, 8 | { 9 | "start": 2, 10 | "pitch": 60 11 | }, 12 | { 13 | "start": 4, 14 | "pitch": 62 15 | }, 16 | { 17 | "start": 6, 18 | "pitch": 60 19 | }, 20 | { 21 | "start": 8, 22 | "pitch": 63 23 | }, 24 | { 25 | "start": 10, 26 | "pitch": 60 27 | }, 28 | { 29 | "start": 12, 30 | "pitch": 61 31 | }, 32 | { 33 | "start": 14, 34 | "pitch": 60 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchPlus.rse: -------------------------------------------------------------------------------- 1 | rm(8) ns(c3 d3 e3) +pitch(1 2 3 4) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/pitchPlus.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 61 7 | }, 8 | { 9 | "start": 2, 10 | "pitch": 64 11 | }, 12 | { 13 | "start": 4, 14 | "pitch": 67 15 | }, 16 | { 17 | "start": 6, 18 | "pitch": 64 19 | }, 20 | { 21 | "start": 8, 22 | "pitch": 64 23 | }, 24 | { 25 | "start": 10, 26 | "pitch": 68 27 | }, 28 | { 29 | "start": 12, 30 | "pitch": 66 31 | }, 32 | { 33 | "start": 14, 34 | "pitch": 70 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/postAndPreRest.rse: -------------------------------------------------------------------------------- 1 | length(32) rm(_8 4 _4) ns(c#2) 2 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/postAndPreRest.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 2, 6 | "duration": 1 7 | }, 8 | { 9 | "start": 6, 10 | "duration": 1 11 | } 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/range.rse: -------------------------------------------------------------------------------- 1 | rm(rng(1 4) _*) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/range.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 0.25 7 | }, 8 | { 9 | "start": 0.25, 10 | "duration": 0.50 11 | }, 12 | { 13 | "start": 0.75, 14 | "duration": 0.75 15 | }, 16 | { 17 | "start": 1.5, 18 | "duration": 1 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/rangeShorthand.rse: -------------------------------------------------------------------------------- 1 | rm(1..4 _*) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/rangeShorthand.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 0.25 7 | }, 8 | { 9 | "start": 0.25, 10 | "duration": 0.50 11 | }, 12 | { 13 | "start": 0.75, 14 | "duration": 0.75 15 | }, 16 | { 17 | "start": 1.5, 18 | "duration": 1 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/repeatNotes.rse: -------------------------------------------------------------------------------- 1 | rm(4) ns(c2x2 c3x2) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/rest.rse: -------------------------------------------------------------------------------- 1 | rm(_8 4) ns(c4 c5) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/rest.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 2, 6 | "duration": 1, 7 | "pitch": 72 8 | }, 9 | { 10 | "start": 5, 11 | "duration": 1, 12 | "pitch": 84 13 | }, 14 | { 15 | "start": 8, 16 | "duration": 1, 17 | "pitch": 72 18 | }, 19 | { 20 | "start": 11, 21 | "duration": 1, 22 | "pitch": 84 23 | }, 24 | { 25 | "start": 14, 26 | "duration": 1, 27 | "pitch": 72 28 | } 29 | ] 30 | } 31 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/restShouldNotStealNote.rse: -------------------------------------------------------------------------------- 1 | rm(14(4 _4 6)) ns(c3 d3 e3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/restShouldNotStealNote.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "pitch": 60 }, 5 | { "pitch": 62 }, 6 | { "pitch": 64 }, 7 | { "pitch": 60 }, 8 | { "pitch": 62 }, 9 | { "pitch": 64 }, 10 | { "pitch": 60 }, 11 | { "pitch": 62 }, 12 | { "pitch": 64 } 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectAdditive.rse: -------------------------------------------------------------------------------- 1 | length(32) rm(4 4 8 16) ns(c3) even even pitch(12) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectAdditive.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "start": 0, "pitch": 60 }, 5 | { "start": 1, "pitch": 60 }, 6 | { "start": 2, "pitch": 60 }, 7 | { "start": 4, "pitch": 72 } 8 | ] 9 | } 10 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectByIndexList.rse: -------------------------------------------------------------------------------- 1 | rm(4) ns(c3) select(0 1 5 6 8 16 17) pitch(12) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectByIndexList.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "pitch": 72 7 | }, 8 | { 9 | "start": 1, 10 | "pitch": 60 11 | }, 12 | { 13 | "start": 2, 14 | "pitch": 60 15 | }, 16 | { 17 | "start": 3, 18 | "pitch": 60 19 | }, 20 | { 21 | "start": 4, 22 | "pitch": 72 23 | }, 24 | { 25 | "start": 5, 26 | "pitch": 72 27 | }, 28 | { 29 | "start": 6, 30 | "pitch": 60 31 | }, 32 | { 33 | "start": 7, 34 | "pitch": 72 35 | }, 36 | { 37 | "start": 8, 38 | "pitch": 60 39 | }, 40 | { 41 | "start": 9, 42 | "pitch": 60 43 | }, 44 | { 45 | "start": 10, 46 | "pitch": 60 47 | }, 48 | { 49 | "start": 11, 50 | "pitch": 60 51 | }, 52 | { 53 | "start": 12, 54 | "pitch": 60 55 | }, 56 | { 57 | "start": 13, 58 | "pitch": 60 59 | }, 60 | { 61 | "start": 14, 62 | "pitch": 60 63 | }, 64 | { 65 | "start": 15, 66 | "pitch": 72 67 | } 68 | ] 69 | } 70 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectFirst.rse: -------------------------------------------------------------------------------- 1 | rm(16,32,16) ns(c3) first pitch(12) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectFirst.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 72 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 8, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 12, 16 | "duration": 4, 17 | "pitch": 60 18 | } 19 | ] 20 | } 21 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectLast.rse: -------------------------------------------------------------------------------- 1 | rm(16 32 16) ns(c3) last pitch(12) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/selectLast.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 4, 7 | "pitch": 60 8 | }, 9 | { 10 | "start": 4, 11 | "duration": 8, 12 | "pitch": 60 13 | }, 14 | { 15 | "start": 12, 16 | "duration": 4, 17 | "pitch": 72 18 | } 19 | ] 20 | } 21 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/transpose.rse: -------------------------------------------------------------------------------- 1 | scale(cmajor) rm(6) ns(c3) transpose(0 1 2 3 4 5 6 7 8 9 10) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/transpose.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { "pitch": 60 }, 5 | { "pitch": 60 }, 6 | { "pitch": 62 }, 7 | { "pitch": 64 }, 8 | { "pitch": 65 }, 9 | { "pitch": 67 }, 10 | { "pitch": 69 }, 11 | { "pitch": 71 }, 12 | { "pitch": 72 }, 13 | { "pitch": 74 }, 14 | { "pitch": 76 } 15 | ] 16 | } 17 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variables.rse: -------------------------------------------------------------------------------- 1 | $myVar=(4 8 4) 2 | rm($myVar x4) ns(c3) pitch($myVar) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variables.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 1, 7 | "pitch": 64 8 | }, 9 | { 10 | "start": 1, 11 | "duration": 2, 12 | "pitch": 68 13 | }, 14 | { 15 | "start": 3, 16 | "duration": 1, 17 | "pitch": 64 18 | }, 19 | { 20 | "start": 4, 21 | "duration": 1, 22 | "pitch": 64 23 | }, 24 | { 25 | "start": 5, 26 | "duration": 2, 27 | "pitch": 68 28 | }, 29 | { 30 | "start": 7, 31 | "duration": 1, 32 | "pitch": 64 33 | }, 34 | { 35 | "start": 8, 36 | "duration": 1, 37 | "pitch": 64 38 | }, 39 | { 40 | "start": 9, 41 | "duration": 2, 42 | "pitch": 68 43 | }, 44 | { 45 | "start": 11, 46 | "duration": 1, 47 | "pitch": 64 48 | }, 49 | { 50 | "start": 12, 51 | "duration": 1, 52 | "pitch": 64 53 | }, 54 | { 55 | "start": 13, 56 | "duration": 2, 57 | "pitch": 68 58 | }, 59 | { 60 | "start": 15, 61 | "duration": 1, 62 | "pitch": 64 63 | } 64 | ] 65 | } 66 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variablesInline.rse: -------------------------------------------------------------------------------- 1 | rm($myVar=(8 8) $myVar x4) ns(c3) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variablesInline.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 2 7 | }, 8 | { 9 | "start": 2, 10 | "duration": 2 11 | }, 12 | { 13 | "start": 4, 14 | "duration": 2 15 | }, 16 | { 17 | "start": 6, 18 | "duration": 2 19 | }, 20 | { 21 | "start": 8, 22 | "duration": 2 23 | }, 24 | { 25 | "start": 10, 26 | "duration": 2 27 | }, 28 | { 29 | "start": 12, 30 | "duration": 2 31 | }, 32 | { 33 | "start": 14, 34 | "duration": 2 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variablesWithNested.rse: -------------------------------------------------------------------------------- 1 | $myVar=(16(4 8 4)x4) 2 | $myVar2=(16(4 8 4)) 3 | rm($myVar) ns(c3) pitch($myVar2) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/variablesWithNested.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "start": 0, 6 | "duration": 1, 7 | "pitch": 64 8 | }, 9 | { 10 | "start": 1, 11 | "duration": 2, 12 | "pitch": 68 13 | }, 14 | { 15 | "start": 3, 16 | "duration": 1, 17 | "pitch": 64 18 | }, 19 | { 20 | "start": 4, 21 | "duration": 1, 22 | "pitch": 64 23 | }, 24 | { 25 | "start": 5, 26 | "duration": 2, 27 | "pitch": 68 28 | }, 29 | { 30 | "start": 7, 31 | "duration": 1, 32 | "pitch": 64 33 | }, 34 | { 35 | "start": 8, 36 | "duration": 1, 37 | "pitch": 64 38 | }, 39 | { 40 | "start": 9, 41 | "duration": 2, 42 | "pitch": 68 43 | }, 44 | { 45 | "start": 11, 46 | "duration": 1, 47 | "pitch": 64 48 | }, 49 | { 50 | "start": 12, 51 | "duration": 1, 52 | "pitch": 64 53 | }, 54 | { 55 | "start": 13, 56 | "duration": 2, 57 | "pitch": 68 58 | }, 59 | { 60 | "start": 15, 61 | "duration": 1, 62 | "pitch": 64 63 | } 64 | ] 65 | } 66 | ] -------------------------------------------------------------------------------- /recurse-app/testCode/valid/vel.rse: -------------------------------------------------------------------------------- 1 | rm(16) ns(c3) vel(80 90 100 130) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/vel.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "velocity": 80 6 | }, 7 | { 8 | "velocity": 90 9 | }, 10 | { 11 | "velocity": 100 12 | }, 13 | { 14 | "velocity": 127 15 | } 16 | ] 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /recurse-app/testCode/valid/velNested.rse: -------------------------------------------------------------------------------- 1 | rm(48(1x3) 16) ns((c3) d3) vel((80) 120) -------------------------------------------------------------------------------- /recurse-app/testCode/valid/velNested.rse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "notes": [ 4 | { 5 | "pitch": 60, 6 | "velocity": 80 7 | }, 8 | { 9 | "pitch": 60, 10 | "velocity": 80 11 | }, 12 | { 13 | "pitch": 60, 14 | "velocity": 80 15 | }, 16 | { 17 | "pitch": 62, 18 | "velocity": 120 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /recurse-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "noImplicitAny": false, 6 | "outDir": "app/compiled-scripts", 7 | "rootDir": "app/src", 8 | "sourceMap": false 9 | }, 10 | "filesGlob": [ 11 | "typings/tsd.d.ts", 12 | "app/src/**/*.ts" 13 | ], 14 | "files": [ 15 | "typings/tsd.d.ts", 16 | "app/src/compiler/Compiler.ts", 17 | "app/src/compiler/ErrorMessages.ts", 18 | "app/src/compiler/ICompilerSettings.ts", 19 | "app/src/compiler/compiler.spec.ts", 20 | "app/src/converter/IClip.ts", 21 | "app/src/converter/flatten.ts", 22 | "app/src/core/type/Constants.ts", 23 | "app/src/core/type/IRecurseValue.ts", 24 | "app/src/core/type/Note.ts", 25 | "app/src/core/type/NoteEvent.ts", 26 | "app/src/core/type/RecurseObject.ts", 27 | "app/src/core/type/RecurseResult.ts", 28 | "app/src/core/type/RecurseStatus.ts", 29 | "app/src/core/type/Scale.ts", 30 | "app/src/core/util/Helpers.ts", 31 | "app/src/core/util/convertNoteListToRecurseCode.spec.ts", 32 | "app/src/core/util/convertNoteListToRecurseCode.ts", 33 | "app/src/core/util/createLookupTable.ts", 34 | "app/src/core/util/forEachPitch.ts", 35 | "app/src/core/util/forEachSelectedPitch.ts", 36 | "app/src/function/IContext.ts", 37 | "app/src/function/SyntaxTree.ts", 38 | "app/src/function/base/Nested.ts", 39 | "app/src/function/base/Root.ts", 40 | "app/src/function/base/Value.ts", 41 | "app/src/function/base/Variable.ts", 42 | "app/src/function/base/VariableReference.ts", 43 | "app/src/function/generator/NoteSet.ts", 44 | "app/src/function/generator/RhythmicMotive.ts", 45 | "app/src/function/generator/VelocitySet.ts", 46 | "app/src/function/modifier/Pitch.ts", 47 | "app/src/function/modifier/Transpose.ts", 48 | "app/src/function/operator/Alternate.ts", 49 | "app/src/function/operator/Chain.ts", 50 | "app/src/function/operator/Fill.ts", 51 | "app/src/function/operator/GenericOperator.ts", 52 | "app/src/function/operator/Interpolate.spec.ts", 53 | "app/src/function/operator/Interpolate.ts", 54 | "app/src/function/operator/Multiply.ts", 55 | "app/src/function/operator/Random.ts", 56 | "app/src/function/operator/Range.ts", 57 | "app/src/function/operator/Repeat.ts", 58 | "app/src/function/selection/EndSelect.ts", 59 | "app/src/function/selection/Select.ts", 60 | "app/src/function/selection/SelectStrategy.ts", 61 | "app/src/function/setter/Loop.ts", 62 | "app/src/function/setter/PatternLength.ts", 63 | "app/src/function/setter/SetScale.ts", 64 | "app/src/interpreter/Entity.ts", 65 | "app/src/interpreter/INode.ts", 66 | "app/src/interpreter/ISetting.ts", 67 | "app/src/interpreter/ISyntaxTree.ts", 68 | "app/src/interpreter/IToken.ts", 69 | "app/src/interpreter/Lexer.spec.ts", 70 | "app/src/interpreter/Lexer.ts", 71 | "app/src/interpreter/Parser.spec.ts", 72 | "app/src/interpreter/Parser.ts", 73 | "app/src/interpreter/TokenType.ts", 74 | "app/src/interpreter/ValueType.ts", 75 | "app/src/recurse-cli.ts" 76 | ] 77 | } -------------------------------------------------------------------------------- /recurse-app/typings/commander/commander.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for commanderjs 2.3.0 2 | // Project: https://github.com/visionmedia/commander.js 3 | // Definitions by: Marcelo Dezem , vvakame 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare module commander { 9 | interface ICommandStatic { 10 | /** 11 | * Initialize a new `Command`. 12 | * 13 | * @param {String} name 14 | * @api public 15 | */ 16 | new (name?:string):ICommand; 17 | } 18 | 19 | interface ICommand extends NodeJS.EventEmitter { 20 | args: string[]; 21 | _args: { required:boolean; name: string; }[]; 22 | 23 | /** 24 | * Add command `name`. 25 | * 26 | * The `.action()` callback is invoked when the 27 | * command `name` is specified via __ARGV__, 28 | * and the remaining arguments are applied to the 29 | * function for access. 30 | * 31 | * When the `name` is "*" an un-matched command 32 | * will be passed as the first arg, followed by 33 | * the rest of __ARGV__ remaining. 34 | * 35 | * Examples: 36 | * 37 | * program 38 | * .version('0.0.1') 39 | * .option('-C, --chdir ', 'change the working directory') 40 | * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') 41 | * .option('-T, --no-tests', 'ignore test hook') 42 | * 43 | * program 44 | * .command('setup') 45 | * .description('run remote setup commands') 46 | * .action(function(){ 47 | * console.log('setup'); 48 | * }); 49 | * 50 | * program 51 | * .command('exec ') 52 | * .description('run the given remote command') 53 | * .action(function(cmd){ 54 | * console.log('exec "%s"', cmd); 55 | * }); 56 | * 57 | * program 58 | * .command('*') 59 | * .description('deploy the given env') 60 | * .action(function(env){ 61 | * console.log('deploying "%s"', env); 62 | * }); 63 | * 64 | * program.parse(process.argv); 65 | * 66 | * @param {String} name 67 | * @param {String} [desc] 68 | * @param {Mixed} [opts] 69 | * @return {Command} the new command 70 | * @api public 71 | */ 72 | command(name:string, desc?:string, opts?: any):ICommand; 73 | 74 | /** 75 | * Add an implicit `help [cmd]` subcommand 76 | * which invokes `--help` for the given command. 77 | * 78 | * @api private 79 | */ 80 | addImplicitHelpCommand():void; 81 | 82 | /** 83 | * Parse expected `args`. 84 | * 85 | * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. 86 | * 87 | * @param {Array} args 88 | * @return {Command} for chaining 89 | * @api public 90 | */ 91 | parseExpectedArgs(args:string[]):ICommand; 92 | 93 | /** 94 | * Register callback `fn` for the command. 95 | * 96 | * Examples: 97 | * 98 | * program 99 | * .command('help') 100 | * .description('display verbose help') 101 | * .action(function(){ 102 | * // output help here 103 | * }); 104 | * 105 | * @param {Function} fn 106 | * @return {Command} for chaining 107 | * @api public 108 | */ 109 | action(fn:(...args:any[])=>void):ICommand; 110 | 111 | /** 112 | * Define option with `flags`, `description` and optional 113 | * coercion `fn`. 114 | * 115 | * The `flags` string should contain both the short and long flags, 116 | * separated by comma, a pipe or space. The following are all valid 117 | * all will output this way when `--help` is used. 118 | * 119 | * "-p, --pepper" 120 | * "-p|--pepper" 121 | * "-p --pepper" 122 | * 123 | * Examples: 124 | * 125 | * // simple boolean defaulting to false 126 | * program.option('-p, --pepper', 'add pepper'); 127 | * 128 | * --pepper 129 | * program.pepper 130 | * // => Boolean 131 | * 132 | * // simple boolean defaulting to true 133 | * program.option('-C, --no-cheese', 'remove cheese'); 134 | * 135 | * program.cheese 136 | * // => true 137 | * 138 | * --no-cheese 139 | * program.cheese 140 | * // => false 141 | * 142 | * // required argument 143 | * program.option('-C, --chdir ', 'change the working directory'); 144 | * 145 | * --chdir /tmp 146 | * program.chdir 147 | * // => "/tmp" 148 | * 149 | * // optional argument 150 | * program.option('-c, --cheese [type]', 'add cheese [marble]'); 151 | * 152 | * @param {String} flags 153 | * @param {String} description 154 | * @param {Function|Mixed} fn or default 155 | * @param {Mixed} defaultValue 156 | * @return {Command} for chaining 157 | * @api public 158 | */ 159 | option(flags:string, description?:string, fn?:((arg1:any, arg2:any)=>void)|RegExp, defaultValue?:any):ICommand; 160 | option(flags:string, description?:string, defaultValue?:any):ICommand; 161 | 162 | /** 163 | * Allow unknown options on the command line. 164 | * 165 | * @param {Boolean} arg if `true` or omitted, no error will be thrown 166 | * for unknown options. 167 | * @api public 168 | */ 169 | allowUnknownOption(arg?: boolean):ICommand; 170 | 171 | /** 172 | * Parse `argv`, settings options and invoking commands when defined. 173 | * 174 | * @param {Array} argv 175 | * @return {Command} for chaining 176 | * @api public 177 | */ 178 | parse(argv:string[]):ICommand; 179 | 180 | /** 181 | * Execute a sub-command executable. 182 | * 183 | * @param {Array} argv 184 | * @param {Array} args 185 | * @param {Array} unknown 186 | * @api private 187 | */ 188 | executeSubCommand(argv:string[], args:string[], unknown:string[]):any; /* child_process.ChildProcess */ 189 | 190 | /** 191 | * Normalize `args`, splitting joined short flags. For example 192 | * the arg "-abc" is equivalent to "-a -b -c". 193 | * This also normalizes equal sign and splits "--abc=def" into "--abc def". 194 | * 195 | * @param {Array} args 196 | * @return {Array} 197 | * @api private 198 | */ 199 | normalize(args:string[]):string[]; 200 | 201 | /** 202 | * Parse command `args`. 203 | * 204 | * When listener(s) are available those 205 | * callbacks are invoked, otherwise the "*" 206 | * event is emitted and those actions are invoked. 207 | * 208 | * @param {Array} args 209 | * @return {Command} for chaining 210 | * @api private 211 | */ 212 | parseArgs(args:string[], unknown:string[]):ICommand; 213 | 214 | /** 215 | * Return an option matching `arg` if any. 216 | * 217 | * @param {String} arg 218 | * @return {Option} 219 | * @api private 220 | */ 221 | optionFor(arg:string):IOption; 222 | 223 | /** 224 | * Parse options from `argv` returning `argv` 225 | * void of these options. 226 | * 227 | * @param {Array} argv 228 | * @return {Array} 229 | * @api public 230 | */ 231 | parseOptions(argv:string[]): {args:string[]; unknown:string[];}; 232 | 233 | /** 234 | * Return an object containing options as key-value pairs 235 | * 236 | * @return {Object} 237 | * @api public 238 | */ 239 | opts():any; 240 | 241 | /** 242 | * Argument `name` is missing. 243 | * 244 | * @param {String} name 245 | * @api private 246 | */ 247 | missingArgument(name:string):void; 248 | 249 | /** 250 | * `Option` is missing an argument, but received `flag` or nothing. 251 | * 252 | * @param {String} option 253 | * @param {String} flag 254 | * @api private 255 | */ 256 | optionMissingArgument(option:{flags:string;}, flag?:string):void; 257 | 258 | /** 259 | * Unknown option `flag`. 260 | * 261 | * @param {String} flag 262 | * @api private 263 | */ 264 | unknownOption(flag:string):void; 265 | 266 | /** 267 | * Set the program version to `str`. 268 | * 269 | * This method auto-registers the "-V, --version" flag 270 | * which will print the version number when passed. 271 | * 272 | * @param {String} str 273 | * @param {String} flags 274 | * @return {Command} for chaining 275 | * @api public 276 | */ 277 | version(str:string, flags?:string):ICommand; 278 | 279 | /** 280 | * Set the description to `str`. 281 | * 282 | * @param {String} str 283 | * @return {String|Command} 284 | * @api public 285 | */ 286 | description(str:string):ICommand; 287 | description():string; 288 | 289 | /** 290 | * Set an alias for the command 291 | * 292 | * @param {String} alias 293 | * @return {String|Command} 294 | * @api public 295 | */ 296 | alias(alias:string):ICommand; 297 | alias():string; 298 | 299 | /** 300 | * Set / get the command usage `str`. 301 | * 302 | * @param {String} str 303 | * @return {String|Command} 304 | * @api public 305 | */ 306 | usage(str:string):ICommand; 307 | usage():string; 308 | 309 | /** 310 | * Get the name of the command 311 | * 312 | * @param {String} name 313 | * @return {String|Command} 314 | * @api public 315 | */ 316 | name():string; 317 | 318 | /** 319 | * Return the largest option length. 320 | * 321 | * @return {Number} 322 | * @api private 323 | */ 324 | largestOptionLength():number; 325 | 326 | /** 327 | * Return help for options. 328 | * 329 | * @return {String} 330 | * @api private 331 | */ 332 | optionHelp():string; 333 | 334 | /** 335 | * Return command help documentation. 336 | * 337 | * @return {String} 338 | * @api private 339 | */ 340 | commandHelp():string; 341 | 342 | /** 343 | * Return program help documentation. 344 | * 345 | * @return {String} 346 | * @api private 347 | */ 348 | helpInformation():string; 349 | 350 | /** 351 | * Output help information for this command 352 | * 353 | * @api public 354 | */ 355 | outputHelp():void; 356 | 357 | /** 358 | * Output help information and exit. 359 | * 360 | * @api public 361 | */ 362 | help():void; 363 | } 364 | 365 | interface IOptionStatic { 366 | /** 367 | * Initialize a new `Option` with the given `flags` and `description`. 368 | * 369 | * @param {String} flags 370 | * @param {String} description 371 | * @api public 372 | */ 373 | new (flags:string, description?:string):IOption; 374 | } 375 | 376 | interface IOption { 377 | flags:string; 378 | required:boolean; 379 | optional:boolean; 380 | bool:boolean; 381 | short?:string; 382 | long:string; 383 | description:string; 384 | 385 | /** 386 | * Return option name. 387 | * 388 | * @return {String} 389 | * @api private 390 | */ 391 | name():string; 392 | 393 | /** 394 | * Check if `arg` matches the short or long flag. 395 | * 396 | * @param {String} arg 397 | * @return {Boolean} 398 | * @api private 399 | */ 400 | is(arg:string):boolean; 401 | } 402 | 403 | interface IExportedCommand extends ICommand { 404 | Command: commander.ICommandStatic; 405 | Option: commander.IOptionStatic; 406 | } 407 | } 408 | 409 | declare module "commander" { 410 | var _tmp:commander.IExportedCommand; 411 | export = _tmp; 412 | } 413 | -------------------------------------------------------------------------------- /recurse-app/typings/osc-min/osc-min.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'osc-min' { 2 | var _tmp: any; 3 | export = _tmp; 4 | } 5 | -------------------------------------------------------------------------------- /recurse-app/typings/sprintf-js/sprintf-js.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for sprintf-js 2 | // Project: https://www.npmjs.com/package/sprintf-js 3 | // Definitions by: Jason Swearingen 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | 7 | /** sprintf.js is a complete open source JavaScript sprintf implementation for the browser and node.js. 8 | 9 | Its prototype is simple: 10 | 11 | string sprintf(string format , [mixed arg1 [, mixed arg2 [ ,...]]]) 12 | */ 13 | declare module sprintf_js { 14 | /** sprintf.js is a complete open source JavaScript sprintf implementation for the browser and node.js. 15 | Its prototype is simple: 16 | string sprintf(string format , [mixed arg1 [, mixed arg2 [ ,...]]]) 17 | 18 | ==Placeholders== 19 | The placeholders in the format string are marked by % and are followed by one or more of these elements. see "fmt" arg for more docs on placeholders. 20 | 21 | ==Argument swapping== 22 | You can also swap the arguments. That is, the order of the placeholders doesn't have to match the order of the arguments. You can do that by simply indicating in the format string which arguments the placeholders refer to: 23 | sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants") 24 | And, of course, you can repeat the placeholders without having to increase the number of arguments. 25 | 26 | ==Named arguments== 27 | Format strings may contain replacement fields rather than positional placeholders. Instead of referring to a certain argument, you can now refer to a certain key within an object. Replacement fields are surrounded by rounded parentheses - ( and ) - and begin with a keyword that refers to a key: 28 | var user = {name: "Dolly"} 29 | sprintf("Hello %(name)s", user) // Hello Dolly 30 | Keywords in replacement fields can be optionally followed by any number of keywords or indexes: 31 | var users = [{name: "Dolly"},{name: "Molly"},{name: "Polly"}] 32 | sprintf("Hello %(users[0].name)s, %(users[1].name)s and %(users[2].name)s", {users: users}) // Hello Dolly, Molly and Polly 33 | Note: mixing positional and named placeholders is not (yet) supported 34 | 35 | ==Computed values== 36 | You can pass in a function as a dynamic value and it will be invoked (with no arguments) in order to compute the value on-the-fly. 37 | sprintf("Current timestamp: %d", Date.now) // Current timestamp: 1398005382890 38 | sprintf("Current date and time: %s", function() { return new Date().toString() }) 39 | */ 40 | export function sprintf( 41 | /** The placeholders in the format string are marked by % and are followed by one or more of these elements, in this order: 42 | 43 | An optional number followed by a $ sign that selects which argument index to use for the value. If not specified, arguments will be placed in the same order as the placeholders in the input string. 44 | An optional + sign that forces to preceed the result with a plus or minus sign on numeric values. By default, only the - sign is used on negative numbers. 45 | An optional padding specifier that says what character to use for padding (if specified). Possible values are 0 or any other character precedeed by a ' (single quote). The default is to pad with spaces. 46 | An optional - sign, that causes sprintf to left-align the result of this placeholder. The default is to right-align the result. 47 | An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded. 48 | An optional precision modifier, consisting of a . (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used on a string, it causes the result to be truncated. 49 | A type specifier that can be any of: 50 | % - yields a literal % character 51 | b - yields an integer as a binary number 52 | c - yields an integer as the character with that ASCII value 53 | d or i - yields an integer as a signed decimal number 54 | e - yields a float using scientific notation 55 | u - yields an integer as an unsigned decimal number 56 | f - yields a float as is 57 | o - yields an integer as an octal number 58 | s - yields a string as is 59 | x - yields an integer as a hexadecimal number (lower-case) 60 | X - yields an integer as a hexadecimal number (upper-case) 61 | */ 62 | fmt: string, 63 | /** */ 64 | ...args: any[] 65 | ): string; 66 | /** vsprintf is the same as sprintf except that it accepts an array of arguments, rather than a variable number of arguments: 67 | 68 | vsprintf("The first 4 letters of the english alphabet are: %s, %s, %s and %s", ["a", "b", "c", "d"]) 69 | */ 70 | export function vsprintf(fmt: string, args: any[]): string; 71 | } 72 | 73 | declare module "sprintf-js" { 74 | export =sprintf_js; 75 | } 76 | 77 | declare var sprintf: typeof sprintf_js.sprintf; 78 | declare var vsprintf: typeof sprintf_js.vsprintf; 79 | -------------------------------------------------------------------------------- /recurse-app/typings/tape/tape.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for tape v4.2.2 2 | // Project: https://github.com/substack/tape 3 | // Definitions by: Bart van der Schoor , Haoqun Jiang 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare module 'tape' { 9 | export = tape; 10 | 11 | /** 12 | * Create a new test with an optional name string and optional opts object. 13 | * cb(t) fires with the new test object t once all preceeding tests have finished. 14 | * Tests execute serially. 15 | */ 16 | function tape(name: string, cb: tape.TestCase): void; 17 | function tape(name: string, opts: tape.TestOptions, cb: tape.TestCase): void; 18 | function tape(cb: tape.TestCase): void; 19 | function tape(opts: tape.TestOptions, cb: tape.TestCase): void; 20 | 21 | module tape { 22 | 23 | interface TestCase { 24 | (test: Test): void; 25 | } 26 | 27 | /** 28 | * Available opts options for the tape function. 29 | */ 30 | interface TestOptions { 31 | skip?: boolean; // See tape.skip. 32 | timeout?: number; // Set a timeout for the test, after which it will fail. See tape.timeoutAfter. 33 | } 34 | 35 | /** 36 | * Options for the createStream function. 37 | */ 38 | interface StreamOptions { 39 | objectMode?: boolean; 40 | } 41 | 42 | /** 43 | * Generate a new test that will be skipped over. 44 | */ 45 | export function skip(name: string, cb: tape.TestCase): void; 46 | 47 | /** 48 | * Like test(name, cb) except if you use .only this is the only test case that will run for the entire process, all other test cases using tape will be ignored. 49 | */ 50 | export function only(name: string, cb: tape.TestCase): void; 51 | 52 | /** 53 | * Create a new test harness instance, which is a function like test(), but with a new pending stack and test state. 54 | */ 55 | export function createHarness(): typeof tape; 56 | /** 57 | * Create a stream of output, bypassing the default output stream that writes messages to console.log(). 58 | * By default stream will be a text stream of TAP output, but you can get an object stream instead by setting opts.objectMode to true. 59 | */ 60 | export function createStream(opts?: tape.StreamOptions): NodeJS.ReadableStream; 61 | 62 | interface Test { 63 | /** 64 | * Create a subtest with a new test handle st from cb(st) inside the current test. 65 | * cb(st) will only fire when t finishes. 66 | * Additional tests queued up after t will not be run until all subtests finish. 67 | */ 68 | test(name: string, cb: tape.TestCase): void; 69 | 70 | /** 71 | * Declare that n assertions should be run. end() will be called automatically after the nth assertion. 72 | * If there are any more assertions after the nth, or after end() is called, they will generate errors. 73 | */ 74 | plan(n: number): void; 75 | 76 | /** 77 | * Declare the end of a test explicitly. 78 | * If err is passed in t.end will assert that it is falsey. 79 | */ 80 | end(err?: any): void; 81 | 82 | /** 83 | * Generate a failing assertion with a message msg. 84 | */ 85 | fail(msg?: string): void; 86 | 87 | /** 88 | * Generate a passing assertion with a message msg. 89 | */ 90 | pass(msg?: string): void; 91 | 92 | /** 93 | * Automatically timeout the test after X ms. 94 | */ 95 | timeoutAfter(ms: number): void; 96 | 97 | /** 98 | * Generate an assertion that will be skipped over. 99 | */ 100 | skip(msg?: string): void; 101 | 102 | /** 103 | * Assert that value is truthy with an optional description message msg. 104 | */ 105 | ok(value: any, msg?: string): void; 106 | true(value: any, msg?: string): void; 107 | assert(value: any, msg?: string): void; 108 | 109 | /** 110 | * Assert that value is falsy with an optional description message msg. 111 | */ 112 | notOk(value: any, msg?: string): void; 113 | false(value: any, msg?: string): void; 114 | notok(value: any, msg?: string): void; 115 | 116 | /** 117 | * Assert that err is falsy. 118 | * If err is non-falsy, use its err.message as the description message. 119 | */ 120 | error(err: any, msg?: string): void; 121 | ifError(err: any, msg?: string): void; 122 | ifErr(err: any, msg?: string): void; 123 | iferror(err: any, msg?: string): void; 124 | 125 | /** 126 | * Assert that a === b with an optional description msg. 127 | */ 128 | equal(a: any, b: any, msg?: string): void; 129 | equals(a: any, b: any, msg?: string): void; 130 | isEqual(a: any, b: any, msg?: string): void; 131 | is(a: any, b: any, msg?: string): void; 132 | strictEqual(a: any, b: any, msg?: string): void; 133 | strictEquals(a: any, b: any, msg?: string): void; 134 | 135 | /** 136 | * Assert that a !== b with an optional description msg. 137 | */ 138 | notEqual(a: any, b: any, msg?: string): void; 139 | notEquals(a: any, b: any, msg?: string): void; 140 | notStrictEqual(a: any, b: any, msg?: string): void; 141 | notStrictEquals(a: any, b: any, msg?: string): void; 142 | isNotEqual(a: any, b: any, msg?: string): void; 143 | isNot(a: any, b: any, msg?: string): void; 144 | not(a: any, b: any, msg?: string): void; 145 | doesNotEqual(a: any, b: any, msg?: string): void; 146 | isInequal(a: any, b: any, msg?: string): void; 147 | 148 | /** 149 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 150 | */ 151 | deepEqual(a: any, b: any, msg?: string): void; 152 | deepEquals(a: any, b: any, msg?: string): void; 153 | isEquivalent(a: any, b: any, msg?: string): void; 154 | same(a: any, b: any, msg?: string): void; 155 | 156 | /** 157 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 158 | */ 159 | notDeepEqual(a: any, b: any, msg?: string): void; 160 | notEquivalent(a: any, b: any, msg?: string): void; 161 | notDeeply(a: any, b: any, msg?: string): void; 162 | notSame(a: any, b: any, msg?: string): void; 163 | isNotDeepEqual(a: any, b: any, msg?: string): void; 164 | isNotDeeply(a: any, b: any, msg?: string): void; 165 | isNotEquivalent(a: any, b: any, msg?: string): void; 166 | isInequivalent(a: any, b: any, msg?: string): void; 167 | 168 | /** 169 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 170 | */ 171 | deepLooseEqual(a: any, b: any, msg?: string): void; 172 | looseEqual(a: any, b: any, msg?: string): void; 173 | looseEquals(a: any, b: any, msg?: string): void; 174 | 175 | /** 176 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 177 | */ 178 | notDeepLooseEqual(a: any, b: any, msg?: string): void; 179 | notLooseEqual(a: any, b: any, msg?: string): void; 180 | notLooseEquals(a: any, b: any, msg?: string): void; 181 | 182 | /** 183 | * Assert that the function call fn() throws an exception. 184 | * expected, if present, must be a RegExp or Function, which is used to test the exception object. 185 | */ 186 | throws(fn: () => void, msg?: string): void; 187 | throws(fn: () => void, exceptionExpected: RegExp | (() => void), msg?: string): void; 188 | 189 | /** 190 | * Assert that the function call fn() does not throw an exception. 191 | */ 192 | doesNotThrow(fn: () => void, msg?: string): void; 193 | doesNotThrow(fn: () => void, exceptionExpected: RegExp | (() => void), msg?: string): void; 194 | 195 | /** 196 | * Print a message without breaking the tap output. 197 | * (Useful when using e.g. tap-colorize where output is buffered & console.log will print in incorrect order vis-a-vis tap output.) 198 | */ 199 | comment(msg: string): void; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /recurse-app/typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | -------------------------------------------------------------------------------- /recurse-connectors/ableton-live/externals/parseInput.js: -------------------------------------------------------------------------------- 1 | inlets = 1; 2 | outlets = 2; 3 | 4 | //-------------------------------------------------------------------- 5 | // Clip class 6 | 7 | function Clip() { 8 | var path = "live_set view highlighted_clip_slot clip"; 9 | this.liveObject = new LiveAPI(path); 10 | } 11 | 12 | Clip.prototype.getLength = function() { 13 | return this.liveObject.get('length'); 14 | }; 15 | 16 | Clip.prototype._parseNoteData = function(data) { 17 | var notes = []; 18 | // data starts with "notes"/count and ends with "done" (which we ignore) 19 | for(var i=2,len=data.length-1; i 127) return 127; 98 | return this.pitch; 99 | }; 100 | 101 | Note.prototype.getStart = function() { 102 | // we convert to strings with decimals to work around a bug in Max 103 | // otherwise we get an invalid syntax error when trying to set notes 104 | if(this.start <= 0) return "0.0"; 105 | return this.start.toFixed(4); 106 | }; 107 | 108 | Note.prototype.getDuration = function() { 109 | if(this.duration <= Note.MIN_DURATION) return Note.MIN_DURATION; 110 | return this.duration.toFixed(4); // workaround similar bug as with getStart() 111 | }; 112 | 113 | Note.prototype.getVelocity = function() { 114 | if(this.velocity < 0) return 0; 115 | if(this.velocity > 127) return 127; 116 | return this.velocity; 117 | }; 118 | 119 | Note.prototype.getMuted = function() { 120 | if(this.muted) return 1; 121 | return 0; 122 | }; 123 | 124 | function set_json(jsonString) { 125 | var input = JSON.parse(jsonString), 126 | i, 127 | output, 128 | notes = [], 129 | note, 130 | clip; 131 | 132 | for (i = 0; i < input.length; i++) { 133 | note = new Note(input[i].pitch, input[i].start, input[i].duration, input[i].velocity, false); 134 | notes.push(note); 135 | //output = 'bar:' + input[i].bar + '_beat:' + input[i].beat + '_sixteenth:' + input[i].sixteenth + '_note:' + input[i].note; 136 | outlet(0, 'append', note.start); 137 | } 138 | 139 | clip = new Clip(); 140 | clip.replaceAllNotes(notes); 141 | } 142 | 143 | // gets intervals from currently selected clips and converts them to recurse code, then sends resulting code as OSC message to /recurse/intervals 144 | function get_intervals(strategy) { 145 | var clip = new Clip(), 146 | noteList = clip.getNotes(), 147 | clipLength = clip.getLength(); 148 | 149 | if (strategy === void 0) { strategy = 0; } 150 | var results = [{ intervals: [], notes: [], velocities: [] }], currentResultIndex = 0, currentStartPos, currentEndPos, backlog, output = "", velocitiesNeeded = false; 151 | 152 | noteList.sort(function (a, b) { 153 | if (a.start > b.start) { 154 | return 1; 155 | } 156 | if (a.start < b.start) { 157 | return -1; 158 | } 159 | return 0; 160 | }); 161 | 162 | do { 163 | backlog = []; 164 | currentEndPos = currentStartPos = 0; 165 | var currentPitch = noteList[0].pitch; 166 | for (var _i = 0, noteList_1 = noteList; _i < noteList_1.length; _i++) { 167 | var note = noteList_1[_i]; 168 | if (!results[currentResultIndex]) { 169 | results[currentResultIndex] = { intervals: [], notes: [], velocities: [] }; 170 | } 171 | var currentResults = results[currentResultIndex]; 172 | if (currentEndPos <= note.start && (note.pitch === currentPitch || strategy === 0)) { 173 | if (currentEndPos < note.start) { 174 | currentResults.intervals.push(0 - ((note.start - currentEndPos) * 4)); 175 | } 176 | currentResults.intervals.push(note.duration * 4); 177 | currentResults.notes.push(note.pitch); 178 | currentResults.velocities.push(note.velocity); 179 | if (note.velocity !== 127 && !velocitiesNeeded) { 180 | velocitiesNeeded = true; 181 | } 182 | currentStartPos = note.start; 183 | currentEndPos = note.start + note.duration; 184 | } 185 | else { 186 | backlog.push(note); 187 | } 188 | } 189 | noteList = backlog; 190 | currentResultIndex++; 191 | } while (backlog.length !== 0); 192 | 193 | var compactArray = function (elements) { 194 | if (elements.length < 2) { 195 | return elements; 196 | } 197 | for (var i = 1; i < elements.length; i++) { 198 | if (elements[i - 1] !== elements[i]) { 199 | return elements; 200 | } 201 | } 202 | return [elements[0]]; 203 | }; 204 | 205 | for (var r = 0; r < results.length; r++) { 206 | var result = results[r]; 207 | result.intervals = compactArray(result.intervals); 208 | result.velocities = compactArray(result.velocities); 209 | result.notes = compactArray(result.notes); 210 | 211 | var totalLength = clipLength * 4; 212 | if (totalLength !== 64) { 213 | output += "length(" + totalLength + ") "; 214 | } 215 | var rmOutput = ""; 216 | for (var i = 0; i < result.intervals.length; i++) { 217 | var interval = result.intervals[i]; 218 | if (interval > 0) { 219 | rmOutput += interval; 220 | } 221 | else { 222 | interval = Math.abs(interval); 223 | rmOutput += "_" + interval; 224 | } 225 | totalLength -= interval; 226 | if (i < result.intervals.length - 1) { 227 | rmOutput += " "; 228 | } 229 | } 230 | if (totalLength > 0) { 231 | rmOutput += " _" + totalLength; 232 | } 233 | output += "rm(" + rmOutput + ") "; 234 | var noteOutput = ""; 235 | var noteValueStatic = -1; 236 | for (var i = 0; i < result.notes.length; i++) { 237 | var note = result.notes[i]; 238 | if (noteValueStatic !== note) { 239 | if (noteValueStatic === -1) { 240 | noteValueStatic = note; 241 | } 242 | else { 243 | noteValueStatic = -2; 244 | } 245 | } 246 | noteOutput += Note.NOTE_NAMES[note].toLowerCase() + (i < result.notes.length - 1 ? " " : ""); 247 | } 248 | if (noteValueStatic >= 0) { 249 | noteOutput = Note.NOTE_NAMES[noteValueStatic].toLowerCase(); 250 | } 251 | output += "ns(" + noteOutput + ")"; 252 | if (velocitiesNeeded) { 253 | var velOutput = ""; 254 | for (var i = 0; i < result.velocities.length; i++) { 255 | var velocity = result.velocities[i]; 256 | velOutput += "" + velocity + (i < result.velocities.length - 1 ? " " : ""); 257 | } 258 | output += " vel(" + velOutput + ")"; 259 | } 260 | output += "" + (r < results.length - 1 ? ";\n" : ""); 261 | } 262 | outlet(1, ['/recurse/intervals', output]); 263 | } 264 | 265 | function convertPitch(pitch) { 266 | if (pitch < 0) return 0; 267 | if (pitch > 127) return 127; 268 | return pitch; 269 | } 270 | 271 | function convertStart(start) { 272 | // we convert to strings with decimals to work around a bug in Max 273 | // otherwise we get an invalid syntax error when trying to set notes 274 | if (start <= 0) return "0.0"; 275 | return start.toFixed(4); 276 | } 277 | 278 | function convertDuration(duration) { 279 | if (duration <= Note.MIN_DURATION) return Note.MIN_DURATION; 280 | return duration.toFixed(4); // workaround similar bug as with getStart() 281 | } 282 | 283 | function convertVelocity(velocity) { 284 | if (velocity < 0) return 0; 285 | if (velocity > 127) return 127; 286 | return velocity; 287 | } 288 | 289 | function convertMuted(muted) { 290 | if (muted) return 1; 291 | return 0; 292 | } 293 | 294 | // creates multiple clips in the currently selected track, starting at clip 0 295 | function set_track_clips(jsonString) { 296 | var input = JSON.parse(jsonString), 297 | liveObject, 298 | basePath = "live_set view selected_track"; 299 | // clip_slot: has_clip, create_clip 300 | // clip: is_midi_clip, length - select_all_notes, 301 | 302 | liveObject = new LiveAPI(basePath); 303 | 304 | if (!liveObject) { 305 | post('Invalid liveObject, exiting...'); 306 | return; 307 | } 308 | 309 | // the liveAPI seems to have some weird issues with comparing directly with 1 and 0, so we use < and > instead 310 | if (liveObject.get('has_audio_input') < 1 && liveObject.get('has_midi_input') > 0) { 311 | post('track type is valid'); 312 | 313 | for (var i = 0; i < input.length; i++) { 314 | var notes = input[i].notes, 315 | loopLength = input[i].loopLength; 316 | 317 | liveObject.goto(basePath + ' clip_slots ' + i); 318 | 319 | if (liveObject.get('has_clip') < 1) { 320 | liveObject.call('create_clip', '4.0'); 321 | } else { 322 | post('no clip to create'); 323 | } 324 | 325 | liveObject.goto(basePath + ' clip_slots ' + i + ' clip'); 326 | liveObject.call('select_all_notes'); 327 | liveObject.call('replace_selected_notes'); 328 | 329 | liveObject.call('notes', notes.length); 330 | for (var c = 0; c < notes.length; c++) { 331 | liveObject.call('note', convertPitch(notes[c].pitch), 332 | convertStart(notes[c].start), convertDuration(notes[c].duration), 333 | convertVelocity(notes[c].velocity), convertMuted(false)); 334 | } 335 | liveObject.call('done'); 336 | liveObject.set('looping', 1); 337 | liveObject.set('loop_end', loopLength); 338 | } 339 | } else { 340 | post('not a midi track!'); 341 | } 342 | } 343 | 344 | -------------------------------------------------------------------------------- /recurse-sublime/plugin/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Recurse: Get currently selected clip from Ableton Live (Drum mode)", 4 | "command": "getclipdrum" 5 | }, 6 | { 7 | "caption": "Recurse: Get currently selected clip from Ableton Live (Compact mode)", 8 | "command": "getclipcompact" 9 | } 10 | ] -------------------------------------------------------------------------------- /recurse-sublime/plugin/recurse.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import sys 3 | import os 4 | sys.path.append(os.path.join(os.path.dirname(__file__), "osc4py3-1.0.1")) 5 | 6 | from osc4py3.as_eventloop import * 7 | from osc4py3 import oscbuildparse 8 | import time 9 | 10 | class A(object): pass 11 | 12 | a = A() 13 | a.value = "" 14 | 15 | def handlerfunction(args): 16 | a.value = "".join(args) 17 | 18 | def plugin_loaded(): 19 | osc_startup() 20 | osc_udp_client("127.0.0.1", 8009, "sender") 21 | osc_udp_server("127.0.0.1", 8008, "receiver") 22 | osc_method("/recurse/intervals", handlerfunction) 23 | 24 | def plugin_unloaded(): 25 | osc_terminate() 26 | 27 | class GetclipdrumCommand(sublime_plugin.TextCommand): 28 | def run(self, edit): 29 | msg = oscbuildparse.OSCMessage("/recurse/getintervalsdrum", None, []) 30 | osc_send(msg, "sender") 31 | 32 | targettime = time.time() + 1 33 | while time.time() < targettime and a.value == "": 34 | osc_process() 35 | 36 | for pos in self.view.sel(): 37 | self.view.insert(edit, pos.begin(), a.value) 38 | 39 | a.value = ""; 40 | 41 | class GetclipcompactCommand(sublime_plugin.TextCommand): 42 | def run(self, edit): 43 | msg = oscbuildparse.OSCMessage("/recurse/getintervalscompact", None, []) 44 | osc_send(msg, "sender") 45 | 46 | targettime = time.time() + 1 47 | while time.time() < targettime and a.value == "": 48 | osc_process() 49 | 50 | for pos in self.view.sel(): 51 | self.view.insert(edit, pos.begin(), a.value) 52 | 53 | a.value = ""; 54 | --------------------------------------------------------------------------------