Add chord number labels with click-to-siren
- Add label nodes at top of each chord column - Modify xforce layout to skip label nodes - Click label to play entire chord on siren - Use chordIndex/nodeIndex (server calculates frequency) - Fix server to support both calling conventions - Fix client to use proper JSON headers for kill-siren
This commit is contained in:
parent
62e6a75f4f
commit
90f00e56c6
|
|
@ -35,6 +35,8 @@
|
|||
|
||||
const state = new Map();
|
||||
nodes.forEach(n => {
|
||||
if (n.data('isLabel')) return; // Skip label nodes
|
||||
|
||||
const chordLabel = n.data('chordLabel');
|
||||
const bounds = opts.bounds[chordLabel] || { min: 0, max: graphWidth };
|
||||
state.set(n.id(), {
|
||||
|
|
@ -330,6 +332,32 @@
|
|||
return parseFloat(fracStr);
|
||||
}
|
||||
|
||||
// Play all nodes in a chord on the siren
|
||||
function playChordOnSiren(chordIdx) {
|
||||
// Find all nodes in this chord (exclude label nodes)
|
||||
const nodes = cy.nodes().filter(n =>
|
||||
n.data('chordIndex') === chordIdx && !n.data('isLabel')
|
||||
);
|
||||
|
||||
// Clear old sirenActive markers
|
||||
cy.nodes().forEach(n => n.data('sirenActive', ''));
|
||||
|
||||
// Play each node using chordIndex + localId (let server calculate frequency)
|
||||
nodes.forEach((node) => {
|
||||
const localId = node.data('localId');
|
||||
|
||||
fetch('/api/play-siren', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
chordIndex: chordIdx,
|
||||
nodeIndex: localId
|
||||
})
|
||||
});
|
||||
node.data('sirenActive', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle between cents and frequency display
|
||||
function toggleDisplayUnit() {
|
||||
displayMode = displayMode === 'cents' ? 'frequency' : 'cents';
|
||||
|
|
@ -386,7 +414,7 @@
|
|||
container: document.getElementById('graph-container'),
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
selector: 'node[color]',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'width': 32,
|
||||
|
|
@ -402,6 +430,22 @@
|
|||
'border-width': 0,
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[isLabel = "true"]',
|
||||
style: {
|
||||
'background-color': '#888888',
|
||||
'width': 40,
|
||||
'height': 40,
|
||||
'label': 'data(chordIndex)',
|
||||
'color': '#ffffff',
|
||||
'font-size': '18px',
|
||||
'font-weight': 'bold',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-outline-width': 0,
|
||||
'border-width': 0,
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[sirenActive = "true"]',
|
||||
style: {
|
||||
|
|
@ -501,6 +545,13 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle label node clicks
|
||||
if (node.data('isLabel')) {
|
||||
const chordIdx = node.data('chordIndex');
|
||||
playChordOnSiren(chordIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
const chordIndex = node.data('chordIndex');
|
||||
const localId = node.data('localId');
|
||||
|
||||
|
|
@ -600,7 +651,8 @@
|
|||
|
||||
const nodes = graph.nodes;
|
||||
const edges = graph.edges || [];
|
||||
const xBase = 100 + chordIdx * chordSpacing;
|
||||
const gap = 50;
|
||||
const xBase = 100 + chordIdx * (chordSpacing + gap);
|
||||
const idMap = {};
|
||||
|
||||
// Create unique IDs per chord to avoid collisions
|
||||
|
|
@ -641,6 +693,18 @@
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Add label node for this chord (locked so layout doesn't move it)
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: `label_${chordIdx}`,
|
||||
chordIndex: chordIdx,
|
||||
isLabel: "true",
|
||||
},
|
||||
position: { x: xBase + (chordSpacing * 0.15), y: 30 },
|
||||
locked: true,
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
|
|
|||
|
|
@ -129,8 +129,21 @@ def play_freq():
|
|||
def play_siren():
|
||||
"""Play a single frequency for a node on the siren."""
|
||||
data = request.json
|
||||
|
||||
# Support two modes:
|
||||
# 1. chordIndex + nodeIndex (from clicking a single node)
|
||||
# 2. frequency + voice (from clicking a chord label)
|
||||
chord_index = data.get("chordIndex")
|
||||
node_index = data.get("nodeIndex")
|
||||
frequency = data.get("frequency")
|
||||
voice = data.get("voice")
|
||||
|
||||
if frequency is None and voice is None:
|
||||
# Mode 1: use chordIndex/nodeIndex
|
||||
if chord_index is None or node_index is None:
|
||||
return jsonify(
|
||||
{"error": "Missing chordIndex/nodeIndex or frequency/voice"}
|
||||
), 400
|
||||
|
||||
if chord_index < 0 or chord_index >= len(chords):
|
||||
return jsonify({"error": "Invalid chord index"}), 400
|
||||
|
|
@ -143,6 +156,10 @@ def play_siren():
|
|||
fraction = Fraction(pitch.get("fraction", "1"))
|
||||
frequency = fundamental * float(fraction)
|
||||
voice = node_index + 1 # 1-indexed
|
||||
else:
|
||||
# Mode 2: use provided frequency/voice
|
||||
if frequency is None or voice is None:
|
||||
return jsonify({"error": "Missing frequency or voice"}), 400
|
||||
|
||||
# Send to siren (192.168.4.200:54001) using current fundamental
|
||||
siren_sender = OSCSender(ip="192.168.4.200", port=54001, fundamental=fundamental)
|
||||
|
|
@ -153,8 +170,6 @@ def play_siren():
|
|||
{
|
||||
"frequency": frequency,
|
||||
"voice": voice,
|
||||
"fundamental": fundamental,
|
||||
"fraction": pitch.get("fraction", "1"),
|
||||
"destination": "siren",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue