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', {
|
||||
method: 'POST',
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -308,6 +308,27 @@
|
|||
color: #999999;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -344,13 +365,21 @@
|
|||
</div>
|
||||
<span style="margin-left: 15px;">Siren IP:</span>
|
||||
<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 class="controls">
|
||||
<button id="prevBtn" disabled>← Previous</button>
|
||||
<span class="index-display">Index: <span id="currentIndex">0</span> / <span id="totalSteps">0</span></span>
|
||||
<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 id="graph-container"></div>
|
||||
|
|
@ -374,6 +403,8 @@
|
|||
<script>
|
||||
// Global state
|
||||
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 totalSteps = 0;
|
||||
let hasPrev = false;
|
||||
|
|
@ -381,6 +412,31 @@
|
|||
let allGraphsData = null;
|
||||
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) {
|
||||
const input = document.getElementById(id);
|
||||
const min = parseFloat(input.min) || -Infinity;
|
||||
|
|
@ -416,17 +472,24 @@
|
|||
// Play each node using chordIndex + localId (let server calculate frequency)
|
||||
nodes.forEach((node) => {
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
chordIndex: chordIdx,
|
||||
nodeIndex: localId,
|
||||
ip: sirenIp
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
node.data('sirenActive', 'true');
|
||||
node.data('borderColor', voiceColors[localId % 4]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -459,16 +522,23 @@
|
|||
nodes.forEach(node => node.data('sirenActive', 'true'));
|
||||
}
|
||||
|
||||
fetch('/api/ramp-to-chord', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(rampBody)
|
||||
}).then(r => r.json()).then(data => {
|
||||
console.log('Ramping:', data);
|
||||
}).catch(err => {
|
||||
|
|
@ -482,6 +552,7 @@
|
|||
const btn = document.getElementById('toggleUnitBtn');
|
||||
if (btn) {
|
||||
btn.textContent = displayMode === 'cents' ? 'Show: Cents' : 'Show: Frequency';
|
||||
btn.classList.toggle('active', displayMode === 'frequency');
|
||||
}
|
||||
|
||||
if (!cy) return;
|
||||
|
|
@ -568,7 +639,7 @@
|
|||
selector: 'node[sirenActive = "true"]',
|
||||
style: {
|
||||
'border-width': 4,
|
||||
'border-color': '#ffffff',
|
||||
'border-color': 'data(borderColor)',
|
||||
'border-opacity': 1,
|
||||
}
|
||||
},
|
||||
|
|
@ -665,12 +736,11 @@
|
|||
|
||||
// Check modifiers
|
||||
const isShift = evt.originalEvent && evt.originalEvent.shiftKey;
|
||||
const isRamp = evt.originalEvent && evt.originalEvent.ctrlKey;
|
||||
|
||||
// Handle label node clicks
|
||||
if (node.data('isLabel')) {
|
||||
const chordIdx = node.data('chordIndex');
|
||||
if (isRamp) {
|
||||
if (rampMode || isShift) {
|
||||
rampToChord(chordIdx);
|
||||
} else {
|
||||
playChordOnSiren(chordIdx);
|
||||
|
|
@ -680,45 +750,75 @@
|
|||
|
||||
const chordIndex = node.data('chordIndex');
|
||||
const localId = node.data('localId');
|
||||
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||
|
||||
// Handle ramp modifier
|
||||
if (isRamp) {
|
||||
// Determine action: ramp or play
|
||||
if (rampMode || isShift) {
|
||||
// Ramp to siren
|
||||
rampToChord(chordIndex, localId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Shift key is held - send to siren, otherwise send to SuperCollider
|
||||
const endpoint = isShift ? '/api/play-siren' : '/api/play-freq';
|
||||
const destination = isShift ? 'siren' : 'SuperCollider';
|
||||
// Check if Shift key is held - send to SuperCollider
|
||||
if (isShift) {
|
||||
// Send to SuperCollider
|
||||
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({
|
||||
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,
|
||||
octave: parseInt(document.getElementById('octaveInput').value) || 0,
|
||||
ip: sirenIp
|
||||
})
|
||||
}).then(r => r.json()).then(data => {
|
||||
console.log('Playing on', destination + ':', data.frequency.toFixed(2), 'Hz on voice', data.voice);
|
||||
ip: sirenIp,
|
||||
sirenNumber: actualSiren
|
||||
};
|
||||
|
||||
// If playing on siren, add white circle around node for this voice
|
||||
if (isShift) {
|
||||
const nodeColor = node.data('color');
|
||||
// Find any existing node with same color that has sirenActive and remove it
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(requestBody)
|
||||
}).then(r => r.json()).then(data => {
|
||||
console.log('Playing on', destination + ':', data.frequency.toFixed(2), 'Hz on siren', data.siren);
|
||||
|
||||
// Add colored circle around node based on siren
|
||||
const sirenColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700'];
|
||||
const borderColor = sirenColors[(actualSiren - 1) % 4];
|
||||
|
||||
// Find any existing node with same border color that has sirenActive and remove it
|
||||
cy.nodes().forEach(n => {
|
||||
if (n.data('color') === nodeColor && 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);
|
||||
}
|
||||
node.data('borderColor', borderColor);
|
||||
console.log('Added sirenActive to', node.id(), 'color:', borderColor);
|
||||
}).catch(err => {
|
||||
console.log('Error playing freq:', err);
|
||||
});
|
||||
|
|
@ -1224,8 +1324,35 @@
|
|||
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
|
||||
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") {
|
||||
navigate('prev');
|
||||
} else if (e.key === "ArrowRight") {
|
||||
|
|
@ -1242,6 +1369,21 @@
|
|||
const zoom = cy.zoom();
|
||||
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") {
|
||||
// Soft kill - send 20 Hz to stop voices gently
|
||||
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||
|
|
@ -1252,7 +1394,10 @@
|
|||
}).then(r => r.json()).then(data => {
|
||||
console.log('Soft kill sent (20 Hz)');
|
||||
// Clear all siren circles
|
||||
cy.nodes().forEach(n => n.removeData('sirenActive'));
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeData('sirenActive');
|
||||
n.removeData('borderColor');
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log('Error sending kill:', err);
|
||||
});
|
||||
|
|
@ -1266,7 +1411,10 @@
|
|||
}).then(r => r.json()).then(data => {
|
||||
console.log('Hard kill sent (0 Hz)');
|
||||
// Clear all siren circles
|
||||
cy.nodes().forEach(n => n.removeData('sirenActive'));
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeData('sirenActive');
|
||||
n.removeData('borderColor');
|
||||
});
|
||||
}).catch(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")
|
||||
node_index = data.get("nodeIndex")
|
||||
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:
|
||||
return jsonify({"error": "Missing chordIndex/nodeIndex"}), 400
|
||||
|
|
@ -158,7 +159,7 @@ def play_siren():
|
|||
pitch = chord[node_index]
|
||||
fraction = Fraction(pitch.get("fraction", "1"))
|
||||
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
|
||||
siren_sender = get_siren_sender(siren_ip)
|
||||
|
|
@ -169,6 +170,7 @@ def play_siren():
|
|||
{
|
||||
"frequency": frequency,
|
||||
"voice": voice,
|
||||
"siren": siren_number,
|
||||
"destination": "siren",
|
||||
"ip": siren_ip,
|
||||
}
|
||||
|
|
@ -214,6 +216,7 @@ def ramp_to_chord():
|
|||
duration_ms = data.get("duration", 3000)
|
||||
exponent = data.get("exponent", 1.0)
|
||||
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:
|
||||
return jsonify({"error": "Missing chordIndex"}), 400
|
||||
|
|
@ -234,7 +237,7 @@ def ramp_to_chord():
|
|||
pitch = chord[node_index]
|
||||
fraction = Fraction(pitch.get("fraction", "1"))
|
||||
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
|
||||
siren_sender.ramp_to_pitch(
|
||||
|
|
@ -527,4 +530,4 @@ if __name__ == "__main__":
|
|||
print(f"Loading chords from: {filepath}")
|
||||
load_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