Skip to content

Commit abfac86

Browse files
committed
Add event highlighting to the TidalCycles package
Delete styles/tidal.less Restore tidal.less file Add condition to ensure, that meta data will only be added, when the feature is enabled. Refactor osc handling Introduce eventIds Add multiple text editor support Fix PR issues Refactor osc server transformer logic chore: move highlight feature enable check to main Add LineProcessor helper class Extract regex statements Add audio motion analyzer Add styles Use MrReason setup directory as stack enrtry point Change stack package path Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Continue rebasing Fix remaining rebase issues Fix remote control Add safety check Handle osc style changes Add a event highlighting web worker Replace old event queue with worker queue Add set tests Remove rebase duplication Add osc style description to README Remove polyfills set import in main Add first attempt of state resetting when receiving hush Add a discard counter in the worker Fix bugs Remove worker Group tidal events by timestamp Remove missThreshold Rename messageBuffer array Refactor event-highlighter Update README Fix unit tests Revert mrreason setup changes Remove fonts and styles Add fps description Remove hush handling Remove line processor fixes
1 parent f39bcdf commit abfac86

File tree

10 files changed

+394
-208
lines changed

10 files changed

+394
-208
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ Filter all the console old logs and show only the last one
9090

9191
### Event Highlighting
9292

93-
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.
93+
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.
9494

9595
#### TidalCycles configuration
9696

9797
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:
9898

9999
```haskell
100-
let editorTarget = Target {oName = "editor", oAddress = "127.0.0.1", oPort = 6013, oLatency = 0.02, oSchedule = Pre BundleStamp, oWindow = Nothing, oHandshake = False, oBusPort = Nothing }
100+
let editorTarget = Target {oName = "editor", oAddress = "127.0.0.1", oPort = 6013, oLatency = 0.2, oSchedule = Pre BundleStamp, oWindow = Nothing, oHandshake = False, oBusPort = Nothing }
101101
let editorShape = OSCContext "/editor/highlights"
102102

103-
tidal <- startStream (defaultConfig {cFrameTimespan = 1/50}) [(superdirtTarget {oLatency = 0.2}, [superdirtShape]), (editorTarget, [editorShape])]
103+
tidal <- startStream (defaultConfig {cFrameTimespan = 1/30, , cProcessAhead = (1/20)}) [(superdirtTarget {oLatency = 0.02}, [superdirtShape]), (editorTarget, [editorShape])]
104104
```
105105

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

lib/config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,14 @@ export default {
140140
},
141141
'fps': {
142142
type: 'number',
143-
default: 30,
144143
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.',
145-
order: 100,
144+
default: 30,
145+
order: 96,
146+
},
147+
'delay': {
148+
type: 'number',
149+
default: 0.0,
150+
order: 98,
146151
},
147152
'ip': {
148153
type: 'string',

lib/event-highlighter.js

Lines changed: 125 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import OscServer from './osc-server';
44
import LineProcessor from './line-processor';
55

6+
if (!Set.prototype.union) {
7+
Set.prototype.union = function (otherSet) {
8+
return new Set([...this, ...otherSet])
9+
}
10+
}
11+
612
const CLASS = Object.freeze({
713
base: "event-highlight",
814
idPrefix: "event-highlight-",
@@ -28,15 +34,12 @@ export default class EventHighlighter {
2834
// Data‑structures -------------------------------------------------------
2935
this.markers = new Map(); // textbuffer.id → row → col → Marker
3036
this.highlights = new Map(); // eventId → texteditor.id → col → Marker
31-
this.filteredMessages = new Map(); // eventId → event
32-
this.receivedThisFrame = new Map(); // eventId → event
33-
this.addedThisFrame = new Set(); // Set<event>
3437
this.eventIds = []; // [{textbufferId, row}]
38+
this.messageBuffer = new Map();
39+
this.activeMessages = new Map();
3540

36-
// Animation state -------------------------------------------------------
37-
this.then = 0; // time at previous frame
41+
this.then = 0;
3842

39-
// Bind instance methods used as callbacks -----------------------------
4043
this.animate = this.animate.bind(this);
4144
}
4245

@@ -48,11 +51,62 @@ export default class EventHighlighter {
4851
init() {
4952
this.#installBaseHighlightStyle();
5053

51-
// Kick‑off animation loop
5254
this.then = window.performance.now();
5355
requestAnimationFrame(this.animate);
5456
}
5557

58+
/** requestAnimationFrame callback */
59+
animate(now) {
60+
const elapsed = now - this.then;
61+
62+
const configFPS = atom.config.get('tidalcycles.eventHighlighting.fps');
63+
const missThreshold = atom.config.get('tidalcycles.eventHighlighting.missThreshold');
64+
const delay = atom.config.get('tidalcycles.eventHighlighting.delay');
65+
66+
const fpsInterval = 1000 / configFPS;
67+
let activeThisFrame = new Set();
68+
69+
if (elapsed >= fpsInterval) {
70+
this.then = now - (elapsed % fpsInterval);
71+
72+
[...this.messageBuffer.entries()]
73+
.filter(([ts]) => (ts + (delay * -1)) < this.then)
74+
.forEach(([ts, event]) => {
75+
activeThisFrame = activeThisFrame.union(event);
76+
this.messageBuffer.delete(ts);
77+
}
78+
);
79+
80+
const { active, added, removed } = this.#diffEventMaps(
81+
this.activeMessages,
82+
this.#transformedEvents(activeThisFrame)
83+
);
84+
85+
removed.forEach(evt => {
86+
const cols = this.activeMessages.get(evt.eventId);
87+
if (cols) {
88+
cols.delete(evt.colStart);
89+
if (cols.size === 0) {
90+
this.activeMessages.delete(evt.eventId);
91+
}
92+
}
93+
});
94+
95+
this.activeMessages = this.#transformedEvents(
96+
[...active, ...added]
97+
);
98+
99+
added.forEach((evt) => {
100+
this.#addHighlight(evt);
101+
});
102+
103+
removed.forEach((evt) => this.#removeHighlight(evt));
104+
105+
}
106+
107+
requestAnimationFrame(this.animate);
108+
}
109+
56110
/** Clean‑up resources when package is deactivated */
57111
destroy() {
58112
try {
@@ -92,53 +146,24 @@ export default class EventHighlighter {
92146
}
93147
}
94148

149+
95150
highlightTransformer(args) {
96151
const result = [
97-
{value: "id"}, args[0],
98-
{value: "duration"}, args[1],
99-
{value: "cycle"}, args[2],
100-
{value: "colStart"}, args[3],
101-
{value: "eventId"}, {value: args[4].value - 1},
102-
{value: "colEnd"}, args[5],
152+
{value: "time"}, {value: args[0]},
153+
{value: "id"}, args[1],
154+
{value: "duration"}, args[2],
155+
{value: "cycle"}, args[3],
156+
{value: "colStart"}, args[4],
157+
{value: "eventId"}, {value: args[5].value - 1},
158+
{value: "colEnd"}, args[6],
103159
];
104160

105161
return result;
106162
}
107163

108-
/** requestAnimationFrame callback */
109-
animate(now) {
110-
const elapsed = now - this.then;
111-
const configFPS = atom.config.get('tidalcycles.eventHighlighting.fps');
112-
const fpsInterval = 1000 / configFPS;
113-
114-
if (elapsed >= fpsInterval) {
115-
this.then = now - (elapsed % fpsInterval);
116-
117-
// Add newly‑received highlights -------------------------------------
118-
this.addedThisFrame.forEach((evt) => {
119-
this.#addHighlight(evt);
120-
});
121-
122-
// Remove highlights no longer present ------------------------------
123-
const { updated, removed } = this.#diffEventMaps(
124-
this.filteredMessages,
125-
this.receivedThisFrame,
126-
);
127-
this.filteredMessages = updated;
128-
removed.forEach((evt) => this.#removeHighlight(evt));
129-
130-
// Reset per‑frame collections --------------------------------------
131-
this.receivedThisFrame.clear();
132-
this.addedThisFrame.clear();
133-
}
134-
135-
requestAnimationFrame(this.animate);
136-
}
137-
138164
// ----------------------------------------------------------------------
139165
// Private helpers
140166
// ----------------------------------------------------------------------
141-
142167
/** Injects the base CSS rule used for all highlights */
143168
#installBaseHighlightStyle() {
144169
atom.styles.addStyleSheet(`
@@ -149,18 +174,6 @@ export default class EventHighlighter {
149174
`);
150175
}
151176

152-
/** Store events until the next animation frame */
153-
#queueEvent(event) {
154-
const eventMap = ensureNestedMap(this.filteredMessages, event.eventId);
155-
const recvMap = ensureNestedMap(this.receivedThisFrame, event.eventId);
156-
157-
if (!eventMap.has(event.colStart)) eventMap.set(event.colStart, event);
158-
if (!recvMap.has(event.colStart)) {
159-
this.addedThisFrame.add(event);
160-
recvMap.set(event.colStart, event);
161-
}
162-
}
163-
164177
// Highlight management
165178
#addHighlight({ id, colStart, eventId }) {
166179
const bufferId = this.eventIds[eventId].bufferId;
@@ -192,16 +205,14 @@ export default class EventHighlighter {
192205
editor.decorateMarker(marker, { type: "text", class: `${CLASS.idPrefix}${id}` });
193206

194207
textEditorEvent.set(colStart, marker);
195-
// eventId → texteditor.id → col → Marker
196208
});
197209
}
198210

199211
#removeHighlight({ colStart, eventId }) {
200212

201213
const highlightEvents = this.highlights.get(eventId);
202-
// console.log("removeHighlight", highlightEvents, eventId, colStart);
203214

204-
if (!highlightEvents.size) return;
215+
if (!highlightEvents || !highlightEvents.size) return;
205216

206217
highlightEvents.forEach(textEditorIdEvent => {
207218
const marker = textEditorIdEvent.get(colStart);
@@ -210,8 +221,6 @@ export default class EventHighlighter {
210221
if (!marker) return;
211222
marker.destroy();
212223
})
213-
214-
215224
}
216225

217226
// Marker generation (per line)
@@ -234,26 +243,66 @@ export default class EventHighlighter {
234243
});
235244
}
236245

246+
#ensureNestedMap(root, key) {
247+
if (!root.has(key)) root.set(key, new Map());
248+
return root.get(key);
249+
}
250+
251+
#queueEvent(event) {
252+
if (!this.messageBuffer.has(event.time)) this.messageBuffer.set(event.time, new Set());
253+
this.messageBuffer.get(event.time).add(event);
254+
}
255+
237256
#diffEventMaps(prevEvents, currentEvents) {
238-
const removed = new Set();
239-
const updated = new Map(prevEvents);
240-
241-
for (const [event, prevCols] of prevEvents) {
242-
const currCols = currentEvents.get(event);
243-
if (!currCols) {
244-
for (const [, prevEvt] of prevCols) removed.add(prevEvt);
245-
updated.delete(event);
246-
continue;
257+
const removed = new Set();
258+
const added = new Set();
259+
const active = new Set();
260+
261+
for (const [event, prevCols] of prevEvents) {
262+
const currCols = currentEvents.get(event);
263+
if (!currCols) {
264+
for (const [, prevEvt] of prevCols) removed.add(prevEvt);
265+
continue;
266+
}
267+
268+
for (const [col, prevEvt] of prevCols) {
269+
if (!currCols.has(col)) {
270+
removed.add(prevEvt);
271+
} else {
272+
active.add(prevEvt);
273+
}
274+
}
247275
}
248276

249-
for (const [col, prevEvt] of prevCols) {
250-
if (!currCols.has(col)) {
251-
removed.add(prevEvt);
252-
updated.get(event).delete(col);
277+
for (const [event, currCols] of currentEvents) {
278+
const prevCols = prevEvents.get(event);
279+
if (!prevCols) {
280+
for (const [, currEvt] of currCols) added.add(currEvt);
281+
continue;
282+
}
283+
284+
for (const [col, currEvt] of currCols) {
285+
if (!prevCols.has(col)) {
286+
added.add(currEvt);
287+
}
253288
}
254289
}
255-
}
256290

257-
return { updated, removed };
291+
return { removed, added, active };
258292
}
293+
294+
#transformedEvents(events) {
295+
const resultEvents = new Map();
296+
events.forEach(event => {
297+
if (!resultEvents.get(event.eventId)) {
298+
resultEvents.set(event.eventId, new Map());
299+
}
300+
const cols = resultEvents.get(event.eventId);
301+
302+
cols.set(event.colStart, event);
303+
});
304+
305+
return resultEvents;
306+
}
307+
259308
}

0 commit comments

Comments
 (0)