2026-04-08 13:24:58 +02:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>Path Generator</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: #000000;
|
|
|
|
|
|
color: #cccccc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
font-weight: 300;
|
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-section {
|
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid #1a1a1a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-section h3 {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input[type="number"],
|
|
|
|
|
|
.form-group input[type="text"] {
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
|
color: #888888;
|
|
|
|
|
|
border: 1px solid #222222;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 11:07:18 +02:00
|
|
|
|
/* Hide default number input spinners */
|
|
|
|
|
|
input[type="number"]::-webkit-inner-spin-button,
|
|
|
|
|
|
input[type="number"]::-webkit-outer-spin-button {
|
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
input[type="number"] {
|
|
|
|
|
|
-moz-appearance: textfield;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 13:24:58 +02:00
|
|
|
|
.form-group input:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: #444444;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input[type="checkbox"] {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-group {
|
|
|
|
|
|
flex-direction: row !important;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 11:07:18 +02:00
|
|
|
|
/* Number input with inline +/- buttons */
|
|
|
|
|
|
.number-input-group {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.number-input-group input {
|
|
|
|
|
|
width: 50px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.number-btn {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
border: 1px solid #222222;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.number-btn:hover {
|
|
|
|
|
|
background: #151515;
|
|
|
|
|
|
color: #999999;
|
|
|
|
|
|
border-color: #444444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 13:24:58 +02:00
|
|
|
|
.checkbox-group label {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
text-transform: none;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #888888;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons button {
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
|
color: #888888;
|
|
|
|
|
|
border: 1px solid #222222;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons button:hover {
|
|
|
|
|
|
background: #151515;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border-color: #444444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons button.primary {
|
|
|
|
|
|
background: #1a3a1a;
|
|
|
|
|
|
border-color: #2a5a2a;
|
|
|
|
|
|
color: #66cc66;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons button.primary:hover {
|
|
|
|
|
|
background: #2a4a2a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#message {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#message.success {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
background: #1a3a1a;
|
|
|
|
|
|
border: 1px solid #2a5a2a;
|
|
|
|
|
|
color: #66cc66;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#message.error {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
background: #3a1a1a;
|
|
|
|
|
|
border: 1px solid #5a2a2a;
|
|
|
|
|
|
color: #cc6666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-link {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-link a {
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-link a:hover {
|
|
|
|
|
|
color: #888888;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons-row button {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
border: 1px solid #222222;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.buttons-row button:hover {
|
|
|
|
|
|
background: #151515;
|
|
|
|
|
|
color: #888888;
|
|
|
|
|
|
border-color: #444444;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<h1>Path Generator</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="buttons-row">
|
|
|
|
|
|
<button type="button" id="exportSettingsBtn">Export Settings</button>
|
|
|
|
|
|
<button type="button" id="importSettingsBtn">Import Settings</button>
|
|
|
|
|
|
<button type="button" id="loadDefaultsBtn">Load Defaults</button>
|
|
|
|
|
|
<input type="file" id="importFileInput" accept=".json" style="display: none;">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="message"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<form id="generatorForm">
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Basic Settings</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Dims</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('dims', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="dims" value="7" min="1" max="10">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('dims', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Chord Size</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('chordSize', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="chordSize" value="3" min="1" max="10">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('chordSize', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Max Path</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('maxPath', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="maxPath" value="50" min="1">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('maxPath', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Seed (optional)</label>
|
|
|
|
|
|
<input type="number" id="seed" placeholder="random">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Output Directory</label>
|
|
|
|
|
|
<input type="text" id="outputDir" value="output">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Fundamental (Hz)</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('fundamental', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="fundamental" value="55" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('fundamental', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Transcribe Name</label>
|
|
|
|
|
|
<input type="text" id="transcribeName" value="compact_sets_transcription">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Symdiff / Melodic</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Symdiff Min</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('symdiffMin', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="symdiffMin" value="2" min="0">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('symdiffMin', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Symdiff Max</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('symdiffMax', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="symdiffMax" value="2" min="0">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('symdiffMax', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Melodic Min</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('melodicMin', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="melodicMin" value="0" min="0">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('melodicMin', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Melodic Max</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('melodicMax', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="melodicMax" value="500" min="0">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('melodicMax', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Target Register</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Target Register (octaves)</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegister', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="targetRegister" value="0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegister', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Target Register Power</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterPower', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="targetRegisterPower" value="1.0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterPower', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Oscillations</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterOscillations', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="targetRegisterOscillations" value="0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterOscillations', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Amplitude</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterAmplitude', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="targetRegisterAmplitude" value="0.25" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterAmplitude', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Voice Leading</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group checkbox-group">
|
|
|
|
|
|
<input type="checkbox" id="allowVoiceCrossing">
|
|
|
|
|
|
<label>Allow Voice Crossing</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group checkbox-group">
|
|
|
|
|
|
<input type="checkbox" id="disableDirectTuning">
|
|
|
|
|
|
<label>Disable Direct Tuning</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group checkbox-group">
|
|
|
|
|
|
<input type="checkbox" id="uniformSymdiff">
|
|
|
|
|
|
<label>Uniform Symdiff</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Weights</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight Melodic</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightMelodic', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightMelodic" value="1" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightMelodic', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight Contrary Motion</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightContraryMotion', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightContraryMotion" value="0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightContraryMotion', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight DCA Hamiltonian</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightDcaHamiltonian', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightDcaHamiltonian" value="1" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightDcaHamiltonian', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight DCA Voice Movement</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightDcaVoiceMovement', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightDcaVoiceMovement" value="1" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightDcaVoiceMovement', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight RGR Voice Movement</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightRgrVoiceMovement', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightRgrVoiceMovement" value="0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightRgrVoiceMovement', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>RGR Threshold</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('rgrVoiceMovementThreshold', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="rgrVoiceMovementThreshold" value="5" min="1">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('rgrVoiceMovementThreshold', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight Harmonic Compactness</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightHarmonicCompactness', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightHarmonicCompactness" value="0" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightHarmonicCompactness', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Weight Target Register</label>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
<div class="number-input-group">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightTargetRegister', -1)">−</button>
|
|
|
|
|
|
<input type="number" id="weightTargetRegister" value="1" step="any">
|
|
|
|
|
|
<button type="button" class="number-btn" onclick="adjustValue('weightTargetRegister', 1)">+</button>
|
|
|
|
|
|
</div>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>Caching</h3>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Cache Directory</label>
|
|
|
|
|
|
<input type="text" id="cacheDir" value="cache">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group checkbox-group">
|
|
|
|
|
|
<input type="checkbox" id="rebuildCache">
|
|
|
|
|
|
<label>Rebuild Cache</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group checkbox-group">
|
|
|
|
|
|
<input type="checkbox" id="noCache">
|
|
|
|
|
|
<label>No Cache</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="buttons">
|
2026-04-19 01:34:56 +02:00
|
|
|
|
<button type="button" id="reseedGenerateBtn">Reseed & Generate</button>
|
2026-04-08 13:24:58 +02:00
|
|
|
|
<button type="button" id="generateBtn">Generate</button>
|
|
|
|
|
|
<button type="button" id="viewBtn">View</button>
|
|
|
|
|
|
<button type="button" id="transcribeBtn" class="primary">Transcribe</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="nav-link">
|
|
|
|
|
|
<a href="/">View Path Navigator</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-09 11:07:18 +02:00
|
|
|
|
function adjustValue(id, delta) {
|
|
|
|
|
|
const input = document.getElementById(id);
|
|
|
|
|
|
const min = parseFloat(input.min) || -Infinity;
|
|
|
|
|
|
const max = parseFloat(input.max) || Infinity;
|
|
|
|
|
|
const step = parseFloat(input.step) || 1;
|
|
|
|
|
|
let val = parseFloat(input.value) || 0;
|
|
|
|
|
|
val = Math.max(min, Math.min(max, val + (delta * step)));
|
|
|
|
|
|
input.value = step % 1 !== 0 ? parseFloat(val.toFixed(2)) : Math.round(val);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 13:24:58 +02:00
|
|
|
|
function getFormData() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
dims: parseInt(document.getElementById('dims').value) || 7,
|
|
|
|
|
|
chordSize: parseInt(document.getElementById('chordSize').value) || 3,
|
|
|
|
|
|
maxPath: parseInt(document.getElementById('maxPath').value) || 50,
|
|
|
|
|
|
seed: document.getElementById('seed').value ? parseInt(document.getElementById('seed').value) : null,
|
|
|
|
|
|
outputDir: document.getElementById('outputDir').value || 'output',
|
|
|
|
|
|
fundamental: parseFloat(document.getElementById('fundamental').value) || 55,
|
|
|
|
|
|
symdiffMin: parseInt(document.getElementById('symdiffMin').value) || 2,
|
|
|
|
|
|
symdiffMax: parseInt(document.getElementById('symdiffMax').value) || 2,
|
|
|
|
|
|
melodicMin: parseInt(document.getElementById('melodicMin').value) || 0,
|
|
|
|
|
|
melodicMax: parseInt(document.getElementById('melodicMax').value) || 500,
|
|
|
|
|
|
targetRegister: parseFloat(document.getElementById('targetRegister').value) || 0,
|
|
|
|
|
|
targetRegisterPower: parseFloat(document.getElementById('targetRegisterPower').value) || 1.0,
|
|
|
|
|
|
targetRegisterOscillations: parseFloat(document.getElementById('targetRegisterOscillations').value) || 0,
|
|
|
|
|
|
targetRegisterAmplitude: parseFloat(document.getElementById('targetRegisterAmplitude').value) || 0.25,
|
|
|
|
|
|
allowVoiceCrossing: document.getElementById('allowVoiceCrossing').checked,
|
|
|
|
|
|
disableDirectTuning: document.getElementById('disableDirectTuning').checked,
|
|
|
|
|
|
uniformSymdiff: document.getElementById('uniformSymdiff').checked,
|
2026-04-18 15:33:49 +02:00
|
|
|
|
weightMelodic: (() => { const v = parseFloat(document.getElementById('weightMelodic').value); return isNaN(v) ? 1 : v; })(),
|
|
|
|
|
|
weightContraryMotion: (() => { const v = parseFloat(document.getElementById('weightContraryMotion').value); return isNaN(v) ? 0 : v; })(),
|
|
|
|
|
|
weightDcaHamiltonian: (() => { const v = parseFloat(document.getElementById('weightDcaHamiltonian').value); return isNaN(v) ? 1 : v; })(),
|
|
|
|
|
|
weightDcaVoiceMovement: (() => { const v = parseFloat(document.getElementById('weightDcaVoiceMovement').value); return isNaN(v) ? 1 : v; })(),
|
|
|
|
|
|
weightRgrVoiceMovement: (() => { const v = parseFloat(document.getElementById('weightRgrVoiceMovement').value); return isNaN(v) ? 0 : v; })(),
|
2026-04-08 13:24:58 +02:00
|
|
|
|
rgrVoiceMovementThreshold: parseInt(document.getElementById('rgrVoiceMovementThreshold').value) || 5,
|
2026-04-18 15:33:49 +02:00
|
|
|
|
weightHarmonicCompactness: (() => { const v = parseFloat(document.getElementById('weightHarmonicCompactness').value); return isNaN(v) ? 0 : v; })(),
|
|
|
|
|
|
weightTargetRegister: (() => { const v = parseFloat(document.getElementById('weightTargetRegister').value); return isNaN(v) ? 1 : v; })(),
|
2026-04-08 13:24:58 +02:00
|
|
|
|
cacheDir: document.getElementById('cacheDir').value || 'cache',
|
|
|
|
|
|
rebuildCache: document.getElementById('rebuildCache').checked,
|
|
|
|
|
|
noCache: document.getElementById('noCache').checked,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showMessage(text, isError = false) {
|
|
|
|
|
|
const msg = document.getElementById('message');
|
|
|
|
|
|
msg.textContent = text;
|
|
|
|
|
|
msg.className = isError ? 'error' : 'success';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hideMessage() {
|
|
|
|
|
|
const msg = document.getElementById('message');
|
|
|
|
|
|
msg.style.display = 'none';
|
|
|
|
|
|
msg.className = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function generate() {
|
|
|
|
|
|
hideMessage();
|
|
|
|
|
|
const data = getFormData();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/generate', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
|
showMessage('Error: ' + result.error, true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update seed field with the seed used
|
|
|
|
|
|
document.getElementById('seed').value = result.seed;
|
|
|
|
|
|
showMessage('Generated ' + result.totalChords + ' chords (seed: ' + result.seed + ')');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showMessage('Error: ' + err.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// View - open Path Navigator in new tab
|
|
|
|
|
|
function viewChords() {
|
|
|
|
|
|
window.open('/', '_blank');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Transcribe - generate if needed, then transcribe
|
|
|
|
|
|
async function transcribe() {
|
|
|
|
|
|
hideMessage();
|
|
|
|
|
|
const transcribeName = document.getElementById('transcribeName').value || 'compact_sets_transcription';
|
|
|
|
|
|
const data = getFormData();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// First generate if chords don't exist
|
|
|
|
|
|
const response = await fetch('/api/generate', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
|
showMessage('Error: ' + result.error, true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update seed field
|
|
|
|
|
|
document.getElementById('seed').value = result.seed;
|
|
|
|
|
|
|
|
|
|
|
|
// Now transcribe
|
|
|
|
|
|
const transcribeResponse = await fetch('/api/transcribe', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify({ name: transcribeName })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const transcribeResult = await transcribeResponse.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (transcribeResult.error) {
|
|
|
|
|
|
showMessage('Transcription error: ' + transcribeResult.error, true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showMessage('Transcribed ' + transcribeResult.chordCount + ' chords to ' + transcribeResult.outputFile);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showMessage('Error: ' + err.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 01:34:56 +02:00
|
|
|
|
function reseedAndGenerate() {
|
|
|
|
|
|
const newSeed = Math.floor(Math.random() * 999999) + 1;
|
|
|
|
|
|
document.getElementById('seed').value = newSeed;
|
|
|
|
|
|
generate();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('reseedGenerateBtn').addEventListener('click', reseedAndGenerate);
|
2026-04-08 13:24:58 +02:00
|
|
|
|
document.getElementById('generateBtn').addEventListener('click', generate);
|
|
|
|
|
|
document.getElementById('viewBtn').addEventListener('click', viewChords);
|
|
|
|
|
|
document.getElementById('transcribeBtn').addEventListener('click', transcribe);
|
|
|
|
|
|
|
|
|
|
|
|
// Export settings to JSON file
|
|
|
|
|
|
function exportSettings() {
|
|
|
|
|
|
const data = getFormData();
|
|
|
|
|
|
data.exportedAt = new Date().toISOString();
|
|
|
|
|
|
const json = JSON.stringify(data, null, 2);
|
|
|
|
|
|
const blob = new Blob([json], {type: 'application/json'});
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
a.download = 'generator_settings.json';
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
showMessage('Settings exported to generator_settings.json');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Import settings from JSON file
|
|
|
|
|
|
function importSettings() {
|
|
|
|
|
|
document.getElementById('importFileInput').click();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySettings(data) {
|
|
|
|
|
|
// Map of field IDs to data keys
|
|
|
|
|
|
const fieldMap = {
|
|
|
|
|
|
dims: 'dims',
|
|
|
|
|
|
chordSize: 'chordSize',
|
|
|
|
|
|
maxPath: 'maxPath',
|
|
|
|
|
|
seed: 'seed',
|
|
|
|
|
|
outputDir: 'outputDir',
|
|
|
|
|
|
fundamental: 'fundamental',
|
|
|
|
|
|
symdiffMin: 'symdiffMin',
|
|
|
|
|
|
symdiffMax: 'symdiffMax',
|
|
|
|
|
|
melodicMin: 'melodicMin',
|
|
|
|
|
|
melodicMax: 'melodicMax',
|
|
|
|
|
|
targetRegister: 'targetRegister',
|
|
|
|
|
|
targetRegisterPower: 'targetRegisterPower',
|
|
|
|
|
|
targetRegisterOscillations: 'targetRegisterOscillations',
|
|
|
|
|
|
targetRegisterAmplitude: 'targetRegisterAmplitude',
|
|
|
|
|
|
allowVoiceCrossing: 'allowVoiceCrossing',
|
|
|
|
|
|
disableDirectTuning: 'disableDirectTuning',
|
|
|
|
|
|
uniformSymdiff: 'uniformSymdiff',
|
|
|
|
|
|
weightMelodic: 'weightMelodic',
|
|
|
|
|
|
weightContraryMotion: 'weightContraryMotion',
|
|
|
|
|
|
weightDcaHamiltonian: 'weightDcaHamiltonian',
|
|
|
|
|
|
weightDcaVoiceMovement: 'weightDcaVoiceMovement',
|
|
|
|
|
|
weightRgrVoiceMovement: 'weightRgrVoiceMovement',
|
|
|
|
|
|
rgrVoiceMovementThreshold: 'rgrVoiceMovementThreshold',
|
|
|
|
|
|
weightHarmonicCompactness: 'weightHarmonicCompactness',
|
|
|
|
|
|
weightTargetRegister: 'weightTargetRegister',
|
|
|
|
|
|
cacheDir: 'cacheDir',
|
|
|
|
|
|
rebuildCache: 'rebuildCache',
|
|
|
|
|
|
noCache: 'noCache',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (const [fieldId, dataKey] of Object.entries(fieldMap)) {
|
|
|
|
|
|
const input = document.getElementById(fieldId);
|
|
|
|
|
|
if (input && data[dataKey] !== undefined) {
|
|
|
|
|
|
if (input.type === 'checkbox') {
|
|
|
|
|
|
input.checked = data[dataKey];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
input.value = data[dataKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('exportSettingsBtn').addEventListener('click', exportSettings);
|
|
|
|
|
|
document.getElementById('importSettingsBtn').addEventListener('click', importSettings);
|
|
|
|
|
|
|
|
|
|
|
|
// Load default settings from server
|
|
|
|
|
|
async function loadDefaults() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/generator_settings.json');
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
// No defaults file, that's fine
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
applySettings(data);
|
|
|
|
|
|
showMessage('Default settings loaded');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// File not found or error, that's fine - use hardcoded defaults
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to load defaults on page load
|
|
|
|
|
|
loadDefaults();
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('loadDefaultsBtn').addEventListener('click', function() {
|
|
|
|
|
|
// Reset form to hardcoded defaults
|
|
|
|
|
|
applySettings({
|
|
|
|
|
|
dims: 7,
|
|
|
|
|
|
chordSize: 3,
|
|
|
|
|
|
maxPath: 50,
|
|
|
|
|
|
seed: null,
|
|
|
|
|
|
outputDir: 'output',
|
|
|
|
|
|
fundamental: 55,
|
|
|
|
|
|
symdiffMin: 2,
|
|
|
|
|
|
symdiffMax: 2,
|
|
|
|
|
|
melodicMin: 0,
|
|
|
|
|
|
melodicMax: 500,
|
|
|
|
|
|
targetRegister: 0,
|
|
|
|
|
|
targetRegisterPower: 1.0,
|
|
|
|
|
|
targetRegisterOscillations: 0,
|
|
|
|
|
|
targetRegisterAmplitude: 0.25,
|
|
|
|
|
|
allowVoiceCrossing: false,
|
|
|
|
|
|
disableDirectTuning: false,
|
|
|
|
|
|
uniformSymdiff: false,
|
|
|
|
|
|
weightMelodic: 1,
|
|
|
|
|
|
weightContraryMotion: 0,
|
|
|
|
|
|
weightDcaHamiltonian: 1,
|
|
|
|
|
|
weightDcaVoiceMovement: 1,
|
|
|
|
|
|
weightRgrVoiceMovement: 0,
|
|
|
|
|
|
rgrVoiceMovementThreshold: 5,
|
|
|
|
|
|
weightHarmonicCompactness: 0,
|
|
|
|
|
|
weightTargetRegister: 1,
|
|
|
|
|
|
cacheDir: 'cache',
|
|
|
|
|
|
rebuildCache: false,
|
|
|
|
|
|
noCache: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
showMessage('Form reset to defaults');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('importFileInput').addEventListener('change', function(e) {
|
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = function(e) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(e.target.result);
|
|
|
|
|
|
applySettings(data);
|
|
|
|
|
|
showMessage('Settings imported from ' + file.name);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showMessage('Error reading file: ' + err.message, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
|
this.value = ''; // Reset for re-import
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|