Skip to content

Commit f8850be

Browse files
roomi-fieldsclaude
andcommitted
CC → structural properties: @Map cc:N -> label.ratio
When a @Map targets label.ratio where label matches a polymetric group label (groove:{A, B}), CC input modifies the group's speed ratio and triggers auto-recompile, same path as drag-resize on timeline. - onStructuralChange checks if target.actor matches a polyGroup label - CC 0-127 scaled to ratio 0.25-4.0 (64=1.0, log-ish) - Calls rewriteGroupRatio + produce (same code as drag) - _injectPolyGroupLabels extracts labels from AST into timeline polyGroups - Demo scene: cc-structure.bps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a92d4f5 commit f8850be

3 files changed

Lines changed: 85 additions & 6 deletions

File tree

web/demos/cc-structure.bps

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// CC → Structure — contrôle les proportions d'un groupe via CC
2+
// CC1 (mod wheel) modifie le ratio du groupe "groove"
3+
// Nécessite un contrôleur MIDI en input
4+
5+
@core
6+
@controls
7+
@alphabet.western:browser
8+
@duration:8b
9+
10+
@map cc:1 -> groove.ratio
11+
12+
S -> groove:{C4 D4 E4 F4, C3 G3 C3 G3}

web/demos/index.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,13 @@
377377
"description": "CC input → flag via @map directive",
378378
"file": "midi-map.bps"
379379
},
380+
{
381+
"id": "cc-structure",
382+
"title": "CC: Structure",
383+
"category": "midi",
384+
"description": "CC1 modifie le ratio d'un groupe polymétrique via @map",
385+
"file": "cc-structure.bps"
386+
},
380387
{
381388
"id": "multi-scene",
382389
"title": "Multi-Scene",

web/index.html

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2517,6 +2517,7 @@ <h2>Controls</h2>
25172517
lastTerminalActorMap = compiled.terminalActorMap || {};
25182518
lastMapTable = compiled.mapTable || [];
25192519
lastSceneControls = _extractSceneControls(compiled.ast);
2520+
window._lastCompiledAST = compiled.ast;
25202521
buildControlsPanel();
25212522

25222523
// Handle @scene directives — create tabs + load child sources
@@ -2844,9 +2845,26 @@ <h2>Controls</h2>
28442845
if (dispatcher._mapEngine) {
28452846
dispatcher._mapEngine.setOnStructuralChange((target, value) => {
28462847
const container = document.getElementById('controlsContent');
2847-
if (!container) return;
28482848

2849-
if (target.kind === 'flag') {
2849+
if (target.kind === 'flag' && target.name === 'ratio' && target.actor) {
2850+
// Structural property: actor is a polymetric group label
2851+
// Find the group by label in the timeline and modify its ratio
2852+
const tl = window._timeline;
2853+
if (tl && tl.polyGroups) {
2854+
const groupIdx = tl.polyGroups.findIndex(g => g.label === target.actor);
2855+
if (groupIdx >= 0) {
2856+
// CC 0-127 → ratio 0.25 to 4.0 (log scale, 64=1.0)
2857+
const ratio = value <= 64
2858+
? 0.25 + (value / 64) * 0.75 // 0→0.25, 64→1.0
2859+
: 1.0 + ((value - 64) / 63) * 3.0; // 64→1.0, 127→4.0
2860+
rewriteGroupRatio(groupIdx, ratio);
2861+
produce();
2862+
return;
2863+
}
2864+
}
2865+
}
2866+
2867+
if (target.kind === 'flag' && container) {
28502868
// Update flag input in Flags section
28512869
const flagInput = container.querySelector(`.flag-input[data-flag="${target.name}"]`);
28522870
if (flagInput) {
@@ -2856,11 +2874,11 @@ <h2>Controls</h2>
28562874
}
28572875
const flagVal = container.querySelector(`.slider-value[data-for="flag_${target.name}"]`);
28582876
if (flagVal) flagVal.textContent = Math.round(value);
2859-
}
28602877

2861-
// Also update knobs/sliders if a control was changed
2862-
if (target.kind === 'flag' && dispatcher.onControlChange) {
2863-
dispatcher.onControlChange(target.name, value);
2878+
// Also update knobs/sliders if a control was changed
2879+
if (dispatcher.onControlChange) {
2880+
dispatcher.onControlChange(target.name, value);
2881+
}
28642882
}
28652883
});
28662884
}
@@ -4338,10 +4356,52 @@ <h2>Controls</h2>
43384356
});
43394357
const src = window.getEditorSource ? getEditorSource() : '';
43404358
window._timeline.load(tokens, { cvTable: lastCVTable, controlTable: lastControlTable, source: src });
4359+
_injectPolyGroupLabels();
43414360
});
43424361
} else {
43434362
const src = window.getEditorSource ? getEditorSource() : '';
43444363
window._timeline.load(tokens, { cvTable: lastCVTable, controlTable: lastControlTable, source: src });
4364+
_injectPolyGroupLabels();
4365+
}
4366+
}
4367+
4368+
/**
4369+
* Inject polymetric group labels from the AST into the timeline's polyGroups.
4370+
* The timeline parses {,} markers from tokens but doesn't know the BPscript labels.
4371+
*/
4372+
function _injectPolyGroupLabels() {
4373+
const tl = window._timeline;
4374+
if (!tl || !tl.polyGroups) return;
4375+
4376+
// Extract labels from AST (in openOrder)
4377+
const labels = [];
4378+
function walkRhs(elements) {
4379+
for (const el of elements) {
4380+
if (el.type === 'Polymetric') {
4381+
labels.push(el.label || null);
4382+
if (el.voices) {
4383+
for (const voice of el.voices) {
4384+
if (Array.isArray(voice)) walkRhs(voice);
4385+
}
4386+
}
4387+
}
4388+
}
4389+
}
4390+
4391+
const ast = window._lastCompiledAST;
4392+
if (ast?.subgrammars) {
4393+
for (const sg of ast.subgrammars) {
4394+
for (const rule of sg.rules || []) {
4395+
if (rule.rhs) walkRhs(rule.rhs);
4396+
}
4397+
}
4398+
}
4399+
4400+
// Match by openOrder
4401+
for (let i = 0; i < tl.polyGroups.length && i < labels.length; i++) {
4402+
if (labels[i]) {
4403+
tl.polyGroups[i].label = labels[i];
4404+
}
43454405
}
43464406
}
43474407

0 commit comments

Comments
 (0)