Sync from GitHub 46f60997
Browse files- apps/web/__tests__/page.test.tsx +126 -0
- apps/web/__tests__/setup.ts +1 -0
- apps/web/app/globals.css +69 -0
- apps/web/app/page.tsx +180 -35
- apps/web/lib/jambuddy/player.ts +70 -0
- apps/web/package.json +4 -0
- apps/web/vitest.config.ts +1 -0
- pnpm-lock.yaml +184 -0
apps/web/__tests__/page.test.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
| 2 |
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
| 3 |
+
import userEvent from "@testing-library/user-event";
|
| 4 |
+
|
| 5 |
+
// The page imports Web Audio + MIDI recorder + canvas visualizers that don't
|
| 6 |
+
// exist cleanly under happy-dom. Mock the noisy, browser-only modules so we
|
| 7 |
+
// can exercise the component's UI logic.
|
| 8 |
+
vi.mock("@/lib/jambuddy/player", async () => {
|
| 9 |
+
const actual =
|
| 10 |
+
await vi.importActual<typeof import("@/lib/jambuddy/player")>(
|
| 11 |
+
"@/lib/jambuddy/player",
|
| 12 |
+
);
|
| 13 |
+
return {
|
| 14 |
+
...actual,
|
| 15 |
+
playTogether: vi.fn(async () => ({
|
| 16 |
+
stop: vi.fn(),
|
| 17 |
+
done: new Promise<void>((r) => setTimeout(r, 5000)),
|
| 18 |
+
})),
|
| 19 |
+
playAudioTogether: vi.fn(async () => ({
|
| 20 |
+
stop: vi.fn(),
|
| 21 |
+
done: new Promise<void>((r) => setTimeout(r, 5000)),
|
| 22 |
+
})),
|
| 23 |
+
};
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
vi.mock("@/lib/jambuddy/recorder", () => ({
|
| 27 |
+
createMidiRecorder: vi.fn(async () => ({
|
| 28 |
+
start: vi.fn(),
|
| 29 |
+
stop: vi.fn(() => ({ notes: [], durationSec: 0 })),
|
| 30 |
+
isActive: vi.fn(() => false),
|
| 31 |
+
dispose: vi.fn(),
|
| 32 |
+
})),
|
| 33 |
+
recordedNotesAsFile: vi.fn(() => new File([""], "cap.mid")),
|
| 34 |
+
}));
|
| 35 |
+
|
| 36 |
+
vi.mock("@/lib/jambuddy/visualizer", () => ({
|
| 37 |
+
Visualizer: () => <div data-testid="visualizer" />,
|
| 38 |
+
}));
|
| 39 |
+
|
| 40 |
+
import HomePage from "@/app/page";
|
| 41 |
+
|
| 42 |
+
// Object URLs the component creates (take audio + response audio).
|
| 43 |
+
const createObjectURL = vi.fn(() => "blob:fake-audio");
|
| 44 |
+
vi.stubGlobal("URL.createObjectURL", createObjectURL);
|
| 45 |
+
|
| 46 |
+
/** Drive the app to a state with an audio take + a buddy response. */
|
| 47 |
+
async function setUpTakeAndResponse(user: ReturnType<typeof userEvent.setup>) {
|
| 48 |
+
render(<HomePage />);
|
| 49 |
+
// Mock the /api/jambuddy/detect + /api/jambuddy calls (fetch).
|
| 50 |
+
vi.stubGlobal(
|
| 51 |
+
"fetch",
|
| 52 |
+
vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
|
| 53 |
+
const u = String(url);
|
| 54 |
+
if (u.includes("/detect")) {
|
| 55 |
+
return new Response(
|
| 56 |
+
JSON.stringify({ bpm: 158, duration: 24 }),
|
| 57 |
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
// joinIn -> return the generated audio blob.
|
| 61 |
+
return new Response(
|
| 62 |
+
new Blob([new Uint8Array([1, 2, 3])], { type: "audio/wav" }),
|
| 63 |
+
{
|
| 64 |
+
status: 200,
|
| 65 |
+
headers: {
|
| 66 |
+
"X-Jam-Buddy-BPM": "158",
|
| 67 |
+
"X-Jam-Buddy-Time": "1000",
|
| 68 |
+
"Content-Type": "audio/wav",
|
| 69 |
+
},
|
| 70 |
+
},
|
| 71 |
+
);
|
| 72 |
+
}),
|
| 73 |
+
);
|
| 74 |
+
|
| 75 |
+
// Upload an audio take so playBoth takes the audio-mix path.
|
| 76 |
+
const file = new File([new Uint8Array([1, 2, 3, 4])], "take.wav", {
|
| 77 |
+
type: "audio/wav",
|
| 78 |
+
});
|
| 79 |
+
const input = document.querySelector<HTMLInputElement>(
|
| 80 |
+
'input[type="file"]',
|
| 81 |
+
);
|
| 82 |
+
if (!input) throw new Error("file input not found");
|
| 83 |
+
fireEvent.change(input, { target: { files: [file] } });
|
| 84 |
+
|
| 85 |
+
// Generate the response.
|
| 86 |
+
await user.click(screen.getByRole("button", { name: "JOIN IN" }));
|
| 87 |
+
await screen.findByText(/Done — your buddy produced/i);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
describe("PLAY TOGETHER toggle", () => {
|
| 91 |
+
beforeEach(() => {
|
| 92 |
+
vi.clearAllMocks();
|
| 93 |
+
vi.unstubAllGlobals();
|
| 94 |
+
vi.stubGlobal("URL.createObjectURL", createObjectURL);
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
afterEach(() => {
|
| 98 |
+
cleanup();
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
it("is disabled when there is no generated response", () => {
|
| 102 |
+
render(<HomePage />);
|
| 103 |
+
const btn = screen.getByRole("button", { name: "PLAY TOGETHER" });
|
| 104 |
+
expect(btn).toBeDisabled();
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
it("stays enabled while playing and toggles to STOP, then back", async () => {
|
| 108 |
+
const user = userEvent.setup();
|
| 109 |
+
await setUpTakeAndResponse(user);
|
| 110 |
+
|
| 111 |
+
const btn = screen.getByRole("button", { name: "PLAY TOGETHER" });
|
| 112 |
+
expect(btn).toBeEnabled();
|
| 113 |
+
|
| 114 |
+
// Start playback.
|
| 115 |
+
await user.click(btn);
|
| 116 |
+
const stopBtn = screen.getByRole("button", { name: "■ STOP" });
|
| 117 |
+
// REGRESSION GUARD: must remain enabled so it can be clicked to stop.
|
| 118 |
+
expect(stopBtn).toBeEnabled();
|
| 119 |
+
|
| 120 |
+
// Click again to stop.
|
| 121 |
+
await user.click(stopBtn);
|
| 122 |
+
expect(
|
| 123 |
+
screen.getByRole("button", { name: "PLAY TOGETHER" }),
|
| 124 |
+
).toBeEnabled();
|
| 125 |
+
});
|
| 126 |
+
});
|
apps/web/__tests__/setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import "@testing-library/jest-dom/vitest";
|
apps/web/app/globals.css
CHANGED
|
@@ -121,6 +121,75 @@
|
|
| 121 |
cursor: wait;
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
/* Big RED record button — a physical record arm with a glowing tip. */
|
| 125 |
.jambuddy-record {
|
| 126 |
border-radius: 0.5rem;
|
|
|
|
| 121 |
cursor: wait;
|
| 122 |
}
|
| 123 |
|
| 124 |
+
/* Sampler/sequencer transport pad — a big backlit pad with an LED color
|
| 125 |
+
* (set via --pad-c) that brightens when pressed. */
|
| 126 |
+
.jambuddy-padbig {
|
| 127 |
+
border-radius: 0.6rem;
|
| 128 |
+
border: 2px solid rgba(0, 0, 0, 0.6);
|
| 129 |
+
background:
|
| 130 |
+
radial-gradient(circle at 50% 30%, rgba(255, 255, 255, 0.06), transparent 60%),
|
| 131 |
+
linear-gradient(180deg, #2a2d3d 0%, #1a1c28 100%);
|
| 132 |
+
color: var(--pad-c, #f4a261);
|
| 133 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 134 |
+
font-size: 1.1rem;
|
| 135 |
+
font-weight: 800;
|
| 136 |
+
letter-spacing: 0.12em;
|
| 137 |
+
padding: 1.1rem 0;
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
box-shadow:
|
| 140 |
+
inset 0 -4px 8px rgba(0, 0, 0, 0.6),
|
| 141 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
| 142 |
+
0 3px 0 rgba(0, 0, 0, 0.5);
|
| 143 |
+
transition: transform 0.08s, box-shadow 0.08s, filter 0.15s;
|
| 144 |
+
}
|
| 145 |
+
.jambuddy-padbig:hover {
|
| 146 |
+
filter: brightness(1.12);
|
| 147 |
+
}
|
| 148 |
+
.jambuddy-padbig[aria-pressed="true"],
|
| 149 |
+
.jambuddy-padbig:active {
|
| 150 |
+
transform: translateY(2px);
|
| 151 |
+
box-shadow:
|
| 152 |
+
inset 0 2px 6px rgba(0, 0, 0, 0.6),
|
| 153 |
+
0 0 16px var(--pad-c, #f4a261),
|
| 154 |
+
0 1px 0 rgba(0, 0, 0, 0.5);
|
| 155 |
+
}
|
| 156 |
+
.jambuddy-padbig:disabled {
|
| 157 |
+
opacity: 0.4;
|
| 158 |
+
cursor: wait;
|
| 159 |
+
}
|
| 160 |
+
.jambuddy-padbig__label {
|
| 161 |
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/* Icon download button pinned beside a waveform — sized to match the track,
|
| 165 |
+
* vertically centered, with a visible border and hover glow. */
|
| 166 |
+
.jambuddy-save {
|
| 167 |
+
display: inline-flex;
|
| 168 |
+
align-items: center;
|
| 169 |
+
justify-content: center;
|
| 170 |
+
align-self: center;
|
| 171 |
+
width: 3rem;
|
| 172 |
+
height: 3rem;
|
| 173 |
+
border-radius: 0.5rem;
|
| 174 |
+
border: 1px solid #2a2d3d;
|
| 175 |
+
background: linear-gradient(180deg, #2a2d3d 0%, #1a1c28 100%);
|
| 176 |
+
color: #5fd38a;
|
| 177 |
+
cursor: pointer;
|
| 178 |
+
transition: filter 0.15s, box-shadow 0.15s, transform 0.08s;
|
| 179 |
+
}
|
| 180 |
+
.jambuddy-save:hover {
|
| 181 |
+
filter: brightness(1.2);
|
| 182 |
+
box-shadow: 0 0 10px rgba(95, 211, 138, 0.3);
|
| 183 |
+
}
|
| 184 |
+
.jambuddy-save:active {
|
| 185 |
+
transform: translateY(1px);
|
| 186 |
+
}
|
| 187 |
+
.jambuddy-save:disabled {
|
| 188 |
+
opacity: 0.4;
|
| 189 |
+
cursor: default;
|
| 190 |
+
box-shadow: none;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
/* Big RED record button — a physical record arm with a glowing tip. */
|
| 194 |
.jambuddy-record {
|
| 195 |
border-radius: 0.5rem;
|
apps/web/app/page.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
| 16 |
type BuddyMood,
|
| 17 |
type InputInstrument,
|
| 18 |
} from "@/lib/jambuddy/prompt";
|
| 19 |
-
import { parseMidi, isPercussion, midiDuration } from "@/lib/jambuddy/player";
|
| 20 |
import { createMidiRecorder, recordedNotesAsFile, type MidiRecorder } from "@/lib/jambuddy/recorder";
|
| 21 |
import { Visualizer } from "@/lib/jambuddy/visualizer";
|
| 22 |
|
|
@@ -134,8 +134,11 @@ export default function HomePage() {
|
|
| 134 |
const [midiFile, setMidiFile] = useState<File | null>(null);
|
| 135 |
const [midiBytes, setMidiBytes] = useState<ArrayBuffer | null>(null);
|
| 136 |
const [audioFile, setAudioFile] = useState<File | null>(null);
|
|
|
|
|
|
|
| 137 |
const [status, setStatus] = useState<string>("Ready.");
|
| 138 |
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
|
|
| 139 |
const [usedBpm, setUsedBpm] = useState<number | null>(null);
|
| 140 |
const [usedSeconds, setUsedSeconds] = useState<number | null>(null);
|
| 141 |
const [busy, setBusy] = useState(false);
|
|
@@ -191,6 +194,8 @@ export default function HomePage() {
|
|
| 191 |
setMidiFile(null);
|
| 192 |
setMidiBytes(null);
|
| 193 |
setAudioFile(null);
|
|
|
|
|
|
|
| 194 |
if (!f) {
|
| 195 |
setStatus("Ready.");
|
| 196 |
return;
|
|
@@ -215,6 +220,7 @@ export default function HomePage() {
|
|
| 215 |
);
|
| 216 |
} else {
|
| 217 |
setAudioFile(f);
|
|
|
|
| 218 |
// Audio is heard by the buddy (audio-to-audio). Clear the MIDI-driven
|
| 219 |
// input-instrument default so the user's declaration reflects the audio.
|
| 220 |
setInputInstrument("other");
|
|
@@ -310,6 +316,79 @@ export default function HomePage() {
|
|
| 310 |
setStatus("Saved your recorded MIDI take.");
|
| 311 |
}
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
async function joinIn() {
|
| 314 |
setBusy(true);
|
| 315 |
setStatus(
|
|
@@ -437,6 +516,13 @@ export default function HomePage() {
|
|
| 437 |
/>
|
| 438 |
</section>
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
{/* Your-take-is knob — declares what instrument the user is playing
|
| 441 |
so the buddy can complement it (no MIR on input). */}
|
| 442 |
<section
|
|
@@ -522,44 +608,44 @@ export default function HomePage() {
|
|
| 522 |
: "Local CPU · free · supports negative prompt · slower"}
|
| 523 |
</span>
|
| 524 |
</div>
|
| 525 |
-
|
|
|
|
| 526 |
<button
|
| 527 |
type="button"
|
| 528 |
onClick={toggleRecord}
|
| 529 |
disabled={busy}
|
| 530 |
aria-pressed={recording}
|
| 531 |
aria-label={recording ? "Stop recording" : "Record from MIDI controller"}
|
| 532 |
-
className="jambuddy-
|
|
|
|
| 533 |
>
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
</button>
|
| 536 |
<button
|
| 537 |
type="button"
|
| 538 |
-
onClick={
|
| 539 |
-
disabled={
|
| 540 |
-
className="jambuddy-
|
| 541 |
-
style={{
|
| 542 |
-
background: "linear-gradient(180deg,#5fd38a 0%,#3aa55f 100%)",
|
| 543 |
-
boxShadow: "0 2px 0 #256b3f",
|
| 544 |
-
}}
|
| 545 |
>
|
| 546 |
-
|
|
|
|
|
|
|
| 547 |
</button>
|
| 548 |
<button
|
| 549 |
type="button"
|
| 550 |
-
onClick={
|
| 551 |
-
disabled={
|
| 552 |
-
|
|
|
|
|
|
|
| 553 |
>
|
| 554 |
-
|
|
|
|
|
|
|
| 555 |
</button>
|
| 556 |
-
<div className="font-mono text-xs text-[#7f829c]">
|
| 557 |
-
<div className="uppercase tracking-widest">Prompt</div>
|
| 558 |
-
<div className="mt-1 max-w-[16rem] truncate text-[#e8e8f0]">
|
| 559 |
-
{prompt}
|
| 560 |
-
</div>
|
| 561 |
-
<div className="mt-1 opacity-60">neg: {negativePrompt}</div>
|
| 562 |
-
</div>
|
| 563 |
</div>
|
| 564 |
|
| 565 |
{/* Status + playback */}
|
|
@@ -588,26 +674,85 @@ export default function HomePage() {
|
|
| 588 |
</p>
|
| 589 |
)}
|
| 590 |
{audioUrl && (
|
| 591 |
-
<audio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
Your browser does not support audio playback.
|
| 593 |
</audio>
|
| 594 |
)}
|
| 595 |
</section>
|
| 596 |
|
| 597 |
-
{/* Take + response visualizers
|
| 598 |
-
|
|
|
|
| 599 |
<section
|
| 600 |
aria-label="Take and response"
|
| 601 |
-
className="mt-4
|
| 602 |
>
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
</section>
|
| 612 |
)}
|
| 613 |
</div>
|
|
|
|
| 16 |
type BuddyMood,
|
| 17 |
type InputInstrument,
|
| 18 |
} from "@/lib/jambuddy/prompt";
|
| 19 |
+
import { parseMidi, isPercussion, midiDuration, playTogether, playAudioTogether } from "@/lib/jambuddy/player";
|
| 20 |
import { createMidiRecorder, recordedNotesAsFile, type MidiRecorder } from "@/lib/jambuddy/recorder";
|
| 21 |
import { Visualizer } from "@/lib/jambuddy/visualizer";
|
| 22 |
|
|
|
|
| 134 |
const [midiFile, setMidiFile] = useState<File | null>(null);
|
| 135 |
const [midiBytes, setMidiBytes] = useState<ArrayBuffer | null>(null);
|
| 136 |
const [audioFile, setAudioFile] = useState<File | null>(null);
|
| 137 |
+
// Object URL of the uploaded audio take (for playback + waveform).
|
| 138 |
+
const [takeAudioUrl, setTakeAudioUrl] = useState<string | null>(null);
|
| 139 |
const [status, setStatus] = useState<string>("Ready.");
|
| 140 |
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
| 141 |
+
const [isPlayingTogether, setIsPlayingTogether] = useState(false);
|
| 142 |
const [usedBpm, setUsedBpm] = useState<number | null>(null);
|
| 143 |
const [usedSeconds, setUsedSeconds] = useState<number | null>(null);
|
| 144 |
const [busy, setBusy] = useState(false);
|
|
|
|
| 194 |
setMidiFile(null);
|
| 195 |
setMidiBytes(null);
|
| 196 |
setAudioFile(null);
|
| 197 |
+
if (takeAudioUrl) URL.revokeObjectURL(takeAudioUrl);
|
| 198 |
+
setTakeAudioUrl(null);
|
| 199 |
if (!f) {
|
| 200 |
setStatus("Ready.");
|
| 201 |
return;
|
|
|
|
| 220 |
);
|
| 221 |
} else {
|
| 222 |
setAudioFile(f);
|
| 223 |
+
setTakeAudioUrl(URL.createObjectURL(f));
|
| 224 |
// Audio is heard by the buddy (audio-to-audio). Clear the MIDI-driven
|
| 225 |
// input-instrument default so the user's declaration reflects the audio.
|
| 226 |
setInputInstrument("other");
|
|
|
|
| 316 |
setStatus("Saved your recorded MIDI take.");
|
| 317 |
}
|
| 318 |
|
| 319 |
+
/** Download the generated buddy response (audio) as a file. */
|
| 320 |
+
function saveGeneratedAudio() {
|
| 321 |
+
if (!audioUrl) {
|
| 322 |
+
setStatus("Generate a response first, then save it.");
|
| 323 |
+
return;
|
| 324 |
+
}
|
| 325 |
+
const a = document.createElement("a");
|
| 326 |
+
a.href = audioUrl;
|
| 327 |
+
a.download = `jambuddy-response-${new Date()
|
| 328 |
+
.toISOString()
|
| 329 |
+
.replace(/[-:]/g, "")
|
| 330 |
+
.replace(/\.\d+Z$/, "Z")}.${mode === "api" ? "mp3" : "wav"}`;
|
| 331 |
+
a.rel = "noopener";
|
| 332 |
+
document.body.appendChild(a);
|
| 333 |
+
a.click();
|
| 334 |
+
document.body.removeChild(a);
|
| 335 |
+
setStatus("Saved the buddy's response.");
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/** Play the take and the buddy response together (audio mix, or MIDI synth).
|
| 339 |
+
* Toggle: pressing again stops playback.
|
| 340 |
+
*
|
| 341 |
+
* Re-entry guard: isPlayingTogether is React state (async), so a fast second
|
| 342 |
+
* click can read a stale `false` and start a SECOND simultaneous layer. Keep
|
| 343 |
+
* a synchronous ref so the toggle is atomic. */
|
| 344 |
+
const playbackRef = useRef<{ stop: () => void } | null>(null);
|
| 345 |
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
| 346 |
+
async function playBoth() {
|
| 347 |
+
if (playbackRef.current) {
|
| 348 |
+
// Stop: kill current playback (synchronous — immune to stale state).
|
| 349 |
+
playbackRef.current.stop();
|
| 350 |
+
playbackRef.current = null;
|
| 351 |
+
setIsPlayingTogether(false);
|
| 352 |
+
setStatus("Stopped.");
|
| 353 |
+
return;
|
| 354 |
+
}
|
| 355 |
+
if (!audioUrl) {
|
| 356 |
+
setStatus("Generate a response first.");
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
// Pause the standalone audio element so the response isn't heard twice.
|
| 360 |
+
audioRef.current?.pause();
|
| 361 |
+
playbackRef.current = { stop: () => {} }; // claim the toggle synchronously
|
| 362 |
+
setIsPlayingTogether(true);
|
| 363 |
+
setStatus("Playing your take + the buddy together…");
|
| 364 |
+
try {
|
| 365 |
+
let handle: { stop: () => void; done: Promise<void> };
|
| 366 |
+
if (midiBytes) {
|
| 367 |
+
// MIDI take: render with the built-in synth, layered with the buddy.
|
| 368 |
+
handle = await playTogether(midiBytes, audioUrl);
|
| 369 |
+
} else if (takeAudioUrl) {
|
| 370 |
+
// Audio take: mix the two audio files on the same clock.
|
| 371 |
+
handle = await playAudioTogether(takeAudioUrl, audioUrl);
|
| 372 |
+
} else {
|
| 373 |
+
playbackRef.current = null;
|
| 374 |
+
setIsPlayingTogether(false);
|
| 375 |
+
setStatus("Load a take first to play it with the response.");
|
| 376 |
+
return;
|
| 377 |
+
}
|
| 378 |
+
playbackRef.current = handle;
|
| 379 |
+
await handle.done;
|
| 380 |
+
if (playbackRef.current === handle) {
|
| 381 |
+
playbackRef.current = null;
|
| 382 |
+
setIsPlayingTogether(false);
|
| 383 |
+
setStatus("Done — both played together.");
|
| 384 |
+
}
|
| 385 |
+
} catch (e) {
|
| 386 |
+
playbackRef.current = null;
|
| 387 |
+
setIsPlayingTogether(false);
|
| 388 |
+
setStatus(`Playback error: ${String(e)}`);
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
async function joinIn() {
|
| 393 |
setBusy(true);
|
| 394 |
setStatus(
|
|
|
|
| 516 |
/>
|
| 517 |
</section>
|
| 518 |
|
| 519 |
+
{/* Prompt — mirrors the knobs, shown right below them */}
|
| 520 |
+
<div className="mb-6 rounded bg-[#15161f] px-4 py-2 font-mono text-xs text-[#7f829c]">
|
| 521 |
+
<div className="uppercase tracking-widest">Prompt</div>
|
| 522 |
+
<div className="mt-1 truncate text-[#e8e8f0]">{prompt}</div>
|
| 523 |
+
<div className="mt-1 opacity-60">neg: {negativePrompt}</div>
|
| 524 |
+
</div>
|
| 525 |
+
|
| 526 |
{/* Your-take-is knob — declares what instrument the user is playing
|
| 527 |
so the buddy can complement it (no MIR on input). */}
|
| 528 |
<section
|
|
|
|
| 608 |
: "Local CPU · free · supports negative prompt · slower"}
|
| 609 |
</span>
|
| 610 |
</div>
|
| 611 |
+
{/* Transport — sampler/sequencer pads */}
|
| 612 |
+
<div className="mb-2 grid grid-cols-3 gap-4">
|
| 613 |
<button
|
| 614 |
type="button"
|
| 615 |
onClick={toggleRecord}
|
| 616 |
disabled={busy}
|
| 617 |
aria-pressed={recording}
|
| 618 |
aria-label={recording ? "Stop recording" : "Record from MIDI controller"}
|
| 619 |
+
className="jambuddy-padbig flex-1"
|
| 620 |
+
style={{ ["--pad-c" as string]: "#e05252" }}
|
| 621 |
>
|
| 622 |
+
<span className="jambuddy-padbig__label">
|
| 623 |
+
{recording ? "■ STOP" : "● RECORD"}
|
| 624 |
+
</span>
|
| 625 |
</button>
|
| 626 |
<button
|
| 627 |
type="button"
|
| 628 |
+
onClick={joinIn}
|
| 629 |
+
disabled={busy}
|
| 630 |
+
className="jambuddy-padbig flex-1"
|
| 631 |
+
style={{ ["--pad-c" as string]: "#f4a261" }}
|
|
|
|
|
|
|
|
|
|
| 632 |
>
|
| 633 |
+
<span className="jambuddy-padbig__label">
|
| 634 |
+
{busy ? "PRODUCING…" : "JOIN IN"}
|
| 635 |
+
</span>
|
| 636 |
</button>
|
| 637 |
<button
|
| 638 |
type="button"
|
| 639 |
+
onClick={playBoth}
|
| 640 |
+
disabled={!audioUrl}
|
| 641 |
+
aria-pressed={isPlayingTogether}
|
| 642 |
+
className="jambuddy-padbig flex-1"
|
| 643 |
+
style={{ ["--pad-c" as string]: "#5fd38a" }}
|
| 644 |
>
|
| 645 |
+
<span className="jambuddy-padbig__label">
|
| 646 |
+
{isPlayingTogether ? "■ STOP" : "PLAY TOGETHER"}
|
| 647 |
+
</span>
|
| 648 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
</div>
|
| 650 |
|
| 651 |
{/* Status + playback */}
|
|
|
|
| 674 |
</p>
|
| 675 |
)}
|
| 676 |
{audioUrl && (
|
| 677 |
+
<audio
|
| 678 |
+
ref={audioRef}
|
| 679 |
+
controls
|
| 680 |
+
src={audioUrl}
|
| 681 |
+
className="mt-3 w-full"
|
| 682 |
+
>
|
| 683 |
Your browser does not support audio playback.
|
| 684 |
</audio>
|
| 685 |
)}
|
| 686 |
</section>
|
| 687 |
|
| 688 |
+
{/* Take + response visualizers — stacked vertically, DAW-style.
|
| 689 |
+
Each waveform has its own SAVE button beside it. */}
|
| 690 |
+
{(midiBytes || takeAudioUrl || audioUrl) && (
|
| 691 |
<section
|
| 692 |
aria-label="Take and response"
|
| 693 |
+
className="mt-4 flex flex-col gap-4"
|
| 694 |
>
|
| 695 |
+
{midiBytes ? (
|
| 696 |
+
<div className="flex items-end gap-3">
|
| 697 |
+
<div className="flex-1">
|
| 698 |
+
<Visualizer
|
| 699 |
+
midiBytes={midiBytes}
|
| 700 |
+
label="Your take (MIDI piano-roll)"
|
| 701 |
+
/>
|
| 702 |
+
</div>
|
| 703 |
+
<button
|
| 704 |
+
type="button"
|
| 705 |
+
onClick={saveRecordedMidi}
|
| 706 |
+
disabled={!recordedMidiUrl}
|
| 707 |
+
className="jambuddy-save"
|
| 708 |
+
title="Download this MIDI take"
|
| 709 |
+
aria-label="Download this MIDI take"
|
| 710 |
+
>
|
| 711 |
+
<svg viewBox="0 0 24 24" width="26" height="26" fill="none"
|
| 712 |
+
stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"
|
| 713 |
+
strokeLinejoin="round" aria-hidden="true">
|
| 714 |
+
<path d="M12 3v12" />
|
| 715 |
+
<path d="M6 11l6 6 6-6" />
|
| 716 |
+
<path d="M4 20h16" />
|
| 717 |
+
</svg>
|
| 718 |
+
</button>
|
| 719 |
+
</div>
|
| 720 |
+
) : takeAudioUrl ? (
|
| 721 |
+
<div className="flex items-end gap-3">
|
| 722 |
+
<div className="flex-1">
|
| 723 |
+
<Visualizer
|
| 724 |
+
audioUrl={takeAudioUrl}
|
| 725 |
+
label="Your take (audio waveform)"
|
| 726 |
+
/>
|
| 727 |
+
</div>
|
| 728 |
+
</div>
|
| 729 |
+
) : null}
|
| 730 |
+
{audioUrl && (
|
| 731 |
+
<div className="flex items-end gap-3">
|
| 732 |
+
<div className="flex-1">
|
| 733 |
+
<Visualizer
|
| 734 |
+
audioUrl={audioUrl}
|
| 735 |
+
label="Buddy response (waveform)"
|
| 736 |
+
/>
|
| 737 |
+
</div>
|
| 738 |
+
<button
|
| 739 |
+
type="button"
|
| 740 |
+
onClick={saveGeneratedAudio}
|
| 741 |
+
disabled={!audioUrl}
|
| 742 |
+
className="jambuddy-save"
|
| 743 |
+
title="Download this response"
|
| 744 |
+
aria-label="Download this response"
|
| 745 |
+
>
|
| 746 |
+
<svg viewBox="0 0 24 24" width="26" height="26" fill="none"
|
| 747 |
+
stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"
|
| 748 |
+
strokeLinejoin="round" aria-hidden="true">
|
| 749 |
+
<path d="M12 3v12" />
|
| 750 |
+
<path d="M6 11l6 6 6-6" />
|
| 751 |
+
<path d="M4 20h16" />
|
| 752 |
+
</svg>
|
| 753 |
+
</button>
|
| 754 |
+
</div>
|
| 755 |
+
)}
|
| 756 |
</section>
|
| 757 |
)}
|
| 758 |
</div>
|
apps/web/lib/jambuddy/player.ts
CHANGED
|
@@ -207,3 +207,73 @@ export function midiDuration(buf: ArrayBuffer): number {
|
|
| 207 |
return 30;
|
| 208 |
}
|
| 209 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
return 30;
|
| 208 |
}
|
| 209 |
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Play an AUDIO take and the buddy response TOGETHER (mixed on the same clock).
|
| 213 |
+
* This is the audio-take analogue of playTogether (which is MIDI + buddy).
|
| 214 |
+
* Both are decoded AudioBuffers scheduled to start at the same time.
|
| 215 |
+
*
|
| 216 |
+
* @param takeAudioUrl the user's audio take (object URL)
|
| 217 |
+
* @param buddyAudioUrl the generated response (object URL)
|
| 218 |
+
* @returns an object with stop() and a promise resolving when both finish.
|
| 219 |
+
*/
|
| 220 |
+
export async function playAudioTogether(
|
| 221 |
+
takeAudioUrl: string,
|
| 222 |
+
buddyAudioUrl: string,
|
| 223 |
+
): Promise<{ stop: () => void; done: Promise<void> }> {
|
| 224 |
+
const ctx = new AudioContext();
|
| 225 |
+
await ctx.resume();
|
| 226 |
+
|
| 227 |
+
const decode = async (url: string) => {
|
| 228 |
+
const resp = await fetch(url);
|
| 229 |
+
const buf = await resp.arrayBuffer();
|
| 230 |
+
return ctx.decodeAudioData(buf);
|
| 231 |
+
};
|
| 232 |
+
|
| 233 |
+
const [takeBuffer, buddyBuffer] = await Promise.all([
|
| 234 |
+
decode(takeAudioUrl),
|
| 235 |
+
decode(buddyAudioUrl),
|
| 236 |
+
]);
|
| 237 |
+
|
| 238 |
+
const master = ctx.createGain();
|
| 239 |
+
master.gain.value = 0.8;
|
| 240 |
+
master.connect(ctx.destination);
|
| 241 |
+
|
| 242 |
+
const startAt = ctx.currentTime + 0.1;
|
| 243 |
+
|
| 244 |
+
const takeSrc = ctx.createBufferSource();
|
| 245 |
+
takeSrc.buffer = takeBuffer;
|
| 246 |
+
takeSrc.connect(master);
|
| 247 |
+
takeSrc.start(startAt);
|
| 248 |
+
|
| 249 |
+
const buddySrc = ctx.createBufferSource();
|
| 250 |
+
buddySrc.buffer = buddyBuffer;
|
| 251 |
+
buddySrc.connect(master);
|
| 252 |
+
buddySrc.start(startAt);
|
| 253 |
+
|
| 254 |
+
const end = startAt + Math.max(takeBuffer.duration, buddyBuffer.duration);
|
| 255 |
+
|
| 256 |
+
const done = new Promise<void>((resolve) => {
|
| 257 |
+
setTimeout(() => {
|
| 258 |
+
try {
|
| 259 |
+
ctx.close();
|
| 260 |
+
} catch {
|
| 261 |
+
/* already closed */
|
| 262 |
+
}
|
| 263 |
+
resolve();
|
| 264 |
+
}, Math.max(0, (end - ctx.currentTime) * 1000) + 200);
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
return {
|
| 268 |
+
stop: () => {
|
| 269 |
+
try {
|
| 270 |
+
takeSrc.stop();
|
| 271 |
+
buddySrc.stop();
|
| 272 |
+
ctx.close();
|
| 273 |
+
} catch {
|
| 274 |
+
/* ignore */
|
| 275 |
+
}
|
| 276 |
+
},
|
| 277 |
+
done,
|
| 278 |
+
};
|
| 279 |
+
}
|
apps/web/package.json
CHANGED
|
@@ -20,6 +20,10 @@
|
|
| 20 |
"react-dom": "^18.3.1"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
"@types/node": "^22.9.3",
|
| 24 |
"@types/react": "^18.3.12",
|
| 25 |
"@types/react-dom": "^18.3.1",
|
|
|
|
| 20 |
"react-dom": "^18.3.1"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
| 23 |
+
"@testing-library/dom": "^10.4.1",
|
| 24 |
+
"@testing-library/jest-dom": "^7.0.1",
|
| 25 |
+
"@testing-library/react": "^16.3.2",
|
| 26 |
+
"@testing-library/user-event": "^14.6.6",
|
| 27 |
"@types/node": "^22.9.3",
|
| 28 |
"@types/react": "^18.3.12",
|
| 29 |
"@types/react-dom": "^18.3.1",
|
apps/web/vitest.config.ts
CHANGED
|
@@ -7,6 +7,7 @@ export default defineConfig({
|
|
| 7 |
test: {
|
| 8 |
environment: "happy-dom",
|
| 9 |
globals: false,
|
|
|
|
| 10 |
include: ["__tests__/**/*.test.ts", "__tests__/**/*.test.tsx"],
|
| 11 |
},
|
| 12 |
resolve: {
|
|
|
|
| 7 |
test: {
|
| 8 |
environment: "happy-dom",
|
| 9 |
globals: false,
|
| 10 |
+
setupFiles: ["./__tests__/setup.ts"],
|
| 11 |
include: ["__tests__/**/*.test.ts", "__tests__/**/*.test.tsx"],
|
| 12 |
},
|
| 13 |
resolve: {
|
pnpm-lock.yaml
CHANGED
|
@@ -30,6 +30,18 @@ importers:
|
|
| 30 |
specifier: ^18.3.1
|
| 31 |
version: 18.3.1(react@18.3.1)
|
| 32 |
devDependencies:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
'@types/node':
|
| 34 |
specifier: ^22.9.3
|
| 35 |
version: 22.20.1
|
|
@@ -75,6 +87,9 @@ importers:
|
|
| 75 |
|
| 76 |
packages:
|
| 77 |
|
|
|
|
|
|
|
|
|
|
| 78 |
'@alloc/quick-lru@5.2.0':
|
| 79 |
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
| 80 |
engines: {node: '>=10'}
|
|
@@ -150,6 +165,10 @@ packages:
|
|
| 150 |
peerDependencies:
|
| 151 |
'@babel/core': ^7.0.0-0
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
'@babel/template@7.29.7':
|
| 154 |
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
| 155 |
engines: {node: '>=6.9.0'}
|
|
@@ -593,12 +612,50 @@ packages:
|
|
| 593 |
'@swc/helpers@0.5.5':
|
| 594 |
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
|
| 595 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
'@tonejs/midi@2.0.28':
|
| 597 |
resolution: {integrity: sha512-RII6YpInPsOZ5t3Si/20QKpNqB1lZ2OCFJSOzJxz38YdY/3zqDr3uaml4JuCWkdixuPqP1/TBnXzhQ39csyoVg==}
|
| 598 |
|
| 599 |
'@tybys/wasm-util@0.10.3':
|
| 600 |
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
| 601 |
|
|
|
|
|
|
|
|
|
|
| 602 |
'@types/babel__core@7.20.5':
|
| 603 |
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
| 604 |
|
|
@@ -863,6 +920,10 @@ packages:
|
|
| 863 |
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
| 864 |
engines: {node: '>=8'}
|
| 865 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
ansi-styles@6.2.3:
|
| 867 |
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
| 868 |
engines: {node: '>=12'}
|
|
@@ -880,6 +941,9 @@ packages:
|
|
| 880 |
argparse@2.0.1:
|
| 881 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
| 882 |
|
|
|
|
|
|
|
|
|
|
| 883 |
aria-query@5.3.2:
|
| 884 |
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
| 885 |
engines: {node: '>= 0.4'}
|
|
@@ -1055,6 +1119,9 @@ packages:
|
|
| 1055 |
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
| 1056 |
engines: {node: '>= 8'}
|
| 1057 |
|
|
|
|
|
|
|
|
|
|
| 1058 |
cssesc@3.0.0:
|
| 1059 |
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
| 1060 |
engines: {node: '>=4'}
|
|
@@ -1110,6 +1177,10 @@ packages:
|
|
| 1110 |
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
| 1111 |
engines: {node: '>= 0.4'}
|
| 1112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
didyoumean@1.2.2:
|
| 1114 |
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
| 1115 |
|
|
@@ -1124,6 +1195,12 @@ packages:
|
|
| 1124 |
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
| 1125 |
engines: {node: '>=6.0.0'}
|
| 1126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
dunder-proto@1.0.1:
|
| 1128 |
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
| 1129 |
engines: {node: '>= 0.4'}
|
|
@@ -1494,6 +1571,10 @@ packages:
|
|
| 1494 |
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
| 1495 |
engines: {node: '>=0.8.19'}
|
| 1496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1497 |
inflight@1.0.6:
|
| 1498 |
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
| 1499 |
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
|
@@ -1717,6 +1798,10 @@ packages:
|
|
| 1717 |
lru-cache@5.1.1:
|
| 1718 |
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
| 1719 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1720 |
magic-string@0.30.21:
|
| 1721 |
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
| 1722 |
|
|
@@ -1735,6 +1820,10 @@ packages:
|
|
| 1735 |
midi-file@1.2.4:
|
| 1736 |
resolution: {integrity: sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==}
|
| 1737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1738 |
minimatch@10.2.6:
|
| 1739 |
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
| 1740 |
engines: {node: 18 || 20 || >=22}
|
|
@@ -1965,6 +2054,10 @@ packages:
|
|
| 1965 |
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
| 1966 |
engines: {node: '>= 0.8.0'}
|
| 1967 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1968 |
prop-types@15.8.1:
|
| 1969 |
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
| 1970 |
|
|
@@ -1983,6 +2076,9 @@ packages:
|
|
| 1983 |
react-is@16.13.1:
|
| 1984 |
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
| 1985 |
|
|
|
|
|
|
|
|
|
|
| 1986 |
react-refresh@0.17.0:
|
| 1987 |
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
| 1988 |
engines: {node: '>=0.10.0'}
|
|
@@ -1998,6 +2094,10 @@ packages:
|
|
| 1998 |
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
| 1999 |
engines: {node: '>=8.10.0'}
|
| 2000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2001 |
reflect.getprototypeof@1.0.10:
|
| 2002 |
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
| 2003 |
engines: {node: '>= 0.4'}
|
|
@@ -2171,6 +2271,10 @@ packages:
|
|
| 2171 |
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
| 2172 |
engines: {node: '>=4'}
|
| 2173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2174 |
strip-json-comments@3.1.1:
|
| 2175 |
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
| 2176 |
engines: {node: '>=8'}
|
|
@@ -2427,6 +2531,8 @@ packages:
|
|
| 2427 |
|
| 2428 |
snapshots:
|
| 2429 |
|
|
|
|
|
|
|
| 2430 |
'@alloc/quick-lru@5.2.0': {}
|
| 2431 |
|
| 2432 |
'@babel/code-frame@7.29.7':
|
|
@@ -2518,6 +2624,8 @@ snapshots:
|
|
| 2518 |
'@babel/core': 7.29.7
|
| 2519 |
'@babel/helper-plugin-utils': 7.29.7
|
| 2520 |
|
|
|
|
|
|
|
| 2521 |
'@babel/template@7.29.7':
|
| 2522 |
dependencies:
|
| 2523 |
'@babel/code-frame': 7.29.7
|
|
@@ -2837,6 +2945,43 @@ snapshots:
|
|
| 2837 |
'@swc/counter': 0.1.3
|
| 2838 |
tslib: 2.8.1
|
| 2839 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2840 |
'@tonejs/midi@2.0.28':
|
| 2841 |
dependencies:
|
| 2842 |
array-flatten: 3.0.0
|
|
@@ -2847,6 +2992,8 @@ snapshots:
|
|
| 2847 |
tslib: 2.8.1
|
| 2848 |
optional: true
|
| 2849 |
|
|
|
|
|
|
|
| 2850 |
'@types/babel__core@7.20.5':
|
| 2851 |
dependencies:
|
| 2852 |
'@babel/parser': 7.29.8
|
|
@@ -3123,6 +3270,8 @@ snapshots:
|
|
| 3123 |
dependencies:
|
| 3124 |
color-convert: 2.0.1
|
| 3125 |
|
|
|
|
|
|
|
| 3126 |
ansi-styles@6.2.3: {}
|
| 3127 |
|
| 3128 |
any-promise@1.3.0: {}
|
|
@@ -3136,6 +3285,10 @@ snapshots:
|
|
| 3136 |
|
| 3137 |
argparse@2.0.1: {}
|
| 3138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3139 |
aria-query@5.3.2: {}
|
| 3140 |
|
| 3141 |
array-buffer-byte-length@1.0.2:
|
|
@@ -3339,6 +3492,8 @@ snapshots:
|
|
| 3339 |
shebang-command: 2.0.0
|
| 3340 |
which: 2.0.2
|
| 3341 |
|
|
|
|
|
|
|
| 3342 |
cssesc@3.0.0: {}
|
| 3343 |
|
| 3344 |
csstype@3.2.3: {}
|
|
@@ -3387,6 +3542,8 @@ snapshots:
|
|
| 3387 |
has-property-descriptors: 1.0.2
|
| 3388 |
object-keys: 1.1.1
|
| 3389 |
|
|
|
|
|
|
|
| 3390 |
didyoumean@1.2.2: {}
|
| 3391 |
|
| 3392 |
dlv@1.1.3: {}
|
|
@@ -3399,6 +3556,10 @@ snapshots:
|
|
| 3399 |
dependencies:
|
| 3400 |
esutils: 2.0.3
|
| 3401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3402 |
dunder-proto@1.0.1:
|
| 3403 |
dependencies:
|
| 3404 |
call-bind-apply-helpers: 1.0.2
|
|
@@ -3948,6 +4109,8 @@ snapshots:
|
|
| 3948 |
|
| 3949 |
imurmurhash@0.1.4: {}
|
| 3950 |
|
|
|
|
|
|
|
| 3951 |
inflight@1.0.6:
|
| 3952 |
dependencies:
|
| 3953 |
once: 1.4.0
|
|
@@ -4170,6 +4333,8 @@ snapshots:
|
|
| 4170 |
dependencies:
|
| 4171 |
yallist: 3.1.1
|
| 4172 |
|
|
|
|
|
|
|
| 4173 |
magic-string@0.30.21:
|
| 4174 |
dependencies:
|
| 4175 |
'@jridgewell/sourcemap-codec': 1.5.5
|
|
@@ -4185,6 +4350,8 @@ snapshots:
|
|
| 4185 |
|
| 4186 |
midi-file@1.2.4: {}
|
| 4187 |
|
|
|
|
|
|
|
| 4188 |
minimatch@10.2.6:
|
| 4189 |
dependencies:
|
| 4190 |
brace-expansion: 5.0.9
|
|
@@ -4401,6 +4568,12 @@ snapshots:
|
|
| 4401 |
|
| 4402 |
prelude-ls@1.2.1: {}
|
| 4403 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4404 |
prop-types@15.8.1:
|
| 4405 |
dependencies:
|
| 4406 |
loose-envify: 1.4.0
|
|
@@ -4419,6 +4592,8 @@ snapshots:
|
|
| 4419 |
|
| 4420 |
react-is@16.13.1: {}
|
| 4421 |
|
|
|
|
|
|
|
| 4422 |
react-refresh@0.17.0: {}
|
| 4423 |
|
| 4424 |
react@18.3.1:
|
|
@@ -4433,6 +4608,11 @@ snapshots:
|
|
| 4433 |
dependencies:
|
| 4434 |
picomatch: 2.3.2
|
| 4435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4436 |
reflect.getprototypeof@1.0.10:
|
| 4437 |
dependencies:
|
| 4438 |
call-bind: 1.0.9
|
|
@@ -4690,6 +4870,10 @@ snapshots:
|
|
| 4690 |
|
| 4691 |
strip-bom@3.0.0: {}
|
| 4692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4693 |
strip-json-comments@3.1.1: {}
|
| 4694 |
|
| 4695 |
styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1):
|
|
|
|
| 30 |
specifier: ^18.3.1
|
| 31 |
version: 18.3.1(react@18.3.1)
|
| 32 |
devDependencies:
|
| 33 |
+
'@testing-library/dom':
|
| 34 |
+
specifier: ^10.4.1
|
| 35 |
+
version: 10.4.1
|
| 36 |
+
'@testing-library/jest-dom':
|
| 37 |
+
specifier: ^7.0.1
|
| 38 |
+
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@2.1.9(@types/node@22.20.1)(happy-dom@15.11.7))
|
| 39 |
+
'@testing-library/react':
|
| 40 |
+
specifier: ^16.3.2
|
| 41 |
+
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
| 42 |
+
'@testing-library/user-event':
|
| 43 |
+
specifier: ^14.6.6
|
| 44 |
+
version: 14.6.6(@testing-library/dom@10.4.1)
|
| 45 |
'@types/node':
|
| 46 |
specifier: ^22.9.3
|
| 47 |
version: 22.20.1
|
|
|
|
| 87 |
|
| 88 |
packages:
|
| 89 |
|
| 90 |
+
'@adobe/css-tools@4.5.0':
|
| 91 |
+
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
|
| 92 |
+
|
| 93 |
'@alloc/quick-lru@5.2.0':
|
| 94 |
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
| 95 |
engines: {node: '>=10'}
|
|
|
|
| 165 |
peerDependencies:
|
| 166 |
'@babel/core': ^7.0.0-0
|
| 167 |
|
| 168 |
+
'@babel/runtime@7.29.7':
|
| 169 |
+
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
| 170 |
+
engines: {node: '>=6.9.0'}
|
| 171 |
+
|
| 172 |
'@babel/template@7.29.7':
|
| 173 |
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
| 174 |
engines: {node: '>=6.9.0'}
|
|
|
|
| 612 |
'@swc/helpers@0.5.5':
|
| 613 |
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
|
| 614 |
|
| 615 |
+
'@testing-library/dom@10.4.1':
|
| 616 |
+
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
| 617 |
+
engines: {node: '>=18'}
|
| 618 |
+
|
| 619 |
+
'@testing-library/jest-dom@7.0.1':
|
| 620 |
+
resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==}
|
| 621 |
+
engines: {node: '>=22', npm: '>=6', yarn: '>=1'}
|
| 622 |
+
peerDependencies:
|
| 623 |
+
'@testing-library/dom': '>=10 <11'
|
| 624 |
+
vitest: '>= 0.32'
|
| 625 |
+
peerDependenciesMeta:
|
| 626 |
+
vitest:
|
| 627 |
+
optional: true
|
| 628 |
+
|
| 629 |
+
'@testing-library/react@16.3.2':
|
| 630 |
+
resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
|
| 631 |
+
engines: {node: '>=18'}
|
| 632 |
+
peerDependencies:
|
| 633 |
+
'@testing-library/dom': ^10.0.0
|
| 634 |
+
'@types/react': ^18.0.0 || ^19.0.0
|
| 635 |
+
'@types/react-dom': ^18.0.0 || ^19.0.0
|
| 636 |
+
react: ^18.0.0 || ^19.0.0
|
| 637 |
+
react-dom: ^18.0.0 || ^19.0.0
|
| 638 |
+
peerDependenciesMeta:
|
| 639 |
+
'@types/react':
|
| 640 |
+
optional: true
|
| 641 |
+
'@types/react-dom':
|
| 642 |
+
optional: true
|
| 643 |
+
|
| 644 |
+
'@testing-library/user-event@14.6.6':
|
| 645 |
+
resolution: {integrity: sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==}
|
| 646 |
+
engines: {node: '>=12', npm: '>=6'}
|
| 647 |
+
peerDependencies:
|
| 648 |
+
'@testing-library/dom': '>=7.21.4'
|
| 649 |
+
|
| 650 |
'@tonejs/midi@2.0.28':
|
| 651 |
resolution: {integrity: sha512-RII6YpInPsOZ5t3Si/20QKpNqB1lZ2OCFJSOzJxz38YdY/3zqDr3uaml4JuCWkdixuPqP1/TBnXzhQ39csyoVg==}
|
| 652 |
|
| 653 |
'@tybys/wasm-util@0.10.3':
|
| 654 |
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
| 655 |
|
| 656 |
+
'@types/aria-query@5.0.4':
|
| 657 |
+
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
| 658 |
+
|
| 659 |
'@types/babel__core@7.20.5':
|
| 660 |
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
| 661 |
|
|
|
|
| 920 |
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
| 921 |
engines: {node: '>=8'}
|
| 922 |
|
| 923 |
+
ansi-styles@5.2.0:
|
| 924 |
+
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
| 925 |
+
engines: {node: '>=10'}
|
| 926 |
+
|
| 927 |
ansi-styles@6.2.3:
|
| 928 |
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
| 929 |
engines: {node: '>=12'}
|
|
|
|
| 941 |
argparse@2.0.1:
|
| 942 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
| 943 |
|
| 944 |
+
aria-query@5.3.0:
|
| 945 |
+
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
| 946 |
+
|
| 947 |
aria-query@5.3.2:
|
| 948 |
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
| 949 |
engines: {node: '>= 0.4'}
|
|
|
|
| 1119 |
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
| 1120 |
engines: {node: '>= 8'}
|
| 1121 |
|
| 1122 |
+
css.escape@1.5.1:
|
| 1123 |
+
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
| 1124 |
+
|
| 1125 |
cssesc@3.0.0:
|
| 1126 |
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
| 1127 |
engines: {node: '>=4'}
|
|
|
|
| 1177 |
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
| 1178 |
engines: {node: '>= 0.4'}
|
| 1179 |
|
| 1180 |
+
dequal@2.0.3:
|
| 1181 |
+
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
| 1182 |
+
engines: {node: '>=6'}
|
| 1183 |
+
|
| 1184 |
didyoumean@1.2.2:
|
| 1185 |
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
| 1186 |
|
|
|
|
| 1195 |
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
| 1196 |
engines: {node: '>=6.0.0'}
|
| 1197 |
|
| 1198 |
+
dom-accessibility-api@0.5.16:
|
| 1199 |
+
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
| 1200 |
+
|
| 1201 |
+
dom-accessibility-api@0.6.3:
|
| 1202 |
+
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
| 1203 |
+
|
| 1204 |
dunder-proto@1.0.1:
|
| 1205 |
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
| 1206 |
engines: {node: '>= 0.4'}
|
|
|
|
| 1571 |
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
| 1572 |
engines: {node: '>=0.8.19'}
|
| 1573 |
|
| 1574 |
+
indent-string@4.0.0:
|
| 1575 |
+
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
| 1576 |
+
engines: {node: '>=8'}
|
| 1577 |
+
|
| 1578 |
inflight@1.0.6:
|
| 1579 |
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
| 1580 |
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
|
|
|
| 1798 |
lru-cache@5.1.1:
|
| 1799 |
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
| 1800 |
|
| 1801 |
+
lz-string@1.5.0:
|
| 1802 |
+
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
| 1803 |
+
hasBin: true
|
| 1804 |
+
|
| 1805 |
magic-string@0.30.21:
|
| 1806 |
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
| 1807 |
|
|
|
|
| 1820 |
midi-file@1.2.4:
|
| 1821 |
resolution: {integrity: sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==}
|
| 1822 |
|
| 1823 |
+
min-indent@1.0.1:
|
| 1824 |
+
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
| 1825 |
+
engines: {node: '>=4'}
|
| 1826 |
+
|
| 1827 |
minimatch@10.2.6:
|
| 1828 |
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
| 1829 |
engines: {node: 18 || 20 || >=22}
|
|
|
|
| 2054 |
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
| 2055 |
engines: {node: '>= 0.8.0'}
|
| 2056 |
|
| 2057 |
+
pretty-format@27.5.1:
|
| 2058 |
+
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
| 2059 |
+
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
| 2060 |
+
|
| 2061 |
prop-types@15.8.1:
|
| 2062 |
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
| 2063 |
|
|
|
|
| 2076 |
react-is@16.13.1:
|
| 2077 |
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
| 2078 |
|
| 2079 |
+
react-is@17.0.2:
|
| 2080 |
+
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
| 2081 |
+
|
| 2082 |
react-refresh@0.17.0:
|
| 2083 |
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
| 2084 |
engines: {node: '>=0.10.0'}
|
|
|
|
| 2094 |
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
| 2095 |
engines: {node: '>=8.10.0'}
|
| 2096 |
|
| 2097 |
+
redent@3.0.0:
|
| 2098 |
+
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
| 2099 |
+
engines: {node: '>=8'}
|
| 2100 |
+
|
| 2101 |
reflect.getprototypeof@1.0.10:
|
| 2102 |
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
| 2103 |
engines: {node: '>= 0.4'}
|
|
|
|
| 2271 |
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
| 2272 |
engines: {node: '>=4'}
|
| 2273 |
|
| 2274 |
+
strip-indent@3.0.0:
|
| 2275 |
+
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
| 2276 |
+
engines: {node: '>=8'}
|
| 2277 |
+
|
| 2278 |
strip-json-comments@3.1.1:
|
| 2279 |
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
| 2280 |
engines: {node: '>=8'}
|
|
|
|
| 2531 |
|
| 2532 |
snapshots:
|
| 2533 |
|
| 2534 |
+
'@adobe/css-tools@4.5.0': {}
|
| 2535 |
+
|
| 2536 |
'@alloc/quick-lru@5.2.0': {}
|
| 2537 |
|
| 2538 |
'@babel/code-frame@7.29.7':
|
|
|
|
| 2624 |
'@babel/core': 7.29.7
|
| 2625 |
'@babel/helper-plugin-utils': 7.29.7
|
| 2626 |
|
| 2627 |
+
'@babel/runtime@7.29.7': {}
|
| 2628 |
+
|
| 2629 |
'@babel/template@7.29.7':
|
| 2630 |
dependencies:
|
| 2631 |
'@babel/code-frame': 7.29.7
|
|
|
|
| 2945 |
'@swc/counter': 0.1.3
|
| 2946 |
tslib: 2.8.1
|
| 2947 |
|
| 2948 |
+
'@testing-library/dom@10.4.1':
|
| 2949 |
+
dependencies:
|
| 2950 |
+
'@babel/code-frame': 7.29.7
|
| 2951 |
+
'@babel/runtime': 7.29.7
|
| 2952 |
+
'@types/aria-query': 5.0.4
|
| 2953 |
+
aria-query: 5.3.0
|
| 2954 |
+
dom-accessibility-api: 0.5.16
|
| 2955 |
+
lz-string: 1.5.0
|
| 2956 |
+
picocolors: 1.1.1
|
| 2957 |
+
pretty-format: 27.5.1
|
| 2958 |
+
|
| 2959 |
+
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@2.1.9(@types/node@22.20.1)(happy-dom@15.11.7))':
|
| 2960 |
+
dependencies:
|
| 2961 |
+
'@adobe/css-tools': 4.5.0
|
| 2962 |
+
'@testing-library/dom': 10.4.1
|
| 2963 |
+
aria-query: 5.3.2
|
| 2964 |
+
css.escape: 1.5.1
|
| 2965 |
+
dom-accessibility-api: 0.6.3
|
| 2966 |
+
picocolors: 1.1.1
|
| 2967 |
+
redent: 3.0.0
|
| 2968 |
+
optionalDependencies:
|
| 2969 |
+
vitest: 2.1.9(@types/node@22.20.1)(happy-dom@15.11.7)
|
| 2970 |
+
|
| 2971 |
+
'@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
| 2972 |
+
dependencies:
|
| 2973 |
+
'@babel/runtime': 7.29.7
|
| 2974 |
+
'@testing-library/dom': 10.4.1
|
| 2975 |
+
react: 18.3.1
|
| 2976 |
+
react-dom: 18.3.1(react@18.3.1)
|
| 2977 |
+
optionalDependencies:
|
| 2978 |
+
'@types/react': 18.3.31
|
| 2979 |
+
'@types/react-dom': 18.3.7(@types/react@18.3.31)
|
| 2980 |
+
|
| 2981 |
+
'@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)':
|
| 2982 |
+
dependencies:
|
| 2983 |
+
'@testing-library/dom': 10.4.1
|
| 2984 |
+
|
| 2985 |
'@tonejs/midi@2.0.28':
|
| 2986 |
dependencies:
|
| 2987 |
array-flatten: 3.0.0
|
|
|
|
| 2992 |
tslib: 2.8.1
|
| 2993 |
optional: true
|
| 2994 |
|
| 2995 |
+
'@types/aria-query@5.0.4': {}
|
| 2996 |
+
|
| 2997 |
'@types/babel__core@7.20.5':
|
| 2998 |
dependencies:
|
| 2999 |
'@babel/parser': 7.29.8
|
|
|
|
| 3270 |
dependencies:
|
| 3271 |
color-convert: 2.0.1
|
| 3272 |
|
| 3273 |
+
ansi-styles@5.2.0: {}
|
| 3274 |
+
|
| 3275 |
ansi-styles@6.2.3: {}
|
| 3276 |
|
| 3277 |
any-promise@1.3.0: {}
|
|
|
|
| 3285 |
|
| 3286 |
argparse@2.0.1: {}
|
| 3287 |
|
| 3288 |
+
aria-query@5.3.0:
|
| 3289 |
+
dependencies:
|
| 3290 |
+
dequal: 2.0.3
|
| 3291 |
+
|
| 3292 |
aria-query@5.3.2: {}
|
| 3293 |
|
| 3294 |
array-buffer-byte-length@1.0.2:
|
|
|
|
| 3492 |
shebang-command: 2.0.0
|
| 3493 |
which: 2.0.2
|
| 3494 |
|
| 3495 |
+
css.escape@1.5.1: {}
|
| 3496 |
+
|
| 3497 |
cssesc@3.0.0: {}
|
| 3498 |
|
| 3499 |
csstype@3.2.3: {}
|
|
|
|
| 3542 |
has-property-descriptors: 1.0.2
|
| 3543 |
object-keys: 1.1.1
|
| 3544 |
|
| 3545 |
+
dequal@2.0.3: {}
|
| 3546 |
+
|
| 3547 |
didyoumean@1.2.2: {}
|
| 3548 |
|
| 3549 |
dlv@1.1.3: {}
|
|
|
|
| 3556 |
dependencies:
|
| 3557 |
esutils: 2.0.3
|
| 3558 |
|
| 3559 |
+
dom-accessibility-api@0.5.16: {}
|
| 3560 |
+
|
| 3561 |
+
dom-accessibility-api@0.6.3: {}
|
| 3562 |
+
|
| 3563 |
dunder-proto@1.0.1:
|
| 3564 |
dependencies:
|
| 3565 |
call-bind-apply-helpers: 1.0.2
|
|
|
|
| 4109 |
|
| 4110 |
imurmurhash@0.1.4: {}
|
| 4111 |
|
| 4112 |
+
indent-string@4.0.0: {}
|
| 4113 |
+
|
| 4114 |
inflight@1.0.6:
|
| 4115 |
dependencies:
|
| 4116 |
once: 1.4.0
|
|
|
|
| 4333 |
dependencies:
|
| 4334 |
yallist: 3.1.1
|
| 4335 |
|
| 4336 |
+
lz-string@1.5.0: {}
|
| 4337 |
+
|
| 4338 |
magic-string@0.30.21:
|
| 4339 |
dependencies:
|
| 4340 |
'@jridgewell/sourcemap-codec': 1.5.5
|
|
|
|
| 4350 |
|
| 4351 |
midi-file@1.2.4: {}
|
| 4352 |
|
| 4353 |
+
min-indent@1.0.1: {}
|
| 4354 |
+
|
| 4355 |
minimatch@10.2.6:
|
| 4356 |
dependencies:
|
| 4357 |
brace-expansion: 5.0.9
|
|
|
|
| 4568 |
|
| 4569 |
prelude-ls@1.2.1: {}
|
| 4570 |
|
| 4571 |
+
pretty-format@27.5.1:
|
| 4572 |
+
dependencies:
|
| 4573 |
+
ansi-regex: 5.0.1
|
| 4574 |
+
ansi-styles: 5.2.0
|
| 4575 |
+
react-is: 17.0.2
|
| 4576 |
+
|
| 4577 |
prop-types@15.8.1:
|
| 4578 |
dependencies:
|
| 4579 |
loose-envify: 1.4.0
|
|
|
|
| 4592 |
|
| 4593 |
react-is@16.13.1: {}
|
| 4594 |
|
| 4595 |
+
react-is@17.0.2: {}
|
| 4596 |
+
|
| 4597 |
react-refresh@0.17.0: {}
|
| 4598 |
|
| 4599 |
react@18.3.1:
|
|
|
|
| 4608 |
dependencies:
|
| 4609 |
picomatch: 2.3.2
|
| 4610 |
|
| 4611 |
+
redent@3.0.0:
|
| 4612 |
+
dependencies:
|
| 4613 |
+
indent-string: 4.0.0
|
| 4614 |
+
strip-indent: 3.0.0
|
| 4615 |
+
|
| 4616 |
reflect.getprototypeof@1.0.10:
|
| 4617 |
dependencies:
|
| 4618 |
call-bind: 1.0.9
|
|
|
|
| 4870 |
|
| 4871 |
strip-bom@3.0.0: {}
|
| 4872 |
|
| 4873 |
+
strip-indent@3.0.0:
|
| 4874 |
+
dependencies:
|
| 4875 |
+
min-indent: 1.0.1
|
| 4876 |
+
|
| 4877 |
strip-json-comments@3.1.1: {}
|
| 4878 |
|
| 4879 |
styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1):
|