Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ 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

Expand All @@ -105,10 +105,6 @@ tidal <- startStream (defaultConfig {cFrameTimespan = 1/50}) [(superdirtTarget {

The path to the `BootTidal.hs` file can be found in the TidalCycles output console after TidalCycles has been booted in the editor.

#### Framerate

The event highlight animation is in relation to the refresh rate of the users display and the `cFrameTimespan` value of TidalCycles. This means, that the animation fps needs to be smaller then the denominator of the `cFrameTimespan` value. However a good value is somehow between `20 fps` and `30 fps`.

#### Custom Styles

It is possible to customize the event highlighting css styles. For this you can add the css classes under `Pulsar -> Stylesheet...`.
Expand All @@ -133,7 +129,7 @@ And it is possible to override the styles for every individual stream like this:
The pattern of the css class is `.event-highlight-[streamID]`.

### Osc Eval
It's possibile to evaluate tidal code with OSC messages.
It's possible to evaluate tidal code with OSC messages.

#### Port
The plugin is listening on this specified port for incoming osc messages:
Expand Down
6 changes: 0 additions & 6 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,6 @@ export default {
description: 'Check to enable event highlighting.',
order: 95
},
'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,
},
'ip': {
type: 'string',
default: "127.0.0.1",
Expand Down
41 changes: 41 additions & 0 deletions lib/event-highlighting/event-highlight-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const EventMapHelper = require('./event-map-helper')

require("../polyfills/set");

const FRAME_RATE = 1000/30;

let messageBuffer = new Map();
let receivedThisFrame = new Map();

self.onmessage = function(e) {
Copy link
Collaborator

@ndr-brt ndr-brt Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really an expert in js workers, but I see that after the instantiation (event-highlighter.js:52), this onmessage field is overridden. Is then this assignment useless?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is the function that will be triggered, when the web worker receives messages from the main thread. The other one in in event-highlighter.js:52 is the function that will be triggered, when the main thread receives a message from the web worker.

This is the hole main idea. Doing the calculation and message buffer handling within a separate thread and independant from the animation frame.

const event = e.data;

queueEvent(event);
};

setInterval(() => {
const {active, added, removed} = EventMapHelper.diffEventMaps(
messageBuffer,
receivedThisFrame,
);

postMessage({active, added, removed });
messageBuffer = EventMapHelper.setToMap(added.union(active));
receivedThisFrame.clear();

}, FRAME_RATE);

/** Helper that creates (or returns existing) nested maps. */
function ensureNestedMap(root, key) {
if (!root.has(key)) root.set(key, new Map());
return root.get(key);
}

/** Store events until the next animation frame */
function queueEvent(event) {
const recvMap = ensureNestedMap(receivedThisFrame, event.eventId);

if (!recvMap.has(event.colStart)) {
recvMap.set(event.colStart, event);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use babel'

import OscServer from './osc-server';
import OscServer from './../osc-server';
import LineProcessor from './line-processor';
import path from 'path';

const CLASS = Object.freeze({
base: "event-highlight",
Expand All @@ -28,16 +29,8 @@ 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<event>
this.eventIds = []; // [{textbufferId, row}]

// Animation state -------------------------------------------------------
this.then = 0; // time at previous frame

// Bind instance methods used as callbacks -----------------------------
this.animate = this.animate.bind(this);
this.worker;
}

// ----------------------------------------------------------------------
Expand All @@ -48,9 +41,21 @@ export default class EventHighlighter {
init() {
this.#installBaseHighlightStyle();

// Kick‑off animation loop
this.then = window.performance.now();
requestAnimationFrame(this.animate);
this.#startWorker();
}

#startWorker() {
const workerPath = path.join(__dirname, '.', 'event-highlight-worker.js');
this.worker = new Worker(workerPath);

this.worker.onmessage = function(event) {
const {added, removed} = event.data;
added.forEach((evt) => {
this.#addHighlight(evt);
});

removed.forEach((evt) => this.#removeHighlight(evt));
}.bind(this);
}

/** Clean‑up resources when package is deactivated */
Expand Down Expand Up @@ -88,7 +93,10 @@ export default class EventHighlighter {
oscHighlightSubscriber() {
return (args: {}): void => {
const message = OscServer.asDictionary(this.highlightTransformer(args));
this.#queueEvent(message);

if (this.worker) {
this.worker.postMessage(message);
}
}
}

Expand All @@ -105,40 +113,9 @@ export default class EventHighlighter {
return result;
}

/** requestAnimationFrame callback */
animate(now) {
const elapsed = now - this.then;
const configFPS = atom.config.get('tidalcycles.eventHighlighting.fps');
const fpsInterval = 1000 / configFPS;

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();
}

requestAnimationFrame(this.animate);
}

// ----------------------------------------------------------------------
// Private helpers
// ----------------------------------------------------------------------

/** Injects the base CSS rule used for all highlights */
#installBaseHighlightStyle() {
atom.styles.addStyleSheet(`
Expand All @@ -149,18 +126,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;
Expand Down Expand Up @@ -201,7 +166,7 @@ export default class EventHighlighter {
const highlightEvents = this.highlights.get(eventId);
// console.log("removeHighlight", highlightEvents, eventId, colStart);

if (!highlightEvents.size) return;
if (!highlightEvents || !highlightEvents.size) return;

highlightEvents.forEach(textEditorIdEvent => {
const marker = textEditorIdEvent.get(colStart);
Expand All @@ -210,8 +175,6 @@ export default class EventHighlighter {
if (!marker) return;
marker.destroy();
})


}

// Marker generation (per line)
Expand All @@ -234,26 +197,4 @@ export default class EventHighlighter {
});
}

#diffEventMaps(prevEvents, currentEvents) {
const removed = new Set();
const updated = new Map(prevEvents);

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);
}
}
}

return { updated, removed };
}
}
59 changes: 59 additions & 0 deletions lib/event-highlighting/event-map-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use babel'

class EventMapHelper {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit, I'm not the biggest fan of "helpers" and "utils" classes, so I'd suggest to avoid them as much as possible.

In this case, both the functions are only used by the worker, so could them be moved there?

Copy link
Author

@thgrund thgrund Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither. But I am not able to define functions within the web worker and test them properly, because the jasmine tests only works within the main thread. This means I can not simply export them, because a worker is not a module. This would be the tradeoff: extract them somehow (class method or static function) or removing the tests. And to test the worker within the main thread is already an integration test.


static diffEventMaps(prevEvents, currentEvents) {
const removed = new Set();
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);
continue;
}

for (const [col, prevEvt] of prevCols) {
if (!currCols.has(col)) {
removed.add(prevEvt);
} else {
active.add(prevEvt);
}
}
}

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};
}

static setToMap(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;
}
}


module.exports = EventMapHelper;
32 changes: 16 additions & 16 deletions lib/line-processor.js → lib/event-highlighting/line-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ export default class LineProcessor {

// Valid TidalCycles word chars
static isValidTidalWordChar(character) {
const code = character.charCodeAt(0);
// 0-9
const digitMin = 48;
const digitMax = 57;
const code = character.charCodeAt(0);
// 0-9
const digitMin = 48;
const digitMax = 57;
// A-Z
const upperCaseMin = 65;
const upperCaseMax = 90;
// a-z
const lowerCaseMin = 97;
const lowerCaseMax = 122;

return (
(code >= digitMin && code <= digitMax) ||
(code >= upperCaseMin && code <= upperCaseMax) ||
(code >= lowerCaseMin && code <= lowerCaseMax)
);
return (
(code >= digitMin && code <= digitMax) ||
(code >= upperCaseMin && code <= upperCaseMax) ||
(code >= lowerCaseMin && code <= lowerCaseMax)
);
}

static isQuotationMark(character) {
const code = character.charCodeAt(0);
const code = character.charCodeAt(0);
// "
const quotationMark = 34;

return code === quotationMark;
return code === quotationMark;
}

static findTidalWordRanges(line, callback) {
Expand All @@ -38,7 +38,7 @@ export default class LineProcessor {
Array.from(line).forEach((char, index) => {
if (LineProcessor.isQuotationMark(char)) {
insideQuotes = !insideQuotes;
}
}

if (insideQuotes && LineProcessor.isValidTidalWordChar(char)) {
if (!start) {
Expand All @@ -54,14 +54,14 @@ export default class LineProcessor {
end = null;
}
}
})
})
}

static controlPatternsRegex() {
return new RegExp(/"([^"]*)"/g);
return new RegExp(/"([^"]*)"/g);
}

static exceptedFunctionPatterns() {
return new RegExp(/numerals\s*=.*$|p\s.*$/);
return new RegExp(/numerals\s*=.*$|p\s.*$/);
}
}
Loading
Loading