Add multi-siren control to Path Navigator
- Add siren selection (1-4) and auto (A) toggle buttons - Keyboard: 1-4 select siren, 0/A auto, R ramp toggle - Click plays to siren, Shift+click to SuperCollider - Add RAMP toggle button - Colored borders on nodes based on which siren is playing - Fix label clicks to use auto (node's voice) - Add toggle styling to cents/freq button - Disable siren hotkeys when typing in input fields - Disable Flask debug mode for background operation
This commit is contained in:
parent
655cb68f31
commit
5a1a4f03dd
|
|
@ -610,7 +610,10 @@
|
||||||
const transcribeResponse = await fetch('/api/transcribe', {
|
const transcribeResponse = await fetch('/api/transcribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ name: transcribeName })
|
body: JSON.stringify({
|
||||||
|
name: transcribeName,
|
||||||
|
fundamental: parseFloat(document.getElementById('fundamental').value) || 55
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const transcribeResult = await transcribeResponse.json();
|
const transcribeResult = await transcribeResponse.json();
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,27 @@
|
||||||
color: #999999;
|
color: #999999;
|
||||||
border-color: #444444;
|
border-color: #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-btn, .siren-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #666666;
|
||||||
|
border: 1px solid #222222;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
.toggle-btn:hover, .siren-btn:hover {
|
||||||
|
background: #151515;
|
||||||
|
color: #999999;
|
||||||
|
border-color: #444444;
|
||||||
|
}
|
||||||
|
.toggle-btn.active, .siren-btn.active {
|
||||||
|
background: #1a3a1a;
|
||||||
|
color: #66cc66;
|
||||||
|
border-color: #2a5a2a;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -344,13 +365,21 @@
|
||||||
</div>
|
</div>
|
||||||
<span style="margin-left: 15px;">Siren IP:</span>
|
<span style="margin-left: 15px;">Siren IP:</span>
|
||||||
<input type="text" id="sirenIp" value="192.168.4.200" style="width: 100px;">
|
<input type="text" id="sirenIp" value="192.168.4.200" style="width: 100px;">
|
||||||
<button id="toggleUnitBtn" style="margin-left: 15px;">Show: Cents</button>
|
<button id="toggleUnitBtn" class="toggle-btn">Show: Cents</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="prevBtn" disabled>← Previous</button>
|
<button id="prevBtn" disabled>← Previous</button>
|
||||||
<span class="index-display">Index: <span id="currentIndex">0</span> / <span id="totalSteps">0</span></span>
|
<span class="index-display">Index: <span id="currentIndex">0</span> / <span id="totalSteps">0</span></span>
|
||||||
<button id="nextBtn" disabled>Next →</button>
|
<button id="nextBtn" disabled>Next →</button>
|
||||||
|
<span style="margin-left: 20px; border-left: 1px solid #333; padding-left: 20px;">
|
||||||
|
<button id="rampBtn" class="toggle-btn">RAMP: OFF</button>
|
||||||
|
<button id="siren1Btn" class="siren-btn">1</button>
|
||||||
|
<button id="siren2Btn" class="siren-btn">2</button>
|
||||||
|
<button id="siren3Btn" class="siren-btn">3</button>
|
||||||
|
<button id="siren4Btn" class="siren-btn">4</button>
|
||||||
|
<button id="sirenABtn" class="siren-btn">A</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="graph-container"></div>
|
<div id="graph-container"></div>
|
||||||
|
|
@ -374,6 +403,8 @@
|
||||||
<script>
|
<script>
|
||||||
// Global state
|
// Global state
|
||||||
let chords = [];
|
let chords = [];
|
||||||
|
let selectedSiren = 0; // 0 = use node's voice, 1-4 = selected siren
|
||||||
|
let rampMode = false; // false = play, true = ramp
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let totalSteps = 0;
|
let totalSteps = 0;
|
||||||
let hasPrev = false;
|
let hasPrev = false;
|
||||||
|
|
@ -381,6 +412,31 @@
|
||||||
let allGraphsData = null;
|
let allGraphsData = null;
|
||||||
let displayMode = 'cents';
|
let displayMode = 'cents';
|
||||||
|
|
||||||
|
// Update UI buttons based on state
|
||||||
|
function updateRampButton() {
|
||||||
|
const btn = document.getElementById('rampBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.textContent = rampMode ? 'RAMP: ON' : 'RAMP: OFF';
|
||||||
|
btn.classList.toggle('active', rampMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSirenButtons() {
|
||||||
|
const buttons = [
|
||||||
|
{ id: 'siren1Btn', value: 1 },
|
||||||
|
{ id: 'siren2Btn', value: 2 },
|
||||||
|
{ id: 'siren3Btn', value: 3 },
|
||||||
|
{ id: 'siren4Btn', value: 4 },
|
||||||
|
{ id: 'sirenABtn', value: 0 },
|
||||||
|
];
|
||||||
|
buttons.forEach(({ id, value }) => {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('active', selectedSiren === value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function adjustValue(id, delta) {
|
function adjustValue(id, delta) {
|
||||||
const input = document.getElementById(id);
|
const input = document.getElementById(id);
|
||||||
const min = parseFloat(input.min) || -Infinity;
|
const min = parseFloat(input.min) || -Infinity;
|
||||||
|
|
@ -416,17 +472,24 @@
|
||||||
// Play each node using chordIndex + localId (let server calculate frequency)
|
// Play each node using chordIndex + localId (let server calculate frequency)
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
const localId = node.data('localId');
|
const localId = node.data('localId');
|
||||||
|
// Label always uses auto (node's voice)
|
||||||
|
const body = {
|
||||||
|
chordIndex: chordIdx,
|
||||||
|
nodeIndex: localId,
|
||||||
|
ip: sirenIp,
|
||||||
|
};
|
||||||
|
// Only add sirenNumber if a specific siren is selected
|
||||||
|
if (selectedSiren > 0) {
|
||||||
|
body.sirenNumber = selectedSiren;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/play-siren', {
|
fetch('/api/play-siren', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body)
|
||||||
chordIndex: chordIdx,
|
|
||||||
nodeIndex: localId,
|
|
||||||
ip: sirenIp
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
node.data('sirenActive', 'true');
|
node.data('sirenActive', 'true');
|
||||||
|
node.data('borderColor', voiceColors[localId % 4]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,16 +522,23 @@
|
||||||
nodes.forEach(node => node.data('sirenActive', 'true'));
|
nodes.forEach(node => node.data('sirenActive', 'true'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rampBody = {
|
||||||
|
chordIndex: chordIdx,
|
||||||
|
nodeIndex: nodeIdx,
|
||||||
|
duration: durationMs,
|
||||||
|
exponent: exponent,
|
||||||
|
ip: sirenIp
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add sirenNumber if a specific siren is selected (> 0), otherwise use node's voice
|
||||||
|
if (selectedSiren > 0) {
|
||||||
|
rampBody.sirenNumber = selectedSiren;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/ramp-to-chord', {
|
fetch('/api/ramp-to-chord', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(rampBody)
|
||||||
chordIndex: chordIdx,
|
|
||||||
nodeIndex: nodeIdx,
|
|
||||||
duration: durationMs,
|
|
||||||
exponent: exponent,
|
|
||||||
ip: sirenIp
|
|
||||||
})
|
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
console.log('Ramping:', data);
|
console.log('Ramping:', data);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|
@ -482,6 +552,7 @@
|
||||||
const btn = document.getElementById('toggleUnitBtn');
|
const btn = document.getElementById('toggleUnitBtn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = displayMode === 'cents' ? 'Show: Cents' : 'Show: Frequency';
|
btn.textContent = displayMode === 'cents' ? 'Show: Cents' : 'Show: Frequency';
|
||||||
|
btn.classList.toggle('active', displayMode === 'frequency');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cy) return;
|
if (!cy) return;
|
||||||
|
|
@ -568,7 +639,7 @@
|
||||||
selector: 'node[sirenActive = "true"]',
|
selector: 'node[sirenActive = "true"]',
|
||||||
style: {
|
style: {
|
||||||
'border-width': 4,
|
'border-width': 4,
|
||||||
'border-color': '#ffffff',
|
'border-color': 'data(borderColor)',
|
||||||
'border-opacity': 1,
|
'border-opacity': 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -665,12 +736,11 @@
|
||||||
|
|
||||||
// Check modifiers
|
// Check modifiers
|
||||||
const isShift = evt.originalEvent && evt.originalEvent.shiftKey;
|
const isShift = evt.originalEvent && evt.originalEvent.shiftKey;
|
||||||
const isRamp = evt.originalEvent && evt.originalEvent.ctrlKey;
|
|
||||||
|
|
||||||
// Handle label node clicks
|
// Handle label node clicks
|
||||||
if (node.data('isLabel')) {
|
if (node.data('isLabel')) {
|
||||||
const chordIdx = node.data('chordIndex');
|
const chordIdx = node.data('chordIndex');
|
||||||
if (isRamp) {
|
if (rampMode || isShift) {
|
||||||
rampToChord(chordIdx);
|
rampToChord(chordIdx);
|
||||||
} else {
|
} else {
|
||||||
playChordOnSiren(chordIdx);
|
playChordOnSiren(chordIdx);
|
||||||
|
|
@ -680,45 +750,75 @@
|
||||||
|
|
||||||
const chordIndex = node.data('chordIndex');
|
const chordIndex = node.data('chordIndex');
|
||||||
const localId = node.data('localId');
|
const localId = node.data('localId');
|
||||||
|
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||||
|
|
||||||
// Handle ramp modifier
|
// Determine action: ramp or play
|
||||||
if (isRamp) {
|
if (rampMode || isShift) {
|
||||||
|
// Ramp to siren
|
||||||
rampToChord(chordIndex, localId);
|
rampToChord(chordIndex, localId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Shift key is held - send to siren, otherwise send to SuperCollider
|
// Check if Shift key is held - send to SuperCollider
|
||||||
const endpoint = isShift ? '/api/play-siren' : '/api/play-freq';
|
if (isShift) {
|
||||||
const destination = isShift ? 'siren' : 'SuperCollider';
|
// Send to SuperCollider
|
||||||
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
const endpoint = '/api/play-freq';
|
||||||
|
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||||
|
|
||||||
console.log('Sending play request to', destination, ':', chordIndex, localId);
|
const requestBody = {
|
||||||
|
chordIndex: chordIndex,
|
||||||
|
nodeIndex: localId,
|
||||||
|
octave: parseInt(document.getElementById('octaveInput').value) || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Sending to SuperCollider:', chordIndex, localId);
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain click: play to siren
|
||||||
|
const endpoint = '/api/play-siren';
|
||||||
|
const destination = 'siren';
|
||||||
|
|
||||||
|
// Determine which siren: if selectedSiren > 0 use it, otherwise use node's voice (localId + 1)
|
||||||
|
const actualSiren = selectedSiren > 0 ? selectedSiren : localId + 1;
|
||||||
|
|
||||||
|
console.log('Sending to siren:', chordIndex, localId, 'siren:', actualSiren);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
chordIndex: chordIndex,
|
||||||
|
nodeIndex: localId,
|
||||||
|
ip: sirenIp,
|
||||||
|
sirenNumber: actualSiren
|
||||||
|
};
|
||||||
|
|
||||||
fetch(endpoint, {
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody)
|
||||||
chordIndex: chordIndex,
|
|
||||||
nodeIndex: localId,
|
|
||||||
octave: parseInt(document.getElementById('octaveInput').value) || 0,
|
|
||||||
ip: sirenIp
|
|
||||||
})
|
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
console.log('Playing on', destination + ':', data.frequency.toFixed(2), 'Hz on voice', data.voice);
|
console.log('Playing on', destination + ':', data.frequency.toFixed(2), 'Hz on siren', data.siren);
|
||||||
|
|
||||||
// If playing on siren, add white circle around node for this voice
|
// Add colored circle around node based on siren
|
||||||
if (isShift) {
|
const sirenColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700'];
|
||||||
const nodeColor = node.data('color');
|
const borderColor = sirenColors[(actualSiren - 1) % 4];
|
||||||
// Find any existing node with same color that has sirenActive and remove it
|
|
||||||
cy.nodes().forEach(n => {
|
// Find any existing node with same border color that has sirenActive and remove it
|
||||||
if (n.data('color') === nodeColor && n.data('sirenActive')) {
|
cy.nodes().forEach(n => {
|
||||||
n.data('sirenActive', '');
|
if (n.data('borderColor') === borderColor && n.data('sirenActive')) {
|
||||||
}
|
n.data('sirenActive', '');
|
||||||
});
|
n.data('borderColor', '');
|
||||||
// Add sirenActive to clicked node
|
}
|
||||||
node.data('sirenActive', 'true');
|
});
|
||||||
console.log('Added sirenActive to', node.id(), 'color:', nodeColor);
|
// Add sirenActive to clicked node
|
||||||
}
|
node.data('sirenActive', 'true');
|
||||||
|
node.data('borderColor', borderColor);
|
||||||
|
console.log('Added sirenActive to', node.id(), 'color:', borderColor);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log('Error playing freq:', err);
|
console.log('Error playing freq:', err);
|
||||||
});
|
});
|
||||||
|
|
@ -1224,8 +1324,35 @@
|
||||||
navigate('next');
|
navigate('next');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle buttons
|
||||||
|
document.getElementById("rampBtn").addEventListener("click", () => {
|
||||||
|
rampMode = !rampMode;
|
||||||
|
console.log('Ramp mode:', rampMode);
|
||||||
|
updateRampButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("siren1Btn").addEventListener("click", () => { selectedSiren = 1; updateSirenButtons(); });
|
||||||
|
document.getElementById("siren2Btn").addEventListener("click", () => { selectedSiren = 2; updateSirenButtons(); });
|
||||||
|
document.getElementById("siren3Btn").addEventListener("click", () => { selectedSiren = 3; updateSirenButtons(); });
|
||||||
|
document.getElementById("siren4Btn").addEventListener("click", () => { selectedSiren = 4; updateSirenButtons(); });
|
||||||
|
document.getElementById("sirenABtn").addEventListener("click", () => { selectedSiren = 0; updateSirenButtons(); });
|
||||||
|
|
||||||
|
// Initialize button states
|
||||||
|
updateRampButton();
|
||||||
|
updateSirenButtons();
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
|
// Don't trigger siren/ramp hotkeys when typing in input fields
|
||||||
|
if (e.target.tagName === 'INPUT') {
|
||||||
|
// Skip siren/ramp hotkeys but allow other keys
|
||||||
|
if (e.key >= '1' && e.key <= '4' ||
|
||||||
|
e.key === '0' || e.key === 'a' || e.key === 'A' ||
|
||||||
|
e.key === 'r' || e.key === 'R') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === "ArrowLeft") {
|
if (e.key === "ArrowLeft") {
|
||||||
navigate('prev');
|
navigate('prev');
|
||||||
} else if (e.key === "ArrowRight") {
|
} else if (e.key === "ArrowRight") {
|
||||||
|
|
@ -1242,6 +1369,21 @@
|
||||||
const zoom = cy.zoom();
|
const zoom = cy.zoom();
|
||||||
cy.zoom({ zoomLevel: Math.max(0.3, zoom / 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } });
|
cy.zoom({ zoomLevel: Math.max(0.3, zoom / 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } });
|
||||||
}
|
}
|
||||||
|
} else if (e.key >= '1' && e.key <= '4') {
|
||||||
|
// Select siren 1-4
|
||||||
|
selectedSiren = parseInt(e.key);
|
||||||
|
console.log('Selected siren:', selectedSiren);
|
||||||
|
updateSirenButtons();
|
||||||
|
} else if (e.key === '0' || e.key === 'a' || e.key === 'A') {
|
||||||
|
// Auto mode - use node's voice
|
||||||
|
selectedSiren = 0;
|
||||||
|
console.log('Auto mode: use node voice');
|
||||||
|
updateSirenButtons();
|
||||||
|
} else if (e.key === 'r' || e.key === 'R') {
|
||||||
|
// Toggle ramp mode
|
||||||
|
rampMode = !rampMode;
|
||||||
|
console.log('Ramp mode:', rampMode);
|
||||||
|
updateRampButton();
|
||||||
} else if (e.key === "k") {
|
} else if (e.key === "k") {
|
||||||
// Soft kill - send 20 Hz to stop voices gently
|
// Soft kill - send 20 Hz to stop voices gently
|
||||||
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||||
|
|
@ -1252,7 +1394,10 @@
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
console.log('Soft kill sent (20 Hz)');
|
console.log('Soft kill sent (20 Hz)');
|
||||||
// Clear all siren circles
|
// Clear all siren circles
|
||||||
cy.nodes().forEach(n => n.removeData('sirenActive'));
|
cy.nodes().forEach(n => {
|
||||||
|
n.removeData('sirenActive');
|
||||||
|
n.removeData('borderColor');
|
||||||
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log('Error sending kill:', err);
|
console.log('Error sending kill:', err);
|
||||||
});
|
});
|
||||||
|
|
@ -1266,7 +1411,10 @@
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
console.log('Hard kill sent (0 Hz)');
|
console.log('Hard kill sent (0 Hz)');
|
||||||
// Clear all siren circles
|
// Clear all siren circles
|
||||||
cy.nodes().forEach(n => n.removeData('sirenActive'));
|
cy.nodes().forEach(n => {
|
||||||
|
n.removeData('sirenActive');
|
||||||
|
n.removeData('borderColor');
|
||||||
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log('Error sending kill:', err);
|
console.log('Error sending kill:', err);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
47
webapp/server.log
Normal file
47
webapp/server.log
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
Starting Path Navigator server...
|
||||||
|
Loading chords from: /home/mwinter/Sketches/compact_sets/output/output_chords.json
|
||||||
|
Loaded 64 chords
|
||||||
|
* Serving Flask app 'server'
|
||||||
|
* Debug mode: on
|
||||||
|
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||||
|
* Running on all addresses (0.0.0.0)
|
||||||
|
* Running on http://127.0.0.1:8080
|
||||||
|
* Running on http://192.168.178.32:8080
|
||||||
|
Press CTRL+C to quit
|
||||||
|
* Restarting with stat
|
||||||
|
* Debugger is active!
|
||||||
|
* Debugger PIN: 477-276-956
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:07] "GET / HTTP/1.1" 304 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:08] "GET /api/chords HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:08] "POST /api/batch-calculate-cents HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:24] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:27] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:29] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:31] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:32] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:35] "POST /api/play-freq HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:37] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:38] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:41] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:41] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:41] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:41] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:45] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:45] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:45] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:45] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:56] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:57] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:58] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:19:59] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:02] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:03] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:06] "POST /api/ramp-to-chord HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:11] "POST /api/ramp-to-chord HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:23] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:23] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:23] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:23] "POST /api/play-siren HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [20/Apr/2026 12:20:26] "POST /api/ramp-to-chord HTTP/1.1" 200 -
|
||||||
|
/usr/lib/python3.14/multiprocessing/resource_tracker.py:396: UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown: {'/mp-naea0l96'}
|
||||||
|
warnings.warn(
|
||||||
|
|
@ -144,6 +144,7 @@ def play_siren():
|
||||||
chord_index = data.get("chordIndex")
|
chord_index = data.get("chordIndex")
|
||||||
node_index = data.get("nodeIndex")
|
node_index = data.get("nodeIndex")
|
||||||
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
||||||
|
siren_number = data.get("sirenNumber") # NEW: optional 1-4
|
||||||
|
|
||||||
if chord_index is None or node_index is None:
|
if chord_index is None or node_index is None:
|
||||||
return jsonify({"error": "Missing chordIndex/nodeIndex"}), 400
|
return jsonify({"error": "Missing chordIndex/nodeIndex"}), 400
|
||||||
|
|
@ -158,7 +159,7 @@ def play_siren():
|
||||||
pitch = chord[node_index]
|
pitch = chord[node_index]
|
||||||
fraction = Fraction(pitch.get("fraction", "1"))
|
fraction = Fraction(pitch.get("fraction", "1"))
|
||||||
frequency = fundamental * float(fraction)
|
frequency = fundamental * float(fraction)
|
||||||
voice = node_index + 1 # 1-indexed
|
voice = siren_number if siren_number else node_index + 1 # 1-indexed
|
||||||
|
|
||||||
# Send to siren using cached sender
|
# Send to siren using cached sender
|
||||||
siren_sender = get_siren_sender(siren_ip)
|
siren_sender = get_siren_sender(siren_ip)
|
||||||
|
|
@ -169,6 +170,7 @@ def play_siren():
|
||||||
{
|
{
|
||||||
"frequency": frequency,
|
"frequency": frequency,
|
||||||
"voice": voice,
|
"voice": voice,
|
||||||
|
"siren": siren_number,
|
||||||
"destination": "siren",
|
"destination": "siren",
|
||||||
"ip": siren_ip,
|
"ip": siren_ip,
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +216,7 @@ def ramp_to_chord():
|
||||||
duration_ms = data.get("duration", 3000)
|
duration_ms = data.get("duration", 3000)
|
||||||
exponent = data.get("exponent", 1.0)
|
exponent = data.get("exponent", 1.0)
|
||||||
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
||||||
|
siren_number = data.get("sirenNumber") # NEW: optional 1-4
|
||||||
|
|
||||||
if chord_index is None:
|
if chord_index is None:
|
||||||
return jsonify({"error": "Missing chordIndex"}), 400
|
return jsonify({"error": "Missing chordIndex"}), 400
|
||||||
|
|
@ -234,7 +237,7 @@ def ramp_to_chord():
|
||||||
pitch = chord[node_index]
|
pitch = chord[node_index]
|
||||||
fraction = Fraction(pitch.get("fraction", "1"))
|
fraction = Fraction(pitch.get("fraction", "1"))
|
||||||
frequency = fundamental * float(fraction)
|
frequency = fundamental * float(fraction)
|
||||||
voice = node_index + 1 # 1-indexed
|
voice = siren_number if siren_number else (node_index + 1) # 1-indexed
|
||||||
|
|
||||||
# Ramp single voice - let sender get start frequency from current position
|
# Ramp single voice - let sender get start frequency from current position
|
||||||
siren_sender.ramp_to_pitch(
|
siren_sender.ramp_to_pitch(
|
||||||
|
|
@ -527,4 +530,4 @@ if __name__ == "__main__":
|
||||||
print(f"Loading chords from: {filepath}")
|
print(f"Loading chords from: {filepath}")
|
||||||
load_chords()
|
load_chords()
|
||||||
print(f"Loaded {len(chords)} chords")
|
print(f"Loaded {len(chords)} chords")
|
||||||
app.run(host="0.0.0.0", port=8080, debug=True)
|
app.run(host="0.0.0.0", port=8080, debug=False)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue