diff --git a/README.md b/README.md index b428130..a0bfe32 100644 --- a/README.md +++ b/README.md @@ -90,17 +90,17 @@ Filter all the console old logs and show only the last one ### Event Highlighting -The event highlighting allows to visualize the active events within a mini notation pattern. This means, that only events within quotation marks will be considered. +The event highlighting allows to visualize the active events within a mini notation pattern. This means, that only events within quotation marks will be considered. #### TidalCycles configuration TidalCycles needs to be configured to send editor highlight events. This is usually done by modifying the `BootTidal.hs` file and adding an editor highlight target. Here is a working example: ```haskell -let editorTarget = Target {oName = "editor", oAddress = "127.0.0.1", oPort = 6013, oLatency = 0.02, oSchedule = Pre BundleStamp, oWindow = Nothing, oHandshake = False, oBusPort = Nothing } +let editorTarget = Target {oName = "editor", oAddress = "127.0.0.1", oPort = 6013, oLatency = 0.2, oSchedule = Pre BundleStamp, oWindow = Nothing, oHandshake = False, oBusPort = Nothing } let editorShape = OSCContext "/editor/highlights" -tidal <- startStream (defaultConfig {cFrameTimespan = 1/50}) [(superdirtTarget {oLatency = 0.2}, [superdirtShape]), (editorTarget, [editorShape])] +tidal <- startStream (defaultConfig {cFrameTimespan = 1/50, cProcessAhead = (1/20)}) [(superdirtTarget {oLatency = 0.02}, [superdirtShape]), (editorTarget, [editorShape])] ``` The path to the `BootTidal.hs` file can be found in the TidalCycles output console after TidalCycles has been booted in the editor. diff --git a/lib/config.js b/lib/config.js index 9d193df..9b1740e 100644 --- a/lib/config.js +++ b/lib/config.js @@ -140,9 +140,14 @@ export default { }, 'fps': { type: 'number', - default: 30, description: 'Reduce this value if the event highlighting flickers. Higher values make the result smoother, but require more computational capacity and it is limited by the cFrameTimespan value configured in TidalCycles. It is recommended to use a value between 20 and 30 fps.', - order: 100, + default: 30, + order: 96, + }, + 'delay': { + type: 'number', + default: 0.0, + order: 98, }, 'ip': { type: 'string', diff --git a/lib/event-highlighter.js b/lib/event-highlighter.js index 1bbae84..189003a 100644 --- a/lib/event-highlighter.js +++ b/lib/event-highlighter.js @@ -28,15 +28,12 @@ export default class EventHighlighter { // Data‑structures ------------------------------------------------------- this.markers = new Map(); // textbuffer.id → row → col → Marker this.highlights = new Map(); // eventId → texteditor.id → col → Marker - this.filteredMessages = new Map(); // eventId → event - this.receivedThisFrame = new Map(); // eventId → event - this.addedThisFrame = new Set(); // Set this.eventIds = []; // [{textbufferId, row}] + this.messageBuffer = new Map(); + this.activeMessages = new Map(); - // Animation state ------------------------------------------------------- - this.then = 0; // time at previous frame + this.then = 0; - // Bind instance methods used as callbacks ----------------------------- this.animate = this.animate.bind(this); } @@ -48,11 +45,61 @@ export default class EventHighlighter { init() { this.#installBaseHighlightStyle(); - // Kick‑off animation loop this.then = window.performance.now(); requestAnimationFrame(this.animate); } + /** requestAnimationFrame callback */ + animate(now) { + const elapsed = now - this.then; + + const configFPS = atom.config.get('tidalcycles.eventHighlighting.fps'); + const delay = atom.config.get('tidalcycles.eventHighlighting.delay'); + + const fpsInterval = 1000 / configFPS; + let activeThisFrame = new Set(); + + if (elapsed >= fpsInterval) { + this.then = now - (elapsed % fpsInterval); + + [...this.messageBuffer.entries()] + .filter(([ts]) => (ts + (delay * -1)) < this.then) + .forEach(([ts, event]) => { + activeThisFrame = new Set([...activeThisFrame, ...event]); + this.messageBuffer.delete(ts); + } + ); + + const { active, added, removed } = this.#diffEventMaps( + this.activeMessages, + this.#transformedEvents(activeThisFrame) + ); + + removed.forEach(evt => { + const cols = this.activeMessages.get(evt.eventId); + if (cols) { + cols.delete(evt.colStart); + if (cols.size === 0) { + this.activeMessages.delete(evt.eventId); + } + } + }); + + this.activeMessages = this.#transformedEvents( + [...active, ...added] + ); + + added.forEach((evt) => { + this.#addHighlight(evt); + }); + + removed.forEach((evt) => this.#removeHighlight(evt)); + + } + + requestAnimationFrame(this.animate); + } + /** Clean‑up resources when package is deactivated */ destroy() { try { @@ -86,59 +133,27 @@ export default class EventHighlighter { /** Handle OSC message describing a highlight event */ oscHighlightSubscriber() { - return (args: {}): void => { - const message = OscServer.asDictionary(this.highlightTransformer(args)); - this.#queueEvent(message); - } - } + return (args, time) => { - highlightTransformer(args) { - const result = [ - {value: "id"}, args[0], - {value: "duration"}, args[1], - {value: "cycle"}, args[2], - {value: "colStart"}, args[3], - {value: "eventId"}, {value: args[4].value - 1}, - {value: "colEnd"}, args[5], - ]; - - return result; - } + const transformedArgs = [ + { value: "time" }, { value: time }, + { value: "id" }, args[0], + { value: "duration" }, args[1], + { value: "cycle" }, args[2], + { value: "colStart" }, args[3], + { value: "eventId" }, { value: args[4].value - 1 }, + { value: "colEnd" }, args[5], + ]; - /** requestAnimationFrame callback */ - animate(now) { - const elapsed = now - this.then; - const configFPS = atom.config.get('tidalcycles.eventHighlighting.fps'); - const fpsInterval = 1000 / configFPS; + const message = OscServer.asDictionary(transformedArgs); - if (elapsed >= fpsInterval) { - this.then = now - (elapsed % fpsInterval); - - // Add newly‑received highlights ------------------------------------- - this.addedThisFrame.forEach((evt) => { - this.#addHighlight(evt); - }); - - // Remove highlights no longer present ------------------------------ - const { updated, removed } = this.#diffEventMaps( - this.filteredMessages, - this.receivedThisFrame, - ); - this.filteredMessages = updated; - removed.forEach((evt) => this.#removeHighlight(evt)); - - // Reset per‑frame collections -------------------------------------- - this.receivedThisFrame.clear(); - this.addedThisFrame.clear(); + this.#queueEvent(message); } - - requestAnimationFrame(this.animate); } // ---------------------------------------------------------------------- // Private helpers // ---------------------------------------------------------------------- - /** Injects the base CSS rule used for all highlights */ #installBaseHighlightStyle() { atom.styles.addStyleSheet(` @@ -149,18 +164,6 @@ export default class EventHighlighter { `); } - /** Store events until the next animation frame */ - #queueEvent(event) { - const eventMap = ensureNestedMap(this.filteredMessages, event.eventId); - const recvMap = ensureNestedMap(this.receivedThisFrame, event.eventId); - - if (!eventMap.has(event.colStart)) eventMap.set(event.colStart, event); - if (!recvMap.has(event.colStart)) { - this.addedThisFrame.add(event); - recvMap.set(event.colStart, event); - } - } - // Highlight management #addHighlight({ id, colStart, eventId }) { const bufferId = this.eventIds[eventId].bufferId; @@ -177,22 +180,21 @@ export default class EventHighlighter { const highlightEvent = ensureNestedMap(this.highlights, eventId); editors.forEach(editor => { - const textEditorEvent = ensureNestedMap(highlightEvent, editor.id); + const textEditorEvent = ensureNestedMap(highlightEvent, editor.id); - if (textEditorEvent.has(colStart)) return; // already highlighted + if (textEditorEvent.has(colStart)) return; // already highlighted - const marker = editor.markBufferRange(baseMarker.getBufferRange(), { - invalidate: "inside", - }); + const marker = editor.markBufferRange(baseMarker.getBufferRange(), { + invalidate: "inside", + }); - // Base style - editor.decorateMarker(marker, { type: "text", class: CLASS.base }); + // Base style + editor.decorateMarker(marker, { type: "text", class: CLASS.base }); - // Style by numeric id - editor.decorateMarker(marker, { type: "text", class: `${CLASS.idPrefix}${id}` }); + // Style by numeric id + editor.decorateMarker(marker, { type: "text", class: `${CLASS.idPrefix}${id}` }); - textEditorEvent.set(colStart, marker); - // eventId → texteditor.id → col → Marker + textEditorEvent.set(colStart, marker); }); } @@ -202,15 +204,13 @@ export default class EventHighlighter { if (!highlightEvents || !highlightEvents.size) return; - highlightEvents.forEach(textEditorIdEvent => { + highlightEvents.forEach(textEditorIdEvent => { const marker = textEditorIdEvent.get(colStart); textEditorIdEvent.delete(colStart); if (!marker) return; marker.destroy(); }) - - } // Marker generation (per line) @@ -227,32 +227,72 @@ export default class EventHighlighter { const rowMarkers = ensureNestedMap(textBufferIdMarkers, lineNumber); LineProcessor.findTidalWordRanges(line, (range) => { - const bufferRange = [[lineNumber, range.start], [lineNumber, range.end + 1]]; - const marker = currentEditor.markBufferRange(bufferRange, { invalidate: "inside" }); - rowMarkers.set(range.start, marker); + const bufferRange = [[lineNumber, range.start], [lineNumber, range.end + 1]]; + const marker = currentEditor.markBufferRange(bufferRange, { invalidate: "inside" }); + rowMarkers.set(range.start, marker); }); } + #ensureNestedMap(root, key) { + if (!root.has(key)) root.set(key, new Map()); + return root.get(key); + } + + #queueEvent(event) { + if (!this.messageBuffer.has(event.time)) this.messageBuffer.set(event.time, new Set()); + this.messageBuffer.get(event.time).add(event); + } + #diffEventMaps(prevEvents, currentEvents) { const removed = new Set(); - const updated = new Map(prevEvents); + const added = new Set(); + const active = new Set(); for (const [event, prevCols] of prevEvents) { const currCols = currentEvents.get(event); if (!currCols) { for (const [, prevEvt] of prevCols) removed.add(prevEvt); - updated.delete(event); continue; } for (const [col, prevEvt] of prevCols) { if (!currCols.has(col)) { removed.add(prevEvt); - updated.get(event).delete(col); + } else { + active.add(prevEvt); } } } - return { updated, removed }; + for (const [event, currCols] of currentEvents) { + const prevCols = prevEvents.get(event); + if (!prevCols) { + for (const [, currEvt] of currCols) added.add(currEvt); + continue; + } + + for (const [col, currEvt] of currCols) { + if (!prevCols.has(col)) { + added.add(currEvt); + } + } + } + + return { removed, added, active }; } + + #transformedEvents(events) { + const resultEvents = new Map(); + events.forEach(event => { + if (!resultEvents.get(event.eventId)) { + resultEvents.set(event.eventId, new Map()); + } + const cols = resultEvents.get(event.eventId); + + cols.set(event.colStart, event); + }); + + return resultEvents; + } + } diff --git a/lib/osc-eval.js b/lib/osc-eval.js index ad6237d..eaa6cc3 100644 --- a/lib/osc-eval.js +++ b/lib/osc-eval.js @@ -5,17 +5,19 @@ import {Editors} from './editors'; import OscServer from './osc-server'; export function oscEvalSubscriber(tidalRepl: Repl, editors: Editors) { - return (args: {}): void => { - const message = OscServer.asDictionary(args); + return (args: {}): void => { + const message = OscServer.asDictionary(args); - if (message['tab'] !== undefined) { - atom.workspace.getPanes()[0].setActiveItem(atom.workspace.getTextEditors()[message['tab']]) - } + if (message['tab'] !== undefined) { + atom.workspace.getPanes()[0].setActiveItem(atom.workspace.getTextEditors()[message['tab']]) + } - if (message['row'] && message['column']) { - editors.goTo(message['row'] - 1, message['column']) - } + if (message['row'] && message['column']) { + editors.goTo(message['row'] - 1, message['column']) + } - tidalRepl.eval(message['type'], false); + if (message['type']) { + tidalRepl.eval(message['type'], false); } + } } diff --git a/lib/osc-server.js b/lib/osc-server.js index eb91592..062f4eb 100644 --- a/lib/osc-server.js +++ b/lib/osc-server.js @@ -5,86 +5,94 @@ const osc = require('osc-min'); export default class OscServer { - port = null; - ip = null; - sock = null; - consoleView = null; - subscribers: Map = new Map(); - transformers: Map = new Map(); - - constructor(consoleView, ip, port) { - this.ip = ip; - this.port = port; - this.consoleView = consoleView; - this.sock = dgram.createSocket('udp4'); - } - - start(): Promise { - let promise = new Promise((resolve, reject) => { - this.sock.on('listening', () => { - resolve() - }); - - this.sock.on('error', (err) => { - reject(err); - }); - }); - - this.sock.bind(this.port, this.ip); - - this.sock.on('message', (message) => { - let oscMessage = osc.fromBuffer(message); - - if (oscMessage.oscType === "bundle") { - oscMessage.elements.forEach(element => { - this.handleOscMessage(element.address, element.args); - } - ); - } else { - this.handleOscMessage(oscMessage.address, oscMessage.args); - } + port = null; + ip = null; + sock = null; + consoleView = null; + subscribers: Map = new Map(); + transformers: Map = new Map(); + + constructor(consoleView, ip, port) { + this.ip = ip; + this.port = port; + this.consoleView = consoleView; + this.sock = dgram.createSocket('udp4'); + } + + start(): Promise { + let promise = new Promise((resolve, reject) => { + this.sock.on('listening', () => { + resolve() + }); + + this.sock.on('error', (err) => { + reject(err); + }); + }); + + this.sock.bind(this.port, this.ip); + + this.sock.on('message', (message) => { + let oscMessage = osc.fromBuffer(message); + + if (oscMessage.oscType === "bundle") { + oscMessage.elements.forEach(element => { + element.time = OscServer.fromNTPTime(oscMessage.timetag); + this.handleOscMessage(element.address, element.args, element.time); }); - - return promise - .then(() => { - this.consoleView.logStdout(`Listening for external osc messages on ${this.ip}:${this.port}`) - }) - .catch(err => { - this.consoleView.logStderr(`OSC server error: \n${err.stack}`) - this.sock.close(); - }); + } else { + this.handleOscMessage(oscMessage.address, oscMessage.args); + } + }); + + return promise + .then(() => { + this.consoleView.logStdout(`Listening for external osc messages on ${this.ip}:${this.port}`) + }) + .catch(err => { + this.consoleView.logStderr(`OSC server error: \n${err.stack}`) + this.sock.close(); + }); + } + + handleOscMessage(address, args, time) { + let subscriber = this.subscribers.get(address); + + if (subscriber) { + subscriber(args, time); + } else { + this.consoleView.logStderr(`Received OSC message on unsupported ${address} address`); } + } - handleOscMessage(address, args) { - let subscriber = this.subscribers.get(address); - - if (subscriber) { - subscriber(args); - } else { - this.consoleView.logStderr(`Received OSC message on unsupported ${address} address`); - } - } - - stop() { - if (this.sock) { - this.sock.close(); - } - } - - destroy() { - this.stop(); - } - - register(address: string, listener: Function) { - this.subscribers.set(address, listener) + stop() { + if (this.sock) { + this.sock.close(); } - - static asDictionary (oscMap): {} { - let dictionary = {} - for (let i = 0; i < oscMap.length; i += 2) { - dictionary[oscMap[i].value] = oscMap[i+1].value - } - return dictionary + } + + destroy() { + this.stop(); + } + + register(address: string, listener: Function) { + this.subscribers.set(address, listener) + } + + static fromNTPTime([seconds, fractional]) { + return ( + (seconds - 2208988800 + fractional / 4294967295) * // NTP Epoch to Unix Seconds + 1000 - // In milliseconds + performance.timeOrigin // Offset by timeOrigin + ); + } + + static asDictionary(oscMap): {} { + let dictionary = {} + for (let i = 0; i < oscMap.length; i += 2) { + dictionary[oscMap[i].value] = oscMap[i + 1].value } + return dictionary + } } diff --git a/lib/tidalcycles.js b/lib/tidalcycles.js index 6187dbb..ebc5779 100644 --- a/lib/tidalcycles.js +++ b/lib/tidalcycles.js @@ -3,7 +3,7 @@ import ConsoleView from './console-view'; import Repl from './repl'; import TidalListenerRepl from './tidal-listener-repl'; -import {oscEvalSubscriber} from './osc-eval'; +import { oscEvalSubscriber } from './osc-eval'; import AutocompleteProvider from './autocomplete-provider'; import Ghc from './ghc'; import BootTidal from './boot-tidal'; @@ -16,128 +16,128 @@ const { LINE, MULTI_LINE, WHOLE_EDITOR, Editors } = require('./editors') const config = require('./config') export default { - consoleView: null, - repl: null, - eventHighlighter: null, - config: config, - status: new Status(), - editors: new Editors(), - superDirt: new SuperDirt(), - oscServers: [], - - activate() { - if (atom.config.get('tidalcycles.superDirt.autostart')) { - this.superDirt.start(); - } + consoleView: null, + repl: null, + eventHighlighter: null, + config: config, + status: new Status(), + editors: new Editors(), + superDirt: new SuperDirt(), + oscServers: [], + + activate() { + if (atom.config.get('tidalcycles.superDirt.autostart')) { + this.superDirt.start(); + } - this.consoleView = new ConsoleView(this.status); - this.consoleView.initUI(); + this.consoleView = new ConsoleView(this.status); + this.consoleView.initUI(); - if (atom.config.get('tidalcycles.eventHighlighting.enable')) { - const oscEventHighligthingServer = new OscServer(this.consoleView, atom.config.get('tidalcycles.eventHighlighting.ip'), atom.config.get('tidalcycles.eventHighlighting.port')); - this.eventHighlighter = new EventHighlighter(this.consoleView, this.editors); - this.eventHighlighter.init(); - oscEventHighligthingServer.register("/editor/highlights", this.eventHighlighter.oscHighlightSubscriber()); - this.oscServers.push(oscEventHighligthingServer); - } + if (atom.config.get('tidalcycles.eventHighlighting.enable')) { + const oscEventHighligthingServer = new OscServer(this.consoleView, atom.config.get('tidalcycles.eventHighlighting.ip'), atom.config.get('tidalcycles.eventHighlighting.port')); + this.eventHighlighter = new EventHighlighter(this.consoleView, this.editors); + this.eventHighlighter.init(); + oscEventHighligthingServer.register("/editor/highlights", this.eventHighlighter.oscHighlightSubscriber()); + this.oscServers.push(oscEventHighligthingServer); + } - if (atom.config.get('tidalcycles.interpreter') === 'listener') { - this.repl = new TidalListenerRepl(this.consoleView, this.status, this.editors); - } else { - this.ghc = new Ghc(this.consoleView); - this.ghc.init(); - this.bootTidal = new BootTidal(this.ghc, atom.project.rootDirectories, this.consoleView); - this.repl = new Repl(this.consoleView, this.ghc, this.bootTidal, this.status, this.editors, this.eventHighlighter); - } + if (atom.config.get('tidalcycles.interpreter') === 'listener') { + this.repl = new TidalListenerRepl(this.consoleView, this.status, this.editors); + } else { + this.ghc = new Ghc(this.consoleView); + this.ghc.init(); + this.bootTidal = new BootTidal(this.ghc, atom.project.rootDirectories, this.consoleView); + this.repl = new Repl(this.consoleView, this.ghc, this.bootTidal, this.status, this.editors, this.eventHighlighter); + } - if (atom.config.get('tidalcycles.oscEval.enable')) { - const oscEvalServer = new OscServer(this.consoleView, atom.config.get('tidalcycles.oscEval.ip'), atom.config.get('tidalcycles.oscEval.port')); - oscEvalServer.register(atom.config.get('tidalcycles.oscEval.address'), oscEvalSubscriber(this.repl, this.editors)); - this.oscServers.push(oscEvalServer); - } + if (atom.config.get('tidalcycles.oscEval.enable')) { + const oscEvalServer = new OscServer(this.consoleView, atom.config.get('tidalcycles.oscEval.ip'), atom.config.get('tidalcycles.oscEval.port')); + oscEvalServer.register(atom.config.get('tidalcycles.oscEval.address'), oscEvalSubscriber(this.repl, this.editors)); + this.oscServers.push(oscEvalServer); + } + + this.soundBrowser = new SoundBrowser(); + this.soundBrowser.init(atom.config.get('tidalcycles.soundBrowser.folders')); - this.soundBrowser = new SoundBrowser(); - this.soundBrowser.init(atom.config.get('tidalcycles.soundBrowser.folders')); - - this.oscServers.forEach(server => server.start()); - - atom.commands.add('atom-workspace', { - 'tidalcycles:boot': () => { - if (this.editors.currentIsTidal()) { - this.consoleView.logStdout('Start TidalCycles plugin') - this.repl.start(); - } else { - atom.notifications.addError('Cannot start Tidal from a non ".tidal" file') - } - }, - 'tidalcycles:reboot': () => { - this.consoleView.logStdout('Reboot TidalCycles plugin') - this.repl.destroy(); + this.oscServers.forEach(server => server.start()); + + atom.commands.add('atom-workspace', { + 'tidalcycles:boot': () => { + if (this.editors.currentIsTidal()) { + this.consoleView.logStdout('Start TidalCycles plugin') this.repl.start(); - }, - 'tidalcycles:boot-superdirt': () => { - this.consoleView.logStdout('Boot SuperDirt') - this.superDirt.destroy(); - this.superDirt.start(); - } - }); - - atom.commands.add('atom-text-editor', { - 'tidalcycles:eval': () => this.repl.eval(LINE, false), - 'tidalcycles:eval-multi-line': () => this.repl.eval(MULTI_LINE, false), - 'tidalcycles:eval-whole-editor': () => this.repl.eval(WHOLE_EDITOR, false), - 'tidalcycles:eval-copy': () => this.repl.eval(LINE, true), - 'tidalcycles:eval-multi-line-copy': () => this.repl.eval(MULTI_LINE, true), - 'tidalcycles:unmuteAll': () => this.repl.unmuteAll(), - 'tidalcycles:toggle-mute-1': () => this.repl.toggleMute('1'), - 'tidalcycles:toggle-mute-2': () => this.repl.toggleMute('2'), - 'tidalcycles:toggle-mute-3': () => this.repl.toggleMute('3'), - 'tidalcycles:toggle-mute-4': () => this.repl.toggleMute('4'), - 'tidalcycles:toggle-mute-5': () => this.repl.toggleMute('5'), - 'tidalcycles:toggle-mute-6': () => this.repl.toggleMute('6'), - 'tidalcycles:toggle-mute-7': () => this.repl.toggleMute('7'), - 'tidalcycles:toggle-mute-8': () => this.repl.toggleMute('8'), - 'tidalcycles:toggle-mute-9': () => this.repl.toggleMute('9'), - 'tidalcycles:toggle-mute-10': () => this.repl.toggleMute('10'), - 'tidalcycles:toggle-mute-11': () => this.repl.toggleMute('11'), - 'tidalcycles:toggle-mute-12': () => this.repl.toggleMute('12'), - 'tidalcycles:toggle-mute-13': () => this.repl.toggleMute('13'), - 'tidalcycles:toggle-mute-14': () => this.repl.toggleMute('14'), - 'tidalcycles:toggle-mute-15': () => this.repl.toggleMute('15'), - 'tidalcycles:toggle-mute-16': () => this.repl.toggleMute('16'), - 'tidalcycles:hush': () => this.repl.hush() - }); - - atom.workspace.onWillDestroyPaneItem(event => { - if (event.item.getURI() === 'atom://tidalcycles/superdirt-console') { - this.superDirt.destroy(); + } else { + atom.notifications.addError('Cannot start Tidal from a non ".tidal" file') } - }) - }, - - deactivate() { - this.consoleView.destroy(); - this.superDirt.destroy(); - this.repl.destroy(); - this.soundBrowser.destroy(); - this.oscServers.forEach(server => server.destroy()); - this.oscServers = []; - }, - - serialize() { - return { - consoleViewState: this.consoleView.serialize(), - superdirtConsoleState: this.superDirt.serialize() - }; - }, - - autocompleteProvider() { - if (this.ghc) { - return new AutocompleteProvider(this.ghc) - } else { - return undefined + }, + 'tidalcycles:reboot': () => { + this.consoleView.logStdout('Reboot TidalCycles plugin') + this.repl.destroy(); + this.repl.start(); + }, + 'tidalcycles:boot-superdirt': () => { + this.consoleView.logStdout('Boot SuperDirt') + this.superDirt.destroy(); + this.superDirt.start(); } - + }); + + atom.commands.add('atom-text-editor', { + 'tidalcycles:eval': () => this.repl.eval(LINE, false), + 'tidalcycles:eval-multi-line': () => this.repl.eval(MULTI_LINE, false), + 'tidalcycles:eval-whole-editor': () => this.repl.eval(WHOLE_EDITOR, false), + 'tidalcycles:eval-copy': () => this.repl.eval(LINE, true), + 'tidalcycles:eval-multi-line-copy': () => this.repl.eval(MULTI_LINE, true), + 'tidalcycles:unmuteAll': () => this.repl.unmuteAll(), + 'tidalcycles:toggle-mute-1': () => this.repl.toggleMute('1'), + 'tidalcycles:toggle-mute-2': () => this.repl.toggleMute('2'), + 'tidalcycles:toggle-mute-3': () => this.repl.toggleMute('3'), + 'tidalcycles:toggle-mute-4': () => this.repl.toggleMute('4'), + 'tidalcycles:toggle-mute-5': () => this.repl.toggleMute('5'), + 'tidalcycles:toggle-mute-6': () => this.repl.toggleMute('6'), + 'tidalcycles:toggle-mute-7': () => this.repl.toggleMute('7'), + 'tidalcycles:toggle-mute-8': () => this.repl.toggleMute('8'), + 'tidalcycles:toggle-mute-9': () => this.repl.toggleMute('9'), + 'tidalcycles:toggle-mute-10': () => this.repl.toggleMute('10'), + 'tidalcycles:toggle-mute-11': () => this.repl.toggleMute('11'), + 'tidalcycles:toggle-mute-12': () => this.repl.toggleMute('12'), + 'tidalcycles:toggle-mute-13': () => this.repl.toggleMute('13'), + 'tidalcycles:toggle-mute-14': () => this.repl.toggleMute('14'), + 'tidalcycles:toggle-mute-15': () => this.repl.toggleMute('15'), + 'tidalcycles:toggle-mute-16': () => this.repl.toggleMute('16'), + 'tidalcycles:hush': () => this.repl.hush() + }); + + atom.workspace.onWillDestroyPaneItem(event => { + if (event.item.getURI() === 'atom://tidalcycles/superdirt-console') { + this.superDirt.destroy(); + } + }) + }, + + deactivate() { + this.consoleView.destroy(); + this.superDirt.destroy(); + this.repl.destroy(); + this.soundBrowser.destroy(); + this.oscServers.forEach(server => server.destroy()); + this.oscServers = []; + }, + + serialize() { + return { + consoleViewState: this.consoleView.serialize(), + superdirtConsoleState: this.superDirt.serialize() + }; + }, + + autocompleteProvider() { + if (this.ghc) { + return new AutocompleteProvider(this.ghc) + } else { + return undefined } + } + }; diff --git a/spec/event-highlighter.spec.js b/spec/event-highlighter.spec.js new file mode 100644 index 0000000..4e0bc81 --- /dev/null +++ b/spec/event-highlighter.spec.js @@ -0,0 +1,87 @@ + +import EventHighlighter from '../lib/event-highlighter'; + +/* TEST DATA */ +const messageBuffer = new Map([ + [20, new Map([ + [8, { id: 1, colStart: 8, eventId: 20 }], + [32, { id: 1, colStart: 32, eventId: 20 }] + ])], + [33, new Map([ + [10, { id: 2, colStart: 10, eventId: 33 }], + ])] +]); + +const receivedMessagesThisFrame = new Map([ + [20, new Map([ + [8, { id: 1, colStart: 8, eventId: 20 }], + ])], + [33, new Map([ + [10, { id: 2, colStart: 10, eventId: 33 }], + ])], + [50, new Map([ + [24, { id: 3, colStart: 24, eventId: 50 }], + ])] +]); + +const eventHighlighter = new EventHighlighter(); + +describe('Event Map Helper', () => { + + describe('diffEventMaps', () => { + + it('should return the correct differences for deleted events', () => { + const { removed } = eventHighlighter.diffEventMaps(messageBuffer, receivedMessagesThisFrame); + + expectedResult = new Set([{ id: 1, colStart: 32, eventId: 20 }]); + + expect([...removed]).toEqual([...expectedResult]); + }) + + it('should return the correct differences for added events', () => { + const { added } = eventHighlighter.diffEventMaps(messageBuffer, receivedMessagesThisFrame); + + expectedResult = new Set([{ id: 3, colStart: 24, eventId: 50 }]); + + expect([...added]).toEqual([...expectedResult]); + }) + + it('should return the correct differences for active events', () => { + const { active } = eventHighlighter.diffEventMaps(messageBuffer, receivedMessagesThisFrame); + + const expectedResult = new Set([ + { id: 1, colStart: 8, eventId: 20 } + , { id: 2, colStart: 10, eventId: 33 } + ]); + + expect([...active]).toEqual([...expectedResult]); + + }) + }) + + describe('transformedEvents', () => { + it('should return the transformed Sets into correspondings Maps', () => { + const events = new Set([ + { id: 1, colStart: 8, eventId: 20 } + , { id: 2, colStart: 10, eventId: 33 } + , { id: 3, colStart: 24, eventId: 50 } + ]); + + const transformedEvents = eventHighlighter.transformedEvents(events); + + const expectedResult = new Map([ + [20, new Map([ + [8, { id: 1, colStart: 8, eventId: 20 }] + ])], + [33, new Map([ + [10, { id: 2, colStart: 10, eventId: 33 }] + ])], + [50, new Map([ + [24, { id: 3, colStart: 24, eventId: 50 }] + ])] + ]); + + expect(transformedEvents).toEqual(expectedResult); + }) + }) +}) diff --git a/spec/osc-server-spec.js b/spec/osc-server-spec.js index 46e8804..3d654a8 100644 --- a/spec/osc-server-spec.js +++ b/spec/osc-server-spec.js @@ -4,64 +4,78 @@ const dgram = require('dgram'); describe('OscServer', () => { - let oscServer; - const port = 36111; + let oscServer; + const port = 36111; - beforeEach((done) => { - oscServer = new OscServer({}, "127.0.0.1", port); - oscServer.start().finally(done); - }) + beforeEach((done) => { + oscServer = new OscServer({}, "127.0.0.1", port); + oscServer.start().finally(done); + }) - afterEach(() => { - oscServer.stop(); - }) + afterEach(() => { + oscServer.stop(); + }) - it('should start an osc server and receive a message', done => { - let listener = (message) => { + it('should start an osc server and receive a message', done => { + let listener = (message) => { - if (message) { - const args = OscServer.asDictionary(message); - expect(args.key).toBe("value"); - done(); + if (message) { + const args = OscServer.asDictionary(message); + expect(args.key).toBe("value"); + done(); - } else { - done.fail("Received message is empty"); - } - } + } else { + done.fail("Received message is empty"); + } + } + + oscServer.register('/address', listener); + + const message = osc.toBuffer({ address: "/address", args: ["key", "value"] }); + dgram.createSocket('udp4').send(message, 0, message.byteLength, port, "127.0.0.1") + }) + + it('should start an osc server and receive a message of type bundle', done => { + + let messages = []; + + const ntpTime = [3944678400, 0]; + + const expectedTime = OscServer.fromNTPTime(ntpTime); - oscServer.register('/address', listener); + const expected = + [ + { key1: 'value1', time: expectedTime }, + { key2: 'value2', time: expectedTime } + ] - const message = osc.toBuffer({ address: "/address", args: ["key", "value"] }); - dgram.createSocket('udp4').send(message, 0, message.byteLength, port, "127.0.0.1") - }) + let listener = (message, time) => { + const args = OscServer.asDictionary(message); + args.time = time; + messages.push(args); + if (messages.length === expected.length) { + expect(messages).toEqual(expected); + done(); + } + } - it('should start an osc server and receive a message of type bundle', done => { - let messages = []; - const expected = [{key1: 'value1'}, {key2: 'value2'}] + oscServer.register('/address', listener); - let listener = (message) => { - messages.push(OscServer.asDictionary(message)); - if (messages.length === expected.length) { - expect(messages).toEqual(expected); - done(); - } + const message = osc.toBuffer({ + elements: [ + { + address: "/address", + args: ["key1", "value1"] + }, + { + address: "/address", + args: ["key2", "value2"] } + ], + timetag: ntpTime, + oscType: 'bundle' + }); + dgram.createSocket('udp4').send(message, 0, message.byteLength, port, "127.0.0.1") - oscServer.register('/address', listener); - - const message = osc.toBuffer({ - elements: [ - { - address: "/address", - args: ["key1", "value1"] - }, - { - address: "/address", - args: ["key2", "value2"] - } - ], - oscType: 'bundle' - }); - dgram.createSocket('udp4').send(message, 0, message.byteLength, port, "127.0.0.1") - }) + }) })