Skip to content

Instantly share code, notes, and snippets.

@semanticentity
Created September 16, 2025 19:31
Show Gist options
  • Select an option

  • Save semanticentity/51c4fa8e476846ef457cbe8fbde3069a to your computer and use it in GitHub Desktop.

Select an option

Save semanticentity/51c4fa8e476846ef457cbe8fbde3069a to your computer and use it in GitHub Desktop.
WHITE DWARF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WHITE DWARF</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Doto:wght,ROND@100..900,100&family=Roboto+Mono:wght@100..700&display=swap" rel="stylesheet">
<style>
:root {
--main-bg: #111113;
--panel-bg: rgba(28, 28, 32, 0.85);
--text-color: #d8d8d8;
--accent-color: #ff8c00;
--accent-hover: #ffaf4d;
--border-color: #444;
--shadow-color: rgba(255, 140, 0, 0.2);
--lyra-color: #00aaff;
--lyra-shadow: rgba(0, 170, 255, 0.2);
}
body,
html {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
background: var(--main-bg);
color: var(--text-color);
font-family: 'Roboto Mono', 'Courier New', monospace;
font-weight: 300;
font-size: 13px;
}
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
opacity: 0.8;
}
#controls {
position: fixed;
top: 10px;
left: 10px;
z-index: 10;
background: var(--panel-bg);
padding: 20px;
border-radius: 4px;
border: 1px solid var(--border-color);
width: 678px;
box-shadow: 0 0 25px var(--shadow-color);
max-height: calc(100vh - 20px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--accent-color) var(--main-bg);
}
#controls::-webkit-scrollbar {
width: 8px;
}
#controls::-webkit-scrollbar-track {
background: var(--main-bg);
}
#controls::-webkit-scrollbar-thumb {
background-color: var(--accent-color);
border-radius: 4px;
border: 2px solid var(--main-bg);
}
h2 {
color: var(--accent-hover);
text-align: center;
margin-top: 0;
margin-bottom: 20px;
font-size: 2em;
letter-spacing: 4px;
text-shadow: 0 0 10px var(--shadow-color);
font-weight: 300;
}
h3 {
margin: 20px 0 10px 0;
color: var(--text-color);
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
font-weight: 300;
}
h4 {
margin: 10px 0 5px 0;
color: var(--text-color);
font-weight: 400;
opacity: 0.7;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
gap: 10px;
}
.lfo-rate-control {
display: flex;
width: 100%;
align-items: center;
gap: 10px;
}
.lfo-rate-control button {
flex-grow: 0;
padding: 8px 10px;
min-width: 60px;
}
label {
font-size: 0.9em;
white-space: nowrap;
margin-right: 10px;
}
button {
background: #333;
color: var(--text-color);
border: 1px solid #555;
padding: 10px 15px;
cursor: pointer;
border-radius: 3px;
font-weight: 700;
font-family: 'Doto';
font-size: 1.25em;
text-transform: uppercase;
flex-grow: 1;
transition: all 0.2s ease;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
button:hover:not(:disabled) {
background: #444;
border-color: var(--accent-hover);
color: #fff;
}
button.active,
#playButton.active {
background: var(--accent-color);
color: var(--main-bg);
border-color: var(--accent-hover);
box-shadow: 0 0 10px var(--shadow-color);
font-weight: 900;
}
#lyraContainer button.active {
background: var(--lyra-color);
color: var(--main-bg);
border-color: var(--lyra-color);
box-shadow: 0 0 10px var(--lyra-shadow);
}
#randomizeButton {
margin-left: 10px;
}
input[type="range"] {
width: 100%;
accent-color: var(--accent-color);
}
input[type="text"] {
background: #222;
color: var(--text-color);
border: 1px solid #555;
padding: 8px;
border-radius: 3px;
font-family: inherit;
width: 100%;
}
select {
background: #222;
color: var(--text-color);
border: 1px solid #555;
padding: 8px;
border-radius: 3px;
font-family: inherit;
width: 100%;
}
details,
.control-group,
#presetManager {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
padding: 15px;
margin-top: 20px;
border-radius: 4px;
}
details {
margin-top: 0;
padding-top: 0;
padding-bottom: 5px;
}
/* NEW: Shared style for summary elements */
details>summary {
cursor: pointer;
margin: 20px 0 10px 0;
color: var(--text-color);
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
font-weight: 300;
outline: none;
list-style: none;
/* Hide default triangle */
position: relative;
}
details>summary::-webkit-details-marker {
display: none;
/* Hide default triangle in Chrome */
}
details>summary::after {
content: '+';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%) rotate(0deg);
font-weight: bold;
transition: transform 0.2s ease-in-out;
}
details[open]>summary::after {
transform: translateY(-50%) rotate(45deg);
}
details[open]>summary {
margin-bottom: 15px;
}
#presetManager h3 {
margin-top: 0;
text-align: center;
border-bottom-color: var(--accent-color);
}
#patchBay {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
padding: 15px;
margin-top: 20px;
border-radius: 4px;
}
#patchBay h3 {
margin-top: 0;
text-align: center;
border-bottom: 2px solid var(--accent-color);
}
.patch-creator .row {
margin-bottom: 8px;
}
.patch-creator label {
flex-basis: 30%;
text-align: right;
}
.patch-creator select {
flex-basis: 65%;
}
#addPatchButton {
width: 100%;
margin-top: 15px;
background-color: var(--accent-color);
font-weight: 900;
color: var(--main-bg);
}
#activePatches {
margin-top: 20px;
}
.patch-instance {
background: #2a2a2e;
border-left: 4px solid var(--accent-color);
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 3px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85em;
}
.patch-instance {
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.patch-instance:hover {
background-color: #38383e;
}
.patch-instance button {
background: #502200;
color: var(--accent-hover);
border: 1px solid #804400;
padding: 3px 8px;
font-size: 0.8em;
flex-grow: 0;
}
.patch-instance span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
display: inline-block;
}
.pattern-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
}
.pattern-label {
width: 30px;
text-align: right;
font-size: 0.9em;
font-weight: bold;
color: var(--accent-hover);
cursor: pointer;
user-select: none;
padding: 5px 0;
border-radius: 3px;
}
.pattern-label:hover {
background-color: #333;
}
.pattern-row[data-type="lyraGate"] .pattern-label {
color: var(--lyra-color);
}
.steps-container {
display: flex;
flex-wrap: nowrap;
/* Ensures single line by default */
overflow-x: auto;
/* Allows horizontal scrolling */
gap: 3px;
flex-grow: 1;
background: rgba(0, 0, 0, 0.2);
padding: 5px;
border-radius: 3px;
}
.steps-container::-webkit-scrollbar {
height: 4px;
}
.steps-container::-webkit-scrollbar-thumb {
background: #555;
}
.steps-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
.pattern-row .step {
width: 14px;
height: 14px;
border: 1px solid #444;
background: #222;
cursor: pointer;
flex-shrink: 0;
/* Prevents steps from shrinking */
border-radius: 2px;
transition: background-color 0.1s, opacity 0.1s, border-color 0.1s;
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 9px;
font-weight: bold;
text-shadow: 0 0 2px black;
box-sizing: border-box;
/* Important for border display */
}
.pattern-row .step.active {
background-color: var(--accent-color);
}
.pattern-row[data-type="lyraGate"] .step.active {
background-color: var(--lyra-color);
}
/* BASS ACCENT FOR OCTAVE DROPS */
.pattern-row[data-type="bass"] .step.active.accent {
border-bottom: 2px solid #ff4136;
}
/* --- TRIG CONDITION STYLES --- */
@keyframes flash-inactive {
0%,
100% {
background-color: var(--accent-color);
opacity: 0.3;
}
50% {
background-color: #222;
}
}
/* --- PROBABILITY STYLES --- */
.step.active.has-prob {
box-shadow: inset var(--lyra-color) -2px -2px 0, inset var(--lyra-color) 2px 2px 0;
}
.pattern-row[data-type="lyraGate"] .step.active.has-prob {
box-shadow: inset var(--accent-color) -2px -2px 0, inset var(--accent-color) 2px 2px 0;
}
/* Ensure the playhead indicator is always solid and visible */
.pattern-row .step.current {
box-shadow: 0 0 8px #fff;
border-color: #fff !important;
/* Overrides prob and trig borders */
border-style: solid !important;
/* Overrides prob dashed style */
}
.step.active.trig-first {
border-top: 2px solid white;
line-height: 10px;
/* Adjust if text is misaligned */
}
.step.active.trig-second {
border-bottom: 2px solid white;
}
.pattern-row[data-type="lyraGate"] .step.active.trig-first {
border-top: 2px solid #aadeff;
}
.pattern-row[data-type="lyraGate"] .step.active.trig-second {
border-bottom: 2px solid #aadeff;
}
/* A step with a condition that is currently inactive */
.step.active.flashing-inactive {
/* The animation will override the background-color from .active */
animation: flash-inactive 1s infinite;
}
/* --- END TRIG CONDITION STYLES --- */
#kaosPad {
position: fixed;
right: 20px;
top: 20px;
width: 200px;
height: 200px;
background: linear-gradient(to top right, rgba(255, 140, 0, 0.1), rgba(0, 0, 0, 0.3));
border: 2px solid var(--accent-color);
border-radius: 4px;
box-shadow: 0 0 20px var(--shadow-color);
cursor: crosshair;
z-index: 100;
touch-action: none;
transition: width 0.3s ease-in-out, height 0.3s ease-in-out;
}
#kaosPuck {
position: absolute;
width: 12px;
height: 12px;
background: var(--accent-hover);
border-radius: 50%;
border: 1px solid var(--main-bg);
transform: translate(-50%, -50%);
pointer-events: none;
top: 50%;
left: 50%;
}
@media (max-width: 959px) {
#kaosPad {
width: 60px;
height: 60px;
transition: width 0.3s ease-in-out, height 0.3s ease-in-out;
}
#kaosPuck {
width: 6px;
height: 6px;
}
}
#lyraContainer h3 {
color: var(--lyra-color);
border-color: var(--lyra-color);
cursor: pointer;
position: relative;
user-select: none;
transition: margin-bottom 0.3s ease-in-out;
}
#lyraContainer h3::after {
content: '+';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-weight: bold;
font-size: 1.5em;
transition: transform 0.3s ease-in-out;
}
#lyraContainer.open h3::after {
transform: translateY(-50%) rotate(45deg);
}
#lyraContainer.open h3 {
margin-bottom: 0;
}
#lyraContainer.open #lyraControlsWrapper {
max-height: 2000px;
transition: max-height 0.6s ease-in;
padding-top: 15px;
}
#lyraControlsWrapper {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease-out, padding-top 0.4s ease-out;
padding-top: 0;
}
#lyraContainer .control-group {
border-left: 3px solid #005577;
padding-left: 10px;
margin-bottom: 15px;
background: none;
border-width: 0 0 0 3px;
padding: 0 0 0 10px;
margin-top: 0;
}
/* Sequencer Modules randomize buttons */
.control-group h4>button {
display: inline-block;
margin-left: 10px;
margin-bottom: 10px;
font-size: 12px;
padding: 5px 10px;
background: var(--accent-color);
color: var(--main-bg);
}
#lyraContainer input[type="range"] {
accent-color: var(--lyra-color);
}
#lyraContainer select {
accent-color: var(--lyra-color);
background: #2a2a2e;
border-color: #005577;
}
#lyraLooperControls button.recording {
background-color: #ff4136;
box-shadow: 0 0 10px #ff4136;
color: white;
}
#lyraLooperControls button.playing {
background-color: var(--lyra-color);
color: var(--main-bg);
box-shadow: 0 0 10px var(--lyra-color);
}
button#lyraSeqToggle.active {
font-weight: 900;
}
#lyraWipeButton {
background: #500000;
color: #ffaaaa;
border-color: #800000;
flex-grow: 0.5;
}
#lyraKeyDisplay {
margin-top: 15px;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
text-align: center;
color: var(--lyra-color);
font-size: 0.9em;
letter-spacing: 2px;
}
#lyraKeyDisplay br {
display: none;
}
#lyraKeyDisplay code {
color: var(--accent-color);
}
.fx-panel {
padding: 10px 0;
border-top: 1px dashed #005577;
margin-top: 10px;
}
.lyra-step {
height: 35px;
background: #2a2a2e;
border: 2px solid #444;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.8em;
font-weight: bold;
transition: all 0.1s;
position: relative;
user-select: none;
}
.lyra-step:hover {
background: #3a3a3e;
border-color: var(--lyra-color);
}
.lyra-step.active {
background: var(--lyra-color);
color: var(--main-bg);
border-color: #0088cc;
}
.lyra-step.slide {
border-left: 4px solid #ff8c00;
}
.lyra-step.accent {
border-top: 3px solid #ff4136;
}
.lyra-step.current {
box-shadow: 0 0 10px #fff;
border: 2px solid #fff;
}
.lyra-step.active.slide {
border-left: 4px solid #ffff00;
}
.lyra-step.active.accent {
border-top: 3px solid #ff6666;
}
.modal-overlay {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.7);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #22252a;
margin: auto;
padding: 20px;
border: 1px solid var(--accent-color);
border-radius: 5px;
width: 80%;
max-width: 600px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.modal-content h4 {
margin-top: 0;
color: var(--accent-hover);
text-align: center;
}
.modal-content textarea {
width: 100%;
height: 300px;
background: #111;
color: var(--text-color);
border: 1px solid #444;
font-family: 'Roboto Mono', monospace;
font-size: 12px;
resize: vertical;
margin-bottom: 15px;
box-sizing: border-box;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* Style the value readouts in the modal */
#patchEditModal span {
font-family: 'Roboto Mono', monospace;
color: var(--accent-hover);
font-size: 0.9em;
}
#patchEditModal .row {
align-items: center;
/* Better vertical alignment for sliders and labels */
}
.context-menu hr {
border: none;
height: 1px;
background-color: var(--border-color);
margin: 5px 0;
}
/* Adaptive layout for the sequencer */
@media (max-width: 769px) {
#controls {
padding: 10px;
width: 98vw;
left: 1vw;
box-sizing: border-box;
}
#controls button {
font-size: 12px;
padding: 9px 3px;
}
/* Switch to a 16x2 grid on smaller screens */
.steps-container {
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: 4px;
overflow-x: hidden;
/* Hide scrollbar in grid mode */
}
h3 span {
display: none;
}
/* hide keyboard tips */
.pattern-row .step {
width: auto;
height: 15px;
min-width: 6px;
}
input#lyraPortamento {
max-width: 80px;
}
label[for=lyraPortamento] {
font-size: 8px;
margin-right: -10px;
}
#lyraKeyDisplay br {
display: block;
}
}
/* Styles for the Synth Step Editor Modal */
#synthStepEditModal .row {
align-items: center;
margin-bottom: 18px;
}
#synthStepEditModal label {
flex-basis: 25%;
text-align: right;
margin-right: 15px;
color: var(--text-color);
opacity: 0.8;
}
#synthStepEditModal select,
#synthStepEditModal input[type="range"] {
flex-grow: 1;
}
#synthStepEditModal .step-toggles {
justify-content: space-around;
margin-top: 25px;
}
#synthStepEditModal .toggle-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
/* Visually appealing checkboxes */
#synthStepEditModal .styled-checkbox {
appearance: none;
-webkit-appearance: none;
width: 18px;
height: 18px;
border: 2px solid var(--border-color);
border-radius: 3px;
background-color: #333;
cursor: pointer;
position: relative;
transition: background-color 0.2s, border-color 0.2s;
}
#synthStepEditModal .styled-checkbox:hover {
border-color: var(--lyra-color);
}
#synthStepEditModal .styled-checkbox:checked {
background-color: var(--lyra-color);
border-color: var(--lyra-color);
}
#synthStepEditModal .styled-checkbox:checked::after {
content: '✔';
font-size: 14px;
color: var(--main-bg);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.context-menu {
position: fixed;
background: #28282e;
border: 1px solid var(--border-color);
z-index: 2000;
padding: 5px 0;
border-radius: 4px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
font-size: 0.9em;
min-width: 200px;
color: var(--text-color);
}
.context-menu-item {
padding: 8px 15px;
cursor: pointer;
user-select: none;
/* Prevents text selection on click */
}
.context-menu-item:hover {
background-color: var(--accent-color);
}
.context-menu-item.disabled {
cursor: not-allowed;
color: #666;
}
.context-menu-item.disabled:hover {
background-color: transparent;
}
.context-menu-separator {
border: 0;
height: 1px;
background-color: var(--border-color);
margin: 5px 0;
}
.context-menu-back-button {
padding: 8px 15px;
cursor: pointer;
font-weight: bold;
color: var(--accent-color);
border-bottom: 1px solid var(--border-color);
margin: -5px 0 5px 0;
background-color: #333;
}
/* --- ADDED FOR UX FEATURES --- */
@keyframes pulse-border {
0% {
border-color: var(--border-color);
}
50% {
border-color: var(--accent-hover);
}
100% {
border-color: var(--border-color);
}
}
.row.is-modulated>label {
color: var(--accent-hover);
text-shadow: 0 0 5px var(--shadow-color);
}
.row.is-modulated>input,
.row.is-modulated>select {
animation: pulse-border 2s infinite ease-in-out;
}
.track-controls {
display: flex;
gap: 4px;
}
.track-controls button {
font-size: 10px;
padding: 3px 6px;
min-width: 22px;
flex-grow: 0;
font-family: 'Roboto Mono', monospace;
font-weight: bold;
}
.mute-btn.active {
background-color: #ff4136;
color: white;
}
.solo-btn.active {
background-color: #39CCCC;
color: white;
}
.track-length {
width: 15px;
padding: 4px;
font-size: 11px;
background-color: #222;
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 2px;
text-align: center;
-moz-appearance: textfield;
}
.track-length::-webkit-outer-spin-button,
.track-length::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pattern-row .step.disabled {
background-color: #1a1a1a;
border-color: #333;
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="kaosPad">
<div id="kaosPuck"></div>
</div>
<div id="controls">
<!-- <h2>WHITE DWARF</h2> -->
<pre style="display: block; margin: 0 auto; padding: 0; font-size: clamp(2px, 1.125vw, 8.25px); color: rgba(255, 255, 255, 0.85); color: var(--accent-hover);">
_| _| _| _| _|_|
_| _| _| _|_|_| _|_|_|_| _|_| _|_|_| _| _| _| _|_|_| _| _|_| _|
_| _| _| _| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_|
_| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _|
_| _| _| _| _| _|_| _|_|_| _|_|_| _| _| _|_|_| _| _|
</pre>
<canvas id="masterViz" width="600" height="1" style="margin-top:15px; background:#000; max-width: 100%;"></canvas>
<br><br>
<div class="row">
<button id="playButton">PLAY ▶</button>
<button id="randomizeButton">SHUFFLE SEQUENCER</button>
<button id="randomizeLyraSettingsButton">RANDOMIZE SYNTH</button>
</div>
<div class="row">
<label for="masterVolume">VOLUME:</label>
<input type="range" id="masterVolume" min="0" max="1" step="0.01" value="0.4" data-default="0.4">
<span class="param-value" style="min-width: 40px;">0.40</span>
</div>
<div class="row">
<label for="bpm">BPM:</label>
<input type="range" id="bpm" min="60" max="190" value="138" data-default="138">
<span id="bpmValue" style="min-width: 30px;">138</span>
</div>
<!-- === MODIFIED: SEED Block with Random Seed Generator === -->
<div class="row">
<label for="seedInput">SEED:</label>
<input type="text" id="seedInput" placeholder="Enter text or leave blank for random...">
<button id="randomSeedButton" title="Generate Random Seed" style="flex-grow: 0; font-size: 1.2em; padding: 8px 12px;">🎲</button>
<button id="generateFromSeedButton" style="flex-grow: 0.7;">GENERATE</button>
</div>
<h3>32-STEP SEQUENCER <span style="font-size:0.8em; opacity: 0.7; float:right; margin-top:2px;">Shift+Click for Trig Conditions | Ctrl+Click for Bass Accent</span></h3>
<div class="pattern-row" data-type="kick">
<span class="pattern-label">BD:</span>
<div class="track-controls">
<button class="mute-btn" data-track="kick" title="Mute Track">M</button>
<button class="solo-btn" data-track="kick" title="Solo Track">S</button>
</div>
<input type="number" class="track-length" min="1" max="32" value="32" data-track="kick" title="Track Length">
<div class="steps-container"></div>
</div>
<div class="pattern-row" data-type="snare">
<span class="pattern-label">SD:</span>
<div class="track-controls">
<button class="mute-btn" data-track="snare" title="Mute Track">M</button>
<button class="solo-btn" data-track="snare" title="Solo Track">S</button>
</div>
<input type="number" class="track-length" min="1" max="32" value="32" data-track="snare" title="Track Length">
<div class="steps-container"></div>
</div>
<div class="pattern-row" data-type="hat">
<span class="pattern-label">HHT:</span>
<div class="track-controls">
<button class="mute-btn" data-track="hat" title="Mute Track">M</button>
<button class="solo-btn" data-track="hat" title="Solo Track">S</button>
</div>
<input type="number" class="track-length" min="1" max="32" value="32" data-track="hat" title="Track Length">
<div class="steps-container"></div>
</div>
<div class="pattern-row" data-type="bass">
<span class="pattern-label">BASS:</span>
<div class="track-controls">
<button class="mute-btn" data-track="bass" title="Mute Track">M</button>
<button class="solo-btn" data-track="bass" title="Solo Track">S</button>
</div>
<input type="number" class="track-length" min="1" max="32" value="32" data-track="bass" title="Track Length">
<div class="steps-container"></div>
</div>
<div class="pattern-row" data-type="lyraGate">
<span class="pattern-label">SYNTH:</span>
<div class="track-controls">
<button class="mute-btn" data-track="synth" title="Mute Track">M</button>
<button class="solo-btn" data-track="synth" title="Solo Track">S</button>
</div>
<input type="number" class="track-length" min="1" max="32" value="32" data-track="lyraGate" title="Track Length">
<div class="steps-container"></div>
</div>
<br>
<details>
<summary>MIXER</summary>
<div style="padding-top: 5px;">
<div class="row">
<label for="kickVolume">Kick Volume:</label>
<input type="range" id="kickVolume" min="0" max="1" step="0.01" value="1" data-default="1">
<span class="param-value" style="min-width: 40px;">1.00</span>
</div>
<div class="row">
<label for="snareVolume">Snare Volume:</label>
<input type="range" id="snareVolume" min="0" max="1" step="0.01" value="0.8" data-default="0.8">
<span class="param-value" style="min-width: 40px;">0.80</span>
</div>
<div class="row">
<label for="hatVolume">Hat Volume:</label>
<input type="range" id="hatVolume" min="0" max="1" step="0.01" value="0.8" data-default="0.8">
<span class="param-value" style="min-width: 40px;">0.80</span>
</div>
<div class="row">
<label for="bassVolume">Bass Volume:</label>
<input type="range" id="bassVolume" min="0" max="1" step="0.01" value="0.8" data-default="0.8">
<span class="param-value" style="min-width: 40px;">0.80</span>
</div>
<div class="row">
<label for="synthVolume">Synth Volume:</label>
<input type="range" id="synthVolume" min="0" max="1" step="0.01" value="0.7" data-default="0.7">
<span class="param-value" style="min-width: 40px;">0.70</span>
</div>
</div>
</details>
<br>
<details>
<summary>SEQUENCER MACROS</summary>
<h5>PATTERN GENERATORS</h5>
<div class="row">
<button onclick="(function(){
window.kickMode=(window.kickMode||0)+1;
var mode=window.kickMode%13;
var s=app.audioSystem.sequences.kick;
for(var i=0;i<s.length;i++){
if(mode===0){s[i]=(i%4===0)}
else if(mode===1){s[i]=(i%8===0)}
else if(mode===2){s[i]=(i===0||i===20)}
else if(mode===3){s[i]=(i===0)}
else if(mode===4){s[i]=(i===0||i===6)}
else if(mode===5){s[i]=(i===0||i===10)}
else if(mode===6){s[i]=(i===0||i===8||i===14)}
else if(mode===7){s[i]=(i%16===0)}
else if(mode===8){s[i]=(i===0||i===5||i===10||i===16||i===21||i===26)}
else if(mode===9){s[i]=(i===0||i===6||i===10||i===16||i===22||i===26)}
else if(mode===10){s[i]=(i%8===0)}
else if(mode===11){s[i]=(i===0||i===7||i===12||i===19||i===24)}
else if(mode===12){s[i]=(i%16===0||i%16===2)}
}
app.audioSystem.updatePatternEditor();
})()">KICKCYC</button>
<button onclick="(function(){
window.sdMode = (window.sdMode || 0) + 1
const patterns = [
[4, 12, 20, 28],
[5, 13, 21, 29],
[2, 6, 10, 14, 18, 22, 26, 30],
[4, 5, 12, 13, 20, 21, 28, 29],
[12, 28],
[4, 5, 20, 21],
[3, 4, 5, 11, 12, 13],
[5, 10, 21, 26],
[6, 14, 22, 30],
[7, 15, 23, 31],
[4, 10, 12, 20, 26, 28]
]
const mode = window.sdMode % patterns.length
const s = app.audioSystem.sequences.snare
for (let i = 0; i < s.length; i++){
s[i] = patterns[mode].includes(i)
}
app.audioSystem.updatePatternEditor()
})()">SNARECYC</button>
<button onclick="(function(){
window.hatMode=(window.hatMode||0)+1;
if(window.hatMode>3){window.hatMode=0}
var s=app.audioSystem.sequences.hat;
for(var i=0;i<s.length;i++){
if(window.hatMode===0){s[i]=(i%2===0)}
else if(window.hatMode===1){s[i]=(i%2===1)}
else if(window.hatMode===2){s[i]=(Math.random()>0.5)}
else if(window.hatMode===3){s[i]=(i%4===2)}
}
app.audioSystem.updatePatternEditor();
})()">HATCYC</button>
<button onclick="window.bassClicks=(window.bassClicks||0)+1,(function(){var s=app.audioSystem.sequences.bass;for(var i=0;i<s.length;i++){s[i]=window.bassClicks%3===0||Math.random()>0.5}})(),app.audioSystem.updatePatternEditor()">BASSLINE</button>
<button onclick="(function(){
function euclidean(pulses, steps){
var pattern = new Array(steps).fill(false);
if (pulses === 0) return pattern;
var bucket = 0;
for(var i = 0; i < steps; i++){
bucket += pulses;
if(bucket >= steps){
pattern[i] = true;
bucket -= steps;
}
}
return pattern;
}
var seqs = app.audioSystem.sequences;
const totalSteps = 32;
seqs.snare.fill(false);
seqs.snare[4] = true;
seqs.snare[12] = true;
seqs.snare[20] = true;
seqs.snare[28] = true;
const hatPulses = Math.floor(Math.random() * 10) + 7;
seqs.hat = euclidean(hatPulses, totalSteps);
for(let i = 0; i < totalSteps; i++){
if(seqs.snare[i]){
seqs.hat[i] = false;
}
}
app.audioSystem.stepMeta.snare = {};
app.audioSystem.stepMeta.hat = {};
app.audioSystem.updatePatternEditor();
})()">EUCLID</button>
</div>
<h5>RHYTHM TRANSFORMS</h5>
<div class="row">
<button onclick="['kick','snare','hat','bass','lyraGate'].forEach(function(t){var s=app.audioSystem.sequences[t];app.audioSystem.sequences[t]=s.map(function(v){return!v})});app.audioSystem.updatePatternEditor()">INVERT</button>
<button onclick="['kick','snare','hat','bass','lyraGate'].forEach(function(t){app.audioSystem.sequences[t].reverse()}),app.audioSystem.updatePatternEditor()">REVERSE</button>
<button onclick="['kick','snare','hat','bass'].forEach(function(t){for(var i=0;i<32;i++){app.audioSystem.sequences[t][i]=Math.random()<0.5}});app.audioSystem.updatePatternEditor()">RAND50</button>
<button onclick="(function(){
window.velClick=(window.velClick||0)+1;
var mode=Math.floor((window.velClick-1)/2)%3;
['kick','snare','hat','bass'].forEach(function(t){
var s=app.audioSystem,sequences=s.sequences[t],meta=s.stepMeta[t];
for(var i=0;i<sequences.length;i++){
if(sequences[i]){
meta[i]=meta[i]||{velocity:1,prob:1,ratchet:1};
if(mode===0){ meta[i].velocity=(i%8===0)?1:(0.4+Math.random()*0.4) }
else if(mode===1){ meta[i].velocity=(i%8===0)?1:(0.3+Math.random()*0.7) }
else if(mode===2){ meta[i].velocity=1 }
}
}
});
app.audioSystem.updatePatternEditor();
})()">HUMANIZE</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.kick
var m=app.audioSystem.stepMeta.kick
for(var i=0;i<s.length;i++){
if(s[i]){
m[i]=m[i]||{}
m[i].ratchet=2
}
}
app.audioSystem.updatePatternEditor()
})()">BOUNCE</button>
</div>
<h5>KICK PATTERNS</h5>
<div class="row">
<button onclick="(function(){
var s=app.audioSystem.sequences.kick;
var m=app.audioSystem.stepMeta.kick={};
s.forEach((v,i)=>s[i]=i%8===0);
app.audioSystem.updatePatternEditor();
})()">STAMP</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.kick;
var m=app.audioSystem.stepMeta.kick;
for(var i=0;i<s.length;i++){
if(!s[i] && i%8===4){
s[i]=true
m[i]={velocity:0.4,prob:1,ratchet:1}
}
}
app.audioSystem.updatePatternEditor()
})()">GHOST</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.kick;
s.forEach((v,i)=>s[i]=Math.random()<0.3);
app.audioSystem.stepMeta.kick={};
app.audioSystem.updatePatternEditor();
})()">SHUFFLE</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.kick;
for(var i=0;i<s.length;i++){
s[i]=(i%16===0||i%16===2)
}
app.audioSystem.stepMeta.kick={};
app.audioSystem.updatePatternEditor();
})()">HEART</button>
<button onclick="(function(){
app.audioSystem.sequences.kick.fill(false);
app.audioSystem.stepMeta.kick={};
app.audioSystem.updatePatternEditor();
})()">KCLR</button>
</div>
<h5>SNARE PATTERNS</h5>
<div class="row">
<button onclick="(function(){
var s=app.audioSystem.sequences.snare;
s.forEach((v,i)=>s[i]=[4,12,20,28].includes(i));
app.audioSystem.stepMeta.snare={};
app.audioSystem.updatePatternEditor();
})()">BACKBEAT</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.snare;
s.forEach((v,i)=>s[i]=i%4===2);
app.audioSystem.stepMeta.snare={};
app.audioSystem.updatePatternEditor();
})()">OFFBEAT</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.snare;
for(var i=0;i<s.length;i++){
s[i]=false;
}
s[8]=true;
s[24]=true;
app.audioSystem.updatePatternEditor();
})()">HALFTIME</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.snare;
for(var i=0;i<s.length;i++){
s[i]=false;
}
for(var j=8;j<16;j++){
s[j]=true;
}
app.audioSystem.updatePatternEditor();
})()">ROLL</button>
<button onclick="(function(){
app.audioSystem.sequences.snare.fill(false);
app.audioSystem.stepMeta.snare={};
app.audioSystem.updatePatternEditor();
})()">SCLR</button>
</div>
<h5>HI-HAT EFFECTS</h5>
<div class="row">
<button onclick="(function(){
var seq=app.audioSystem.sequences.hat
var meta=app.audioSystem.stepMeta.hat
for(var i=0;i<seq.length;i++){
seq[i]=i%3===0
delete meta[i]
}
app.audioSystem.updatePatternEditor()
})()">TRIPLET</button>
<button onclick="(function(){
var seq=app.audioSystem.sequences.hat
var meta=app.audioSystem.stepMeta.hat
for(var i=0;i<seq.length;i++){
if(seq[i]){
meta[i]=meta[i]||{}
meta[i].ratchet=[2,3,4][Math.floor(Math.random()*3)]
}
}
app.audioSystem.updatePatternEditor()
})()">RATCHET</button>
<button onclick="(function(){
var seq=app.audioSystem.sequences.hat
var meta=app.audioSystem.stepMeta.hat
for(var i=0;i<seq.length;i++){
if(seq[i]){
meta[i]=meta[i]||{}
meta[i].velocity=1-(i/(seq.length-1))
}
}
app.audioSystem.updatePatternEditor()
})()">FADE</button>
<button onclick="(function(){
var seq=app.audioSystem.sequences.hat
var meta=app.audioSystem.stepMeta.hat
for(var i=0;i<seq.length;i++){
if(seq[i]){
meta[i]=meta[i]||{}
meta[i].prob=Math.random()
}
}
app.audioSystem.updatePatternEditor()
})()">PROB</button>
<button onclick="(function(){
app.audioSystem.sequences.hat.fill(false);
app.audioSystem.stepMeta.hat={};
app.audioSystem.updatePatternEditor()
})()">HCLR</button>
</div>
<h5>BASS PATTERNS</h5>
<div class="row">
<button onclick="(function(){
var s=app.audioSystem.sequences.bass;
s.forEach((v,i)=>s[i]=i%4===0);
app.audioSystem.stepMeta.bass={};
app.audioSystem.updatePatternEditor();
})()">PULSE</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.bass;
s.forEach((v,i)=>s[i]=i%3===0);
app.audioSystem.stepMeta.bass={};
app.audioSystem.updatePatternEditor();
})()">STRIDE</button>
<button onclick="(function(){
var b=app.audioSystem.sequences.bass
var p=[0,4,7,12,16,20,23,28]
for(var i=0;i<b.length;i++){
b[i]=p.indexOf(i)!==-1
}
app.audioSystem.updatePatternEditor()
})()">FUNK</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.bass;
var m=app.audioSystem.stepMeta.bass={};
s.forEach((v,i)=>{ if(v){ m[i]={slide:true}; }});
app.audioSystem.updatePatternEditor();
})()">GLIDE</button>
<button onclick="(function(){
app.audioSystem.sequences.bass.fill(false);
app.audioSystem.stepMeta.bass={};
app.audioSystem.updatePatternEditor();
})()">BCLR</button>
</div>
<h5>PERFORMANCE TOOLS</h5>
<div class="row">
<button onclick="window.gateClicks=(window.gateClicks||0)+1,(function(){var s=app.audioSystem.sequences.lyraGate;for(var i=0;i<s.length;i++){s[i]=window.gateClicks%3===0||Math.random()>0.5}})(),app.audioSystem.updatePatternEditor()">GATE</button>
<button onclick="(function(){
var b=app.audioSystem.sequences.bass
var m=app.audioSystem.stepMeta.bass
for(var i=0;i<b.length;i++){
if(b[i]){
m[i]=m[i]||{}
m[i].velocity=Math.random()
}
}
app.audioSystem.updatePatternEditor()
})()">THROB</button>
<button onclick="(function(){
var s=app.audioSystem.sequences.snare
for(var i=0;i<s.length;i++){
s[i]=i%4===0
}
app.audioSystem.stepMeta.snare={}
app.audioSystem.updatePatternEditor()
})()">DROP</button>
<button onclick="(function(){
var b=app.audioSystem.sequences.bass
var m=app.audioSystem.stepMeta.bass
for(var i=0;i<b.length;i++){
if(b[i]){
m[i]=m[i]||{}
m[i].slide=!m[i].slide
}
}
app.audioSystem.updatePatternEditor()
})()">SLIDE</button>
<button onclick="(function(){
['kick','snare','hat','bass','lyraGate'].forEach(function(t){
app.audioSystem.sequences[t].fill(false);
app.audioSystem.stepMeta[t]={};
});
app.audioSystem.updatePatternEditor();
})()">RESET</button>
</div>
<h5>COMBO EFFECTS</h5>
<div class="row">
<button onclick="(function(){
// Progressive buildup over time - adds complexity each cycle
window.buildupStep = (window.buildupStep || 0) + 1;
var phase = window.buildupStep % 4;
var seqs = app.audioSystem.sequences;
var meta = app.audioSystem.stepMeta;
if(phase === 0) {
// Start with just kick
seqs.kick.forEach((v,i) => seqs.kick[i] = i%8===0);
seqs.snare.fill(false);
seqs.hat.fill(false);
seqs.bass.fill(false);
} else if(phase === 1) {
// Add snare
seqs.snare.forEach((v,i) => seqs.snare[i] = [4,12,20,28].includes(i));
} else if(phase === 2) {
// Add hats
seqs.hat.forEach((v,i) => seqs.hat[i] = i%2===0);
} else if(phase === 3) {
// Add bass and complexity
seqs.bass.forEach((v,i) => seqs.bass[i] = i%4===0);
// Add some ratchets for energy
for(var i=0;i<32;i++){
if(seqs.hat[i] && Math.random()<0.3){
meta.hat[i] = meta.hat[i] || {};
meta.hat[i].ratchet = 2;
}
}
}
app.audioSystem.updatePatternEditor();
})()">BUILDUP</button>
<button onclick="(function(){
// Progressive breakdown - removes elements
window.breakStep = (window.breakStep || 0) + 1;
var phase = window.breakStep % 4;
var seqs = app.audioSystem.sequences;
if(phase === 0) {
// Remove bass first
seqs.bass.fill(false);
app.audioSystem.stepMeta.bass = {};
} else if(phase === 1) {
// Remove most hats
seqs.hat.forEach((v,i) => seqs.hat[i] = i%8===0);
app.audioSystem.stepMeta.hat = {};
} else if(phase === 2) {
// Simplify snare to halftime
seqs.snare.fill(false);
seqs.snare[8] = true;
seqs.snare[24] = true;
} else if(phase === 3) {
// Just kick on 1
seqs.kick.fill(false);
seqs.kick[0] = true;
seqs.snare.fill(false);
seqs.hat.fill(false);
}
app.audioSystem.updatePatternEditor();
})()">BREAKDOWN</button>
<button onclick="(function(){
// Add tension through probability and ratchets
var seqs = app.audioSystem.sequences;
var meta = app.audioSystem.stepMeta;
['kick','snare','hat','bass'].forEach(function(track){
for(var i=0;i<32;i++){
if(seqs[track][i]){
meta[track][i] = meta[track][i] || {};
// Random probability between 0.6-0.9 for uncertainty
meta[track][i].prob = 0.6 + Math.random() * 0.3;
// Occasional ratchets for chaos
if(Math.random() < 0.2){
meta[track][i].ratchet = [2,3][Math.floor(Math.random()*2)];
}
}
}
});
app.audioSystem.updatePatternEditor();
})()">TENSION</button>
<button onclick="(function(){
// Clean, satisfying resolution
var seqs = app.audioSystem.sequences;
// Simple, solid patterns
seqs.kick.forEach((v,i) => seqs.kick[i] = i%4===0);
seqs.snare.forEach((v,i) => seqs.snare[i] = [4,12,20,28].includes(i));
seqs.hat.forEach((v,i) => seqs.hat[i] = i%2===0);
seqs.bass.forEach((v,i) => seqs.bass[i] = i%8===0);
// Clear all metadata for clean sound
['kick','snare','hat','bass'].forEach(function(track){
app.audioSystem.stepMeta[track] = {};
});
app.audioSystem.updatePatternEditor();
})()">RELEASE</button>
<button onclick="(function(){
// Chaotic glitch effects
var seqs = app.audioSystem.sequences;
var meta = app.audioSystem.stepMeta;
['kick','snare','hat','bass'].forEach(function(track){
for(var i=0;i<32;i++){
if(seqs[track][i] && Math.random()<0.4){
meta[track][i] = meta[track][i] || {};
// Micro-stutters
meta[track][i].ratchet = [3,4,5][Math.floor(Math.random()*3)];
// Velocity cuts
meta[track][i].velocity = Math.random() < 0.3 ? 0.2 : 1;
// Some drops out completely
if(Math.random() < 0.1) meta[track][i].prob = 0;
}
}
});
app.audioSystem.updatePatternEditor();
})()">GLITCH</button>
</div>
<h5>TIMING EFFECTS</h5>
<div class="row">
<button onclick="(function(){
// Toggle swing amount
var swingSlider = document.getElementById('swing');
var currentSwing = parseFloat(swingSlider.value);
var newSwing = currentSwing === 0 ? 0.33 : (currentSwing < 0.5 ? 0.66 : 0);
swingSlider.value = newSwing;
swingSlider.dispatchEvent(new Event('input'));
})()">SWING</button>
<button onclick="(function(){
// Slightly rush the timing on random hits via velocity/ratchet
var meta = app.audioSystem.stepMeta;
['kick','snare','hat'].forEach(function(track){
var seq = app.audioSystem.sequences[track];
for(var i=0;i<32;i++){
if(seq[i] && Math.random()<0.3){
meta[track][i] = meta[track][i] || {};
meta[track][i].ratchet = 2; // Quick double hit feels rushed
meta[track][i].velocity = 0.8;
}
}
});
app.audioSystem.updatePatternEditor();
})()">RUSH</button>
<button onclick="(function(){
// Create dragging feel with probability drops
var meta = app.audioSystem.stepMeta;
['kick','snare','hat'].forEach(function(track){
var seq = app.audioSystem.sequences[track];
for(var i=0;i<32;i++){
if(seq[i] && Math.random()<0.4){
meta[track][i] = meta[track][i] || {};
meta[track][i].prob = 0.7; // Some hits drop out = dragging feel
}
}
});
app.audioSystem.updatePatternEditor();
})()">DRAG</button>
<button onclick="(function(){
// Rapid fire stutters
var seqs = app.audioSystem.sequences;
var meta = app.audioSystem.stepMeta;
// Pick 2-4 random steps across all tracks
var targets = [];
for(var i=0;i<4;i++){
targets.push(Math.floor(Math.random()*32));
}
['kick','snare','hat','bass'].forEach(function(track){
targets.forEach(function(step){
if(!seqs[track][step]){
seqs[track][step] = true;
meta[track][step] = {ratchet: [4,5,6][Math.floor(Math.random()*3)], velocity: 0.8};
}
});
});
app.audioSystem.updatePatternEditor();
})()">STUTTER</button>
<button onclick="(function(){
// Reset swing and clear timing-related metadata
document.getElementById('swing').value = 0;
document.getElementById('swing').dispatchEvent(new Event('input'));
var meta = app.audioSystem.stepMeta;
['kick','snare','hat','bass'].forEach(function(track){
Object.keys(meta[track]).forEach(function(step){
if(meta[track][step].prob < 1 || meta[track][step].ratchet > 1){
delete meta[track][step];
}
});
});
app.audioSystem.updatePatternEditor();
})()">SYNC</button>
</div>
</details> <!-- END OF SEQUENCER MACROS -->
<!-- END OF PATTERN EDITOR -->
<div id="presetManager">
<h3>PRESET MANAGER</h3>
<div class="row">
<select id="presetSelect">
<option value="white dwarf"></option>
</select>
<button id="loadPresetButton">LOAD</button>
<button id="deletePresetButton" style="background-color: #500000;">DEL</button>
</div>
<div class="row">
<input type="text" id="presetNameInput" placeholder="New Preset Name...">
<button id="savePresetButton" style="background-color: #8B4000;">SAVE</button>
</div>
<div class="row" style="margin-top: 10px;">
<button id="importStateButton">IMPORT</button>
<button id="exportStateButton">EXPORT</button>
</div>
<div class="row" style="margin-top: 5px;">
<button id="exportAllButton" style="background-color: #004466; width: 100%;">EXPORT ALL PRESETS</button>
</div>
</div>
<div id="lyraContainer" class="closed">
<h3>16-STEP SYNTH</h3>
<div class="control-group">
<h4>STEP SEQUENCER</h4>
<div class="row">
<button id="lyraSeqToggle" class="active">SEQ ON</button>
<button id="lyraSeqRandomize" style="background-color: #8B4000;">ACID RIFF</button>
<button onclick="(function(){
const pulses = Math.floor(Math.random() * 8) + 3; // Random pulses from 3 to 10
const steps = 16;
const lyraSeq = app.audioSystem.lyraSequencer.steps;
let bucket = 0;
for (let i = 0; i < steps; i++) {
bucket += pulses;
if (bucket >= steps) {
lyraSeq[i].active = true;
bucket -= steps;
} else {
lyraSeq[i].active = false;
}
}
app.audioSystem.updateLyraSequencerUI();
})()">EUCLID</button>
<label for="lyraPortamento">SLIDE (ms):</label>
<input type="range" id="lyraPortamento" min="0" max="200" step="1" value="80" data-default="80" style="width: 100px;">
</div>
<div id="lyraStepGrid" style="display: grid; grid-template-columns: repeat(8, 1fr); gap: 3px; margin-top: 10px;">
<div class="lyra-step active accent" id="lyraStep0" data-step="0">D#</div>
<div class="lyra-step active" id="lyraStep1" data-step="1">G#</div>
<div class="lyra-step active" id="lyraStep2" data-step="2">C</div>
<div class="lyra-step active slide accent" id="lyraStep3" data-step="3">A#</div>
<div class="lyra-step" id="lyraStep4" data-step="4">—</div>
<div class="lyra-step active" id="lyraStep5" data-step="5">F</div>
<div class="lyra-step active accent" id="lyraStep6" data-step="6">D+</div>
<div class="lyra-step active" id="lyraStep7" data-step="7">D</div>
<div class="lyra-step active" id="lyraStep8" data-step="8">D#</div>
<div class="lyra-step active" id="lyraStep9" data-step="9">D+</div>
<div class="lyra-step active" id="lyraStep10" data-step="10">D#+</div>
<div class="lyra-step active" id="lyraStep11" data-step="11">F</div>
<div class="lyra-step" id="lyraStep12" data-step="12">—</div>
<div class="lyra-step active slide" id="lyraStep13" data-step="13">G</div>
<div class="lyra-step active" id="lyraStep14" data-step="14">G</div>
<div class="lyra-step" id="lyraStep15" data-step="15">—</div>
</div>
<div style="font-size: 0.7em; margin-top: 5px; color: #888;">
Click: Note On/Off | Shift+Click: Slide | Ctrl/Cmd+Click: Accent | Right-Click: Edit
</div>
</div>
<div id="lyraKeyDisplay">
Black Keys: <code>WETYUO</code>
<br>
White Keys: <code>ASDFGHJKL</code>
</div>
<div id="lyraControlsWrapper">
<div id="lyraLooperControls" class="row">
<button id="lyraRecButton">REC</button>
<button id="lyraPlayButton">PLAY</button>
<button id="lyraWipeButton">WIPE</button>
</div>
<div class="control-group">
<h4>OSCILLATORS</h4>
<div class="row">
<label for="lyraOsc1Type">OSC1 Wave:</label>
<select id="lyraOsc1Type">
<option>sawtooth</option>
<option>square</option>
<option selected>sine</option>
<option>triangle</option>
</select>
</div>
<div class="row">
<label for="lyraOsc1Oct">OSC1 Oct:</label>
<input type="range" id="lyraOsc1Oct" min="-2" max="2" value="1" step="1" data-default="1">
<span class="param-value" style="min-width: 40px;">1</span>
</div>
<div class="row">
<label for="lyraOsc2Type">OSC2 Wave:</label>
<select id="lyraOsc2Type">
<option>sawtooth</option>
<option>square</option>
<option selected>sine</option>
<option>triangle</option>
</select>
</div>
<div class="row">
<label for="lyraOsc2Oct">OSC2 Oct:</label>
<input type="range" id="lyraOsc2Oct" min="-2" max="2" value="0" step="1" data-default="0">
<span class="param-value" style="min-width: 40px;">0</span>
</div>
</div>
<div class="control-group">
<h4>MIX & MOD</h4>
<div class="row">
<label for="lyraDetune">DETUNE:</label>
<input type="range" id="lyraDetune" min="0" max="50" value="3" data-default="3">
<span class="param-value" style="min-width: 40px;">3</span>
</div>
<div class="row">
<label for="lyraFMAmt">FM AMT:</label>
<input type="range" id="lyraFMAmt" min="0" max="2000" value="256" data-default="256">
<span class="param-value" style="min-width: 40px;">256</span>
</div>
<div class="row">
<label for="lyraWaveFold">WAVE FOLD:</label>
<input type="range" id="lyraWaveFold" min="0" max="1" step="0.01" value="0.03" data-default="0.03">
<span class="param-value" style="min-width: 40px;">0.03</span>
</div>
<div class="row">
<label for="lyraDriveMode">DRIVE MODE:</label>
<select id="lyraDriveMode">
<option value="soft clip">Soft Clip (tanh)</option>
<option value="hard clip">Hard Clip</option>
<option value="exponential">Exponential</option>
<option value="wavefold">Wavefold</option>
</select>
</div>
<div class="row">
<label for="lyraDrive">DRIVE:</label>
<input type="range" id="lyraDrive" min="0" max="1" step="0.01" value="0.04" data-default="0.04">
<span class="param-value" style="min-width: 40px;">0.04</span>
</div>
</div>
<div class="control-group">
<h4>FILTER</h4>
<div class="row">
<label for="lyraFilterType">TYPE:</label>
<select id="lyraFilterType">
<option selected value="lowpass">Low Pass</option>
<option value="highpass">High Pass</option>
<option value="bandpass">Band Pass</option>
<option value="notch">Notch</option>
</select>
</div>
<div class="row">
<label for="lyraFilterCutoff">CUTOFF:</label>
<input type="range" id="lyraFilterCutoff" min="20" max="20000" value="3845" data-default="3845">
<span class="param-value" style="min-width: 40px;">3845</span>
</div>
<div class="row">
<label for="lyraFilterQ">RESO:</label>
<input type="range" id="lyraFilterQ" min="0" max="18" step="0.1" value="11.1" data-default="11.1">
<span class="param-value" style="min-width: 40px;">11.1</span>
</div>
<div class="row">
<label for="lyraFilterEnv">ENV AMT:</label>
<input type="range" id="lyraFilterEnv" min="-5000" max="5000" value="-752" data-default="-752">
<span class="param-value" style="min-width: 40px;">-752</span>
</div>
</div>
<div class="control-group">
<h4>LFO</h4>
<div class="row">
<label for="lyraLfoRate">RATE:</label>
<div class="lfo-rate-control">
<input type="range" id="lyraLfoRate" min="0.1" max="20" step="0.1" value="0.3" data-default="0.3" style="display: none;">
<select id="lyraLfoSyncRate" style="display: block;">
<option value="16">1/64</option>
<option value="8">1/32</option>
<option value="4">1/16</option>
<option value="2">1/8</option>
<option value="1">1/4</option>
<option value="0.5">1/2</option>
<option value="0.25" selected>1 Bar</option>
</select>
<button id="lyraLfoSyncToggle" class="active">SYNC</button>
</div>
</div>
<div class="row">
<label for="lyraLfoDepth">DEPTH:</label>
<input type="range" id="lyraLfoDepth" min="0" max="5000" value="1524" data-default="1524">
<span class="param-value" style="min-width: 40px;">1524</span>
</div>
</div>
<div class="control-group">
<h4>ENVELOPE</h4>
<div class="row">
<label for="lyraAttack">ATTACK:</label>
<input type="range" id="lyraAttack" min="0.01" max="2" step="0.01" value="0.09" data-default="0.09">
<span class="param-value" style="min-width: 40px;">0.09</span>
</div>
<div class="row">
<label for="lyraDecay">DECAY:</label>
<input type="range" id="lyraDecay" min="0.01" max="2" step="0.01" value="0.05" data-default="0.05">
<span class="param-value" style="min-width: 40px;">0.05</span>
</div>
<div class="row">
<label for="lyraSustain">SUSTAIN:</label>
<input type="range" id="lyraSustain" min="0" max="1" step="0.01" value="0" data-default="0">
<span class="param-value" style="min-width: 40px;">0.00</span>
</div>
<div class="row">
<label for="lyraRelease">RELEASE:</label>
<input type="range" id="lyraRelease" min="0.01" max="5" step="0.01" value="0.25" data-default="0.25">
<span class="param-value" style="min-width: 40px;">0.25</span>
</div>
</div>
<div class="control-group">
<h4>EFFECTS</h4>
<div class="row">
<label for="lyraFxType">FX TYPE:</label>
<select id="lyraFxType">
<option value="Delay">Delay</option>
<option value="Phaser">Bubbly Phaser</option>
<option value="Flanger">Pure Flanger</option>
<option value="Quazar">Acidy Quazar</option>
<option value="Chopper" selected>Rhythmic Chopper</option>
<option value="Bitcrusher">Digital Grime</option>
</select>
</div>
<div id="lyraFxControls">
<div id="fxControls-Delay" class="fx-panel" style="display: none;">
<div class="row">
<label for="lyraDelayTime">TIME:</label>
<input type="range" id="lyraDelayTime" min="0.01" max="1.0" step="0.01" value="0.45" data-default="0.45">
<span class="param-value" style="min-width: 40px;">0.45</span>
</div>
<div class="row">
<label for="lyraDelayFeedback">FEEDBACK:</label>
<input type="range" id="lyraDelayFeedback" min="0" max="0.95" step="0.01" value="0.2" data-default="0.2">
<span class="param-value" style="min-width: 40px;">0.20</span>
</div>
<div class="row">
<label for="lyraDelayMix">MIX:</label>
<input type="range" id="lyraDelayMix" min="0" max="1.0" step="0.01" value="0.35" data-default="0.35">
<span class="param-value" style="min-width: 40px;">0.35</span>
</div>
</div>
<div id="fxControls-Phaser" class="fx-panel" style="display: none;">
<div class="row">
<label for="phaserRate">RATE:</label>
<input type="range" id="phaserRate" min="0.1" max="10" step="0.1" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.5</span>
</div>
<div class="row">
<label for="phaserDepth">DEPTH:</label>
<input type="range" id="phaserDepth" min="100" max="5000" step="10" value="1200" data-default="1200">
<span class="param-value" style="min-width: 40px;">1200</span>
</div>
<div class="row">
<label for="phaserFeedback">FEEDBACK:</label>
<input type="range" id="phaserFeedback" min="0" max="0.9" step="0.01" value="0.3" data-default="0.3">
<span class="param-value" style="min-width: 40px;">0.30</span>
</div>
<div class="row">
<label for="phaserMix">MIX:</label>
<input type="range" id="phaserMix" min="0" max="1.0" step="0.01" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
</div>
<div id="fxControls-Flanger" class="fx-panel" style="display: none;">
<div class="row">
<label for="flangerRate">RATE:</label>
<input type="range" id="flangerRate" min="0.05" max="5" step="0.01" value="0.2" data-default="0.2">
<span class="param-value" style="min-width: 40px;">0.20</span>
</div>
<div class="row">
<label for="flangerDepth">DEPTH:</label>
<input type="range" id="flangerDepth" min="0.001" max="0.02" step="0.001" value="0.005" data-default="0.005">
<span class="param-value" style="min-width: 40px;">0.005</span>
</div>
<div class="row">
<label for="flangerFeedback">FEEDBACK:</label>
<input type="range" id="flangerFeedback" min="0" max="0.95" step="0.01" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
<div class="row">
<label for="flangerMix">MIX:</label>
<input type="range" id="flangerMix" min="0" max="1.0" step="0.01" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
</div>
<div id="fxControls-Quazar" class="fx-panel" style="display: none;">
<div class="row">
<label for="quazarRate">RATE:</label>
<input type="range" id="quazarRate" min="0.1" max="15" step="0.1" value="2" data-default="2">
<span class="param-value" style="min-width: 40px;">2.0</span>
</div>
<div class="row">
<label for="quazarReso">RESO (Q):</label>
<input type="range" id="quazarReso" min="5" max="50" step="0.5" value="25" data-default="25">
<span class="param-value" style="min-width: 40px;">25.0</span>
</div>
<div class="row">
<label for="quazarFreq">CENTER HZ:</label>
<input type="range" id="quazarFreq" min="200" max="8000" step="10" value="1500" data-default="1500">
<span class="param-value" style="min-width: 40px;">1500</span>
</div>
<div class="row">
<label for="quazarMix">MIX:</label>
<input type="range" id="quazarMix" min="0" max="1.0" step="0.01" value="1.0" data-default="1.0">
<span class="param-value" style="min-width: 40px;">1.00</span>
</div>
</div>
<div id="fxControls-Chopper" class="fx-panel" style="display: block;">
<div class="row">
<label for="chopperRate">RATE:</label>
<select id="chopperRate">
<option value="8">1/32</option>
<option value="4">1/16</option>
<option value="2" selected>1/8</option>
<option value="1">1/4</option>
<option value="0.5">1/2</option>
</select>
</div>
<div class="row">
<label for="chopperDepth">DEPTH:</label>
<input type="range" id="chopperDepth" min="0" max="1.0" step="0.01" value="0.05" data-default="0.05">
<span class="param-value" style="min-width: 40px;">0.05</span>
</div>
</div>
<div id="fxControls-Bitcrusher" class="fx-panel" style="display: none;">
<div class="row">
<label for="crusherBitDepth">BIT DEPTH:</label>
<input type="range" id="crusherBitDepth" min="1" max="16" step="1" value="8" data-default="8">
<span class="param-value" style="min-width: 40px;">8</span>
</div>
<div class="row">
<label for="crusherFreq">DOWNSAMPLE:</label>
<input type="range" id="crusherFreq" min="0" max="1" step="0.01" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
<div class="row">
<label for="crusherMix">MIX:</label>
<input type="range" id="crusherMix" min="0" max="1.0" step="0.01" value="0.5" data-default="0.5">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="patchBay">
<h3>PATCH BAY</h3>
<div class="patch-creator">
<div class="row"><label for="patchSource">FROM:</label><select id="patchSource"></select></div>
<div class="row"><label for="patchDest">TO:</label><select id="patchDest"></select></div>
<div class="row">
<label for="patchAmount">AMOUNT:</label>
<input type="range" id="patchAmount" min="-2" max="2" step="0.05" value="0.5" data-default="0.5" style="width: 280px;">
<span class="param-value" style="min-width: 40px;">0.50</span>
</div>
<button id="addPatchButton">+ CONNECT PATCH</button>
</div>
<div id="activePatches"></div>
</div>
<h3>SEQUENCER MODULES</h3>
<div class="control-group">
<h4>BD <button onclick="['kickPitch','kickDecay','kickDrive'].forEach(function(id){var e=document.getElementById(id);e.value=(Math.random()*(e.max-e.min)+ +e.min).toFixed(2);e.dispatchEvent(new Event('input'));}); app.syncUiToAudioState();">RAND</button></h4>
<div class="row">
<label>PITCH:</label>
<input type="range" id="kickPitch" min="20" max="100" value="51" data-default="51">
<span class="param-value" style="min-width: 40px;">51</span>
</div>
<div class="row">
<label>DECAY:</label>
<input type="range" id="kickDecay" min="0.05" max="0.5" step="0.01" value="0.17" data-default="0.17">
<span class="param-value" style="min-width: 40px;">0.17</span>
</div>
<div class="row">
<label>DRIVE:</label>
<input type="range" id="kickDrive" min="0" max="1" step="0.01" value="0.39" data-default="0.39">
<span class="param-value" style="min-width: 40px;">0.39</span>
</div>
</div>
<div class="control-group">
<h4>BASS <button onclick="['bassOctave','bassDetune','bassFilterCutoff','bassFilterQ'].forEach(function(id){var e=document.getElementById(id);e.value=(Math.random()*(e.max-e.min)+ +e.min).toFixed(2);e.dispatchEvent(new Event('input'));}); app.syncUiToAudioState();">RAND</button></h4>
<div class="row">
<label>OCTAVE:</label>
<input type="range" id="bassOctave" min="-2" max="2" value="-1" step="1" data-default="-1">
<span class="param-value" style="min-width: 40px;">-1</span>
</div>
<div class="row">
<label>DETUNE:</label>
<input type="range" id="bassDetune" min="0" max="25" value="13" data-default="13">
<span class="param-value" style="min-width: 40px;">13</span>
</div>
<div class="row">
<label>FILTER:</label>
<input type="range" id="bassFilterCutoff" min="100" max="8000" value="2265" data-default="2265">
<span class="param-value" style="min-width: 40px;">2265</span>
</div>
<div class="row">
<label>RESO:</label>
<input type="range" id="bassFilterQ" min="0" max="20" value="17" data-default="17">
<span class="param-value" style="min-width: 40px;">17</span>
</div>
</div>
<div class="control-group">
<h4>SD <button onclick="['snareTone','snareSnappy','snareDecay'].forEach(function(id){var e=document.getElementById(id);e.value=(Math.random()*(e.max-e.min)+ +e.min).toFixed(2);e.dispatchEvent(new Event('input'));}); app.syncUiToAudioState();">RAND</button></h4>
<div class="row">
<label>TONE:</label>
<input type="range" id="snareTone" min="100" max="500" value="129" data-default="129">
<span class="param-value" style="min-width: 40px;">129</span>
</div>
<div class="row">
<label>SNAPPY:</label>
<input type="range" id="snareSnappy" min="1000" max="9000" value="5245" data-default="5245">
<span class="param-value" style="min-width: 40px;">5245</span>
</div>
<div class="row">
<label>DECAY:</label>
<input type="range" id="snareDecay" min="0.05" max="0.4" step="0.01" value="0.25" data-default="0.25">
<span class="param-value" style="min-width: 40px;">0.25</span>
</div>
</div>
<div class="control-group">
<h4>HHT <button onclick="['hatDecay','hatMetal'].forEach(function(id){var e=document.getElementById(id);e.value=(Math.random()*(e.max-e.min)+ +e.min).toFixed(2);e.dispatchEvent(new Event('input'));}); app.syncUiToAudioState();">RAND</button></h4>
<div class="row">
<label>DECAY:</label>
<input type="range" id="hatDecay" min="0.01" max="0.3" step="0.01" value="0.16" data-default="0.16">
<span class="param-value" style="min-width: 40px;">0.16</span>
</div>
<div class="row">
<label>METAL:</label>
<input type="range" id="hatMetal" min="5000" max="15000" value="7571" data-default="7571">
<span class="param-value" style="min-width: 40px;">7571</span>
</div>
</div>
<h3>MASTER OUT</h3>
<div class="control-group">
<div class="row">
<button id="safetyToggle" class="active" title="Toggles modulation safety. When active (orange), modulation is clamped to a safe range. When inactive, modulation can go into negative ranges for a harsher effect.">
SAFETY: ON
</button>
</div>
<div class="row">
<label for="masterDist">DISTORT:</label>
<input type="range" id="masterDist" min="0" max="1" step="0.01" value="0.03" data-default="0.03">
<span class="param-value" style="min-width: 40px;">0.03</span>
</div>
<div class="row">
<label for="masterReverb">REVERB:</label>
<input type="range" id="masterReverb" min="0" max="0.5" step="0.01" value="0.44" data-default="0.44">
<span class="param-value" style="min-width: 40px;">0.44</span>
</div>
</div>
<br>
</div>
<div id="exportModal" class="modal-overlay">
<div class="modal-content">
<h4>EXPORT PRESET SNIPPET</h4>
<textarea id="exportTextarea" readonly></textarea>
<div class="modal-buttons">
<button id="downloadPresetButton" style="background-color: var(--lyra-color);">DOWNLOAD .TXT</button>
<button id="closeExportModalButton">CLOSE</button>
</div>
</div>
</div>
<div id="importModal" class="modal-overlay">
<div class="modal-content">
<h4>IMPORT TRACK</h4>
<textarea id="importTextarea" placeholder="Paste a Share Code or full Preset Code here..."></textarea>
<div class="modal-buttons">
<button id="applyImportButton" style="background-color: var(--accent-color);">APPLY</button>
<button id="closeImportModalButton">CLOSE</button>
</div>
</div>
</div>
<!-- Add this new modal at the end of your HTML body -->
<div id="patchEditModal" class="modal-overlay">
<div class="modal-content">
<h4 id="patchEditTitle" style="margin-bottom: 25px;">EDIT PATCH</h4>
<div class="row">
<label for="patchEditAmount" style="flex-basis: 30%;">Amount:</label>
<input type="range" id="patchEditAmount" min="-2" max="2" step="0.05" value="1">
<span id="patchEditAmountValue" style="min-width: 40px; text-align: left;">1.00</span>
</div>
<div class="row">
<label for="patchEditMin" style="flex-basis: 30%;">Mod Min:</label>
<input type="range" id="patchEditMin" min="-1" max="1" step="0.01" value="-1">
<span id="patchEditMinValue" style="min-width: 40px; text-align: left;">-1.00</span>
</div>
<div class="row">
<label for="patchEditMax" style="flex-basis: 30%;">Mod Max:</label>
<input type="range" id="patchEditMax" min="-1" max="1" step="0.01" value="1">
<span id="patchEditMaxValue" style="min-width: 40px; text-align: left;">1.00</span>
</div>
<div class="modal-buttons" style="margin-top: 20px;">
<button id="applyPatchEditButton" style="background-color: var(--accent-color);">APPLY</button>
<button id="closePatchEditModalButton">CLOSE</button>
</div>
</div>
</div>
<div id="synthStepEditModal" class="modal-overlay">
<div class="modal-content">
<h4 id="synthStepEditTitle">EDIT SYNTH STEP</h4>
<!-- Main Controls -->
<div class="synth-step-editor-grid">
<!-- Note Selection -->
<div class="row">
<label for="synthStepNoteSelect">Note:</label>
<select id="synthStepNoteSelect">
<!-- Options will be populated by JavaScript -->
</select>
</div>
<!-- Gate/Duration Control -->
<div class="row">
<label for="synthStepGate">Gate:</label>
<input type="range" id="synthStepGate" min="0.1" max="1.0" step="0.05" value="1.0">
<span id="synthStepGateValue" style="min-width: 40px; text-align: left;">100%</span>
</div>
<!-- Boolean Toggles -->
<div class="row step-toggles">
<div class="toggle-wrapper">
<input type="checkbox" id="synthStepActiveToggle" class="styled-checkbox">
<label for="synthStepActiveToggle">Active</label>
</div>
<div class="toggle-wrapper">
<input type="checkbox" id="synthStepAccentToggle" class="styled-checkbox">
<label for="synthStepAccentToggle">Accent</label>
</div>
<div class="toggle-wrapper">
<input type="checkbox" id="synthStepSlideToggle" class="styled-checkbox">
<label for="synthStepSlideToggle">Slide</label>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="modal-buttons" style="margin-top: 20px;">
<button id="applySynthStepEditButton" style="background-color: var(--lyra-color);">APPLY</button>
<button id="closeSynthStepEditModalButton">CLOSE</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js';
function openCellEditor(audio, row, idx, meta) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.display = 'flex';
const box = document.createElement('div');
box.className = 'modal-content';
box.innerHTML = '<h4>Step ' + row + ' ' + (idx + 1) + '</h4>' +
'Velocity <input id="vel" type="range" min="0" max="1" step="0.01" value="' + meta.velocity + '"><br>' +
'Probability <input id="prob" type="range" min="0" max="1" step="0.01" value="' + meta.prob + '"><br>' +
'Ratchet <input id="rat" type="number" min="1" max="8" value="' + meta.ratchet + '"><br>';
const ok = document.createElement('button');
ok.textContent = 'OK';
const cancel = document.createElement('button');
cancel.textContent = 'CLOSE';
const bar = document.createElement('div');
bar.className = 'modal-buttons';
bar.appendChild(ok);
bar.appendChild(cancel);
box.appendChild(bar);
overlay.appendChild(box);
document.body.appendChild(overlay);
const focusable = Array.from(box.querySelectorAll('input, button'));
let keydownHandler;
const closeModal = () => {
if (keydownHandler) {
document.removeEventListener('keydown', keydownHandler);
}
overlay.remove();
audio.updatePatternEditor(); // Update visuals on close
};
keydownHandler = (e) => {
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
if (e.key === 'Escape') {
closeModal();
}
if (e.key === 'Tab') {
if (e.shiftKey) { // Shift+Tab
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else { // Tab
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
};
document.addEventListener('keydown', keydownHandler);
ok.onclick = () => {
ok.disabled = true;
if (!audio.stepMeta[row]) audio.stepMeta[row] = {};
const ratchetVal = parseInt(box.querySelector('#rat').value, 10);
audio.stepMeta[row][idx] = {
velocity: parseFloat(box.querySelector('#vel').value),
prob: parseFloat(box.querySelector('#prob').value),
ratchet: isNaN(ratchetVal) ? 1 : Math.max(1, Math.min(8, ratchetVal)),
triggerPass: audio.stepMeta[row][idx]?.triggerPass
};
closeModal();
};
cancel.onclick = closeModal;
focusable[0].focus();
}
/**
* =========================================================================
* THE CORRECT CONTEXT MENU MANAGER (The "No-Garbage-UX" Engine)
* - Parent menus STAY OPEN. This is the one you want.
* =========================================================================
*/
class ContextMenuManager {
constructor() {
this.menuStack = [];
this.activeTrigger = null;
this.audio = null;
this._handleKeyDown = this._handleKeyDown.bind(this);
this._handleDocumentClick = this._handleDocumentClick.bind(this);
}
isOpen() {
return this.menuStack.length > 0;
}
open(evt, audio, type, menuDefinition) {
evt.preventDefault();
evt.stopPropagation();
if (this.isOpen() && this.activeTrigger === evt.currentTarget) {
this.closeAll();
return;
}
if (this.isOpen()) this.closeAll();
this.audio = audio;
this.activeTrigger = evt.currentTarget;
const menuEl = this._createMenu(menuDefinition, type);
document.body.appendChild(menuEl);
const rect = menuEl.getBoundingClientRect();
let x = evt.clientX,
y = evt.clientY;
if (x + rect.width > window.innerWidth) x = window.innerWidth - rect.width - 5;
if (y + rect.height > window.innerHeight) y = window.innerHeight - rect.height - 5;
menuEl.style.left = `${x}px`;
menuEl.style.top = `${y}px`;
this.menuStack.push(menuEl);
this._attachGlobalListeners();
}
closeAll() {
this.menuStack.forEach(menu => menu.remove());
this.menuStack = [];
this.activeTrigger = null;
this.audio = null;
this._removeGlobalListeners();
}
_attachGlobalListeners() {
document.addEventListener('click', this._handleDocumentClick, true);
document.addEventListener('keydown', this._handleKeyDown);
}
_removeGlobalListeners() {
document.removeEventListener('click', this._handleDocumentClick, true);
document.removeEventListener('keydown', this._handleKeyDown);
}
_handleDocumentClick(evt) {
if (this.isOpen() && !this.menuStack.some(menu => menu.contains(evt.target))) {
this.closeAll();
}
}
_handleKeyDown(evt) {
if (evt.key === 'Escape') this.closeAll();
}
_createMenu(items, type) {
const menu = document.createElement('div');
menu.className = 'context-menu';
items.forEach(itemData => {
if (itemData.separator) {
const hr = document.createElement('hr');
hr.className = 'context-menu-separator';
menu.appendChild(hr);
return;
}
const item = document.createElement('div');
item.className = 'context-menu-item';
item.textContent = itemData.label;
const isDisabled = typeof itemData.disabled === 'function' ? itemData.disabled(this.audio) : itemData.disabled;
if (isDisabled) {
item.classList.add('disabled');
} else {
if (itemData.action) {
item.onclick = (e) => {
e.stopPropagation();
itemData.action(this.audio, type);
this.closeAll();
};
} else if (itemData.submenu) {
item.textContent += ' ▸';
item.onclick = (e) => {
e.stopPropagation();
const parentMenu = item.closest('.context-menu');
const currentLevel = this.menuStack.indexOf(parentMenu);
while (this.menuStack.length > currentLevel + 1) {
this.menuStack.pop().remove();
}
const subMenuEl = this._createMenu(itemData.submenu, type);
document.body.appendChild(subMenuEl);
const parentRect = item.getBoundingClientRect();
const subRect = subMenuEl.getBoundingClientRect();
let subX = parentRect.right + 2;
let subY = parentRect.top;
if (subX + subRect.width > window.innerWidth) subX = parentRect.left - subRect.width - 2;
if (subY + subRect.height > window.innerHeight) subY = window.innerHeight - subRect.height - 5;
subMenuEl.style.left = `${subX}px`;
subMenuEl.style.top = `${subY}px`;
this.menuStack.push(subMenuEl);
};
}
}
menu.appendChild(item);
});
return menu;
}
}
/**
* =========================================================================
* SINGLE INSTANCE CREATION
* =========================================================================
*/
const contextMenuManager = new ContextMenuManager();
/**
* =========================================================================
* THE FULLY UPGRADED ROW CONTEXT MENU WITH BASS FX
* =========================================================================
*/
function openRowContextMenu(audio, type, evt) {
const clearMeta = () => {
audio.stepMeta[type] = {};
audio.updatePatternEditor();
};
const updateAndClearMeta = (action) => {
action();
clearMeta();
};
const euclidean = (pulses, steps) => {
const pattern = new Array(steps).fill(false);
if (pulses === 0 || pulses > steps) return pattern;
let bucket = 0;
for (let i = 0; i < steps; i++) {
bucket += pulses;
if (bucket >= steps) {
pattern[i] = true;
bucket -= steps;
}
}
return pattern;
};
const setParamOnStep = (stepIndex, param, value) => {
audio.stepMeta[type] = audio.stepMeta[type] || {};
audio.stepMeta[type][stepIndex] = audio.stepMeta[type][stepIndex] || {
velocity: 1,
prob: 1,
ratchet: 1
};
audio.stepMeta[type][stepIndex][param] = value;
};
const allPresets = {
'Kick: Four on the Floor': {
pattern: Array(32).fill(false).map((_, i) => i % 8 === 0),
types: ['kick']
},
'Kick: Heartbeat': {
pattern: Array(32).fill(false).map((_, i) => i % 16 === 0 || i % 16 === 2),
types: ['kick']
},
'Snare: Backbeat': {
pattern: Array(32).fill(false).map((_, i) => i % 8 === 4),
types: ['snare']
},
'Snare: Halftime': {
pattern: Array(32).fill(false).map((_, i) => i === 8 || i === 24),
types: ['snare']
},
'Hat: Every 8th': {
pattern: Array(32).fill(false).map((_, i) => i % 2 === 0),
types: ['hat']
},
'Hat: Off-beat 8th': {
pattern: Array(32).fill(false).map((_, i) => i % 2 !== 0),
types: ['hat']
},
'Hat: Triplets': {
pattern: Array(32).fill(false).map((_, i) => i % 3 === 0),
types: ['hat']
},
'Bass: Standard Pulse': {
pattern: Array(32).fill(false).map((_, i) => i % 4 === 0),
types: ['bass']
},
'Rhythm: Tresillo': {
pattern: [true, false, false, true, false, false, true, false, true, false, false, true, false, false, true, false, true, false, false, true, false, false, true, false, true, false, false, true, false, false, true, false],
types: ['kick', 'snare', 'bass', 'hat']
},
'Gate: Trance Gate': {
pattern: Array(32).fill(true).map((_, i) => i % 4 !== 3),
types: ['lyraGate']
}
};
const availablePresets = Object.entries(allPresets)
.filter(([name, data]) => data.types.includes(type))
.map(([name, data]) => ({
label: name,
action: () => {
audio.sequences[type] = data.pattern.slice(0, audio.sequenceLength);
clearMeta();
}
}));
let menuDefinition = [{
label: 'Copy',
submenu: [{
label: 'Copy Full Pattern',
action: () => {
audio.patternClipboard = {
sequence: [...audio.sequences[type]],
stepMeta: JSON.parse(JSON.stringify(audio.stepMeta[type]))
};
}
},
{
label: 'Copy Steps Only',
action: () => {
audio.patternClipboard = {
sequence: [...audio.sequences[type]]
};
}
},
{
label: 'Copy Parameters Only',
action: () => {
audio.patternClipboard = {
stepMeta: JSON.parse(JSON.stringify(audio.stepMeta[type]))
};
}
}
]
},
{
label: 'Paste',
submenu: [{
label: 'Paste Full Pattern',
action: () => {
if (audio.patternClipboard) {
if (audio.patternClipboard.sequence) audio.sequences[type] = [...audio.patternClipboard.sequence];
if (audio.patternClipboard.stepMeta) audio.stepMeta[type] = JSON.parse(JSON.stringify(audio.patternClipboard.stepMeta));
audio.updatePatternEditor();
}
},
disabled: !audio.patternClipboard
},
{
label: 'Paste Steps Only',
action: () => {
if (audio.patternClipboard?.sequence) {
audio.sequences[type] = [...audio.patternClipboard.sequence];
audio.updatePatternEditor();
}
},
disabled: !audio.patternClipboard?.sequence
},
{
label: 'Paste Parameters Only',
action: () => {
if (audio.patternClipboard?.stepMeta) {
audio.stepMeta[type] = JSON.parse(JSON.stringify(audio.patternClipboard.stepMeta));
audio.updatePatternEditor();
}
},
disabled: !audio.patternClipboard?.stepMeta
}
]
},
{
separator: true
},
{
label: 'Generate',
submenu: [{
label: 'Fill Every 2 Steps',
action: () => updateAndClearMeta(() => audio.sequences[type] = Array(32).fill(false).map((_, i) => i % 2 === 0))
},
{
label: 'Fill Every 4 Steps',
action: () => updateAndClearMeta(() => audio.sequences[type] = Array(32).fill(false).map((_, i) => i % 4 === 0))
},
{
label: 'Euclidean (5/32)',
action: () => updateAndClearMeta(() => audio.sequences[type] = euclidean(5, 32))
},
{
label: 'Euclidean (7/32)',
action: () => updateAndClearMeta(() => audio.sequences[type] = euclidean(7, 32))
},
]
},
{
label: 'Mutate',
submenu: [{
label: 'Invert (Flip On/Off)',
action: () => {
audio.sequences[type] = audio.sequences[type].map(v => !v);
audio.updatePatternEditor();
}
},
{
label: 'Reverse',
action: () => {
audio.sequences[type].reverse();
audio.updatePatternEditor();
}
},
{
label: 'Rotate Left <',
action: () => audio.nudgePattern(type, -1)
},
{
label: 'Rotate Right >',
action: () => audio.nudgePattern(type, 1)
},
{
separator: true
},
{
label: 'Double Speed (Halve)',
action: () => {
const s = audio.sequences[type];
const half = s.slice(0, 16);
audio.sequences[type] = [...half, ...half];
audio.updatePatternEditor();
}
},
{
label: 'Half Speed (Double)',
action: () => {
const s = audio.sequences[type];
const newSeq = [];
for (let i = 0; i < s.length; i += 2) {
newSeq.push(s[i]);
}
audio.sequences[type] = [...newSeq, ...Array(16).fill(false)];
audio.updatePatternEditor();
}
},
{
separator: true
},
{
label: 'Thin Out (Remove 25%)',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v && Math.random() < 0.25) audio.sequences[type][i] = false;
});
audio.updatePatternEditor();
}
},
{
label: 'Thicken (Add 25%)',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (!v && Math.random() < 0.25) audio.sequences[type][i] = true;
});
audio.updatePatternEditor();
}
},
]
},
{
label: 'Parameters',
submenu: [{
label: 'Velocity Ramp Down',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'velocity', 1.0 - (i / 31))
});
audio.updatePatternEditor();
}
},
{
label: 'Velocity Ramp Up',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'velocity', i / 31)
});
audio.updatePatternEditor();
}
},
{
label: 'Humanize Velocity',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'velocity', 0.6 + Math.random() * 0.4)
});
audio.updatePatternEditor();
}
},
{
separator: true
},
{
label: 'Probability Fade In',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'prob', i / 31)
});
audio.updatePatternEditor();
}
},
{
label: 'Probability Fade Out',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'prob', 1.0 - (i / 31))
});
audio.updatePatternEditor();
}
},
{
label: 'Humanize Probability',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'prob', 0.75 + Math.random() * 0.25)
});
audio.updatePatternEditor();
}
},
{
separator: true
},
{
label: 'Create Ratchet Buildup',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) {
let r = 1;
if (i > 23) r = 4;
else if (i > 15) r = 3;
else if (i > 7) r = 2;
setParamOnStep(i, 'ratchet', r);
}
});
audio.updatePatternEditor();
}
},
{
label: 'Add Random Ratchets',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v && Math.random() < 0.3) setParamOnStep(i, 'ratchet', [2, 3, 4][Math.floor(Math.random() * 3)])
});
audio.updatePatternEditor();
}
},
]
},
{
label: 'Trig Conditions',
submenu: [{
label: 'Set All to 1st Pass',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'triggerPass', 'first')
});
audio.updatePatternEditor();
}
},
{
label: 'Set All to 2nd Pass',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'triggerPass', 'second')
});
audio.updatePatternEditor();
}
},
{
label: 'Alternate Passes',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v) setParamOnStep(i, 'triggerPass', i % 2 === 0 ? 'first' : 'second')
});
audio.updatePatternEditor();
}
},
{
separator: true
},
{
label: 'Clear All Conditions',
action: () => {
audio.sequences[type].forEach((v, i) => {
if (v && audio.stepMeta[type]?.[i]) {
delete audio.stepMeta[type][i].triggerPass;
}
});
audio.updatePatternEditor();
}
}
]
},
{
separator: true
},
{
label: 'Presets',
submenu: availablePresets.length > 0 ? availablePresets : [{
label: 'No presets for this row',
disabled: true
}]
},
{
separator: true
},
{
label: 'Clear...',
submenu: [{
label: 'Clear Steps',
action: () => {
audio.sequences[type].fill(false);
clearMeta();
}
},
{
label: 'Clear Parameters Only',
action: () => clearMeta()
},
{
label: 'Clear All (Steps & Params)',
action: () => {
audio.sequences[type].fill(false);
clearMeta();
}
}
]
}
];
contextMenuManager.open(evt, audio, type, menuDefinition);
}
function openSynthStepEditor(audio, stepIndex) {
const modal = document.getElementById('synthStepEditModal');
const stepData = audio.lyraSequencer.steps[stepIndex];
if (!modal || !stepData) return;
// --- Get all modal elements ---
const title = document.getElementById('synthStepEditTitle');
const noteSelect = document.getElementById('synthStepNoteSelect');
const gateSlider = document.getElementById('synthStepGate');
const gateValue = document.getElementById('synthStepGateValue');
const activeToggle = document.getElementById('synthStepActiveToggle');
const accentToggle = document.getElementById('synthStepAccentToggle');
const slideToggle = document.getElementById('synthStepSlideToggle');
const applyBtn = document.getElementById('applySynthStepEditButton');
const closeBtn = document.getElementById('closeSynthStepEditModalButton');
// --- Populate Note Select Dropdown ---
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C+', 'C#+', 'D+', 'D#+', 'E+', 'F+', 'F#+', 'G+', 'G#+', 'A+', 'A#+', 'B+', 'C++'];
noteSelect.innerHTML = ''; // Clear previous options
noteNames.forEach((name, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = name;
if (index === stepData.note) {
option.selected = true;
}
noteSelect.appendChild(option);
});
// --- Set initial values from stepData ---
title.textContent = `EDIT SYNTH STEP ${stepIndex + 1}`;
gateSlider.value = stepData.gate ?? 1.0; // Default to 1.0 if not set
gateValue.textContent = `${Math.round(gateSlider.value * 100)}%`;
activeToggle.checked = stepData.active;
accentToggle.checked = stepData.accent;
slideToggle.checked = stepData.slide;
// --- Define event handlers ---
const updateGateValue = () => {
gateValue.textContent = `${Math.round(gateSlider.value * 100)}%`;
};
let keydownHandler; // Declare here to be accessible in closeModal
const closeModal = () => {
modal.style.display = 'none';
// Clean up listeners to prevent memory leaks
gateSlider.removeEventListener('input', updateGateValue);
applyBtn.onclick = null;
closeBtn.onclick = null;
document.removeEventListener('keydown', keydownHandler);
};
const applyChanges = () => {
const newStepData = {
active: activeToggle.checked,
note: parseInt(noteSelect.value, 10),
accent: accentToggle.checked,
slide: slideToggle.checked,
gate: parseFloat(gateSlider.value) // Save the new gate value
};
// Update the main audio system data
audio.lyraSequencer.steps[stepIndex] = newStepData;
// Update the UI to reflect changes
audio.updateLyraSequencerUI();
closeModal();
};
keydownHandler = (e) => {
if (e.key === 'Escape') {
closeModal();
} else if (e.key === 'Enter') {
applyChanges();
}
};
// --- Attach event listeners ---
gateSlider.addEventListener('input', updateGateValue);
applyBtn.onclick = applyChanges;
closeBtn.onclick = closeModal;
document.addEventListener('keydown', keydownHandler);
// --- Show the modal ---
modal.style.display = 'flex';
noteSelect.focus(); // Set focus to the first interactive element
}
class AudioSystem {
constructor() {
this.audioCtx = new(window.AudioContext || window.webkitAudioContext)();
this.reloadUntil = 0;
this.bpm = 138;
this.masterCycleStep = 0;
this.sequenceLength = 32; // DEPRECATED - now per-track
this.masterGain = this.audioCtx.createGain();
this.masterGain.gain.value = 0.4;
this.kickGain = this.audioCtx.createGain();
this.snareGain = this.audioCtx.createGain();
this.hatGain = this.audioCtx.createGain();
this.bassGain = this.audioCtx.createGain();
this.synthGain = this.audioCtx.createGain();
this.kickGain.gain.value = 1.0;
this.snareGain.gain.value = 0.8;
this.hatGain.gain.value = 0.8;
this.bassGain.gain.value = 0.8;
this.synthGain.gain.value = 0.7;
this.lastLoadedPresetState = null;
// --- NEW: Per-track Mute/Solo state ---
this.trackState = {
kick: { muted: false, soloed: false, gain: this.kickGain },
snare: { muted: false, soloed: false, gain: this.snareGain },
hat: { muted: false, soloed: false, gain: this.hatGain },
bass: { muted: false, soloed: false, gain: this.bassGain },
synth: { muted: false, soloed: false, gain: this.synthGain },
};
// --- NEW: Per-track sequence length state ---
this.sequenceLengths = {
kick: 32,
snare: 32,
hat: 32,
bass: 32,
lyraGate: 32
};
this.kickGain.connect(this.masterGain);
this.snareGain.connect(this.masterGain);
this.hatGain.connect(this.masterGain);
this.bassGain.connect(this.masterGain);
this.synthGain.connect(this.masterGain);
this.masterDistortion = this.audioCtx.createWaveShaper();
this.setDistortionCurve(0.1);
this.limiter = this.audioCtx.createDynamicsCompressor();
this.limiter.threshold.setValueAtTime(-1.0, this.audioCtx.currentTime, 0);
this.limiter.knee.setValueAtTime(0, this.audioCtx.currentTime, 0);
this.limiter.ratio.setValueAtTime(20.0, this.audioCtx.currentTime, 0);
this.limiter.attack.setValueAtTime(0.001, this.audioCtx.currentTime, 0);
this.limiter.release.setValueAtTime(0.1, this.audioCtx.currentTime, 0);
this.reverbNode = this.createReverb(1.5);
this.reverbSend = this.audioCtx.createGain();
this.reverbSend.gain.value = 0.15;
this.masterGain.connect(this.masterDistortion);
this.masterDistortion.connect(this.limiter);
this.masterDistortion.connect(this.reverbSend);
this.reverbSend.connect(this.reverbNode);
this.reverbNode.connect(this.limiter);
this.limiter.connect(this.audioCtx.destination);
this.analyzer = this.audioCtx.createAnalyser();
this.limiter.connect(this.analyzer);
this.analyzer.fftSize = 256;
this.dataArray = new Uint8Array(this.analyzer.frequencyBinCount);
/* MASTER VU METER WITH HEADER COLOR SWITCH */
const canvas = document.getElementById("masterViz");
const ctx = canvas.getContext("2d");
const ascii = document.querySelector("pre");
function sizeVU() {
const dpr = window.devicePixelRatio || 1;
const cssW = canvas.clientWidth || 600;
const cssH = 1; // target height in CSS pixels
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
canvas.width = Math.max(1, Math.floor(cssW * dpr));
canvas.height = Math.max(1, Math.floor(cssH * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixel units
}
sizeVU();
window.addEventListener("resize", sizeVU);
const intensityThreshold = 0.575;
const alpha = 0.85;
const drawViz = () => {
requestAnimationFrame(drawViz);
// flash blue while reloading
if (performance.now() < this.reloadUntil) {
const w = canvas.clientWidth || 600;
const h = canvas.clientHeight || 1;
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#0095ff";
ctx.fillRect(0, 0, w, h);
ascii.style.color = "rgba(0,170,255,.85)";
return;
}
this.analyzer.getByteFrequencyData(this.dataArray);
const avg = this.dataArray.reduce((sum, v) => sum + v, 0) / this.dataArray.length;
const w = canvas.clientWidth || 600;
const h = canvas.clientHeight || 1;
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#ff8c00";
const frac = avg / 255; // 0..1
ctx.fillRect(0, 0, Math.floor(w * frac), h);
const f = frac;
let color;
if (f > intensityThreshold) {
color = "rgba(0,170,255," + alpha + ")";
} else {
const r = 255;
const g = Math.round(255 - f * 115);
const b = Math.round(255 - f * 255);
color = "rgba(" + r + "," + g + "," + b + "," + alpha + ")";
}
ascii.style.color = color;
};
drawViz();
/* === END VU METER === */
this.patternClipboard = null;
this.params = {
kickPitch: 50,
kickDecay: .15,
kickDrive: .2,
bassOctave: 0,
bassDetune: 12,
bassFilterCutoff: 800,
bassFilterQ: 5,
snareTone: 220,
snareSnappy: 5000,
snareDecay: .12,
hatDecay: .08,
hatMetal: 9000
};
// --- NEW BASS WOBBLE PROPERTY ---
this.bassLfo = null;
// Master volume defaults to 40%
document.getElementById('masterVolume').addEventListener('input', e => {
this.masterGain.gain.value = parseFloat(e.target.value);
});
this.sequences = {
kick: [true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, false, false, true, false, false, false, false, false],
snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false],
hat: [false, true, true, false, false, false, false, false, true, true, true, true, false, false, false, false, false, true, true, true, false, false, false, false, false, true, true, false, true, false, false, false],
bass: [false, false, true, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, true, false, false, false, false],
lyraGate: (() => {
const arr = Array(32).fill(true);
arr[14] = false;
arr[27] = false;
arr[31] = false;
return arr;
})()
};
this.stepMeta = {
kick: {},
snare: {},
hat: {},
bass: {},
lyraGate: {}
};
this.presetVersion = 2;
this.lyraGlobalGateProbability = 1.0;
this.lyraParams = {
osc1Type: 'sine',
osc1Oct: 0,
osc2Type: 'sine',
osc2Oct: -1,
detune: 30,
fmAmount: 600,
waveFold: 0.2,
drive: 0.175,
driveMode: 'soft clip',
filterType: 'lowpass',
filterCutoff: 3000,
filterQ: 14.4,
filterEnv: 2500,
lfoRate: 0.3,
lfoSyncRate: '0.25',
lfoIsSynced: true,
lfoDepth: 2000,
attack: 0.3,
decay: 0.6,
sustain: 0.4,
release: 2.25,
portamento: 0.08
};
this.numLyraVoices = 8;
this.lyraVoices = [];
this.lyraVoiceIndex = 0;
this.lyraFxIn = this.audioCtx.createGain();
this.lyraFxOut = this.audioCtx.createGain();
this.lyraFxOut.connect(this.synthGain); // <-- CHANGE THIS CONNECTION
this.lyraFxNodes = {};
this.lyraSequencer = {
isPlaying: true,
currentStep: 0,
stepLength: 16,
forceGate: true,
steps: [{
active: true,
note: 3,
slide: false,
accent: true,
gate: 1.0
},
{
active: true,
note: 8,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 0,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 10,
slide: true,
accent: true,
gate: 1.0
},
{
active: false,
note: 0,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 5,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 14,
slide: false,
accent: true,
gate: 1.0
},
{
active: true,
note: 2,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 3,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 14,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 15,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 5,
slide: false,
accent: false,
gate: 1.0
},
{
active: false,
note: 0,
slide: false,
accent: false,
gate: 1.0
},
{
active: true,
note: 7,
slide: true,
accent: false,
gate: 1.0
},
{
active: true,
note: 7,
slide: false,
accent: false,
gate: 1.0
},
{
active: false,
note: 0,
slide: false,
accent: false,
gate: 1.0
}
],
baseNote: 48,
lastPlayedFreq: 0,
nextNoteTime: 0
};
this.lyraLfoNode = this.audioCtx.createOscillator();
this.lyraLfoNode.start();
this.lyraLfoGain = this.audioCtx.createGain();
this.lyraLfoNode.connect(this.lyraLfoGain);
this.lyraLooper = {
isPlaying: false,
isRecording: false,
notes: [],
loopStartTime: 0,
loopDuration: 0,
nextNoteIndex: 0
};
this.lyraLooperTimerID = null;
this.patches = [];
this.modSources = {
'KAOS X': {
value: .5
},
'KAOS Y': {
value: .5
},
'LFO 1 (Slow)': {
value: 0,
phase: 0
},
'LFO 1 (Slower)': {
value: 0,
phase: 0,
rate: 0.02
},
'LFO 2 (Fast)': {
value: 0,
phase: 0
},
'LFO 2 (Faster)': {
value: 0,
phase: 0,
rate: 0.3
},
'LFO MIX': {
value: 0
},
'S&H (Random)': {
value: 0
},
'S&H (2 steps)': {
value: 0
},
'S&H (4 steps)': {
value: 0
},
'S&H (8 steps)': {
value: 0
},
'S&H (16 steps)': {
value: 0
},
'S&H (32 steps)': {
value: 0
},
'S&H (64 steps)': {
value: 0
},
'BD ENV': {
value: 0,
analyzer: this.createVoiceAnalyzer()
},
'SD ENV': {
value: 0,
analyzer: this.createVoiceAnalyzer()
},
'LYRA ENV': {
value: 0
},
'LYRA LFO': {
node: this.lyraLfoGain,
value: 0
},
'CLK/2': {
value: 0
},
'CLK/4': {
value: 0
},
'CLK/8': {
value: 0
},
'CLK/16': {
value: 0
},
'CLK/32': {
value: 0
}
};
this.modsFrozen = false;
this.isMuted = false;
this.globalModScale = 1.0;
this.swing = 0;
const createWaveTypeSetter = (v) => {
if (v < 0.25) return "sine";
if (v < 0.5) return "square";
if (v < 0.75) return "sawtooth";
return "triangle";
};
this.modDestinationsUnsafe = {
'BD DECAY': v => this.params.kickDecay = .05 + Math.max(0, v) * .8,
'BD DRIVE': v => this.params.kickDrive = Math.max(0, v),
'BD PITCH': v => this.params.kickPitch = Math.max(1, 50 + v * 50),
'HHT DECAY': v => this.params.hatDecay = .01 + Math.max(0, v) * .4,
'HHT METAL': v => this.params.hatMetal = Math.max(100, 8000 + v * 7000),
'SD SNAPPY': v => this.params.snareSnappy = Math.max(100, 5000 + v * 4000),
'SD TONE': v => this.params.snareTone = Math.max(20, 220 + v * 200),
'SNARE DECAY': v => this.params.snareDecay = 0.05 + Math.max(0, v) * 0.35,
'BASS DETUNE': v => this.params.bassDetune = Math.max(0, v) * 50,
'BASS FILTER': v => this.params.bassFilterCutoff = Math.max(20, 100 + v * 7900),
'BASS OCTAVE': v => this.params.bassOctave = Math.round(v * 2),
'BASS RESO': v => this.params.bassFilterQ = Math.max(0, v) * 25,
'MASTER DIST': v => this.setDistortionCurve(Math.max(0, v)),
'MASTER REVERB': v => this.reverbSend.gain.setTargetAtTime(Math.max(0, v) * .7, this.audioCtx.currentTime, .01),
'LYRA DETUNE': v => this.updateLyraParam('detune', v * 50),
'LYRA DRIVE': v => this.updateLyraParam('drive', Math.max(0, v) * parseFloat(document.getElementById('lyraDrive').max)),
'LYRA FM AMT': v => this.updateLyraParam('fmAmount', v * 2000),
'LYRA FOLD': v => this.updateLyraParam('waveFold', v),
'LYRA OSC1 OCT': v => this.updateLyraParam('osc1Oct', Math.min(2, Math.max(-2, Math.round(v * 2)))),
'LYRA OSC2 OCT': v => this.updateLyraParam('osc2Oct', Math.min(2, Math.max(-2, Math.round(v * 2)))),
'LYRA FILTER': v => this.updateLyraParam('filterCutoff', Math.max(20, 20 + v * 19980)),
'LYRA FILTER ENV': v => this.updateLyraParam('filterEnv', v * 5000),
'LYRA RESO': v => this.updateLyraParam('filterQ', Math.max(0, v) * parseFloat(document.getElementById('lyraFilterQ').max)),
"LYRA ATTACK": v => this.updateLyraParam("attack", Math.max(0.001, Math.pow(v, 2)) * 0.5),
"LYRA DECAY": v => this.updateLyraParam("decay", Math.max(0.001, Math.pow(v, 2)) * 0.5),
"LYRA SUSTAIN": v => this.updateLyraParam("sustain", Math.max(0, v) * 0.8),
"LYRA RELEASE": v => this.updateLyraParam("release", Math.max(0.001, Math.pow(v, 2)) * 1),
'LYRA SLIDE': v => {
const basePortamento = parseFloat(document.getElementById('lyraPortamento').value) / 1000;
const finalPortamento = basePortamento + (v * 0.2);
this.updateLyraParam('portamento', Math.max(0.001, finalPortamento));
},
'LYRA LFO DEPTH': v => this.updateLyraParam('lfoDepth', v * 5000),
'LYRA LFO RATE': v => this.updateLyraParam('lfoRate', Math.max(0.01, v * 20)),
'LYRA DELAY FB': v => {
if (this.lyraFxNodes.delay) this.lyraFxNodes.delay.feedback.gain.setTargetAtTime(Math.max(0, v) * 0.95, this.audioCtx.currentTime, 0.01)
},
'LYRA DELAY MIX': v => {
if (document.getElementById('lyraFxType').value === 'Delay') {
const mix = Math.max(0, v);
this.lyraFxNodes.wet.gain.setTargetAtTime(mix, this.audioCtx.currentTime, 0.01);
this.lyraFxNodes.dry.gain.setTargetAtTime(1.0 - mix, this.audioCtx.currentTime, 0.01);
}
},
'LYRA DELAY TIME': v => {
if (this.lyraFxNodes.delay) this.lyraFxNodes.delay.delay.delayTime.setTargetAtTime(Math.max(0.01, v), this.audioCtx.currentTime, 0.01)
},
'PHASER DEPTH': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.lfoGain.gain.setTargetAtTime(Math.max(0, v) * 5000, this.audioCtx.currentTime, 0.01);
},
'PHASER FEEDBACK': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.feedback.gain.setTargetAtTime(Math.max(0, v) * 0.9, this.audioCtx.currentTime, 0.01);
},
'PHASER RATE': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.lfo.frequency.setTargetAtTime(Math.max(0.1, v) * 10, this.audioCtx.currentTime, 0.01);
},
'FLANGER DEPTH': v => {
if (this.lyraFxNodes.flanger) this.lyraFxNodes.flanger.lfoGain.gain.setTargetAtTime(Math.max(0, v) * 0.02, this.audioCtx.currentTime, 0.01);
},
'FLANGER RATE': v => {
if (this.lyraFxNodes.flanger) this.lyraFxNodes.flanger.lfo.frequency.setTargetAtTime(Math.max(0.05, v * 5), this.audioCtx.currentTime, 0.01);
},
'CHOPPER DEPTH': v => {
if (this.lyraFxNodes.chopper) this.lyraFxNodes.chopper.lfoGain.gain.setTargetAtTime(Math.max(0, v), this.audioCtx.currentTime, 0.01);
},
'CRUSHER BITDEPTH': v => {
if (this.lyraFxNodes.bitcrusher?.node) this.lyraFxNodes.bitcrusher.node.parameters.get('bitDepth').setTargetAtTime(1 + Math.max(0, v) * 15, this.audioCtx.currentTime, 0.01);
},
'CRUSHER DOWNSAMPLE': v => {
if (this.lyraFxNodes.bitcrusher?.node) this.lyraFxNodes.bitcrusher.node.parameters.get('frequencyReduction').setTargetAtTime(Math.max(0, v), this.audioCtx.currentTime, 0.01);
},
'QUAZAR FREQ': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.lfoGain.gain.setTargetAtTime(Math.max(20, v * 8000), this.audioCtx.currentTime, 0.01);
},
'QUAZAR RATE': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.lfo.frequency.setTargetAtTime(Math.max(0.1, v * 15), this.audioCtx.currentTime, 0.01);
},
'QUAZAR RESO': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.filter.Q.setTargetAtTime(Math.max(0, v) * 50, this.audioCtx.currentTime, 0.01);
},
'LYRA GATE PROB': v => this.lyraGlobalGateProbability = Math.max(0, v),
'LYRA OSC1 TYPE': v => this.updateLyraParam("osc1Type", createWaveTypeSetter(v)),
'LYRA OSC2 TYPE': v => this.updateLyraParam("osc2Type", createWaveTypeSetter(v)),
};
this.modDestinationsSafe = {
'BD DECAY': v => this.params.kickDecay = .05 + ((v + 1) / 2) * .8,
'BD DRIVE': v => this.params.kickDrive = ((v + 1) / 2),
'BD PITCH': v => this.params.kickPitch = 50 + ((v + 1) / 2) * 50,
'HHT DECAY': v => this.params.hatDecay = .01 + ((v + 1) / 2) * .4,
'HHT METAL': v => this.params.hatMetal = 8000 + ((v + 1) / 2) * 7000,
'SD SNAPPY': v => this.params.snareSnappy = 5000 + ((v + 1) / 2) * 4000,
'SD TONE': v => this.params.snareTone = 220 + ((v + 1) / 2) * 200,
'SNARE DECAY': v => this.params.snareDecay = 0.05 + ((v + 1) / 2) * 0.35,
'BASS DETUNE': v => this.params.bassDetune = ((v + 1) / 2) * 50,
'BASS FILTER': v => this.params.bassFilterCutoff = 100 + ((v + 1) / 2) * 7900,
'BASS OCTAVE': v => this.params.bassOctave = Math.round(((v + 1) / 2) * 4 - 2), // Maps [0,1] to [-2,2]
'BASS RESO': v => this.params.bassFilterQ = ((v + 1) / 2) * 25,
'MASTER DIST': v => this.setDistortionCurve(((v + 1) / 2)),
'MASTER REVERB': v => this.reverbSend.gain.setTargetAtTime(((v + 1) / 2) * .7, this.audioCtx.currentTime, .01),
'LYRA DETUNE': v => this.updateLyraParam('detune', ((v + 1) / 2) * 50),
'LYRA DRIVE': v => this.updateLyraParam('drive', ((v + 1) / 2) * parseFloat(document.getElementById('lyraDrive').max)),
'LYRA FM AMT': v => this.updateLyraParam('fmAmount', ((v + 1) / 2) * 2000),
'LYRA FOLD': v => this.updateLyraParam('waveFold', ((v + 1) / 2)),
'LYRA OSC1 OCT': v => this.updateLyraParam('osc1Oct', Math.min(2, Math.max(-2, Math.round(((v + 1) / 2) * 4 - 2)))),
'LYRA OSC2 OCT': v => this.updateLyraParam('osc2Oct', Math.min(2, Math.max(-2, Math.round(((v + 1) / 2) * 4 - 2)))),
'LYRA FILTER': v => this.updateLyraParam('filterCutoff', 20 + ((v + 1) / 2) * 19980),
'LYRA FILTER ENV': v => this.updateLyraParam('filterEnv', v * 5000), // Bipolar is often desired for filter env
'LYRA RESO': v => this.updateLyraParam('filterQ', ((v + 1) / 2) * parseFloat(document.getElementById('lyraFilterQ').max)),
"LYRA ATTACK": v => this.updateLyraParam("attack", Math.max(0.001, Math.pow(((v + 1) / 2), 2)) * 0.5),
"LYRA DECAY": v => this.updateLyraParam("decay", Math.max(0.001, Math.pow(((v + 1) / 2), 2)) * 0.5),
"LYRA SUSTAIN": v => this.updateLyraParam("sustain", ((v + 1) / 2) * 0.8),
"LYRA RELEASE": v => this.updateLyraParam("release", Math.max(0.001, Math.pow(((v + 1) / 2), 2)) * 1),
'LYRA SLIDE': v => {
const basePortamento = parseFloat(document.getElementById('lyraPortamento').value) / 1000;
// The safe 'v' is already scaled to [0, 1]. We'll use it to add to the base time.
const modulationAmount = ((v + 1) / 2) * 0.2;
this.updateLyraParam('portamento', basePortamento + modulationAmount);
},
'LYRA LFO DEPTH': v => this.updateLyraParam('lfoDepth', ((v + 1) / 2) * 5000),
'LYRA LFO RATE': v => this.updateLyraParam('lfoRate', ((v + 1) / 2) * 20),
'LYRA DELAY FB': v => {
if (this.lyraFxNodes.delay) this.lyraFxNodes.delay.feedback.gain.setTargetAtTime(((v + 1) / 2) * 0.95, this.audioCtx.currentTime, 0.01)
},
'LYRA DELAY MIX': v => {
if (document.getElementById('lyraFxType').value === 'Delay') {
const mix = ((v + 1) / 2);
this.lyraFxNodes.wet.gain.setTargetAtTime(mix, this.audioCtx.currentTime, 0.01);
this.lyraFxNodes.dry.gain.setTargetAtTime(1.0 - mix, this.audioCtx.currentTime, 0.01);
}
},
'LYRA DELAY TIME': v => {
if (this.lyraFxNodes.delay) this.lyraFxNodes.delay.delay.delayTime.setTargetAtTime(Math.max(0.01, ((v + 1) / 2)), this.audioCtx.currentTime, 0.01)
}, // FIXED
'PHASER DEPTH': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.lfoGain.gain.setTargetAtTime(((v + 1) / 2) * 5000, this.audioCtx.currentTime, 0.01);
},
'PHASER FEEDBACK': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.feedback.gain.setTargetAtTime(((v + 1) / 2) * 0.9, this.audioCtx.currentTime, 0.01);
},
'PHASER RATE': v => {
if (this.lyraFxNodes.phaser) this.lyraFxNodes.phaser.lfo.frequency.setTargetAtTime(Math.max(0.1, ((v + 1) / 2)) * 10, this.audioCtx.currentTime, 0.01);
},
'FLANGER DEPTH': v => {
if (this.lyraFxNodes.flanger) this.lyraFxNodes.flanger.lfoGain.gain.setTargetAtTime(((v + 1) / 2) * 0.02, this.audioCtx.currentTime, 0.01);
},
'FLANGER RATE': v => {
if (this.lyraFxNodes.flanger) this.lyraFxNodes.flanger.lfo.frequency.setTargetAtTime(Math.max(0.05, ((v + 1) / 2)) * 5, this.audioCtx.currentTime, 0.01);
},
'CHOPPER DEPTH': v => {
if (this.lyraFxNodes.chopper) this.lyraFxNodes.chopper.lfoGain.gain.setTargetAtTime(((v + 1) / 2), this.audioCtx.currentTime, 0.01);
},
'CRUSHER BITDEPTH': v => {
if (this.lyraFxNodes.bitcrusher?.node) this.lyraFxNodes.bitcrusher.node.parameters.get('bitDepth').setTargetAtTime(1 + ((v + 1) / 2) * 15, this.audioCtx.currentTime, 0.01);
},
'CRUSHER DOWNSAMPLE': v => {
if (this.lyraFxNodes.bitcrusher?.node) this.lyraFxNodes.bitcrusher.node.parameters.get('frequencyReduction').setTargetAtTime(((v + 1) / 2), this.audioCtx.currentTime, 0.01);
},
'QUAZAR FREQ': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.lfoGain.gain.setTargetAtTime(((v + 1) / 2) * 8000, this.audioCtx.currentTime, 0.01);
},
'QUAZAR RATE': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.lfo.frequency.setTargetAtTime(Math.max(0.1, ((v + 1) / 2)) * 15, this.audioCtx.currentTime, 0.01);
},
'QUAZAR RESO': v => {
if (this.lyraFxNodes.quazar) this.lyraFxNodes.quazar.filter.Q.setTargetAtTime(((v + 1) / 2) * 50, this.audioCtx.currentTime, 0.01);
},
'LYRA GATE PROB': v => this.lyraGlobalGateProbability = ((v + 1) / 2),
'LYRA OSC1 TYPE': v => this.updateLyraParam("osc1Type", createWaveTypeSetter((v + 1) / 2)),
'LYRA OSC2 TYPE': v => this.updateLyraParam("osc2Type", createWaveTypeSetter((v + 1) / 2)),
};
this.isSafetyOn = false;
this.nextNoteTime = 0;
this.scheduleAheadTime = .1;
this.timerID = null;
this.scheduler = this.scheduler.bind(this);
this.updateModulation = this.updateModulation.bind(this);
this.scheduleLyraLoopNotes = this.scheduleLyraLoopNotes.bind(this);
}
async initFx() {
const bitcrusherProcessor = `class BitcrusherProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'bitDepth', defaultValue: 8, minValue: 1, maxValue: 16 }, { name: 'frequencyReduction', defaultValue: 0.5, minValue: 0, maxValue: 1 } ]; } constructor() { super(); this.phase = 0; this.lastSampleValue = 0; } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; const bitDepth = parameters.bitDepth; const freqRed = parameters.frequencyReduction; for (let channel = 0; channel < input.length; channel++) { const inputChannel = input[channel]; const outputChannel = output[channel]; for (let i = 0; i < inputChannel.length; i++) { const step = Math.pow(0.5, bitDepth.length > 1 ? bitDepth[i] : bitDepth[0]); this.phase += freqRed.length > 1 ? freqRed[i] : freqRed[0]; if (this.phase >= 1.0) { this.phase -= 1.0; this.lastSampleValue = step * Math.floor(inputChannel[i] / step + 0.5); } outputChannel[i] = this.lastSampleValue; } } return true; } } registerProcessor('bitcrusher-processor', BitcrusherProcessor);`;
const blob = new Blob([bitcrusherProcessor], {
type: 'application/javascript'
});
const url = URL.createObjectURL(blob);
try {
await this.audioCtx.audioWorklet.addModule(url);
URL.revokeObjectURL(url);
this.lyraFxNodes.bitcrusher = {
node: new AudioWorkletNode(this.audioCtx, 'bitcrusher-processor')
};
} catch (e) {
console.error("Error loading Bitcrusher worklet:", e);
const crusherOption = document.querySelector('#lyraFxType option[value="Bitcrusher"]');
if (crusherOption) {
crusherOption.textContent = "Digital Grime (Failed to load)";
crusherOption.disabled = true;
}
}
const fx = this.lyraFxNodes;
fx.wet = this.audioCtx.createGain();
fx.dry = this.audioCtx.createGain();
fx.wet.connect(this.lyraFxOut);
fx.dry.connect(this.lyraFxOut);
fx.delay = {
delay: this.audioCtx.createDelay(1.0),
feedback: this.audioCtx.createGain()
};
fx.delay.feedback.gain.value = 0.6;
fx.delay.delay.connect(fx.delay.feedback);
fx.delay.feedback.connect(fx.delay.delay);
fx.phaser = {
lfo: this.audioCtx.createOscillator(),
lfoGain: this.audioCtx.createGain(),
feedback: this.audioCtx.createGain(),
filters: [],
input: this.audioCtx.createGain()
};
let lastNode = fx.phaser.input;
for (let i = 0; i < 6; i++) {
const filter = this.audioCtx.createBiquadFilter();
filter.type = 'allpass';
filter.frequency.value = 1000;
fx.phaser.filters.push(filter);
lastNode.connect(filter);
lastNode = filter;
}
lastNode.connect(fx.phaser.feedback);
fx.phaser.feedback.connect(fx.phaser.input);
fx.phaser.lfo.connect(fx.phaser.lfoGain);
fx.phaser.filters.forEach(f => fx.phaser.lfoGain.connect(f.frequency));
fx.phaser.lfo.start();
fx.flanger = {
lfo: this.audioCtx.createOscillator(),
lfoGain: this.audioCtx.createGain(),
delay: this.audioCtx.createDelay(1.0),
feedback: this.audioCtx.createGain()
};
fx.flanger.delay.delayTime.value = 0.005;
fx.flanger.lfo.connect(fx.flanger.lfoGain);
fx.flanger.lfoGain.connect(fx.flanger.delay.delayTime);
fx.flanger.delay.connect(fx.flanger.feedback);
fx.flanger.feedback.connect(fx.flanger.delay);
fx.flanger.lfo.start();
fx.quazar = {
lfo: this.audioCtx.createOscillator(),
lfoGain: this.audioCtx.createGain(),
filter: this.audioCtx.createBiquadFilter()
};
fx.quazar.filter.type = 'bandpass';
fx.quazar.lfo.connect(fx.quazar.lfoGain);
fx.quazar.lfoGain.connect(fx.quazar.filter.frequency);
fx.quazar.lfo.start();
fx.chopper = {
lfo: this.audioCtx.createOscillator(),
lfoGain: this.audioCtx.createGain(),
node: this.audioCtx.createGain(),
};
fx.chopper.lfo.type = 'square';
fx.chopper.lfo.connect(fx.chopper.lfoGain);
fx.chopper.lfoGain.connect(fx.chopper.node.gain);
fx.chopper.lfo.start();
for (let i = 0; i < this.numLyraVoices; i++) {
const voice = {
isActive: false,
noteFrequency: 0,
osc1: this.audioCtx.createOscillator(),
osc2: this.audioCtx.createOscillator(),
fmGain: this.audioCtx.createGain(),
waveShaper: this.audioCtx.createWaveShaper(),
drive: this.audioCtx.createWaveShaper(),
filter: this.audioCtx.createBiquadFilter(),
env: this.audioCtx.createGain(),
filterEnv: this.audioCtx.createGain()
};
voice.osc1.connect(voice.fmGain);
voice.fmGain.connect(voice.osc2.frequency);
voice.osc1.connect(voice.waveShaper);
voice.osc2.connect(voice.waveShaper);
voice.waveShaper.connect(voice.drive);
voice.drive.connect(voice.filter);
voice.filter.connect(voice.env);
this.lyraLfoGain.connect(voice.filter.frequency);
voice.env.connect(voice.filterEnv);
voice.filterEnv.connect(voice.filter.frequency);
voice.env.connect(this.lyraFxIn);
voice.env.gain.value = 0;
voice.osc1.start();
voice.osc2.start();
this.lyraVoices.push(voice);
}
this.updateLyraParam('all');
this.switchLyraFx(document.getElementById('lyraFxType').value);
}
switchLyraFx(fxType) {
const fx = this.lyraFxNodes;
const now = this.audioCtx.currentTime;
this.lyraFxIn.disconnect();
fx.delay.delay.disconnect();
fx.phaser.filters[5].disconnect();
fx.flanger.delay.disconnect();
fx.quazar.filter.disconnect();
if (fx.bitcrusher?.node) fx.bitcrusher.node.disconnect();
fx.chopper.node.disconnect();
fx.delay.delay.connect(fx.delay.feedback);
fx.phaser.filters[5].connect(fx.phaser.feedback);
fx.flanger.delay.connect(fx.flanger.feedback);
if (fxType === 'Chopper') {
fx.wet.gain.setTargetAtTime(0, now, 0.01);
fx.dry.gain.setTargetAtTime(0, now, 0.01);
this.lyraFxIn.connect(fx.chopper.node);
fx.chopper.node.connect(this.lyraFxOut);
return;
}
let mixValue = 0.5;
const mixSliderIdMap = {
'Delay': 'lyraDelayMix',
'Phaser': 'phaserMix',
'Flanger': 'flangerMix',
'Quazar': 'quazarMix',
'Bitcrusher': 'crusherMix',
};
const mixSlider = document.getElementById(mixSliderIdMap[fxType]);
if (mixSlider) {
mixValue = parseFloat(mixSlider.value);
}
fx.wet.gain.setTargetAtTime(mixValue, now, 0.01);
fx.dry.gain.setTargetAtTime(1.0 - mixValue, now, 0.01);
this.lyraFxIn.connect(fx.dry);
switch (fxType) {
case 'Delay':
this.lyraFxIn.connect(fx.delay.delay);
fx.delay.delay.connect(fx.wet);
break;
case 'Phaser':
this.lyraFxIn.connect(fx.phaser.input);
fx.phaser.filters[5].connect(fx.wet);
break;
case 'Flanger':
this.lyraFxIn.connect(fx.flanger.delay);
fx.flanger.delay.connect(fx.wet);
break;
case 'Quazar':
this.lyraFxIn.connect(fx.quazar.filter);
fx.quazar.filter.connect(fx.wet);
break;
case 'Bitcrusher':
if (fx.bitcrusher?.node) {
this.lyraFxIn.connect(fx.bitcrusher.node);
fx.bitcrusher.node.connect(fx.wet);
}
break;
}
}
setParam(t, n, v) {
if (t === 'pulsar') this.params[n] = parseFloat(v);
}
updateDriveCurve(driveNode, mode, amount) {
const n_samples = 44100;
const curve = new Float32Array(n_samples);
const foldAmount = 1 + amount * 10;
const expAmount = 1 + amount * 10;
for (let i = 0; i < n_samples; i++) {
let x = i * 2 / n_samples - 1;
let y;
switch (mode) {
case 'soft clip':
const k = amount * 100;
y = (Math.PI + k) * x / (Math.PI + k * Math.abs(x));
break;
case 'hard clip':
const driveAmount = 1 + amount * 49;
const clipped = x * driveAmount;
y = Math.max(-1, Math.min(1, clipped));
break;
case 'exponential':
const expInput = x * expAmount;
y = expInput * Math.exp(-Math.abs(expInput));
y *= Math.E; // Scale output to be closer to [-1, 1]
break;
case 'wavefold':
let val = x * foldAmount;
// Use a limit to prevent infinite loops with extreme values if they ever occur
for (let j = 0; j < 10; j++) {
if (val > 1) {
val = 2 - val;
} else if (val < -1) {
val = -2 - val;
} else {
break;
}
}
y = val;
break;
default:
y = x;
}
curve[i] = y;
}
driveNode.curve = curve;
}
updateLyraParam(paramName, value) {
if (paramName !== 'all' && value !== undefined) {
this.lyraParams[paramName] = value;
}
const p = this.lyraParams;
const now = this.audioCtx.currentTime;
let lfoFrequency;
if (p.lfoIsSynced) {
const noteDuration = 60.0 / this.bpm;
lfoFrequency = (1 / noteDuration) * p.lfoSyncRate;
} else {
lfoFrequency = p.lfoRate;
}
this.lyraLfoNode.frequency.setTargetAtTime(lfoFrequency, now, 0.01);
this.lyraLfoGain.gain.setTargetAtTime(p.lfoDepth, now, 0.01);
this.lyraVoices.forEach(voice => {
voice.osc1.type = p.osc1Type;
voice.osc2.type = p.osc2Type;
voice.fmGain.gain.setTargetAtTime(p.fmAmount, now, 0.01);
this.updateDriveCurve(voice.drive, p.driveMode, p.drive);
voice.filter.type = p.filterType;
voice.filter.Q.setTargetAtTime(p.filterQ, now, 0.01);
voice.filter.frequency.setTargetAtTime(p.filterCutoff, now, 0.01);
voice.filterEnv.gain.value = p.filterEnv;
const foldAmount = p.waveFold * 25;
if (foldAmount === 0) {
voice.waveShaper.curve = null;
} else {
const n_samples = 44100;
const curve = new Float32Array(n_samples);
for (let i = 0; i < n_samples; i++) {
const x = i * 2 / n_samples - 1;
curve[i] = Math.tanh(x * foldAmount);
}
voice.waveShaper.curve = curve;
}
});
}
getModSources() {
return Object.keys(this.modSources);
}
getModDestinations() {
return Object.keys(this.modDestinationsSafe); // Return safe version as the canonical list
}
addPatch(s, d, a) {
this.patches.push({
id: crypto.randomUUID(),
source: s,
dest: d,
amount: a,
isLocked: false,
lockedValue: 0,
modRangeMin: -1.0,
modRangeMax: 1.0
});
this.renderPatchesUI();
}
removePatch(id) {
const initialLength = this.patches.length;
this.patches = this.patches.filter(p => p.id !== id);
return this.patches.length < initialLength;
}
openPatchEditor(patchId) {
const modal = document.getElementById('patchEditModal');
const patch = this.patches.find(p => p.id === patchId);
if (!patch) return;
const title = document.getElementById('patchEditTitle');
const amountSlider = document.getElementById('patchEditAmount');
const amountValue = document.getElementById('patchEditAmountValue');
const minSlider = document.getElementById('patchEditMin');
const minValue = document.getElementById('patchEditMinValue');
const maxSlider = document.getElementById('patchEditMax');
const maxValue = document.getElementById('patchEditMaxValue');
const applyBtn = document.getElementById('applyPatchEditButton');
const closeBtn = document.getElementById('closePatchEditModalButton');
title.textContent = `EDIT: ${patch.source} → ${patch.dest}`;
amountSlider.value = patch.amount;
amountValue.textContent = parseFloat(patch.amount).toFixed(2);
minSlider.value = patch.modRangeMin;
minValue.textContent = parseFloat(patch.modRangeMin).toFixed(2);
maxSlider.value = patch.modRangeMax;
maxValue.textContent = parseFloat(patch.modRangeMax).toFixed(2);
amountSlider.oninput = () => amountValue.textContent = parseFloat(amountSlider.value).toFixed(2);
minSlider.oninput = () => minValue.textContent = parseFloat(minSlider.value).toFixed(2);
maxSlider.oninput = () => maxValue.textContent = parseFloat(maxSlider.value).toFixed(2);
applyBtn.onclick = () => {
patch.amount = parseFloat(amountSlider.value);
patch.modRangeMin = parseFloat(minSlider.value);
patch.modRangeMax = parseFloat(maxSlider.value);
modal.style.display = 'none';
this.renderPatchesUI();
};
closeBtn.onclick = () => {
modal.style.display = 'none';
};
modal.style.display = 'flex';
}
renderPatchesUI() {
const c = document.getElementById('activePatches');
c.innerHTML = '';
this.patches.forEach(p => {
const e = document.createElement('div');
e.className = 'patch-instance';
e.dataset.patchId = p.id; // Store ID for easier lookup
const textSpan = document.createElement('span');
textSpan.textContent = `${p.source} → ${p.dest} (${p.amount})`;
e.appendChild(textSpan);
const b = document.createElement('button');
b.textContent = 'X';
e.appendChild(b);
c.appendChild(e);
});
}
createVoiceAnalyzer() {
const a = this.audioCtx.createAnalyser();
a.fftSize = 32;
a.smoothingTimeConstant = .5;
return {
analyzer: a,
data: new Uint8Array(a.frequencyBinCount)
};
}
setDistortionCurve(a) {
const k = a * 100,
n = 22050,
c = new Float32Array(n);
for (let i = 0; i < n; ++i) {
const x = i * 2 / n - 1;
c[i] = (Math.PI + k) * x / (Math.PI + k * Math.abs(x));
}
this.masterDistortion.curve = c;
}
createReverb(d) {
const c = this.audioCtx.createConvolver(),
l = this.audioCtx.sampleRate * d,
i = this.audioCtx.createBuffer(2, l, this.audioCtx.sampleRate);
for (let ch = 0; ch < 2; ch++) {
const dt = i.getChannelData(ch);
for (let j = 0; j < l; j++) dt[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / l, 3);
}
c.buffer = i;
return c;
}
getAudioData() {
this.analyzer.getByteFrequencyData(this.dataArray);
return {
averageFrequency: this.dataArray.reduce((s, v) => s + v, 0) / this.dataArray.length
};
}
updateTrackGains() {
const anySolo = Object.values(this.trackState).some(t => t.soloed);
for (const track of Object.values(this.trackState)) {
const isMuted = track.muted;
const isSoloed = track.soloed;
if (anySolo) {
track.gain.gain.setTargetAtTime(isSoloed ? 1.0 : 0.0, this.audioCtx.currentTime, 0.01);
} else {
track.gain.gain.setTargetAtTime(isMuted ? 0.0 : 1.0, this.audioCtx.currentTime, 0.01);
}
}
}
playKick(t, velocity = 1.0) {
const g = this.audioCtx.createGain();
g.connect(this.kickGain);
g.connect(this.modSources['BD ENV'].analyzer.analyzer);
const o = this.audioCtx.createOscillator(),
e = this.audioCtx.createGain(),
d = this.audioCtx.createWaveShaper();
this.setDistortionCurve.call({
masterDistortion: d
}, this.params.kickDrive);
o.connect(d);
d.connect(e);
e.connect(g);
o.frequency.setValueAtTime(this.params.kickPitch * 2, t);
o.frequency.exponentialRampToValueAtTime(this.params.kickPitch, t + .05);
e.gain.setValueAtTime(2.5 * velocity, t);
e.gain.exponentialRampToValueAtTime(.001, t + this.params.kickDecay);
o.start(t);
o.stop(t + this.params.kickDecay);
}
playSnare(t, velocity = 1.0) {
const g = this.audioCtx.createGain();
g.connect(this.snareGain);
g.connect(this.modSources['SD ENV'].analyzer.analyzer);
const n = this.audioCtx.createBufferSource(),
b = this.audioCtx.createBuffer(1, this.audioCtx.sampleRate * .2, this.audioCtx.sampleRate),
d = b.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
n.buffer = b;
const f = this.audioCtx.createBiquadFilter();
f.type = 'highpass';
f.frequency.value = this.params.snareSnappy;
const ne = this.audioCtx.createGain();
n.connect(f);
f.connect(ne);
ne.connect(g);
const o = this.audioCtx.createOscillator();
o.type = 'triangle';
o.frequency.value = this.params.snareTone;
const oe = this.audioCtx.createGain();
o.connect(oe);
oe.connect(g);
ne.gain.setValueAtTime(1.5 * velocity, t);
ne.gain.exponentialRampToValueAtTime(.01, t + this.params.snareDecay);
oe.gain.setValueAtTime(0.7 * velocity, t);
oe.gain.exponentialRampToValueAtTime(.01, t + this.params.snareDecay * .5);
n.start(t);
n.stop(t + this.params.snareDecay);
o.start(t);
o.stop(t + this.params.snareDecay * .5);
}
playHat(t, velocity = 1.0) {
const r = [2, 3, 4.16, 5.43, 6.79, 8.21],
g = this.audioCtx.createGain();
g.gain.value = velocity;
g.connect(this.hatGain);
r.forEach(x => {
const o = this.audioCtx.createOscillator();
o.type = 'square';
o.frequency.value = this.params.hatMetal * x;
const e = this.audioCtx.createGain();
o.connect(e);
e.connect(g);
e.gain.setValueAtTime(1, t);
e.gain.exponentialRampToValueAtTime(.001, t + this.params.hatDecay);
o.start(t);
o.stop(t + this.params.hatDecay);
});
}
playBass(t, velocity = 1.0, stepIndex, meta = {}) {
const isWobbleStep = (meta.ratchet || 1) > 1;
const isAccent = meta.accent === true;
const prevStepIndex = (stepIndex - 1 + this.sequenceLengths.bass) % this.sequenceLengths.bass;
const prevStepIsActive = this.sequences.bass[prevStepIndex];
const prevStepMeta = this.stepMeta.bass[prevStepIndex] || {};
const prevStepWasWobble = prevStepIsActive && (prevStepMeta.ratchet || 1) > 1;
const o1 = this.audioCtx.createOscillator(),
o2 = this.audioCtx.createOscillator(),
e = this.audioCtx.createGain(),
f = this.audioCtx.createBiquadFilter();
f.type = 'lowpass';
const octaveOffset = isAccent ? -1 : 0;
const bf = 55 * Math.pow(2, this.params.bassOctave + octaveOffset);
o1.frequency.value = bf;
o2.frequency.value = bf;
o1.type = 'sine';
o2.type = 'sine';
o1.detune.value = -this.params.bassDetune;
o2.detune.value = this.params.bassDetune;
o1.connect(f);
o2.connect(f);
f.connect(e);
e.connect(this.bassGain);
if (isWobbleStep) {
f.Q.value = 20;
if (!prevStepWasWobble) {
if (this.bassLfo && this.bassLfo.lfo) {
this.bassLfo.lfo.stop(t);
}
const newLfo = this.audioCtx.createOscillator();
newLfo.type = 'sine';
const newLfoGain = this.audioCtx.createGain();
this.bassLfo = {
lfo: newLfo,
lfoGain: newLfoGain
};
this.bassLfo.lfo.connect(this.bassLfo.lfoGain);
this.bassLfo.lfoGain.connect(f.frequency);
this.bassLfo.lfo.start(t);
}
if (this.bassLfo) {
const clampedRatchet = Math.min(meta.ratchet, 4);
const lfoBaseRate = 2;
const lfoSpeed = lfoBaseRate * clampedRatchet;
this.bassLfo.lfo.frequency.setTargetAtTime(lfoSpeed, t, 0.01);
const filterBaseFreq = 100;
f.frequency.setValueAtTime(filterBaseFreq, t);
const lfoDepth = Math.max(0, this.params.bassFilterCutoff - filterBaseFreq);
this.bassLfo.lfoGain.gain.setTargetAtTime(lfoDepth, t, 0.01);
}
} else {
f.Q.value = this.params.bassFilterQ;
f.frequency.value = this.params.bassFilterCutoff;
if (prevStepWasWobble) {
if (this.bassLfo && this.bassLfo.lfo) {
this.bassLfo.lfo.stop(t);
this.bassLfo = null;
}
}
}
const singleStepDur = (60 / this.bpm) / 4;
let dur;
if (isWobbleStep && meta.ratchet >= 3) {
dur = singleStepDur * 2;
} else {
dur = singleStepDur;
}
if (isWobbleStep) {
e.gain.setValueAtTime(0, t);
e.gain.linearRampToValueAtTime(0.4 * velocity, t + 0.01);
e.gain.setValueAtTime(0.4 * velocity, t + dur * 0.98);
e.gain.linearRampToValueAtTime(0, t + dur);
} else {
e.gain.setValueAtTime(2.5, t);
e.gain.linearRampToValueAtTime(.4 * velocity, t + .01);
e.gain.setValueAtTime(.4 * velocity, t + dur * .9);
e.gain.linearRampToValueAtTime(0, t + dur);
}
o1.start(t);
o2.start(t);
o1.stop(t + dur);
o2.stop(t + dur);
}
playLyraNote(freq, time, accent = false, duration = null) {
this.lyraVoiceIndex = (this.lyraVoiceIndex + 1) % this.numLyraVoices;
const voice = this.lyraVoices[this.lyraVoiceIndex];
const p = this.lyraParams;
const now = time || this.audioCtx.currentTime;
const velocity = accent ? 1.5 : 1.0;
voice.isActive = true;
voice.noteFrequency = freq;
voice.osc1.frequency.setTargetAtTime(freq * Math.pow(2, p.osc1Oct), now, 0.001);
voice.osc1.detune.setTargetAtTime(-p.detune, now, 0.001);
voice.osc2.frequency.setTargetAtTime(freq * Math.pow(2, p.osc2Oct), now, 0.001);
voice.osc2.detune.setTargetAtTime(p.detune, now, 0.001);
voice.env.gain.cancelScheduledValues(now);
voice.env.gain.setTargetAtTime(velocity, now, parseFloat(p.attack) || 0.01);
voice.env.gain.setTargetAtTime(parseFloat(p.sustain) * velocity, now + parseFloat(p.attack), parseFloat(p.decay) || 0.01);
if (this.lyraSequencer.forceGate && duration) {
const gateReleaseTime = 0.01;
voice.env.gain.setTargetAtTime(0, now + duration, gateReleaseTime);
}
this.modSources['LYRA ENV'].value = voice.env.gain.value;
}
slideLyraNote(fromFreq, toFreq, time, accent = false, duration = null) {
let voice = this.lyraVoices.find(v => v.isActive && Math.abs(v.noteFrequency - fromFreq) < 1);
if (!voice) {
this.playLyraNote(toFreq, time, accent, duration);
return;
}
const p = this.lyraParams;
const now = time || this.audioCtx.currentTime;
const slideTime = p.portamento;
const velocity = accent ? 1.5 : 1.0;
voice.noteFrequency = toFreq;
voice.osc1.frequency.setTargetAtTime(toFreq * Math.pow(2, p.osc1Oct), now, slideTime);
voice.osc2.frequency.setTargetAtTime(toFreq * Math.pow(2, p.osc2Oct), now, slideTime);
voice.osc1.detune.setTargetAtTime(-p.detune, now, 0.001);
voice.osc2.detune.setTargetAtTime(p.detune, now, 0.001);
voice.env.gain.cancelScheduledValues(now);
if (accent) {
voice.env.gain.setTargetAtTime(velocity, now, parseFloat(p.attack) || 0.01);
voice.env.gain.setTargetAtTime(parseFloat(p.sustain) * velocity, now + parseFloat(p.attack), parseFloat(p.decay) || 0.01);
} else {
voice.env.gain.setTargetAtTime(parseFloat(p.sustain) * velocity, now, 0.001);
}
if (this.lyraSequencer.forceGate && duration) {
const gateReleaseTime = 0.01;
voice.env.gain.setTargetAtTime(0, now + duration, gateReleaseTime);
}
}
playLyraSequencerStep(stepIndex, time) {
const currentStepEl = document.getElementById(`lyraStep${stepIndex}`);
if (currentStepEl) {
const prevStepIndex = (stepIndex === 0) ? this.lyraSequencer.stepLength - 1 : stepIndex - 1;
const prevStepEl = document.getElementById(`lyraStep${prevStepIndex}`);
if (prevStepEl) prevStepEl.classList.remove('current');
currentStepEl.classList.add('current');
}
const lyraGateLength = this.sequenceLengths.lyraGate;
const currentGateStep = this.masterCycleStep % lyraGateLength;
const stepIsGated = this.sequences.lyraGate[currentGateStep];
const probabilityCheck = Math.random() < this.lyraGlobalGateProbability;
if (!stepIsGated || !probabilityCheck) {
return;
}
const step = this.lyraSequencer.steps[stepIndex];
const baseStepDuration = 15.0 / this.bpm;
const gateDuration = step.gate ?? 1.0;
const noteDuration = !step.slide ? (baseStepDuration * gateDuration) : null;
if (!step.active) {
const prevStepIndex = (stepIndex > 0) ? stepIndex - 1 : this.lyraSequencer.stepLength - 1;
const prevStep = this.lyraSequencer.steps[prevStepIndex];
if (prevStep.active && prevStep.slide && this.lyraSequencer.lastPlayedFreq > 0) {
this.stopLyraNote(this.lyraSequencer.lastPlayedFreq, time);
}
return;
}
const midiNote = this.lyraSequencer.baseNote + step.note;
const freq = 440 * Math.pow(2, (midiNote - 69) / 12);
if (step.slide && this.lyraSequencer.lastPlayedFreq > 0) {
this.slideLyraNote(this.lyraSequencer.lastPlayedFreq, freq, time, step.accent, noteDuration);
} else {
this.playLyraNote(freq, time, step.accent, noteDuration);
}
this.lyraSequencer.lastPlayedFreq = freq;
}
stopLyraNote(freq, time) {
const p = this.lyraParams;
const now = time || this.audioCtx.currentTime;
const releaseTime = parseFloat(p.release) || 0.01;
this.lyraVoices.forEach(voice => {
if (voice.isActive && Math.abs(voice.noteFrequency - freq) < 1) {
voice.env.gain.cancelScheduledValues(now);
voice.env.gain.setTargetAtTime(0, now, releaseTime);
voice.isActive = false;
voice.noteFrequency = 0;
}
});
}
stopAllLyraNotes() {
const now = this.audioCtx.currentTime;
this.lyraVoices.forEach(voice => {
if (voice.isActive) {
voice.env.gain.cancelScheduledValues(now);
voice.env.gain.setTargetAtTime(0, now, 0.02);
voice.isActive = false;
voice.noteFrequency = 0;
}
});
}
handleLyraKey(code, isDown) {
if (this.audioCtx.state === "suspended") this.audioCtx.resume();
const keyMap = {
'KeyA': 0, 'KeyS': 2, 'KeyD': 4, 'KeyF': 5, 'KeyG': 7, 'KeyH': 9, 'KeyJ': 11, 'KeyK': 12, 'KeyL': 14,
'KeyW': 1, 'KeyE': 3, 'KeyT': 6, 'KeyY': 8, 'KeyU': 10, 'KeyO': 13,
};
const noteNum = keyMap[code];
if (noteNum === undefined) return;
const freq = 130.81 * Math.pow(2, noteNum / 12);
if (isDown) {
this.playLyraNote(freq);
} else {
this.stopLyraNote(freq);
}
if (this.lyraLooper.isRecording) {
const eventTime = this.audioCtx.currentTime - this.lyraLooper.loopStartTime;
this.lyraLooper.notes.push({
type: isDown ? 'on' : 'off',
freq: freq,
time: eventTime
});
this.lyraLooper.loopDuration = Math.max(this.lyraLooper.loopDuration, eventTime);
}
}
toggleLyraRec() {
const l = this.lyraLooper;
l.isRecording = !l.isRecording;
const recBtn = document.getElementById('lyraRecButton'),
playBtn = document.getElementById('lyraPlayButton');
if (l.isRecording) {
if (l.isPlaying) this.toggleLyraPlay();
l.loopStartTime = this.audioCtx.currentTime;
playBtn.disabled = true;
} else {
if (l.notes.length > 0) {
l.loopDuration += 0.1;
l.notes.sort((a, b) => a.time - b.time);
}
playBtn.disabled = false;
}
recBtn.classList.toggle('recording', l.isRecording);
recBtn.textContent = l.isRecording ? 'STOP REC' : 'REC';
}
wipeLyraLoop() {
if (this.lyraLooper.isPlaying) this.toggleLyraPlay();
if (this.lyraLooper.isRecording) this.toggleLyraRec();
this.lyraLooper.notes = [];
this.lyraLooper.loopDuration = 0;
}
toggleLyraPlay() {
const l = this.lyraLooper;
if (l.isRecording) return;
l.isPlaying = !l.isPlaying;
const playBtn = document.getElementById('lyraPlayButton');
if (l.isPlaying) {
if (l.notes.length === 0 || l.loopDuration <= 0) {
l.isPlaying = false;
return;
}
l.nextNoteIndex = 0;
l.loopStartTime = this.audioCtx.currentTime;
this.lyraLooperTimerID = setInterval(this.scheduleLyraLoopNotes, 25);
} else {
if (this.lyraLooperTimerID) clearInterval(this.lyraLooperTimerID);
this.lyraLooperTimerID = null;
this.stopAllLyraNotes();
}
playBtn.classList.toggle('playing', l.isPlaying);
playBtn.textContent = l.isPlaying ? 'STOP' : 'PLAY';
}
scheduleLyraLoopNotes() {
if (!this.lyraLooper.isPlaying) return;
const lookahead = 0.1;
const loop = this.lyraLooper;
let scheduleUntil = this.audioCtx.currentTime + lookahead;
while (true) {
if (loop.nextNoteIndex >= loop.notes.length) {
loop.nextNoteIndex = 0;
loop.loopStartTime += loop.loopDuration;
}
const event = loop.notes[loop.nextNoteIndex];
const eventTime = loop.loopStartTime + event.time;
if (eventTime < scheduleUntil) {
if (event.type === 'on') {
this.playLyraNote(event.freq, eventTime);
} else {
this.stopLyraNote(event.freq, eventTime);
}
loop.nextNoteIndex++;
} else {
break;
}
}
}
resetAllModulatedParamsToRaw() {
const destinationToElementIdMap = {
'BD DECAY': 'kickDecay', 'BD DRIVE': 'kickDrive', 'BD PITCH': 'kickPitch',
'HHT DECAY': 'hatDecay', 'HHT METAL': 'hatMetal',
'SD SNAPPY': 'snareSnappy', 'SD TONE': 'snareTone', 'SNARE DECAY': 'snareDecay',
'BASS DETUNE': 'bassDetune', 'BASS FILTER': 'bassFilterCutoff', 'BASS OCTAVE': 'bassOctave', 'BASS RESO': 'bassFilterQ',
'MASTER DIST': 'masterDist', 'MASTER REVERB': 'masterReverb',
'LYRA DETUNE': 'lyraDetune', 'LYRA DRIVE': 'lyraDrive', 'LYRA FM AMT': 'lyraFMAmt', 'LYRA FOLD': 'lyraWaveFold',
'LYRA OSC1 OCT': 'lyraOsc1Oct', 'LYRA OSC2 OCT': 'lyraOsc2Oct',
'LYRA FILTER': 'lyraFilterCutoff', 'LYRA FILTER ENV': 'lyraFilterEnv', 'LYRA RESO': 'lyraFilterQ',
"LYRA ATTACK": 'lyraAttack', "LYRA DECAY": 'lyraDecay', "LYRA SUSTAIN": 'lyraSustain', "LYRA RELEASE": 'lyraRelease',
'LYRA SLIDE': 'lyraPortamento', 'LYRA LFO DEPTH': 'lyraLfoDepth', 'LYRA LFO RATE': 'lyraLfoRate',
'LYRA DELAY FB': 'lyraDelayFeedback', 'LYRA DELAY MIX': 'lyraDelayMix', 'LYRA DELAY TIME': 'lyraDelayTime',
'PHASER DEPTH': 'phaserDepth', 'PHASER FEEDBACK': 'phaserFeedback', 'PHASER RATE': 'phaserRate', 'PHASER MIX': 'phaserMix',
'FLANGER DEPTH': 'flangerDepth', 'FLANGER RATE': 'flangerRate', 'FLANGER FEEDBACK': 'flangerFeedback', 'FLANGER MIX': 'flangerMix',
'QUAZAR FREQ': 'quazarFreq', 'QUAZAR RATE': 'quazarRate', 'QUAZAR RESO': 'quazarReso', 'QUAZAR MIX': 'quazarMix',
'CHOPPER DEPTH': 'chopperDepth', 'CRUSHER BITDEPTH': 'crusherBitDepth', 'CRUSHER DOWNSAMPLE': 'crusherFreq', 'CRUSHER MIX': 'crusherMix'
};
for (const destName in destinationToElementIdMap) {
const elementId = destinationToElementIdMap[destName];
const element = document.getElementById(elementId);
if (element) {
element.dispatchEvent(new Event('input', { bubbles: true }));
if (element.tagName === 'SELECT') {
element.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}
}
updateModulation() {
if (this.isMuted || this.modsFrozen || (!this.isPlaying && !this.lyraVoices.some(v => v.isActive) && !this.lyraSequencer.isPlaying)) {
requestAnimationFrame(this.updateModulation);
return;
}
const lfo1 = this.modSources['LFO 1 (Slow)']; lfo1.phase += 0.05 * (this.bpm / 120); lfo1.value = Math.sin(lfo1.phase * 0.1);
const lfo2 = this.modSources['LFO 2 (Fast)']; lfo2.phase += 0.2 * (this.bpm / 120); lfo2.value = Math.sin(lfo2.phase);
const lfo3 = this.modSources['LFO 1 (Slower)']; lfo3.phase += lfo3.rate * (this.bpm / 120); lfo3.value = Math.sin(lfo3.phase);
const lfo4 = this.modSources['LFO 2 (Faster)']; lfo4.phase += lfo4.rate * (this.bpm / 120); lfo4.value = Math.sin(lfo4.phase);
this.modSources['LFO MIX'].value = (lfo1.value * 0.1 + lfo2.value * 0.2 + lfo3.value * 0.4 + lfo4.value * 0.3);
this.modSources['S&H (Random)'].value = Math.random() * 2 - 1;
for (let i = 1; i <= 5; i++) {
const p = Math.pow(2, i);
this.modSources[`CLK/${p}`].value = (this.masterCycleStep % p < p / 2) ? 1 : -1;
}
for (const k of ['BD ENV', 'SD ENV']) {
const s = this.modSources[k];
s.analyzer.analyzer.getByteTimeDomainData(s.analyzer.data);
s.value = s.analyzer.data.reduce((a, v) => a + Math.abs(v - 128), 0) / s.analyzer.data.length / 128;
}
this.modSources['LYRA LFO'].value = Math.sin(this.audioCtx.currentTime * this.lyraLfoNode.frequency.value * 2 * Math.PI);
const activeModDestinations = this.isSafetyOn ? this.modDestinationsSafe : this.modDestinationsUnsafe;
const scale = this.globalModScale;
this.patches.forEach(p => {
const s = this.modSources[p.source];
const d = activeModDestinations[p.dest];
if (s && d) {
let sourceValue = p.isLocked ? p.lockedValue : s.value;
const normalizedValue = (sourceValue + 1) / 2;
const scaledValue = normalizedValue * (p.modRangeMax - p.modRangeMin) + p.modRangeMin;
d(scaledValue * p.amount * scale);
}
});
requestAnimationFrame(this.updateModulation);
}
scheduler() {
if (!this.isPlaying) return;
while (this.nextNoteTime < this.audioCtx.currentTime + this.scheduleAheadTime) {
const masterStep = this.masterCycleStep;
const currentPass = (masterStep < 32) ? 'first' : 'second';
const randomSourceValue = this.modSources['S&H (Random)'].value;
if (masterStep % 2 === 0) this.modSources['S&H (2 steps)'].value = randomSourceValue;
if (masterStep % 4 === 0) this.modSources['S&H (4 steps)'].value = randomSourceValue;
if (masterStep % 8 === 0) this.modSources['S&H (8 steps)'].value = randomSourceValue;
if (masterStep % 16 === 0) this.modSources['S&H (16 steps)'].value = randomSourceValue;
if (masterStep % 32 === 0) this.modSources['S&H (32 steps)'].value = randomSourceValue;
if (masterStep % 64 === 0) this.modSources['S&H (64 steps)'].value = randomSourceValue;
const noteTime = this.nextNoteTime;
const stepDuration = 15.0 / this.bpm;
['kick', 'snare', 'hat', 'bass'].forEach(type => {
const trackLength = this.sequenceLengths[type];
const currentStepInPattern = masterStep % trackLength;
if (this.sequences[type][currentStepInPattern]) {
const meta = (this.stepMeta[type] && this.stepMeta[type][currentStepInPattern]) ? this.stepMeta[type][currentStepInPattern] : {
velocity: 1,
prob: 1,
ratchet: 1
};
const condition = meta.triggerPass || 'always';
if (condition === 'always' || condition === currentPass) {
if (Math.random() < (meta.prob ?? 1.0)) {
const ratchetCount = (type === 'bass' && meta.ratchet > 1) ? 1 : Math.max(1, meta.ratchet ?? 1);
const ratchetInterval = stepDuration / ratchetCount;
let swingDelay = 0;
if (type === 'hat' && this.swing > 0 && currentStepInPattern % 2 !== 0) {
swingDelay = stepDuration * this.swing;
}
for (let i = 0; i < ratchetCount; i++) {
const rachetTime = noteTime + (i * ratchetInterval) + swingDelay;
const velocity = meta.velocity ?? 1.0;
switch (type) {
case 'kick':
this.playKick(rachetTime, velocity);
break;
case 'snare':
this.playSnare(rachetTime, velocity);
break;
case 'hat':
this.playHat(rachetTime, velocity);
break;
case 'bass':
this.playBass(rachetTime, velocity, currentStepInPattern, meta);
break;
}
}
}
}
}
});
if (this.lyraSequencer.isPlaying) {
const lyraStep = masterStep % this.lyraSequencer.stepLength;
const lyraGateLength = this.sequenceLengths.lyraGate;
const currentGateStep = masterStep % lyraGateLength;
const lyraGateMeta = (this.stepMeta.lyraGate && this.stepMeta.lyraGate[currentGateStep]) || {};
const lyraGateCondition = lyraGateMeta.triggerPass || 'always';
if (lyraGateCondition === 'always' || lyraGateCondition === currentPass) {
this.playLyraSequencerStep(lyraStep, noteTime);
}
}
this.nextNoteTime += stepDuration;
this.masterCycleStep = (this.masterCycleStep + 1) % 64;
}
this.updateLooperDisplay();
this.timerID = setTimeout(this.scheduler, 25);
}
nudgePattern(type, direction) {
const seq = this.sequences[type];
const meta = this.stepMeta[type];
const len = 32;
if (direction > 0) {
seq.unshift(seq.pop());
} else {
seq.push(seq.shift());
}
const newMeta = {};
for (const key in meta) {
let newKey = (parseInt(key) + direction + len) % len;
newMeta[newKey] = meta[key];
}
this.stepMeta[type] = newMeta;
this.updatePatternEditor();
}
duplicatePattern(type, factor) {
const seq = this.sequences[type];
const meta = this.stepMeta[type];
const segmentLength = 32 / factor;
const seqSegment = seq.slice(0, segmentLength);
const metaSegment = {};
for (let i = 0; i < segmentLength; i++) {
if (meta[i]) {
metaSegment[i] = meta[i];
}
}
for (let i = 0; i < 32; i++) {
const sourceIndex = i % segmentLength;
seq[i] = seqSegment[sourceIndex];
if (metaSegment[sourceIndex]) {
meta[i] = JSON.parse(JSON.stringify(metaSegment[sourceIndex]));
} else {
delete meta[i];
}
}
this.updatePatternEditor();
}
toggleLyraSequencer() {
this.lyraSequencer.isPlaying = !this.lyraSequencer.isPlaying;
const btn = document.getElementById('lyraSeqToggle');
btn.classList.toggle('active', this.lyraSequencer.isPlaying);
btn.textContent = this.lyraSequencer.isPlaying ? 'SEQ ON' : 'SEQ OFF';
if (!this.lyraSequencer.isPlaying) {
document.querySelectorAll('.lyra-step.current').forEach(el => el.classList.remove('current'));
this.stopAllLyraNotes();
}
}
randomizeLyraSequence(rng = Math.random) {
const notes = [0, 2, 3, 5, 7, 8, 10, 12, 14, 15];
for (let i = 0; i < this.lyraSequencer.stepLength; i++) {
this.lyraSequencer.steps[i] = {
active: rng() > 0.4,
note: notes[Math.floor(rng() * notes.length)],
slide: rng() > 0.7,
accent: rng() > 0.8,
gate: 1.0
};
}
this.lyraSequencer.steps[0].active = true;
this.lyraSequencer.steps[0].accent = true;
this.updateLyraSequencerUI();
}
updateLyraSequencerUI() {
for (let i = 0; i < this.lyraSequencer.stepLength; i++) {
const step = this.lyraSequencer.steps[i];
const stepEl = document.querySelector(`#lyraStep${i}`);
if (stepEl) {
stepEl.classList.toggle('active', step.active);
stepEl.classList.toggle('slide', step.slide);
stepEl.classList.toggle('accent', step.accent);
stepEl.textContent = step.active ? ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C+', 'C#+', 'D+', 'D#+', 'E+', 'F+', 'F#+', 'G+', 'G#+', 'A+', 'A#+', 'B+', 'C++'][step.note] : '—';
}
}
}
randomizeLyraSettings(rng = Math.random) {
const randomPick = (arr) => arr[Math.floor(rng() * arr.length)];
const randomRange = (min, max) => min + rng() * (max - min);
const r = {
osc1Type: randomPick(['sine', 'square', 'sawtooth', 'triangle']),
osc1Oct: Math.floor(randomRange(-2, 3)),
osc2Type: randomPick(['sine', 'square', 'sawtooth', 'triangle']),
osc2Oct: Math.floor(randomRange(-2, 3)),
detune: randomRange(0, 50),
fmAmount: randomRange(0, 200),
waveFold: randomRange(0, 1),
drive: randomRange(0, 1),
driveMode: randomPick(['soft clip', 'hard clip', 'exponential', 'wavefold']),
filterType: randomPick(['lowpass', 'notch']),
filterCutoff: randomRange(2000, 15000),
filterQ: randomRange(0.1, 18),
filterEnv: randomRange(-500, 500),
lfoRate: randomRange(0.1, 20),
lfoDepth: randomRange(10, 500),
attack: randomRange(0.01, 0.1),
decay: randomRange(0.1, 0.5),
sustain: randomRange(0.1, 0.7),
release: randomRange(0.1, 0.5),
portamento: randomRange(0, 200)
};
for (const [k, v] of Object.entries(r)) {
this.updateLyraParam(k, k === 'portamento' ? v / 1000 : v);
}
this.updateLyraParam('all');
}
randomizeDrumModules(rng = Math.random) {
const rand = (min, max) => min + rng() * (max - min);
this.params.kickPitch = rand(20, 100);
this.params.kickDecay = rand(0.05, 0.5);
this.params.kickDrive = rand(0, 1);
this.params.bassOctave = Math.floor(rand(-2, 3));
this.params.bassDetune = rand(0, 25);
this.params.bassFilterCutoff = rand(100, 8000);
this.params.bassFilterQ = rand(0, 20);
this.params.snareTone = rand(100, 500);
this.params.snareSnappy = rand(1000, 9000);
this.params.snareDecay = rand(0.05, 0.4);
this.params.hatDecay = rand(0.01, 0.3);
this.params.hatMetal = rand(5000, 15000);
}
randomizePatches(rng = Math.random) {
this.patches = [];
const numPatches = Math.floor(3 + rng() * 4);
const sources = this.getModSources();
const dests = this.getModDestinations();
const usedCombos = new Set();
for (let i = 0; i < numPatches; i++) {
let source, dest, combo;
let attempts = 0;
do {
source = sources[Math.floor(rng() * sources.length)];
dest = dests[Math.floor(rng() * dests.length)];
combo = `${source}->${dest}`;
attempts++;
} while (usedCombos.has(combo) && attempts < 50);
if (attempts < 50) {
usedCombos.add(combo);
const amount = (rng() * 4 - 2).toFixed(2);
this.addPatch(source, dest, parseFloat(amount));
}
}
}
updateLooperDisplay() {
const masterStep = this.masterCycleStep;
const currentPass = (masterStep < 32) ? 'first' : 'second';
document.querySelectorAll('.pattern-row .step.current').forEach(e => e.classList.remove('current'));
for (const type in this.sequences) {
const container = document.querySelector(`.pattern-row[data-type="${type}"]`);
if (!container) continue;
const trackLength = this.sequenceLengths[type] || 32;
const currentStepInPattern = masterStep % trackLength;
const currentStepEl = container.querySelector(`.step[data-index='${currentStepInPattern}']`);
if (currentStepEl) {
currentStepEl.classList.add('current');
}
container.querySelectorAll('.step').forEach((stepEl, i) => {
if (!this.sequences[type][i]) {
stepEl.classList.remove('flashing-inactive');
return;
}
const meta = (this.stepMeta[type] && this.stepMeta[type][i]) || {};
const condition = meta.triggerPass || 'always';
if (condition === 'always') {
stepEl.classList.remove('flashing-inactive');
} else if (condition === 'first') {
stepEl.classList.toggle('flashing-inactive', currentPass === 'second');
} else if (condition === 'second') {
stepEl.classList.toggle('flashing-inactive', currentPass === 'first');
}
});
}
}
start() {
if (this.isPlaying) return;
if (this.audioCtx.state === "suspended") this.audioCtx.resume();
this.isPlaying = true;
this.masterCycleStep = 0;
this.nextNoteTime = this.audioCtx.currentTime + .05;
this.scheduler();
requestAnimationFrame(this.updateModulation);
document.getElementById("playButton").textContent = "HALT ❚❚";
document.getElementById("playButton").classList.add("active");
}
panic() {
const now = this.audioCtx.currentTime;
this.stopAllLyraNotes();
if (this.lyraLooper.isPlaying) {
this.toggleLyraPlay();
}
const masterGainNode = this.masterGain;
masterGainNode.gain.cancelScheduledValues(now);
masterGainNode.gain.setTargetAtTime(0, now, 0.015);
masterGainNode.gain.setTargetAtTime(0.4, now + 0.02, 0.01);
this.patches.forEach(patch => {
if (patch.isLocked) {
const destFunction = this.modDestinationsSafe[patch.dest];
const controlElement = document.getElementById(Object.keys(this.modDestinationsSafe).find(key => this.modDestinationsSafe[key] === destFunction));
if (controlElement) {
const defaultValue = controlElement.defaultValue || 0.5;
destFunction(defaultValue);
}
}
});
console.log("Audio engine panic: All sounds stopped.");
}
stop() {
if (!this.isPlaying) return;
this.isPlaying = false;
clearTimeout(this.timerID);
if (this.bassLfo && this.bassLfo.lfo) {
this.bassLfo.lfo.stop();
this.bassLfo = null;
}
this.panic();
document.getElementById("playButton").textContent = "PLAY ▶";
document.getElementById("playButton").classList.remove("active");
document.querySelectorAll('.pattern-row .step.current').forEach(el => el.classList.remove('current'));
document.querySelectorAll('.pattern-row .step.flashing-inactive').forEach(el => el.classList.remove('flashing-inactive'));
}
togglePlay() {
this.isPlaying ? this.stop() : this.start();
}
setBPM(v) {
this.bpm = parseInt(v);
if (this.lyraFxNodes.chopper) {
const rateMultiplier = parseFloat(document.getElementById('chopperRate').value);
this.lyraFxNodes.chopper.lfo.frequency.setTargetAtTime((this.bpm / 60) * rateMultiplier, this.audioCtx.currentTime, 0.01);
}
this.updateLyraParam('lfoRate');
}
randomizeSequences(rng = Math.random) {
const k = Array(32).fill(false),
s = Array(32).fill(false);
s[4] = true;
s[12] = true;
s[20] = true;
s[28] = true;
if (rng() < .15) s[12] = false;
if (rng() < .15) s[28] = false;
if (this.bpm < 135) {
k[0] = true;
k[8] = true;
k[16] = true;
k[24] = true;
if (rng() > .6) k[6] = true;
if (rng() > .5) k[14] = true;
if (rng() > .7) k[22] = true;
if (rng() > .4) k[30] = true;
} else {
const p = rng();
if (p < .7) {
k[0] = true;
k[8] = true;
k[16] = true;
k[24] = true;
if (rng() > .4) k[3] = true;
if (rng() > .6) k[13] = true;
if (rng() > .4) k[19] = true;
if (rng() > .6) k[29] = true;
} else {
k[0] = true;
if (rng() > .2) k[6] = true;
k[11] = true;
k[16] = true;
if (rng() > .3) k[22] = true;
k[27] = true;
}
}
this.sequences.kick = k;
this.sequences.snare = s;
this.sequences.hat = this.sequences.hat.map(() => rng() > .4);
this.sequences.bass = this.sequences.bass.map(() => rng() > .7);
this.sequences.lyraGate = this.sequences.lyraGate.map(() => rng() > .2);
this.stepMeta = {
kick: {},
snare: {},
hat: {},
bass: {},
lyraGate: {}
};
this.updatePatternEditor();
}
createPrng(seedString) {
function stringToSeed(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return hash;
}
let seed = stringToSeed(seedString);
return function() {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
generateFromSeed(seedString) {
const seededRandom = this.createPrng(seedString);
this.randomizeSequences(seededRandom);
this.randomizeLyraSettings(seededRandom);
this.randomizeLyraSequence(seededRandom);
this.randomizeDrumModules(seededRandom);
this.randomizePatches(seededRandom);
const bpmSlider = document.getElementById('bpm');
const swingSlider = document.getElementById('swing');
bpmSlider.value = 90 + seededRandom() * 80;
swingSlider.value = seededRandom() * 0.75;
bpmSlider.dispatchEvent(new Event('input'));
swingSlider.dispatchEvent(new Event('input'));
}
updatePatternEditor() {
for (const type in this.sequences) {
const container = document.querySelector(`.pattern-row[data-type="${type}"]`);
if (!container) continue;
container.querySelectorAll('.step').forEach((stepEl, i) => {
const isActive = this.sequences[type][i];
stepEl.classList.toggle('active', isActive);
stepEl.classList.remove('trig-first', 'trig-second', 'accent');
if (isActive) {
const meta = (this.stepMeta[type] && this.stepMeta[type][i]) || {};
const velocity = meta.velocity ?? 1.0;
const ratchet = meta.ratchet ?? 1;
const condition = meta.triggerPass || 'always';
const prob = meta.prob ?? 1.0;
const isAccent = meta.accent === true;
stepEl.style.opacity = 0.4 + (velocity * 0.6);
stepEl.textContent = ratchet > 1 ? ratchet : '';
if (condition === 'first') {
stepEl.classList.add('trig-first');
} else if (condition === 'second') {
stepEl.classList.add('trig-second');
}
stepEl.classList.toggle('has-prob', prob < 1.0);
stepEl.classList.toggle('accent', isAccent);
} else {
stepEl.style.opacity = 1;
stepEl.textContent = '';
stepEl.classList.remove('flashing-inactive');
stepEl.classList.remove('has-prob');
}
});
}
}
updateVisibleStepsUI() {
document.querySelectorAll('.pattern-row').forEach(row => {
const type = row.dataset.type;
const length = this.sequenceLengths[type] || 32;
row.querySelectorAll('.step').forEach((step, index) => {
step.classList.toggle('disabled', index >= length);
});
});
}
getState() {
const state = {
controls: [],
patches: this.patches.map(p => ({
id: p.id,
source: p.source,
dest: p.dest,
amount: p.amount,
isLocked: p.isLocked,
...(p.isLocked && {
lockedValue: p.lockedValue
}),
modRangeMin: p.modRangeMin,
modRangeMax: p.modRangeMax
})),
isSafetyOn: this.isSafetyOn,
};
document.querySelectorAll('#controls [id]').forEach(el => {
if (el.tagName === 'INPUT' || el.tagName === 'SELECT') {
const value = el.type === 'checkbox' ? el.checked : el.value;
state.controls.push({
id: el.id,
value: value
});
}
if (el.id === 'lyraLfoSyncToggle' && el.classList.contains('active')) {
state.controls.push({
id: el.id,
value: true
});
}
});
state.sequences = this.sequences;
state.sequenceLengths = this.sequenceLengths;
state.lyraSequencer = {
isPlaying: this.lyraSequencer.isPlaying,
steps: this.lyraSequencer.steps
};
state.stepMeta = this.stepMeta;
state.version = this.presetVersion;
return state;
}
setState(state) {
this.lastLoadedPresetState = state;
this.reloadUntil = performance.now() + 600;
this.isSafetyOn = state.isSafetyOn ?? false;
const safetyBtn = document.getElementById('safetyToggle');
if (safetyBtn) {
safetyBtn.classList.toggle('active', this.isSafetyOn);
safetyBtn.textContent = this.isSafetyOn ? 'SAFETY: ON' : 'SAFETY: OFF';
}
const driveModeControl = state.controls.find(c => c.id === 'lyraDriveMode');
if (!driveModeControl) {
this.lyraParams.driveMode = 'soft clip';
const driveModeSelect = document.getElementById('lyraDriveMode');
if (driveModeSelect) {
driveModeSelect.value = 'soft clip';
}
}
const syncToggleState = state.controls.find(c => c.id === 'lyraLfoSyncToggle');
if (syncToggleState && syncToggleState.value) {
document.getElementById('lyraLfoSyncToggle').classList.add('active');
document.getElementById('lyraLfoRate').style.display = 'none';
document.getElementById('lyraLfoSyncRate').style.display = 'block';
} else {
document.getElementById('lyraLfoSyncToggle').classList.remove('active');
document.getElementById('lyraLfoRate').style.display = 'block';
document.getElementById('lyraLfoSyncRate').style.display = 'none';
}
state.controls.forEach(c => {
if (c.id === 'lyraLfoSyncToggle') return;
const el = document.getElementById(c.id);
if (el) {
if (el.type === 'checkbox') el.checked = c.value;
else el.value = c.value;
el.dispatchEvent(new Event('input', {
bubbles: true
}));
el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
});
if (state.sequences) {
const defaultSequences = {
kick: Array(32).fill(false),
snare: Array(32).fill(false),
hat: Array(32).fill(false),
bass: Array(32).fill(false),
lyraGate: Array(32).fill(true)
};
this.sequences = {
...defaultSequences,
...state.sequences
};
}
const defaultLengths = { kick: 32, snare: 32, hat: 32, bass: 32, lyraGate: 32 };
this.sequenceLengths = { ...defaultLengths, ...(state.sequenceLengths || {}) };
for (const track in this.sequenceLengths) {
const input = document.querySelector(`.track-length[data-track="${track}"]`);
if (input) {
input.value = this.sequenceLengths[track];
}
}
this.updateVisibleStepsUI();
if (state.version && state.version >= 2) {
this.stepMeta = state.stepMeta || {
kick: {},
snare: {},
hat: {},
bass: {},
lyraGate: {}
};
} else {
this.stepMeta = {
kick: {},
snare: {},
hat: {},
bass: {},
lyraGate: {}
};
}
this.updatePatternEditor();
if (state.lyraSequencer) {
this.lyraSequencer.steps = state.lyraSequencer.steps;
this.lyraSequencer.isPlaying = state.lyraSequencer.isPlaying ?? true; // Default to true if not present
this.updateLyraSequencerUI();
const seqBtn = document.getElementById('lyraSeqToggle');
if (seqBtn) {
seqBtn.classList.toggle('active', this.lyraSequencer.isPlaying);
seqBtn.textContent = this.lyraSequencer.isPlaying ? 'SEQ ON' : 'SEQ OFF';
}
}
if (state.patches && Array.isArray(state.patches)) {
this.patches = state.patches.map(p => ({
...p,
id: p.id || crypto.randomUUID(),
isLocked: p.isLocked || false,
lockedValue: p.lockedValue || 0,
modRangeMin: p.modRangeMin ?? -1.0,
modRangeMax: p.modRangeMax ?? 1.0
}));
} else {
this.patches = [];
}
this.renderPatchesUI();
}
savePreset(name) {
if (!name) {
alert("Please enter a preset name.");
return;
}
const state = this.getState();
localStorage.setItem(`pulsar_lyra_preset_${name}`, JSON.stringify(state));
this.populatePresets();
}
loadPreset(name) {
if (this.isPlaying) {
this.stop();
}
this.panic();
const stateJSON = localStorage.getItem(`pulsar_lyra_preset_${name}`);
if (stateJSON) {
this.setState(JSON.parse(stateJSON));
} else {
alert(`Preset "${name}" not found.`);
}
}
deletePreset(name) {
if (confirm(`Are you sure you want to delete preset "${name}"?`)) {
localStorage.removeItem(`pulsar_lyra_preset_${name}`);
this.populatePresets();
return true;
}
return false;
}
populatePresets() {
const select = document.getElementById('presetSelect');
select.innerHTML = '';
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('pulsar_lyra_preset_')) {
const name = key.replace('pulsar_lyra_preset_', '');
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
}
}
exportAllPresets() {
const allPresets = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('pulsar_lyra_preset_')) {
const name = key.replace('pulsar_lyra_preset_', '');
allPresets[name] = JSON.parse(localStorage.getItem(key));
}
}
const blob = new Blob([JSON.stringify(allPresets, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), {
href: url,
download: `whitedwarf-presets-${new Date().toISOString().slice(0,10)}.json`
});
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
}
class VisualSystem {
constructor(c, a) {
this.audioSystem = a;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, .1, 1000);
this.camera.position.z = 4;
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
c.appendChild(this.renderer.domElement);
this.uniforms = {
time: {
value: 0
},
audioLevel: {
value: 0
}
};
const g = new THREE.SphereGeometry(2.5, 250, 250);
const m = new THREE.ShaderMaterial({
uniforms: this.uniforms,
vertexShader: `
uniform float time;
uniform float audioLevel;
varying vec3 vNormal;
void main() {
vNormal = normal;
vec3 pos = position + normal * sin(time * 4.0 + position.y * 12.0) * audioLevel * 0.9;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 vNormal;
uniform float audioLevel;
void main() {
float light = dot(normalize(vNormal), normalize(vec3(0.5, 0.2, 1.0)));
vec3 starColor = vec3(1.0, 1.0, 1.0);
vec3 glowColor = vec3(0.3, 0.6, 1.0);
float glow = pow(light, 4.0) + audioLevel * 2.5;
vec3 finalColor = mix(starColor, glowColor, glow);
gl_FragColor = vec4(finalColor * (0.5 + 0.5 * light + audioLevel), 1.0);
}
`,
transparent: true,
wireframe: false
});
this.mesh = new THREE.Mesh(g, m);
this.scene.add(this.mesh);
this.animate = this.animate.bind(this);
requestAnimationFrame(this.animate);
window.addEventListener('resize', this.onWindowResize.bind(this), false)
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight)
}
animate(t) {
requestAnimationFrame(this.animate);
const n = t * .001;
this.uniforms.time.value = n;
const a = this.audioSystem.getAudioData();
this.uniforms.audioLevel.value = a.averageFrequency / 255;
this.mesh.rotation.x = n * .1;
this.mesh.rotation.y = n * .2;
this.renderer.render(this.scene, this.camera)
}
}
class App {
constructor() {
this.audioSystem = new AudioSystem();
this.visualSystem = new VisualSystem(document.getElementById("canvas-container"), this.audioSystem);
this.eventHandlers = []; // For cleanup
this.modWatcherInterval = null; // For modulation feedback
this.init();
}
syncUiToAudioState() {
const audio = this.audioSystem;
const paramToElementIdMap = {
kickPitch: 'kickPitch', kickDecay: 'kickDecay', kickDrive: 'kickDrive',
bassOctave: 'bassOctave', bassDetune: 'bassDetune', bassFilterCutoff: 'bassFilterCutoff', bassFilterQ: 'bassFilterQ',
snareTone: 'snareTone', snareSnappy: 'snareSnappy', snareDecay: 'snareDecay',
hatDecay: 'hatDecay', hatMetal: 'hatMetal',
osc1Type: 'lyraOsc1Type', osc1Oct: 'lyraOsc1Oct',
osc2Type: 'lyraOsc2Type', osc2Oct: 'lyraOsc2Oct',
detune: 'lyraDetune', fmAmount: 'lyraFMAmt', waveFold: 'lyraWaveFold',
driveMode: 'lyraDriveMode', drive: 'lyraDrive',
filterType: 'lyraFilterType', filterCutoff: 'lyraFilterCutoff', filterQ: 'lyraFilterQ', filterEnv: 'lyraFilterEnv',
lfoDepth: 'lyraLfoDepth',
attack: 'lyraAttack', decay: 'lyraDecay', sustain: 'lyraSustain', release: 'lyraRelease',
lfoRate: 'lyraLfoRate', portamento: 'lyraPortamento',
};
for (const param in audio.params) {
const elementId = paramToElementIdMap[param];
if (elementId) {
const el = document.getElementById(elementId);
if (el) el.value = audio.params[param];
}
}
for (const param in audio.lyraParams) {
const elementId = paramToElementIdMap[param];
if (elementId) {
const el = document.getElementById(elementId);
if (el) {
if (param === 'portamento') {
el.value = audio.lyraParams[param] * 1000;
} else {
el.value = audio.lyraParams[param];
}
}
}
}
document.getElementById('controls').dispatchEvent(new Event('input', { bubbles: true }));
}
dispose() {
try {
if (this.modWatcherInterval) {
clearInterval(this.modWatcherInterval);
}
if (this.eventHandlers && Array.isArray(this.eventHandlers)) {
for (const h of this.eventHandlers) {
h.element.removeEventListener(h.event, h.handler, h.options);
}
this.eventHandlers.length = 0;
}
if (this.audioSystem) {
this.audioSystem.stop();
if (this.audioSystem.lyraLooperTimerID) {
clearInterval(this.audioSystem.lyraLooperTimerID);
this.audioSystem.lyraLooperTimerID = null;
}
if (this.audioSystem.timerID) {
clearTimeout(this.audioSystem.timerID);
this.audioSystem.timerID = null;
}
}
if (this.visualSystem && this.visualSystem.renderer) {
const r = this.visualSystem.renderer;
const gl = r.getContext();
if (gl && gl.getExtension) {
const ext = gl.getExtension("WEBGL_lose_context");
if (ext && ext.loseContext) ext.loseContext();
}
r.dispose();
}
} catch (e) {
console.error("dispose error", e);
}
}
async init() {
await this.audioSystem.initFx();
this.setupControls();
this.installDefaultPresets();
this.audioSystem.populatePresets();
this.audioSystem.loadPreset('white dwarf');
this.updateModulationHighlights();
window.addEventListener("beforeunload", () => this.dispose());
}
installDefaultPresets() {
const myPreset = {
"controls": [{ "id": "bpm", "value": "138" }, { "id": "presetSelect", "value": "white dwarf" }, { "id": "presetNameInput", "value": "" }, { "id": "lyraPortamento", "value": "80" }, { "id": "lyraOsc1Type", "value": "sine" }, { "id": "lyraOsc1Oct", "value": "1" }, { "id": "lyraOsc2Type", "value": "sine" }, { "id": "lyraOsc2Oct", "value": "0" }, { "id": "lyraDetune", "value": "3" }, { "id": "lyraFMAmt", "value": "256" }, { "id": "lyraWaveFold", "value": "0.03" }, { "id": "lyraDrive", "value": "0.04" }, { "id": "lyraDriveMode", "value": "soft clip" }, { "id": "lyraFilterType", "value": "lowpass" }, { "id": "lyraFilterCutoff", "value": "3845" }, { "id": "lyraFilterQ", "value": "11.1" }, { "id": "lyraFilterEnv", "value": "-752" }, { "id": "lyraLfoRate", "value": "0.3" }, { "id": "lyraLfoSyncRate", "value": "0.25" }, { "id": " lyraLfoSyncToggle", "value": true }, { "id": "lyraLfoDepth", "value": "1524" }, { "id": "lyraAttack", "value": "0.09" }, { "id": "lyraDecay", "value": "0.05" }, { "id": "lyraSustain", "value": "0.06" }, { "id": "lyraRelease", "value": "0.25" }, { "id": "lyraFxType", "value": "Chopper" }, { "id": "lyraDelayTime", "value": "0.45" }, { "id": "lyraDelayFeedback", "value": "0.2" }, { "id": "lyraDelayMix", "value": "0.35" }, { "id": "phaserRate", "value": "0.5" }, { "id": "phaserDepth", "value": "1200" }, { "id": "phaserFeedback", "value": "0.3" }, { "id": "phaserMix", "value": "0.5" }, { "id": "flangerRate", "value": "0.2" }, { "id": "flangerDepth", "value": "0.005" }, { "id": "flangerFeedback", "value": "0.5" }, { "id": "flangerMix", "value": "0.5" }, { "id": "quazarRate", "value": "2" }, { "id": "quazarReso", "value": "25" }, { "id": "quazarFreq", "value": "1500" }, { "id": "quazarMix", "value": "1" }, { "id": "chopperRate", "value": "2" }, { "id": "chopperDepth", "value": "0.05" }, { "id": "crusherBitDepth", "value": "8" }, { "id": "crusherFreq", "value": "0.5" }, { "id": "crusherMix", "value": "0.5" }, { "id": "patchSource", "value": "KAOS X" }, { "id": "patchDest", "value": "BD PITCH" }, { "id": "patchAmount", "value": "1.2" }, { "id": "kickPitch", "value": "51" }, { "id": "kickDecay", "value": "0.17" }, { "id": "kickDrive", "value": "0.39" }, { "id": "bassOctave", "value": "-1" }, { "id": "bassDetune", "value": "13" }, { "id": "bassFilterCutoff", "value": "2265" }, { "id": "bassFilterQ", "value": "17" }, { "id": "snareTone", "value": "129" }, { "id": "snareSnappy", "value": "5245" }, { "id": "snareDecay", "value": "0.25" }, { "id": "hatDecay", "value": "0.16" }, { "id": "hatMetal", "value": "7571" }, { "id": "masterDist", "value": "0.05" }, { "id": "masterReverb", "value": "0.44" }],
"patches": [{ "source": "S&H (Random)", "dest": "HHT METAL", "amount": 0.5 }, { "source": "LFO 2 (Fast)", "dest": "BASS OCTAVE", "amount": 0.5 }, { "source": "KAOS X", "dest": "LYRA GATE PROB", "amount": 0.5 }, { "source": "KAOS Y", "dest": "LYRA DELAY TIME", "amount": 1.3 }, { "source": "LFO 2 (Fast)", "dest": "SD SNAPPY", "amount": 1.3 }, { "source": "LYRA LFO", "dest": "LYRA RESO", "amount": 1.3 }],
"sequences": { "kick": [true, false, false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false], "snare": [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], "hat": [false, true, true, false, true, true, false, false, true, true, true, true, false, false, false, true, true, true, true, true, false, false, false, false, false, true, true, true, true, true, true, true], "bass": [false, false, true, false, true, false, true, false, false, false, true, true, false, false, true, true, false, false, true, false, true, false, false, true, false, false, true, true, false, false, false, false], "lyraGate": [true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, false] },
"lyraSequencer": { "isPlaying": true, "steps": [{ "active": true, "note": 3, "slide": false, "accent": true }, { "active": true, "note": 8, "slide": false, "accent": true }, { "active": true, "note": 0, "slide": true, "accent": true }, { "active": true, "note": 10, "slide": false, "accent": true }, { "active": false, "note": 0, "slide": false, "accent": false }, { "active": true, "note": 5, "slide": true, "accent": true }, { "active": true, "note": 14, "slide": true, "accent": true }, { "active": true, "note": 2, "slide": false, "accent": true }, { "active": true, "note": 3, "slide": false, "accent": true }, { "active": true, "note": 14, "slide": false, "accent": true }, { "active": true, "note": 15, "slide": false, "accent": true }, { "active": true, "note": 5, "slide": false, "accent": true }, { "active": false, "note": 0, "slide": false, "accent": false }, { "active": true, "note": 7, "slide": false, "accent": true }, { "active": true, "note": 7, "slide": false, "accent": true }, { "active": false, "note": 0, "slide": false, "accent": false }] }
};
const presetName = "white dwarf";
const key = `pulsar_lyra_preset_${presetName}`;
if (!localStorage.getItem(key)) {
if (myPreset.patches && Array.isArray(myPreset.patches)) {
myPreset.patches.forEach(patch => {
patch.id = crypto.randomUUID();
});
}
localStorage.setItem(key, JSON.stringify(myPreset));
}
}
updateModulationHighlights() {
const modulatedDests = new Set(this.audioSystem.patches.map(p => p.dest));
const destToElementIdMap = {
'BD PITCH': 'kickPitch', 'BD DECAY': 'kickDecay', 'BD DRIVE': 'kickDrive',
'BASS OCTAVE': 'bassOctave', 'BASS DETUNE': 'bassDetune', 'BASS FILTER': 'bassFilterCutoff', 'BASS RESO': 'bassFilterQ',
'SD TONE': 'snareTone', 'SD SNAPPY': 'snareSnappy', 'SNARE DECAY': 'snareDecay',
'HHT DECAY': 'hatDecay', 'HHT METAL': 'hatMetal',
'MASTER DIST': 'masterDist', 'MASTER REVERB': 'masterReverb',
'LYRA OSC1 OCT': 'lyraOsc1Oct', 'LYRA OSC2 OCT': 'lyraOsc2Oct', 'LYRA DETUNE': 'lyraDetune',
'LYRA FM AMT': 'lyraFMAmt', 'LYRA FOLD': 'lyraWaveFold', 'LYRA DRIVE': 'lyraDrive',
'LYRA FILTER': 'lyraFilterCutoff', 'LYRA RESO': 'lyraFilterQ', 'LYRA FILTER ENV': 'lyraFilterEnv',
'LYRA LFO DEPTH': 'lyraLfoDepth', 'LYRA LFO RATE': 'lyraLfoRate',
'LYRA ATTACK': 'lyraAttack', 'LYRA DECAY': 'lyraDecay', 'LYRA SUSTAIN': 'lyraSustain', 'LYRA RELEASE': 'lyraRelease',
'LYRA SLIDE': 'lyraPortamento',
'LYRA DELAY TIME': 'lyraDelayTime', 'LYRA DELAY FB': 'lyraDelayFeedback', 'LYRA DELAY MIX': 'lyraDelayMix',
'PHASER RATE': 'phaserRate', 'PHASER DEPTH': 'phaserDepth', 'PHASER FEEDBACK': 'phaserFeedback', 'PHASER MIX': 'phaserMix',
'FLANGER RATE': 'flangerRate', 'FLANGER DEPTH': 'flangerDepth', 'FLANGER FEEDBACK': 'flangerFeedback', 'FLANGER MIX': 'flangerMix',
'QUAZAR RATE': 'quazarRate', 'QUAZAR RESO': 'quazarReso', 'QUAZAR FREQ': 'quazarFreq', 'QUAZAR MIX': 'quazarMix',
'CHOPPER DEPTH': 'chopperDepth',
'CRUSHER BITDEPTH': 'crusherBitDepth', 'CRUSHER DOWNSAMPLE': 'crusherFreq', 'CRUSHER MIX': 'crusherMix',
};
document.querySelectorAll('.row.is-modulated').forEach(row => row.classList.remove('is-modulated'));
for (const dest of modulatedDests) {
const elementId = destToElementIdMap[dest];
if (elementId) {
const el = document.getElementById(elementId);
if (el) {
el.closest('.row').classList.add('is-modulated');
}
}
}
}
setupControls() {
const audio = this.audioSystem;
const addListener = (element, event, handler, options = {}) => {
element.addEventListener(event, handler, options);
this.eventHandlers.push({
element,
event,
handler,
options
});
};
const controlsContainer = document.getElementById('controls');
addListener(controlsContainer, 'dblclick', (e) => {
if (e.target.tagName === 'INPUT' && e.target.type === 'range') {
const controlId = e.target.id;
const presetState = this.audioSystem.lastLoadedPresetState;
if (presetState && presetState.controls) {
const presetControl = presetState.controls.find(c => c.id === controlId);
if (presetControl) {
e.target.value = presetControl.value;
e.target.dispatchEvent(new Event('input', { bubbles: true }));
}
}
else if (e.target.dataset.default) {
e.target.value = e.target.dataset.default;
e.target.dispatchEvent(new Event('input', { bubbles: true }));
}
}
});
addListener(controlsContainer, 'input', (e) => {
if (e.target.type === 'range') {
const readout = e.target.nextElementSibling;
if (readout && readout.classList.contains('param-value')) {
let value = parseFloat(e.target.value);
const step = parseFloat(e.target.step) || 1;
const decimals = step < 1 ? (step.toString().split('.')[1] || '').length : 0;
readout.textContent = value.toFixed(decimals);
}
}
});
addListener(document.getElementById("playButton"), "click", () => audio.togglePlay());
addListener(document.getElementById("randomizeButton"), "click", () => audio.randomizeSequences());
addListener(document.getElementById("bpm"), "input", e => {
const v = parseFloat(e.target.value);
audio.setBPM(v);
document.getElementById("bpmValue").textContent = String(v);
});
addListener(document.getElementById("generateFromSeedButton"), "click", () => {
const seedInput = document.getElementById("seedInput");
let seed = seedInput.value.trim();
if (!seed) {
seed = Date.now().toString(36) + Math.random().toString(36).substring(2);
seedInput.value = seed;
}
audio.generateFromSeed(seed);
this.syncUiToAudioState();
});
addListener(document.getElementById("randomSeedButton"), "click", () => {
const seedInput = document.getElementById("seedInput");
seedInput.value = Date.now().toString(36) + Math.random().toString(36).substring(2);
});
const swingSlider = document.createElement('input');
swingSlider.type = 'range';
swingSlider.id = 'swing';
swingSlider.min = 0;
swingSlider.max = 0.75;
swingSlider.step = 0.01;
swingSlider.value = 0;
const swingLabel = document.createElement('label');
swingLabel.htmlFor = 'swing';
swingLabel.textContent = 'SWING:';
const swingValue = document.createElement('span');
swingValue.id = 'swingValue';
swingValue.textContent = '0%';
swingValue.style.minWidth = '35px';
const swingRow = document.createElement('div');
swingRow.className = 'row';
swingRow.appendChild(swingLabel);
swingRow.appendChild(swingSlider);
swingRow.appendChild(swingValue);
const bpmRow = document.getElementById('bpm').parentElement;
bpmRow.parentElement.insertBefore(swingRow, bpmRow.nextSibling);
addListener(swingSlider, 'input', e => {
audio.swing = parseFloat(e.target.value);
swingValue.textContent = `${Math.round(audio.swing * 100)}%`;
});
['kickPitch', 'kickDecay', 'kickDrive', 'bassOctave', 'bassDetune', 'bassFilterCutoff', 'bassFilterQ', 'snareTone', 'snareSnappy', 'snareDecay', 'hatDecay', 'hatMetal'].forEach(p => addListener(document.getElementById(p), 'input', e => audio.setParam('pulsar', p, e.target.value)));
addListener(document.getElementById('masterDist'), 'input', e => audio.setDistortionCurve(parseFloat(e.target.value)));
addListener(document.getElementById('masterReverb'), 'input', e => audio.reverbSend.gain.setTargetAtTime(parseFloat(e.target.value), audio.audioCtx.currentTime, .01));
const setVolume = (gainNode, value) => {
gainNode.gain.setTargetAtTime(value, audio.audioCtx.currentTime, 0.015);
};
addListener(document.getElementById("kickVolume"), "input", e => setVolume(audio.kickGain, parseFloat(e.target.value)));
addListener(document.getElementById("snareVolume"), "input", e => setVolume(audio.snareGain, parseFloat(e.target.value)));
addListener(document.getElementById("hatVolume"), "input", e => setVolume(audio.hatGain, parseFloat(e.target.value)));
addListener(document.getElementById("bassVolume"), "input", e => setVolume(audio.bassGain, parseFloat(e.target.value)));
addListener(document.getElementById("synthVolume"), "input", e => setVolume(audio.synthGain, parseFloat(e.target.value)));
addListener(document.getElementById('masterVolume'), 'input', e => setVolume(audio.masterGain, parseFloat(e.target.value)));
document.querySelectorAll('.mute-btn, .solo-btn').forEach(btn => {
addListener(btn, 'click', () => {
const track = btn.dataset.track;
const isSolo = btn.classList.contains('solo-btn');
if (isSolo) {
audio.trackState[track].soloed = !audio.trackState[track].soloed;
if (audio.trackState[track].soloed) {
audio.trackState[track].muted = false;
document.querySelector(`.mute-btn[data-track="${track}"]`).classList.remove('active');
}
} else {
audio.trackState[track].muted = !audio.trackState[track].muted;
}
for (const t in audio.trackState) {
document.querySelector(`.mute-btn[data-track="${t}"]`).classList.toggle('active', audio.trackState[t].muted);
document.querySelector(`.solo-btn[data-track="${t}"]`).classList.toggle('active', audio.trackState[t].soloed);
}
audio.updateTrackGains();
});
});
document.querySelectorAll('.track-length').forEach(input => {
addListener(input, 'change', (e) => {
const track = e.target.dataset.track;
let len = parseInt(e.target.value, 10);
if (isNaN(len) || len < 1) len = 1;
if (len > 32) len = 32;
e.target.value = len;
audio.sequenceLengths[track] = len;
audio.updateVisibleStepsUI();
});
});
const safetyBtn = document.getElementById('safetyToggle');
addListener(safetyBtn, 'click', () => {
audio.isSafetyOn = !audio.isSafetyOn;
safetyBtn.classList.toggle('active', audio.isSafetyOn);
safetyBtn.textContent = audio.isSafetyOn ? 'SAFETY: ON' : 'SAFETY: OFF';
});
addListener(document.getElementById('savePresetButton'), 'click', () => audio.savePreset(document.getElementById('presetNameInput').value));
addListener(document.getElementById('loadPresetButton'), 'click', () => {
audio.loadPreset(document.getElementById('presetSelect').value);
this.updateModulationHighlights();
});
addListener(document.getElementById('deletePresetButton'), 'click', () => {
const select = document.getElementById('presetSelect');
const nameToDelete = select.value;
if (nameToDelete && audio.deletePreset(nameToDelete)) {
if (select.options.length > 0) {
select.selectedIndex = 0;
} else {
document.getElementById('presetNameInput').value = '';
}
}
});
const exportModal = document.getElementById('exportModal');
const importModal = document.getElementById('importModal');
addListener(document.getElementById('exportStateButton'), 'click', () => {
const state = audio.getState();
const cleanState = {
...state,
patches: state.patches.map(({
source,
dest,
amount,
modRangeMin,
modRangeMax
}) => ({
source,
dest,
amount,
modRangeMin,
modRangeMax
}))
};
const stateString = `const myPreset = ${JSON.stringify(cleanState, null, 2)};`;
document.getElementById('exportTextarea').value = stateString;
exportModal.style.display = 'flex';
});
addListener(document.getElementById('exportAllButton'), 'click', () => audio.exportAllPresets());
addListener(document.getElementById('downloadPresetButton'), 'click', () => {
const text = document.getElementById('exportTextarea').value;
const blob = new Blob([text], {
type: 'text/plain'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `pulsar-lyra-preset-${document.getElementById('presetSelect').value}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
addListener(document.getElementById('closeExportModalButton'), 'click', () => exportModal.style.display = 'none');
addListener(document.getElementById('importStateButton'), 'click', () => {
importModal.style.display = 'flex';
});
addListener(document.getElementById('applyImportButton'), 'click', () => {
const text = document.getElementById('importTextarea').value.trim();
if (!text) {
alert('Text area is empty!');
return;
}
try {
const startIndex = text.indexOf('{');
const endIndex = text.lastIndexOf('}');
if (startIndex === -1 || endIndex === -1) throw new Error("Could not find a valid JSON object.");
const jsonString = text.substring(startIndex, endIndex + 1);
const data = JSON.parse(jsonString);
if (data.controls && data.sequences) {
if (data.patches && Array.isArray(data.patches)) {
data.patches.forEach(p => p.id = crypto.randomUUID());
}
audio.setState(data);
alert('Track loaded successfully!');
} else {
let count = 0;
for (const [name, presetData] of Object.entries(data)) {
if (presetData.controls && presetData.sequences) {
if (presetData.patches && Array.isArray(presetData.patches)) {
presetData.patches.forEach(p => p.id = crypto.randomUUID());
}
localStorage.setItem(`pulsar_lyra_preset_${name}`, JSON.stringify(presetData));
count++;
}
}
audio.populatePresets();
alert(`${count} presets imported successfully!`);
}
importModal.style.display = 'none';
this.updateModulationHighlights();
} catch (e) {
console.error("Import error:", e);
alert(`Failed to load. Please check the code.\nError: ${e.message}`);
}
});
addListener(document.getElementById('closeImportModalButton'), 'click', () => importModal.style.display = 'none');
const lyraHeader = document.querySelector('#lyraContainer > h3');
addListener(lyraHeader, 'click', () => document.getElementById('lyraContainer').classList.toggle('open'));
const lyraControls = {
lyraOsc1Type: 'osc1Type',
lyraOsc1Oct: 'osc1Oct',
lyraOsc2Type: 'osc2Type',
lyraOsc2Oct: 'osc2Oct',
lyraDetune: 'detune',
lyraFMAmt: 'fmAmount',
lyraWaveFold: 'waveFold',
lyraDriveMode: 'driveMode',
lyraDrive: 'drive',
lyraFilterType: 'filterType',
lyraFilterCutoff: 'filterCutoff',
lyraFilterQ: 'filterQ',
lyraFilterEnv: 'filterEnv',
lyraLfoDepth: 'lfoDepth',
lyraAttack: 'attack',
lyraDecay: 'decay',
lyraSustain: 'sustain',
lyraRelease: 'release'
};
Object.entries(lyraControls).forEach(([id, paramName]) => {
const el = document.getElementById(id);
addListener(el, el.tagName === 'SELECT' ? 'change' : 'input', e => {
const value = el.type && el.type.includes('range') ? parseFloat(e.target.value) : e.target.value;
audio.updateLyraParam(paramName, value);
});
});
const lfoRateSlider = document.getElementById('lyraLfoRate');
const lfoSyncSelect = document.getElementById('lyraLfoSyncRate');
const lfoSyncToggle = document.getElementById('lyraLfoSyncToggle');
addListener(lfoRateSlider, 'input', e => audio.updateLyraParam('lfoRate', parseFloat(e.target.value)));
addListener(lfoSyncSelect, 'change', e => audio.updateLyraParam('lfoSyncRate', parseFloat(e.target.value)));
addListener(lfoSyncToggle, 'click', () => {
const isSynced = !lfoSyncToggle.classList.contains('active');
lfoSyncToggle.classList.toggle('active', isSynced);
audio.updateLyraParam('lfoIsSynced', isSynced);
lfoRateSlider.style.display = isSynced ? 'none' : 'block';
lfoSyncSelect.style.display = isSynced ? 'block' : 'none';
const k = isSynced ? 'lfoSyncRate' : 'lfoRate';
const v = isSynced ? parseFloat(lfoSyncSelect.value) : parseFloat(lfoRateSlider.value);
audio.updateLyraParam(k, v);
});
const fx = audio.lyraFxNodes;
const now = () => audio.audioCtx.currentTime;
addListener(document.getElementById('lyraFxType'), 'change', e => {
document.querySelectorAll('.fx-panel').forEach(p => p.style.display = 'none');
const panel = document.getElementById(`fxControls-${e.target.value}`);
if (panel) panel.style.display = 'block';
audio.switchLyraFx(e.target.value);
});
const setupFxSlider = (sliderId, wetUpdater, dryUpdater) => {
addListener(document.getElementById(sliderId), 'input', e => {
const mix = parseFloat(e.target.value);
if (wetUpdater) wetUpdater(mix);
if (dryUpdater) dryUpdater(1.0 - mix);
});
};
const setWet = val => fx.wet.gain.setTargetAtTime(val, now(), 0.01);
const setDry = val => fx.dry.gain.setTargetAtTime(val, now(), 0.01);
addListener(document.getElementById('lyraDelayTime'), 'input', e => fx.delay.delay.delayTime.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('lyraDelayFeedback'), 'input', e => fx.delay.feedback.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
setupFxSlider('lyraDelayMix', setWet, setDry);
addListener(document.getElementById('phaserRate'), 'input', e => fx.phaser.lfo.frequency.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('phaserDepth'), 'input', e => fx.phaser.lfoGain.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('phaserFeedback'), 'input', e => fx.phaser.feedback.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
setupFxSlider('phaserMix', setWet, setDry);
addListener(document.getElementById('flangerRate'), 'input', e => fx.flanger.lfo.frequency.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('flangerDepth'), 'input', e => fx.flanger.lfoGain.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('flangerFeedback'), 'input', e => fx.flanger.feedback.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
setupFxSlider('flangerMix', setWet, setDry);
addListener(document.getElementById('quazarRate'), 'input', e => fx.quazar.lfo.frequency.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('quazarReso'), 'input', e => fx.quazar.filter.Q.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('quazarFreq'), 'input', e => fx.quazar.lfoGain.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
setupFxSlider('quazarMix', setWet, setDry);
addListener(document.getElementById('chopperRate'), 'change', e => audio.setBPM(audio.bpm));
addListener(document.getElementById('chopperDepth'), 'input', e => fx.chopper.lfoGain.gain.setTargetAtTime(parseFloat(e.target.value), now(), 0.01));
addListener(document.getElementById('crusherBitDepth'), 'input', e => {
if (fx.bitcrusher?.node) fx.bitcrusher.node.parameters.get('bitDepth').setTargetAtTime(parseFloat(e.target.value), now(), 0.01)
});
addListener(document.getElementById('crusherFreq'), 'input', e => {
if (fx.bitcrusher?.node) fx.bitcrusher.node.parameters.get('frequencyReduction').setTargetAtTime(parseFloat(e.target.value), now(), 0.01)
});
setupFxSlider('crusherMix', setWet, setDry);
addListener(document.getElementById('lyraRecButton'), 'click', () => audio.toggleLyraRec());
addListener(document.getElementById('lyraPlayButton'), 'click', () => audio.toggleLyraPlay());
addListener(document.getElementById('lyraWipeButton'), 'click', () => audio.wipeLyraLoop());
const keyMap = {
'KeyA': 1, 'KeyS': 1, 'KeyD': 1, 'KeyF': 1, 'KeyG': 1, 'KeyH': 1, 'KeyJ': 1, 'KeyK': 1, 'KeyL': 1,
'KeyW': 1, 'KeyE': 1, 'KeyT': 1, 'KeyY': 1, 'KeyU': 1, 'KeyO': 1
};
const onKeyDown = e => {
if (keyMap[e.code] && !e.repeat) {
if (audio.audioCtx.state === "suspended") audio.audioCtx.resume();
audio.handleLyraKey(e.code, true);
}
};
const onKeyUp = e => {
if (keyMap[e.code]) audio.handleLyraKey(e.code, false);
};
addListener(document, 'keydown', onKeyDown);
addListener(document, 'keyup', onKeyUp);
const sS = document.getElementById('patchSource'),
dS = document.getElementById('patchDest');
audio.getModSources().forEach(s => sS.innerHTML += `<option value="${s}">${s}</option>`);
audio.getModDestinations().forEach(d => dS.innerHTML += `<option value="${d}">${d}</option>`);
addListener(document.getElementById('addPatchButton'), 'click', () => {
const amtRaw = parseFloat(document.getElementById('patchAmount').value);
const amt = Math.round((isNaN(amtRaw) ? 0 : amtRaw) * 100) / 100;
audio.addPatch(sS.value, dS.value, amt);
this.updateModulationHighlights();
});
addListener(document.getElementById('activePatches'), 'click', (e) => {
const patchInstance = e.target.closest('.patch-instance');
if (!patchInstance) return;
if (e.target.tagName === 'BUTTON' && e.target.textContent === 'X') {
if (audio.removePatch(patchInstance.dataset.patchId)) {
audio.renderPatchesUI();
this.updateModulationHighlights();
}
} else {
audio.openPatchEditor(patchInstance.dataset.patchId);
}
});
const patchBay = document.getElementById('patchBay');
const globalControlsContainer = document.createElement('div');
globalControlsContainer.className = 'control-group';
globalControlsContainer.style.marginTop = '15px';
const title = document.createElement('h4');
title.textContent = 'GLOBAL CONTROLS';
globalControlsContainer.appendChild(title);
const buttonRow = document.createElement('div');
buttonRow.className = 'row';
const freezeButton = document.createElement("button");
freezeButton.id = "freezeModsButton";
freezeButton.textContent = "FREEZE MODS";
freezeButton.style.fontSize = '0.8em';
buttonRow.appendChild(freezeButton);
let modsFrozen = false;
addListener(freezeButton, "click", () => {
modsFrozen = !modsFrozen;
audio.modsFrozen = modsFrozen;
audio.patches.forEach(p => {
if (modsFrozen) {
p.isLocked = true;
p.lockedValue = audio.modSources[p.source].value;
} else {
p.isLocked = false;
}
});
freezeButton.textContent = modsFrozen ? "UNFREEZE" : "FREEZE MODS";
freezeButton.classList.toggle('active', modsFrozen);
});
const muteButton = document.createElement("button");
muteButton.id = "modMuteButton";
muteButton.textContent = "MUTE MODS";
muteButton.style.fontSize = '0.8em';
buttonRow.appendChild(muteButton);
addListener(muteButton, "click", () => {
audio.isMuted = !audio.isMuted;
if (audio.isMuted) {
audio.resetAllModulatedParamsToRaw();
}
muteButton.textContent = audio.isMuted ? "UNMUTE" : "MUTE MODS";
muteButton.classList.toggle('active', audio.isMuted);
});
const shuffleButton = document.createElement("button");
shuffleButton.id = "shufflePatchesButton";
shuffleButton.textContent = "SHUFFLE";
shuffleButton.style.fontSize = '0.8em';
buttonRow.appendChild(shuffleButton);
addListener(shuffleButton, "click", () => {
const p = audio.patches;
for (let i = p.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[p[i], p[j]] = [p[j], p[i]];
}
audio.renderPatchesUI();
this.updateModulationHighlights();
});
globalControlsContainer.appendChild(buttonRow);
const gmDiv = document.createElement("div");
gmDiv.className = 'row';
gmDiv.style.marginTop = "15px";
gmDiv.innerHTML = `<label for="globalModScale" style="font-size:0.8em;">GLOBAL MOD:</label><input type="range" id="globalModScale" min="0" max="2" step="0.01" value="1" data-default="1"><span class="param-value" style="min-width: 40px;">1.00</span>`;
globalControlsContainer.appendChild(gmDiv);
const gms = gmDiv.querySelector("#globalModScale");
addListener(gms, "input", e => {
audio.globalModScale = parseFloat(e.target.value);
});
patchBay.insertBefore(globalControlsContainer, patchBay.querySelector('#activePatches'));
for (const t in audio.sequences) {
const c = document.querySelector(`.pattern-row[data-type="${t}"] .steps-container`);
if (!c) continue;
for (let i = 0; i < 32; i++) {
const s = document.createElement('div');
s.className = 'step';
s.dataset.index = i;
s.classList.toggle('active', audio.sequences[t][i]);
addListener(s, 'click', (e) => {
if (e.shiftKey) {
if (!audio.sequences[t][i]) return;
const meta = audio.stepMeta[t][i] = audio.stepMeta[t][i] || {
velocity: 1,
prob: 1,
ratchet: 1
};
const currentCond = meta.triggerPass || 'always';
if (currentCond === 'always') meta.triggerPass = 'first';
else if (currentCond === 'first') meta.triggerPass = 'second';
else meta.triggerPass = 'always';
audio.updatePatternEditor();
return;
}
if (e.ctrlKey || e.metaKey) {
if (t === 'bass' && audio.sequences.bass[i]) {
const meta = audio.stepMeta.bass[i] = audio.stepMeta.bass[i] || {};
meta.accent = !meta.accent;
audio.updatePatternEditor();
}
return;
}
audio.sequences[t][i] = !audio.sequences[t][i];
audio.updatePatternEditor();
});
addListener(s, 'contextmenu', e => {
e.preventDefault();
e.stopPropagation();
if (!audio.sequences[t][i]) return;
const meta = (audio.stepMeta[t] && audio.stepMeta[t][i]) ? audio.stepMeta[t][i] : {
velocity: 1,
prob: 1,
ratchet: 1,
accent: false
};
openCellEditor(audio, t, i, meta);
});
c.appendChild(s);
}
}
document.querySelectorAll('.pattern-label').forEach(label => {
const type = label.closest('.pattern-row').dataset.type;
addListener(label, 'click', e => {
e.preventDefault();
openRowContextMenu(audio, type, e);
});
});
const kaosPad = document.getElementById('kaosPad');
let isKaosDragging = false;
const handleKaosMove = e => {
if (!isKaosDragging) return;
e.preventDefault();
const r = kaosPad.getBoundingClientRect(),
cX = e.touches ? e.touches[0].clientX : e.clientX,
cY = e.touches ? e.touches[0].clientY : e.clientY;
let x = (cX - r.left) / r.width,
y = (cY - r.top) / r.height;
x = Math.max(0, Math.min(1, x));
y = Math.max(0, Math.min(1, y));
kaosPad.firstElementChild.style.left = `${x*100}%`;
kaosPad.firstElementChild.style.top = `${y*100}%`;
audio.modSources['KAOS X'].value = x;
audio.modSources['KAOS Y'].value = 1.0 - y;
};
const startDrag = e => {
isKaosDragging = true;
handleKaosMove(e);
};
const stopDrag = () => {
isKaosDragging = false;
};
addListener(kaosPad, 'mousedown', startDrag);
addListener(document, 'mousemove', handleKaosMove);
addListener(document, 'mouseup', stopDrag);
addListener(kaosPad, 'touchstart', startDrag, {
passive: false
});
addListener(document, 'touchmove', handleKaosMove, {
passive: false
});
addListener(document, 'touchend', stopDrag);
addListener(document, 'touchcancel', stopDrag);
addListener(document, "keydown", e => {
if (e.code === "Space" && !e.target.closest('input,button,select,textarea')) {
e.preventDefault();
audio.togglePlay();
}
});
addListener(document, 'visibilitychange', () => {
if (document.visibilityState === 'visible' && audio.audioCtx.state === 'suspended') {
audio.audioCtx.resume();
}
});
addListener(document.getElementById('lyraSeqToggle'), 'click', () => audio.toggleLyraSequencer());
addListener(document.getElementById('lyraSeqRandomize'), 'click', () => audio.randomizeLyraSequence());
addListener(document.getElementById('lyraPortamento'), 'input', e => {
audio.lyraParams.portamento = parseFloat(e.target.value) / 1000;
});
addListener(document.getElementById('randomizeLyraSettingsButton'), 'click', () => {
this.audioSystem.randomizeLyraSettings();
this.syncUiToAudioState();
});
addListener(document.getElementById('lyraStepGrid'), 'click', e => {
const stepEl = e.target.closest('.lyra-step');
if (!stepEl) return;
const stepIndex = parseInt(stepEl.dataset.step, 10);
if (isNaN(stepIndex)) return;
const step = audio.lyraSequencer.steps[stepIndex];
if (e.shiftKey) {
step.slide = !step.slide;
} else if (e.ctrlKey || e.metaKey) {
step.accent = !step.accent;
} else {
step.active = !step.active;
}
audio.updateLyraSequencerUI();
});
addListener(document.getElementById('lyraStepGrid'), 'contextmenu', e => {
e.preventDefault();
const stepEl = e.target.closest('.lyra-step');
if (!stepEl) return;
const stepIndex = parseInt(stepEl.dataset.step, 10);
if (isNaN(stepIndex)) return;
openSynthStepEditor(audio, stepIndex);
});
}
}
window.app = new App();
</script>
</body>
</html>
// // pastable import preset
// const myPreset = {
// controls: [
// {
// id: "masterVolume",
// value: "0.4"
// },
// {
// id: "bpm",
// value: "114"
// },
// {
// id: "presetSelect",
// value: "white dwarf"
// },
// {
// id: "presetNameInput",
// value: "icy particles"
// },
// {
// id: "lyraPortamento",
// value: "166"
// },
// {
// id: "lyraOsc1Type",
// value: "square"
// },
// {
// id: "lyraOsc1Oct",
// value: "-1"
// },
// {
// id: "lyraOsc2Type",
// value: "square"
// },
// {
// id: "lyraOsc2Oct",
// value: "-1"
// },
// {
// id: "lyraDetune",
// value: "0"
// },
// {
// id: "lyraFMAmt",
// value: "0"
// },
// {
// id: "lyraWaveFold",
// value: "0"
// },
// {
// id: "lyraDrive",
// value: "0"
// },
// {
// id: "lyraFilterType",
// value: "bandpass"
// },
// {
// id: "lyraFilterCutoff",
// value: "8564"
// },
// {
// id: "lyraFilterQ",
// value: "0"
// },
// {
// id: "lyraFilterEnv",
// value: "-3802"
// },
// {
// id: "lyraLfoRate",
// value: "4.1"
// },
// {
// id: "lyraLfoSyncRate",
// value: "0.25"
// },
// {
// id: "lyraLfoSyncToggle",
// value: true
// },
// {
// id: "lyraLfoDepth",
// value: "431"
// },
// {
// id: "lyraAttack",
// value: "0.01"
// },
// {
// id: "lyraDecay",
// value: "0.01"
// },
// {
// id: "lyraSustain",
// value: "0.02"
// },
// {
// id: "lyraRelease",
// value: "1.69"
// },
// {
// id: "lyraFxType",
// value: "Chopper"
// },
// {
// id: "lyraDelayTime",
// value: "0.22"
// },
// {
// id: "lyraDelayFeedback",
// value: "0"
// },
// {
// id: "lyraDelayMix",
// value: "0.85"
// },
// {
// id: "phaserRate",
// value: "4.2"
// },
// {
// id: "phaserDepth",
// value: "1200"
// },
// {
// id: "phaserFeedback",
// value: "0.56"
// },
// {
// id: "phaserMix",
// value: "0.5"
// },
// {
// id: "flangerRate",
// value: "0.2"
// },
// {
// id: "flangerDepth",
// value: "0.005"
// },
// {
// id: "flangerFeedback",
// value: "0.5"
// },
// {
// id: "flangerMix",
// value: "0.5"
// },
// {
// id: "quazarRate",
// value: "0.4"
// },
// {
// id: "quazarReso",
// value: "15.5"
// },
// {
// id: "quazarFreq",
// value: "4440"
// },
// {
// id: "quazarMix",
// value: "0.46"
// },
// {
// id: "chopperRate",
// value: "0.5"
// },
// {
// id: "chopperDepth",
// value: "0.27"
// },
// {
// id: "crusherBitDepth",
// value: "1"
// },
// {
// id: "crusherFreq",
// value: "1"
// },
// {
// id: "crusherMix",
// value: "0.81"
// },
// {
// id: "patchSource",
// value: "S&H (2 steps)"
// },
// {
// id: "patchDest",
// value: "SNARE DECAY"
// },
// {
// id: "patchAmount",
// value: "1.9"
// },
// {
// id: "kickPitch",
// value: "41"
// },
// {
// id: "kickDecay",
// value: "0.23"
// },
// {
// id: "kickDrive",
// value: "0.48"
// },
// {
// id: "bassOctave",
// value: "0"
// },
// {
// id: "bassDetune",
// value: "1"
// },
// {
// id: "bassFilterCutoff",
// value: "353"
// },
// {
// id: "bassFilterQ",
// value: "14"
// },
// {
// id: "snareTone",
// value: "122"
// },
// {
// id: "snareSnappy",
// value: "1620"
// },
// {
// id: "snareDecay",
// value: "0.06"
// },
// {
// id: "hatDecay",
// value: "0.1"
// },
// {
// id: "hatMetal",
// value: "5879"
// },
// {
// id: "masterDist",
// value: "0"
// },
// {
// id: "masterReverb",
// value: "0.08"
// }
// ],
// patches: [
// {
// source: "S&H (8 steps)",
// dest: "MASTER REVERB",
// amount: "1.90",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (32 steps)",
// dest: "HHT DECAY",
// amount: 1.9,
// modRangeMin: -1,
// modRangeMax: 0.57
// },
// {
// source: "S&H (32 steps)",
// dest: "LYRA OSC2 OCT",
// amount: "1.90",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (4 steps)",
// dest: "LYRA OSC1 OCT",
// amount: "1.90",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (64 steps)",
// dest: "LYRA SLIDE",
// amount: "1.90",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (64 steps)",
// dest: "LYRA DECAY",
// amount: 1.25,
// modRangeMin: -0.32,
// modRangeMax: 0.19
// },
// {
// source: "S&H (2 steps)",
// dest: "SNARE DECAY",
// amount: "1.90",
// modRangeMin: -1,
// modRangeMax: 1
// }
// ],
// sequences: {
// kick: [
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false
// ],
// snare: [
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false
// ],
// hat: [
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false
// ],
// bass: [
// true,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// true,
// true,
// true,
// false,
// false,
// true,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// true,
// false,
// false,
// false
// ],
// lyraGate: [
// true,
// false,
// true,
// false,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// false,
// true,
// true,
// false,
// false,
// true,
// false,
// true
// ]
// },
// lyraSequencer: {
// isPlaying: true,
// steps: [
// {
// active: true,
// note: 14,
// slide: false,
// accent: true
// },
// {
// active: true,
// note: 3,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 14,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 7,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 10,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 2,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 12,
// slide: true,
// accent: true
// },
// {
// active: true,
// note: 7,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 2,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 3,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 8,
// slide: true,
// accent: true
// },
// {
// active: true,
// note: 10,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 10,
// slide: true,
// accent: true
// },
// {
// active: false,
// note: 3,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: true,
// accent: false
// }
// ]
// },
// stepMeta: {
// kick: {},
// snare: {},
// hat: {},
// bass: {},
// lyraGate: {}
// },
// version: 2
// };
// // pastable import preset
// const myPreset = {
// controls: [
// {
// id: "masterVolume",
// value: "0.4"
// },
// {
// id: "bpm",
// value: "125"
// },
// {
// id: "swing",
// value: "0.1"
// },
// {
// id: "presetSelect",
// value: "quantum mass"
// },
// {
// id: "presetNameInput",
// value: "quantum mass"
// },
// {
// id: "lyraPortamento",
// value: "0"
// },
// {
// id: "lyraOsc1Type",
// value: "sawtooth"
// },
// {
// id: "lyraOsc1Oct",
// value: "2"
// },
// {
// id: "lyraOsc2Type",
// value: "square"
// },
// {
// id: "lyraOsc2Oct",
// value: "1"
// },
// {
// id: "lyraDetune",
// value: "13"
// },
// {
// id: "lyraFMAmt",
// value: "80"
// },
// {
// id: "lyraWaveFold",
// value: "0.07"
// },
// {
// id: "lyraDriveMode",
// value: "soft clip"
// },
// {
// id: "lyraDrive",
// value: "0.38"
// },
// {
// id: "lyraFilterType",
// value: "notch"
// },
// {
// id: "lyraFilterCutoff",
// value: "4981"
// },
// {
// id: "lyraFilterQ",
// value: "0.1"
// },
// {
// id: "lyraFilterEnv",
// value: "-942"
// },
// {
// id: "lyraLfoRate",
// value: "0.3"
// },
// {
// id: "lyraLfoSyncRate",
// value: "0.25"
// },
// {
// id: "lyraLfoSyncToggle",
// value: true
// },
// {
// id: "lyraLfoDepth",
// value: "1499"
// },
// {
// id: "lyraAttack",
// value: "0.02"
// },
// {
// id: "lyraDecay",
// value: "0.08"
// },
// {
// id: "lyraSustain",
// value: "0.04"
// },
// {
// id: "lyraRelease",
// value: "0.07"
// },
// {
// id: "lyraFxType",
// value: "Delay"
// },
// {
// id: "lyraDelayTime",
// value: "0.14"
// },
// {
// id: "lyraDelayFeedback",
// value: "0.37"
// },
// {
// id: "lyraDelayMix",
// value: "0.2"
// },
// {
// id: "phaserRate",
// value: "0.5"
// },
// {
// id: "phaserDepth",
// value: "1200"
// },
// {
// id: "phaserFeedback",
// value: "0.3"
// },
// {
// id: "phaserMix",
// value: "0.5"
// },
// {
// id: "flangerRate",
// value: "0.2"
// },
// {
// id: "flangerDepth",
// value: "0.005"
// },
// {
// id: "flangerFeedback",
// value: "0.5"
// },
// {
// id: "flangerMix",
// value: "0.5"
// },
// {
// id: "quazarRate",
// value: "2"
// },
// {
// id: "quazarReso",
// value: "25"
// },
// {
// id: "quazarFreq",
// value: "1500"
// },
// {
// id: "quazarMix",
// value: "1"
// },
// {
// id: "chopperRate",
// value: "2"
// },
// {
// id: "chopperDepth",
// value: "1"
// },
// {
// id: "crusherBitDepth",
// value: "8"
// },
// {
// id: "crusherFreq",
// value: "0.5"
// },
// {
// id: "crusherMix",
// value: "0.5"
// },
// {
// id: "patchSource",
// value: "S&H (8 steps)"
// },
// {
// id: "patchDest",
// value: "LYRA OSC2 OCT"
// },
// {
// id: "patchAmount",
// value: "0.5"
// },
// {
// id: "globalModScale",
// value: "1"
// },
// {
// id: "kickPitch",
// value: "40"
// },
// {
// id: "kickDecay",
// value: "0.35"
// },
// {
// id: "kickDrive",
// value: "0.5"
// },
// {
// id: "bassOctave",
// value: "-2"
// },
// {
// id: "bassDetune",
// value: "5"
// },
// {
// id: "bassFilterCutoff",
// value: "400"
// },
// {
// id: "bassFilterQ",
// value: "10"
// },
// {
// id: "snareTone",
// value: "180"
// },
// {
// id: "snareSnappy",
// value: "3000"
// },
// {
// id: "snareDecay",
// value: "0.2"
// },
// {
// id: "hatDecay",
// value: "0.3"
// },
// {
// id: "hatMetal",
// value: "6674"
// },
// {
// id: "masterDist",
// value: "0.08"
// },
// {
// id: "masterReverb",
// value: "0.5"
// }
// ],
// patches: [
// {
// source: "S&H (Random)",
// dest: "BASS FILTER",
// amount: 0.8,
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LFO 1 (Slow)",
// dest: "HHT DECAY",
// amount: 1,
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "KAOS X",
// dest: "LYRA FM AMT",
// amount: 1.5,
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "KAOS Y",
// dest: "LYRA DELAY FEEDBACK",
// amount: 0.9,
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (8 steps)",
// dest: "LYRA OSC2 OCT",
// amount: 0.5,
// modRangeMin: -0.5,
// modRangeMax: 1
// }
// ],
// isSafetyOn: false,
// sequences: {
// kick: [
// true,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// true,
// false,
// false,
// false,
// false,
// true
// ],
// snare: [
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false
// ],
// hat: [
// false,
// false,
// true,
// true,
// false,
// false,
// true,
// true,
// false,
// false,
// true,
// false,
// false,
// true,
// true,
// false,
// false,
// false,
// true,
// true,
// false,
// false,
// true,
// true,
// false,
// false,
// true,
// true,
// false,
// false,
// true,
// false
// ],
// bass: [
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false
// ],
// lyraGate: [
// true,
// true,
// true,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// true,
// false,
// true,
// false,
// true,
// true,
// true,
// false,
// false,
// false,
// true,
// false,
// true,
// true,
// false,
// true,
// false,
// true
// ]
// },
// lyraSequencer: {
// isPlaying: true,
// steps: [
// {
// active: true,
// note: 0,
// slide: false,
// accent: true
// },
// {
// active: false,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 7,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 3,
// slide: false,
// accent: true
// },
// {
// active: false,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 3,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: true
// },
// {
// active: false,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 7,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 8,
// slide: true,
// accent: true
// },
// {
// active: false,
// note: 0,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 7,
// slide: false,
// accent: false
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false
// }
// ]
// },
// stepMeta: {
// kick: {
// 2: {
// velocity: 0.35,
// prob: 0.12,
// ratchet: 1
// }
// },
// snare: {},
// hat: {
// 2: {
// velocity: 1,
// prob: 0.06451612903225806,
// ratchet: 1
// },
// 3: {
// velocity: 1,
// prob: 0.0967741935483871,
// ratchet: 1
// },
// 6: {
// velocity: 1,
// prob: 0.1935483870967742,
// ratchet: 1
// },
// 7: {
// velocity: 1,
// prob: 0.22580645161290322,
// ratchet: 1
// },
// 10: {
// velocity: 1,
// prob: 0.3225806451612903,
// ratchet: 1
// },
// 13: {
// velocity: 1,
// prob: 0.41935483870967744,
// ratchet: 1
// },
// 14: {
// velocity: 1,
// prob: 0.45161290322580644,
// ratchet: 1
// },
// 18: {
// velocity: 1,
// prob: 0.5806451612903226,
// ratchet: 1
// },
// 19: {
// velocity: 1,
// prob: 0.6129032258064516,
// ratchet: 1
// },
// 22: {
// velocity: 1,
// prob: 0.7096774193548387,
// ratchet: 1
// },
// 23: {
// velocity: 1,
// prob: 0.7419354838709677,
// ratchet: 1
// },
// 26: {
// velocity: 1,
// prob: 0.8387096774193549,
// ratchet: 1
// },
// 27: {
// velocity: 1,
// prob: 0.8709677419354839,
// ratchet: 1
// },
// 30: {
// velocity: 1,
// prob: 0.967741935483871,
// ratchet: 1
// }
// },
// bass: {},
// lyraGate: {}
// },
// version: 2
// };
// const myPreset = {
// controls: [
// {
// id: "masterVolume",
// value: "0.4"
// },
// {
// id: "bpm",
// value: "84"
// },
// {
// id: "swing",
// value: "0"
// },
// {
// id: "presetSelect",
// value: "waning crescent"
// },
// {
// id: "presetNameInput",
// value: "waning crescent"
// },
// {
// id: "lyraPortamento",
// value: "0"
// },
// {
// id: "lyraOsc1Type",
// value: "sine"
// },
// {
// id: "lyraOsc1Oct",
// value: "-2"
// },
// {
// id: "lyraOsc2Type",
// value: "sine"
// },
// {
// id: "lyraOsc2Oct",
// value: "0"
// },
// {
// id: "lyraDetune",
// value: "21"
// },
// {
// id: "lyraFMAmt",
// value: "43"
// },
// {
// id: "lyraWaveFold",
// value: "0"
// },
// {
// id: "lyraDriveMode",
// value: "wavefold"
// },
// {
// id: "lyraDrive",
// value: "0"
// },
// {
// id: "lyraFilterType",
// value: "lowpass"
// },
// {
// id: "lyraFilterCutoff",
// value: "19620"
// },
// {
// id: "lyraFilterQ",
// value: "1.8"
// },
// {
// id: "lyraFilterEnv",
// value: "2580"
// },
// {
// id: "lyraLfoRate",
// value: "1.2"
// },
// {
// id: "lyraLfoSyncRate",
// value: "0.25"
// },
// {
// id: "lyraLfoSyncToggle",
// value: true
// },
// {
// id: "lyraLfoDepth",
// value: "2635"
// },
// {
// id: "lyraAttack",
// value: "1.05"
// },
// {
// id: "lyraDecay",
// value: "0.26"
// },
// {
// id: "lyraSustain",
// value: "0.17"
// },
// {
// id: "lyraRelease",
// value: "1.58"
// },
// {
// id: "lyraFxType",
// value: "Chopper"
// },
// {
// id: "lyraDelayTime",
// value: "0.32"
// },
// {
// id: "lyraDelayFeedback",
// value: "0.13"
// },
// {
// id: "lyraDelayMix",
// value: "0.18"
// },
// {
// id: "phaserRate",
// value: "4.2"
// },
// {
// id: "phaserDepth",
// value: "1200"
// },
// {
// id: "phaserFeedback",
// value: "0.56"
// },
// {
// id: "phaserMix",
// value: "0.5"
// },
// {
// id: "flangerRate",
// value: "0.2"
// },
// {
// id: "flangerDepth",
// value: "0.005"
// },
// {
// id: "flangerFeedback",
// value: "0.5"
// },
// {
// id: "flangerMix",
// value: "0.5"
// },
// {
// id: "quazarRate",
// value: "0.4"
// },
// {
// id: "quazarReso",
// value: "15.5"
// },
// {
// id: "quazarFreq",
// value: "4440"
// },
// {
// id: "quazarMix",
// value: "0.46"
// },
// {
// id: "chopperRate",
// value: "0.5"
// },
// {
// id: "chopperDepth",
// value: "0.06"
// },
// {
// id: "crusherBitDepth",
// value: "8"
// },
// {
// id: "crusherFreq",
// value: "0.5"
// },
// {
// id: "crusherMix",
// value: "0.5"
// },
// {
// id: "patchSource",
// value: "LFO 1 (Slower)"
// },
// {
// id: "patchDest",
// value: "LYRA OSC2 TYPE"
// },
// {
// id: "patchAmount",
// value: "-2"
// },
// {
// id: "globalModScale",
// value: "1.03"
// },
// {
// id: "kickPitch",
// value: "47"
// },
// {
// id: "kickDecay",
// value: "0.32"
// },
// {
// id: "kickDrive",
// value: "0.1"
// },
// {
// id: "bassOctave",
// value: "-2"
// },
// {
// id: "bassDetune",
// value: "0"
// },
// {
// id: "bassFilterCutoff",
// value: "100"
// },
// {
// id: "bassFilterQ",
// value: "0"
// },
// {
// id: "snareTone",
// value: "167"
// },
// {
// id: "snareSnappy",
// value: "1259"
// },
// {
// id: "snareDecay",
// value: "0.12"
// },
// {
// id: "hatDecay",
// value: "0.18"
// },
// {
// id: "hatMetal",
// value: "6930"
// },
// {
// id: "masterDist",
// value: "0.02"
// },
// {
// id: "masterReverb",
// value: "0.26"
// }
// ],
// patches: [
// {
// source: "CLK/32",
// dest: "LYRA DRIVE",
// amount: 0.05,
// modRangeMin: -1,
// modRangeMax: 0.5
// },
// {
// source: "S&H (8 steps)",
// dest: "LYRA OSC1 OCT",
// amount: 0.5,
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (2 steps)",
// dest: "MASTER REVERB",
// amount: "2.00",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LFO 1 (Slower)",
// dest: "LYRA OSC2 TYPE",
// amount: "-2.00",
// modRangeMin: -1,
// modRangeMax: 1
// }
// ],
// isSafetyOn: false,
// sequences: {
// kick: [
// true,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false
// ],
// snare: [
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false
// ],
// hat: [
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false
// ],
// bass: [
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true
// ],
// lyraGate: [
// false,
// true,
// false,
// true,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// true,
// false,
// true,
// false,
// true,
// true,
// false,
// true,
// false,
// true,
// true,
// false,
// true,
// true,
// true,
// false,
// true,
// false,
// true,
// true,
// true
// ]
// },
// lyraSequencer: {
// isPlaying: true,
// steps: [
// {
// active: false,
// note: 0,
// slide: false,
// accent: true,
// gate: 1
// },
// {
// active: true,
// note: 3,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: false,
// note: 2,
// slide: true,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 14,
// slide: true,
// accent: false,
// gate: 1
// },
// {
// active: false,
// note: 2,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 5,
// slide: true,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 14,
// slide: false,
// accent: true,
// gate: 1
// },
// {
// active: false,
// note: 14,
// slide: true,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 0,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: false,
// note: 15,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 7,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 15,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: false,
// note: 3,
// slide: false,
// accent: false,
// gate: 1
// },
// {
// active: true,
// note: 14,
// slide: false,
// accent: true,
// gate: 1
// },
// {
// active: true,
// note: 12,
// slide: false,
// accent: true,
// gate: 1
// }
// ]
// },
// stepMeta: {
// kick: {},
// snare: {},
// hat: {},
// bass: {},
// lyraGate: {}
// },
// version: 2
// };
// const myPreset = {
// controls: [
// {
// id: "masterVolume",
// value: "0.4"
// },
// {
// id: "bpm",
// value: "111"
// },
// {
// id: "swing",
// value: "0"
// },
// {
// id: "kickVolume",
// value: "1"
// },
// {
// id: "snareVolume",
// value: "0.8"
// },
// {
// id: "hatVolume",
// value: "0.8"
// },
// {
// id: "bassVolume",
// value: "0.8"
// },
// {
// id: "synthVolume",
// value: "0.7"
// },
// {
// id: "presetSelect",
// value: "solar flare"
// },
// {
// id: "presetNameInput",
// value: "lunar standstill"
// },
// {
// id: "lyraPortamento",
// value: "0"
// },
// {
// id: "lyraOsc1Type",
// value: "triangle"
// },
// {
// id: "lyraOsc1Oct",
// value: "1"
// },
// {
// id: "lyraOsc2Type",
// value: "sine"
// },
// {
// id: "lyraOsc2Oct",
// value: "0"
// },
// {
// id: "lyraDetune",
// value: "9"
// },
// {
// id: "lyraFMAmt",
// value: "65"
// },
// {
// id: "lyraWaveFold",
// value: "0.14"
// },
// {
// id: "lyraDriveMode",
// value: "soft clip"
// },
// {
// id: "lyraDrive",
// value: "0.06"
// },
// {
// id: "lyraFilterType",
// value: "highpass"
// },
// {
// id: "lyraFilterCutoff",
// value: "12527"
// },
// {
// id: "lyraFilterQ",
// value: "1.1"
// },
// {
// id: "lyraFilterEnv",
// value: "-4288"
// },
// {
// id: "lyraLfoRate",
// value: "0.3"
// },
// {
// id: "lyraLfoSyncRate",
// value: "2"
// },
// {
// id: "lyraLfoSyncToggle",
// value: true
// },
// {
// id: "lyraLfoDepth",
// value: "881"
// },
// {
// id: "lyraAttack",
// value: "0.13"
// },
// {
// id: "lyraDecay",
// value: "0.05"
// },
// {
// id: "lyraSustain",
// value: "0.01"
// },
// {
// id: "lyraRelease",
// value: "0.22"
// },
// {
// id: "lyraFxType",
// value: "Chopper"
// },
// {
// id: "lyraDelayTime",
// value: "0.45"
// },
// {
// id: "lyraDelayFeedback",
// value: "0.2"
// },
// {
// id: "lyraDelayMix",
// value: "0.35"
// },
// {
// id: "phaserRate",
// value: "0.5"
// },
// {
// id: "phaserDepth",
// value: "1200"
// },
// {
// id: "phaserFeedback",
// value: "0.3"
// },
// {
// id: "phaserMix",
// value: "0.5"
// },
// {
// id: "flangerRate",
// value: "0.2"
// },
// {
// id: "flangerDepth",
// value: "0.005"
// },
// {
// id: "flangerFeedback",
// value: "0.5"
// },
// {
// id: "flangerMix",
// value: "0.5"
// },
// {
// id: "quazarRate",
// value: "2"
// },
// {
// id: "quazarReso",
// value: "25"
// },
// {
// id: "quazarFreq",
// value: "1500"
// },
// {
// id: "quazarMix",
// value: "1"
// },
// {
// id: "chopperRate",
// value: "1"
// },
// {
// id: "chopperDepth",
// value: "0.22"
// },
// {
// id: "crusherBitDepth",
// value: "8"
// },
// {
// id: "crusherFreq",
// value: "0.5"
// },
// {
// id: "crusherMix",
// value: "0.5"
// },
// {
// id: "patchSource",
// value: "LFO 1 (Slow)"
// },
// {
// id: "patchDest",
// value: "LYRA GATE PROB"
// },
// {
// id: "patchAmount",
// value: "0.7"
// },
// {
// id: "globalModScale",
// value: "1"
// },
// {
// id: "kickPitch",
// value: "35"
// },
// {
// id: "kickDecay",
// value: "0.15"
// },
// {
// id: "kickDrive",
// value: "0.39"
// },
// {
// id: "bassOctave",
// value: "-2"
// },
// {
// id: "bassDetune",
// value: "17"
// },
// {
// id: "bassFilterCutoff",
// value: "4332"
// },
// {
// id: "bassFilterQ",
// value: "8"
// },
// {
// id: "snareTone",
// value: "256"
// },
// {
// id: "snareSnappy",
// value: "2342"
// },
// {
// id: "snareDecay",
// value: "0.12"
// },
// {
// id: "hatDecay",
// value: "0.3"
// },
// {
// id: "hatMetal",
// value: "6807"
// },
// {
// id: "masterDist",
// value: "0.21"
// },
// {
// id: "masterReverb",
// value: "0.2"
// }
// ],
// patches: [
// {
// source: "LYRA ENV",
// dest: "HHT METAL",
// amount: "1.20",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LYRA LFO",
// dest: "SD SNAPPY",
// amount: "1.20",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "S&H (Random)",
// dest: "SD TONE",
// amount: "1.20",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LFO 1 (Slow)",
// dest: "BASS OCTAVE",
// amount: "1.20",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "CLK/2",
// dest: "MASTER REVERB",
// amount: "1.20",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LFO 2 (Fast)",
// dest: "LYRA LFO RATE",
// amount: "2.00",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "CLK/4",
// dest: "SD SNAPPY",
// amount: "2.00",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LYRA LFO",
// dest: "LYRA LFO DEPTH",
// amount: "2.00",
// modRangeMin: -1,
// modRangeMax: 1
// },
// {
// source: "LFO 1 (Slow)",
// dest: "LYRA GATE PROB",
// amount: "2.00",
// modRangeMin: -1,
// modRangeMax: 1
// }
// ],
// isSafetyOn: false,
// sequences: {
// kick: [
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// true,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// true,
// true,
// false,
// false,
// false,
// false,
// true,
// true,
// false,
// true,
// false,
// false,
// true,
// false
// ],
// snare: [
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// true,
// false,
// false,
// false,
// false,
// true,
// false,
// false,
// false
// ],
// hat: [
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true,
// true
// ],
// bass: [
// false,
// false,
// true,
// true,
// true,
// false,
// true,
// true,
// false,
// true,
// false,
// true,
// false,
// false,
// false,
// true,
// false,
// true,
// true,
// false,
// true,
// false,
// true,
// true,
// true,
// false,
// false,
// false,
// false,
// true,
// true,
// false
// ],
// lyraGate: [
// true,
// false,
// true,
// true,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// true,
// true,
// false,
// true,
// true,
// true,
// true
// ]
// },
// lyraSequencer: {
// isPlaying: true,
// steps: [
// {
// active: true,
// note: 8,
// slide: true,
// accent: true
// },
// {
// active: true,
// note: 7,
// slide: true,
// accent: true
// },
// {
// active: false,
// note: 15,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 0,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 10,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 3,
// slide: true,
// accent: false
// },
// {
// active: false,
// note: 10,
// slide: false,
// accent: true
// },
// {
// active: false,
// note: 5,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 10,
// slide: false,
// accent: true
// },
// {
// active: true,
// note: 15,
// slide: true,
// accent: false
// },
// {
// active: false,
// note: 7,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 3,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 8,
// slide: false,
// accent: true
// },
// {
// active: true,
// note: 14,
// slide: true,
// accent: false
// },
// {
// active: true,
// note: 8,
// slide: false,
// accent: false
// },
// {
// active: false,
// note: 10,
// slide: false,
// accent: false
// }
// ]
// },
// stepMeta: {
// kick: {},
// snare: {},
// hat: {},
// bass: {},
// lyraGate: {}
// },
// version: 2
// };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment