Let's fucking go
This commit is contained in:
BIN
.playwright-mcp/audio-scope-black.png
Normal file
BIN
.playwright-mcp/audio-scope-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
313
src/App.svelte
313
src/App.svelte
@ -6,6 +6,9 @@
|
|||||||
import FileBrowser from './lib/FileBrowser.svelte';
|
import FileBrowser from './lib/FileBrowser.svelte';
|
||||||
import SidePanel from './lib/SidePanel.svelte';
|
import SidePanel from './lib/SidePanel.svelte';
|
||||||
import Popup from './lib/Popup.svelte';
|
import Popup from './lib/Popup.svelte';
|
||||||
|
import ResizablePopup from './lib/ResizablePopup.svelte';
|
||||||
|
import AudioScope from './lib/AudioScope.svelte';
|
||||||
|
import Spectrogram from './lib/Spectrogram.svelte';
|
||||||
import { csound, csoundLogs, type LogEntry } from './lib/csound';
|
import { csound, csoundLogs, type LogEntry } from './lib/csound';
|
||||||
import { projectManager, type CsoundProject } from './lib/project-system';
|
import { projectManager, type CsoundProject } from './lib/project-system';
|
||||||
import ConfirmDialog from './lib/ConfirmDialog.svelte';
|
import ConfirmDialog from './lib/ConfirmDialog.svelte';
|
||||||
@ -18,13 +21,54 @@
|
|||||||
PanelBottomClose,
|
PanelBottomClose,
|
||||||
PanelBottomOpen,
|
PanelBottomOpen,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Save
|
Save,
|
||||||
|
Share2,
|
||||||
|
Activity
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
let sidePanelVisible = $state(true);
|
let sidePanelVisible = $state(true);
|
||||||
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
|
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
|
||||||
let popupVisible = $state(false);
|
let popupVisible = $state(false);
|
||||||
let editorValue = $state('<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n\nsr = 44100\nksmps = 32\nnchnls = 2\n0dbfs = 1\n\ninstr 1\n aOut oscili 0.5, 440\n outs aOut, aOut\nendin\n\n</CsInstruments>\n<CsScore>\ni 1 0 2\n</CsScore>\n</CsoundSynthesizer>\n');
|
let sharePopupVisible = $state(false);
|
||||||
|
let shareUrl = $state('');
|
||||||
|
let audioPermissionPopupVisible = $state(true);
|
||||||
|
let scopePopupVisible = $state(false);
|
||||||
|
let spectrogramPopupVisible = $state(false);
|
||||||
|
let analyserNode = $state<AnalyserNode | null>(null);
|
||||||
|
let editorValue = $state(`<CsoundSynthesizer>
|
||||||
|
<CsOptions>
|
||||||
|
-odac
|
||||||
|
</CsOptions>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
sr = 44100
|
||||||
|
ksmps = 32
|
||||||
|
nchnls = 2
|
||||||
|
0dbfs = 1
|
||||||
|
|
||||||
|
instr 1
|
||||||
|
iFreq = p4
|
||||||
|
iAmp = p5
|
||||||
|
|
||||||
|
; ADSR envelope
|
||||||
|
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||||
|
|
||||||
|
; Sine wave oscillator
|
||||||
|
aOsc oscili iAmp * kEnv, iFreq
|
||||||
|
|
||||||
|
outs aOsc, aOsc
|
||||||
|
endin
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
<CsScore>
|
||||||
|
; Arpeggio: C4 E4 G4 C5
|
||||||
|
i 1 0.0 0.5 261.63 0.3
|
||||||
|
i 1 0.5 0.5 329.63 0.3
|
||||||
|
i 1 1.0 0.5 392.00 0.3
|
||||||
|
i 1 1.5 0.5 523.25 0.3
|
||||||
|
</CsScore>
|
||||||
|
</CsoundSynthesizer>
|
||||||
|
`);
|
||||||
let interpreterLogs = $state<LogEntry[]>([]);
|
let interpreterLogs = $state<LogEntry[]>([]);
|
||||||
|
|
||||||
let sidePanelRef: SidePanel;
|
let sidePanelRef: SidePanel;
|
||||||
@ -37,10 +81,42 @@
|
|||||||
let showUnsavedDialog = $state(false);
|
let showUnsavedDialog = $state(false);
|
||||||
let showSaveAsDialog = $state(false);
|
let showSaveAsDialog = $state(false);
|
||||||
|
|
||||||
const TEMPLATE_CONTENT = '<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n\nsr = 44100\nksmps = 32\nnchnls = 2\n0dbfs = 1\n\ninstr 1\n aOut oscili 0.5, 440\n outs aOut, aOut\nendin\n\n</CsInstruments>\n<CsScore>\ni 1 0 2\n</CsScore>\n</CsoundSynthesizer>\n';
|
const TEMPLATE_CONTENT = `<CsoundSynthesizer>
|
||||||
|
<CsOptions>
|
||||||
|
-odac
|
||||||
|
</CsOptions>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
sr = 44100
|
||||||
|
ksmps = 32
|
||||||
|
nchnls = 2
|
||||||
|
0dbfs = 1
|
||||||
|
|
||||||
|
instr 1
|
||||||
|
iFreq = p4
|
||||||
|
iAmp = p5
|
||||||
|
|
||||||
|
; ADSR envelope
|
||||||
|
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||||
|
|
||||||
|
; Sine wave oscillator
|
||||||
|
aOsc oscili iAmp * kEnv, iFreq
|
||||||
|
|
||||||
|
outs aOsc, aOsc
|
||||||
|
endin
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
<CsScore>
|
||||||
|
; Arpeggio: C4 E4 G4 C5
|
||||||
|
i 1 0.0 0.5 261.63 0.3
|
||||||
|
i 1 0.5 0.5 329.63 0.3
|
||||||
|
i 1 1.0 0.5 392.00 0.3
|
||||||
|
i 1 1.5 0.5 523.25 0.3
|
||||||
|
</CsScore>
|
||||||
|
</CsoundSynthesizer>
|
||||||
|
`;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await csound.init();
|
|
||||||
await projectManager.init();
|
await projectManager.init();
|
||||||
|
|
||||||
const result = await projectManager.getAllProjects();
|
const result = await projectManager.getAllProjects();
|
||||||
@ -62,6 +138,15 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleEnableAudio() {
|
||||||
|
try {
|
||||||
|
await csound.init();
|
||||||
|
audioPermissionPopupVisible = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize audio:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
await csound.destroy();
|
await csound.destroy();
|
||||||
});
|
});
|
||||||
@ -203,6 +288,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleShare() {
|
||||||
|
if (!currentProjectId) return;
|
||||||
|
|
||||||
|
const result = await projectManager.exportProjectToUrl(currentProjectId);
|
||||||
|
if (result.success) {
|
||||||
|
shareUrl = result.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy to clipboard:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
sharePopupVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenScope() {
|
||||||
|
scopePopupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenSpectrogram() {
|
||||||
|
spectrogramPopupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (scopePopupVisible || spectrogramPopupVisible) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
analyserNode = csound.getAnalyserNode();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const panelTabs = [
|
const panelTabs = [
|
||||||
{
|
{
|
||||||
id: 'editor',
|
id: 'editor',
|
||||||
@ -232,7 +352,8 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<TopBar title="oldboy">
|
<TopBar title="OldBoy">
|
||||||
|
{#snippet leftActions()}
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={saveCurrentProject}
|
onclick={saveCurrentProject}
|
||||||
@ -242,6 +363,7 @@
|
|||||||
>
|
>
|
||||||
<Save size={18} />
|
<Save size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
{/snippet}
|
||||||
<button onclick={toggleSidePanel} class="icon-button">
|
<button onclick={toggleSidePanel} class="icon-button">
|
||||||
{#if sidePanelVisible}
|
{#if sidePanelVisible}
|
||||||
{#if sidePanelPosition === 'left'}
|
{#if sidePanelPosition === 'left'}
|
||||||
@ -264,6 +386,34 @@
|
|||||||
<button onclick={cyclePanelPosition} class="icon-button" title="Change panel position">
|
<button onclick={cyclePanelPosition} class="icon-button" title="Change panel position">
|
||||||
<LayoutGrid size={18} />
|
<LayoutGrid size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleShare}
|
||||||
|
class="icon-button"
|
||||||
|
disabled={!currentProjectId}
|
||||||
|
title="Share project"
|
||||||
|
>
|
||||||
|
<Share2 size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleOpenScope}
|
||||||
|
class="icon-button"
|
||||||
|
title="Open audio scope"
|
||||||
|
>
|
||||||
|
<Activity size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleOpenSpectrogram}
|
||||||
|
class="icon-button"
|
||||||
|
title="Open spectrogram"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<path d="M3 9h18"/>
|
||||||
|
<path d="M3 15h18"/>
|
||||||
|
<path d="M9 3v18"/>
|
||||||
|
<path d="M15 3v18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</TopBar>
|
</TopBar>
|
||||||
|
|
||||||
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
|
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
|
||||||
@ -321,6 +471,88 @@
|
|||||||
<p>It stays on top of everything else.</p>
|
<p>It stays on top of everything else.</p>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
|
<ResizablePopup
|
||||||
|
bind:visible={sharePopupVisible}
|
||||||
|
title="Share Project"
|
||||||
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
|
||||||
|
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
|
||||||
|
width={600}
|
||||||
|
height={200}
|
||||||
|
minWidth={400}
|
||||||
|
minHeight={150}
|
||||||
|
>
|
||||||
|
{#snippet children()}
|
||||||
|
<div class="share-content">
|
||||||
|
<p>Link copied to clipboard!</p>
|
||||||
|
<div class="share-url-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
value={shareUrl}
|
||||||
|
class="share-url-input"
|
||||||
|
onclick={(e) => e.currentTarget.select()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="share-instructions">Anyone with this link can import the project.</p>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</ResizablePopup>
|
||||||
|
|
||||||
|
<ResizablePopup
|
||||||
|
visible={audioPermissionPopupVisible}
|
||||||
|
title="Audio Permission Required"
|
||||||
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
|
||||||
|
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 125 : 200)}
|
||||||
|
width={500}
|
||||||
|
height={250}
|
||||||
|
minWidth={400}
|
||||||
|
minHeight={200}
|
||||||
|
closable={false}
|
||||||
|
>
|
||||||
|
{#snippet children()}
|
||||||
|
<div class="audio-permission-content">
|
||||||
|
<h3>Enable Audio Context</h3>
|
||||||
|
<p>OldBoy needs permission to use audio playback.</p>
|
||||||
|
<p>Click the button below to enable audio and start using the application.</p>
|
||||||
|
<button class="enable-audio-button" onclick={handleEnableAudio}>
|
||||||
|
Enable Audio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</ResizablePopup>
|
||||||
|
|
||||||
|
<ResizablePopup
|
||||||
|
bind:visible={scopePopupVisible}
|
||||||
|
title="Audio Scope"
|
||||||
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
|
||||||
|
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 100)}
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
minWidth={400}
|
||||||
|
minHeight={300}
|
||||||
|
noPadding={true}
|
||||||
|
>
|
||||||
|
{#snippet children()}
|
||||||
|
<AudioScope analyserNode={analyserNode} />
|
||||||
|
{/snippet}
|
||||||
|
</ResizablePopup>
|
||||||
|
|
||||||
|
<ResizablePopup
|
||||||
|
bind:visible={spectrogramPopupVisible}
|
||||||
|
title="Spectrogram"
|
||||||
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
|
||||||
|
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 150)}
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
minWidth={400}
|
||||||
|
minHeight={300}
|
||||||
|
noPadding={true}
|
||||||
|
>
|
||||||
|
{#snippet children()}
|
||||||
|
<Spectrogram analyserNode={analyserNode} />
|
||||||
|
{/snippet}
|
||||||
|
</ResizablePopup>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:visible={showUnsavedDialog}
|
bind:visible={showUnsavedDialog}
|
||||||
title="Unsaved Changes"
|
title="Unsaved Changes"
|
||||||
@ -399,4 +631,75 @@
|
|||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: monospace;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url-input:focus {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-instructions {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-permission-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-permission-content h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-permission-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable-audio-button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
background-color: #646cff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable-audio-button:hover {
|
||||||
|
background-color: #535bdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable-audio-button:active {
|
||||||
|
background-color: #424ab8;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
141
src/lib/AudioScope.svelte
Normal file
141
src/lib/AudioScope.svelte
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
analyserNode?: AnalyserNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { analyserNode }: Props = $props();
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let canvasContext: CanvasRenderingContext2D | null = null;
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let dataArray: Uint8Array | null = null;
|
||||||
|
let width = $state(800);
|
||||||
|
let height = $state(400);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analyserNode && canvasContext) {
|
||||||
|
setupScope(analyserNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupScope(node: AnalyserNode) {
|
||||||
|
try {
|
||||||
|
const bufferLength = node.frequencyBinCount;
|
||||||
|
dataArray = new Uint8Array(bufferLength);
|
||||||
|
startDrawing(node);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to setup scope:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrawing(node: AnalyserNode) {
|
||||||
|
if (!canvasContext || !dataArray) return;
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
if (!canvasContext || !dataArray || !node || !canvas) return;
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
node.getByteTimeDomainData(dataArray);
|
||||||
|
|
||||||
|
canvasContext.fillStyle = '#0a0a0a';
|
||||||
|
canvasContext.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
canvasContext.strokeStyle = '#333';
|
||||||
|
canvasContext.lineWidth = 1;
|
||||||
|
canvasContext.beginPath();
|
||||||
|
canvasContext.moveTo(0, canvas.height / 2);
|
||||||
|
canvasContext.lineTo(canvas.width, canvas.height / 2);
|
||||||
|
canvasContext.stroke();
|
||||||
|
|
||||||
|
canvasContext.lineWidth = 2;
|
||||||
|
canvasContext.strokeStyle = '#646cff';
|
||||||
|
canvasContext.beginPath();
|
||||||
|
|
||||||
|
const sliceWidth = canvas.width / dataArray.length;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < dataArray.length; i++) {
|
||||||
|
const v = dataArray[i] / 255.0;
|
||||||
|
const y = v * canvas.height;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
canvasContext.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
canvasContext.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
x += sliceWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasContext.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSize() {
|
||||||
|
if (container && canvas) {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
width = Math.floor(rect.width);
|
||||||
|
height = Math.floor(rect.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (canvas) {
|
||||||
|
canvasContext = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="audio-scope" bind:this={container}>
|
||||||
|
<canvas bind:this={canvas} {width} {height}></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.audio-scope {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -169,6 +169,22 @@
|
|||||||
onchange={handleMetadataChange}
|
onchange={handleMetadataChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field readonly">
|
||||||
|
<label>Number of Saves</label>
|
||||||
|
<div class="readonly-value">{selectedProject.saveCount}</div>
|
||||||
|
</div>
|
||||||
|
<div class="field readonly">
|
||||||
|
<label>Date Created</label>
|
||||||
|
<div class="readonly-value">{formatDate(selectedProject.dateCreated)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="field readonly">
|
||||||
|
<label>Date Modified</label>
|
||||||
|
<div class="readonly-value">{formatDate(selectedProject.dateModified)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="field readonly">
|
||||||
|
<label>Csound Version</label>
|
||||||
|
<div class="readonly-value">{selectedProject.csoundVersion}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -344,4 +360,13 @@
|
|||||||
.field input:focus {
|
.field input:focus {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field.readonly .readonly-value {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
274
src/lib/ResizablePopup.svelte
Normal file
274
src/lib/ResizablePopup.svelte
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
closable?: boolean;
|
||||||
|
noPadding?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title = 'Popup',
|
||||||
|
visible = $bindable(false),
|
||||||
|
x = 100,
|
||||||
|
y = 100,
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
minWidth = 300,
|
||||||
|
minHeight = 150,
|
||||||
|
closable = true,
|
||||||
|
noPadding = false,
|
||||||
|
onClose,
|
||||||
|
children
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let isResizing = $state(false);
|
||||||
|
let resizeDirection = $state('');
|
||||||
|
let dragStartX = $state(0);
|
||||||
|
let dragStartY = $state(0);
|
||||||
|
let popupX = $state(x);
|
||||||
|
let popupY = $state(y);
|
||||||
|
let popupWidth = $state(width);
|
||||||
|
let popupHeight = $state(height);
|
||||||
|
let startWidth = $state(0);
|
||||||
|
let startHeight = $state(0);
|
||||||
|
let startX = $state(0);
|
||||||
|
let startY = $state(0);
|
||||||
|
|
||||||
|
function handleDragStart(e: MouseEvent) {
|
||||||
|
if ((e.target as HTMLElement).classList.contains('popup-header')) {
|
||||||
|
isDragging = true;
|
||||||
|
dragStartX = e.clientX - popupX;
|
||||||
|
dragStartY = e.clientY - popupY;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeStart(e: MouseEvent, direction: string) {
|
||||||
|
isResizing = true;
|
||||||
|
resizeDirection = direction;
|
||||||
|
dragStartX = e.clientX;
|
||||||
|
dragStartY = e.clientY;
|
||||||
|
startWidth = popupWidth;
|
||||||
|
startHeight = popupHeight;
|
||||||
|
startX = popupX;
|
||||||
|
startY = popupY;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
if (isDragging) {
|
||||||
|
popupX = e.clientX - dragStartX;
|
||||||
|
popupY = e.clientY - dragStartY;
|
||||||
|
} else if (isResizing) {
|
||||||
|
const deltaX = e.clientX - dragStartX;
|
||||||
|
const deltaY = e.clientY - dragStartY;
|
||||||
|
|
||||||
|
if (resizeDirection.includes('e')) {
|
||||||
|
popupWidth = Math.max(minWidth, startWidth + deltaX);
|
||||||
|
}
|
||||||
|
if (resizeDirection.includes('w')) {
|
||||||
|
const newWidth = Math.max(minWidth, startWidth - deltaX);
|
||||||
|
popupWidth = newWidth;
|
||||||
|
popupX = startX + (startWidth - newWidth);
|
||||||
|
}
|
||||||
|
if (resizeDirection.includes('s')) {
|
||||||
|
popupHeight = Math.max(minHeight, startHeight + deltaY);
|
||||||
|
}
|
||||||
|
if (resizeDirection.includes('n')) {
|
||||||
|
const newHeight = Math.max(minHeight, startHeight - deltaY);
|
||||||
|
popupHeight = newHeight;
|
||||||
|
popupY = startY + (startHeight - newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
isDragging = false;
|
||||||
|
isResizing = false;
|
||||||
|
resizeDirection = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible = false;
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div
|
||||||
|
class="resizable-popup"
|
||||||
|
style="left: {popupX}px; top: {popupY}px; width: {popupWidth}px; height: {popupHeight}px;"
|
||||||
|
>
|
||||||
|
<div class="popup-header" onmousedown={handleDragStart}>
|
||||||
|
<span class="popup-title">{title}</span>
|
||||||
|
{#if closable}
|
||||||
|
<button class="close-button" onclick={handleClose}>×</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="popup-content" class:no-padding={noPadding}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resize-handle n" onmousedown={(e) => handleResizeStart(e, 'n')}></div>
|
||||||
|
<div class="resize-handle s" onmousedown={(e) => handleResizeStart(e, 's')}></div>
|
||||||
|
<div class="resize-handle e" onmousedown={(e) => handleResizeStart(e, 'e')}></div>
|
||||||
|
<div class="resize-handle w" onmousedown={(e) => handleResizeStart(e, 'w')}></div>
|
||||||
|
<div class="resize-handle ne" onmousedown={(e) => handleResizeStart(e, 'ne')}></div>
|
||||||
|
<div class="resize-handle nw" onmousedown={(e) => handleResizeStart(e, 'nw')}></div>
|
||||||
|
<div class="resize-handle se" onmousedown={(e) => handleResizeStart(e, 'se')}></div>
|
||||||
|
<div class="resize-handle sw" onmousedown={(e) => handleResizeStart(e, 'sw')}></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.resizable-popup {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #252525;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content.no-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.n,
|
||||||
|
.resize-handle.s {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 8px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.n {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.s {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.e,
|
||||||
|
.resize-handle.w {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 8px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.e {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.w {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.ne,
|
||||||
|
.resize-handle.nw,
|
||||||
|
.resize-handle.se,
|
||||||
|
.resize-handle.sw {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.ne {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.nw {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.se {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.sw {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
src/lib/Spectrogram.svelte
Normal file
133
src/lib/Spectrogram.svelte
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
analyserNode?: AnalyserNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { analyserNode }: Props = $props();
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let canvasContext: CanvasRenderingContext2D | null = null;
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let dataArray: Uint8Array | null = null;
|
||||||
|
let width = $state(800);
|
||||||
|
let height = $state(600);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analyserNode && canvasContext) {
|
||||||
|
setupSpectrogram(analyserNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupSpectrogram(node: AnalyserNode) {
|
||||||
|
try {
|
||||||
|
const bufferLength = node.frequencyBinCount;
|
||||||
|
dataArray = new Uint8Array(bufferLength);
|
||||||
|
startDrawing(node);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to setup spectrogram:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrawing(node: AnalyserNode) {
|
||||||
|
if (!canvasContext || !dataArray) return;
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
if (!canvasContext || !dataArray || !node || !canvas) return;
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
node.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
const imageData = canvasContext.getImageData(1, 0, canvas.width - 1, canvas.height);
|
||||||
|
canvasContext.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
const barWidth = 1;
|
||||||
|
const x = canvas.width - barWidth;
|
||||||
|
|
||||||
|
for (let i = 0; i < dataArray.length; i++) {
|
||||||
|
const value = dataArray[i];
|
||||||
|
const percent = value / 255;
|
||||||
|
const y = canvas.height - (i / dataArray.length) * canvas.height;
|
||||||
|
const height = (canvas.height / dataArray.length);
|
||||||
|
|
||||||
|
const hue = (1 - percent) * 240;
|
||||||
|
const saturation = 100;
|
||||||
|
const lightness = percent * 50;
|
||||||
|
|
||||||
|
canvasContext.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
|
canvasContext.fillRect(x, y - height, barWidth, height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSize() {
|
||||||
|
if (container && canvas) {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
width = Math.floor(rect.width);
|
||||||
|
height = Math.floor(rect.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (canvas) {
|
||||||
|
canvasContext = canvas.getContext('2d');
|
||||||
|
if (canvasContext) {
|
||||||
|
canvasContext.fillStyle = '#000';
|
||||||
|
canvasContext.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="spectrogram" bind:this={container}>
|
||||||
|
<canvas bind:this={canvas} {width} {height}></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrogram {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -3,17 +3,21 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
leftActions?: import('svelte').Snippet;
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title = 'oldboy', children }: Props = $props();
|
let { title = 'OldBoy', leftActions, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
<div class="left-section">
|
||||||
<div class="title-section">
|
<div class="title-section">
|
||||||
<Code2 size={20} />
|
<Code2 size={20} />
|
||||||
<span class="title">{title}</span>
|
<span class="title">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{@render leftActions?.()}
|
||||||
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
@ -31,6 +35,12 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.title-section {
|
.title-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Csound } from '@csound/browser';
|
|||||||
export interface CsoundEngineOptions {
|
export interface CsoundEngineOptions {
|
||||||
onMessage?: (message: string) => void;
|
onMessage?: (message: string) => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
|
onPerformanceEnd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CsoundEngine {
|
export class CsoundEngine {
|
||||||
@ -10,6 +11,9 @@ export class CsoundEngine {
|
|||||||
private initialized = false;
|
private initialized = false;
|
||||||
private running = false;
|
private running = false;
|
||||||
private options: CsoundEngineOptions;
|
private options: CsoundEngineOptions;
|
||||||
|
private scopeNode: AnalyserNode | null = null;
|
||||||
|
private audioNode: AudioNode | null = null;
|
||||||
|
private audioContext: AudioContext | null = null;
|
||||||
|
|
||||||
constructor(options: CsoundEngineOptions = {}) {
|
constructor(options: CsoundEngineOptions = {}) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -18,26 +22,12 @@ export class CsoundEngine {
|
|||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
try {
|
|
||||||
this.csound = await Csound();
|
|
||||||
|
|
||||||
this.csound.on('message', (message: string) => {
|
|
||||||
this.options.onMessage?.(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.csound.setOption('-odac');
|
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
this.log('Csound initialized successfully');
|
this.log('Csound ready');
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = error instanceof Error ? error.message : 'Failed to initialize Csound';
|
|
||||||
this.error(errorMsg);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateCode(code: string): Promise<void> {
|
async evaluateCode(code: string): Promise<void> {
|
||||||
if (!this.initialized || !this.csound) {
|
if (!this.initialized) {
|
||||||
throw new Error('Csound not initialized. Call init() first.');
|
throw new Error('Csound not initialized. Call init() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +36,29 @@ export class CsoundEngine {
|
|||||||
await this.stop();
|
await this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log('Resetting Csound...');
|
this.scopeNode = null;
|
||||||
await this.csound.reset();
|
|
||||||
|
this.log('Creating new Csound instance...');
|
||||||
|
this.csound = await Csound();
|
||||||
|
|
||||||
|
this.csound.on('message', (message: string) => {
|
||||||
|
this.options.onMessage?.(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.csound.on('onAudioNodeCreated', (node: AudioNode) => {
|
||||||
|
this.audioNode = node;
|
||||||
|
this.audioContext = node.context as AudioContext;
|
||||||
|
this.log('Audio node created and captured');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.csound.on('realtimePerformanceEnded', async () => {
|
||||||
|
try {
|
||||||
|
await this.csound?.cleanup();
|
||||||
|
} catch {}
|
||||||
|
this.running = false;
|
||||||
|
this.log('Performance complete');
|
||||||
|
this.options.onPerformanceEnd?.();
|
||||||
|
});
|
||||||
|
|
||||||
this.log('Setting audio output...');
|
this.log('Setting audio output...');
|
||||||
await this.csound.setOption('-odac');
|
await this.csound.setOption('-odac');
|
||||||
@ -63,22 +74,28 @@ export class CsoundEngine {
|
|||||||
const sco = scoMatch[1].trim();
|
const sco = scoMatch[1].trim();
|
||||||
|
|
||||||
this.log('Compiling orchestra...');
|
this.log('Compiling orchestra...');
|
||||||
await this.csound.compileOrc(orc);
|
const compileResult = await this.csound.compileOrc(orc);
|
||||||
|
|
||||||
|
if (compileResult !== 0) {
|
||||||
|
throw new Error('Failed to compile orchestra');
|
||||||
|
}
|
||||||
|
|
||||||
this.log('Reading score...');
|
this.log('Reading score...');
|
||||||
await this.csound.readScore(sco);
|
await this.csound.readScore(sco);
|
||||||
|
|
||||||
this.log('Starting...');
|
this.log('Starting performance...');
|
||||||
|
this.running = true;
|
||||||
await this.csound.start();
|
await this.csound.start();
|
||||||
|
|
||||||
this.log('Performing...');
|
this.setupAnalyser();
|
||||||
this.running = true;
|
|
||||||
await this.csound.perform();
|
|
||||||
|
|
||||||
this.log('Performance complete');
|
|
||||||
this.running = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
|
if (this.csound) {
|
||||||
|
try {
|
||||||
|
await this.csound.cleanup();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
const errorMsg = error instanceof Error ? error.message : 'Evaluation failed';
|
const errorMsg = error instanceof Error ? error.message : 'Evaluation failed';
|
||||||
this.error(errorMsg);
|
this.error(errorMsg);
|
||||||
throw error;
|
throw error;
|
||||||
@ -89,8 +106,9 @@ export class CsoundEngine {
|
|||||||
if (!this.csound || !this.running) return;
|
if (!this.csound || !this.running) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.log('Stopping...');
|
||||||
await this.csound.stop();
|
await this.csound.stop();
|
||||||
await this.csound.reset();
|
await this.csound.cleanup();
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.log('Stopped');
|
this.log('Stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -107,6 +125,36 @@ export class CsoundEngine {
|
|||||||
return this.initialized;
|
return this.initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioContext(): AudioContext | null {
|
||||||
|
return this.audioContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupAnalyser(): void {
|
||||||
|
if (!this.audioNode || !this.audioContext) {
|
||||||
|
this.log('Warning: Audio node not available yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.scopeNode = this.audioContext.createAnalyser();
|
||||||
|
this.scopeNode.fftSize = 2048;
|
||||||
|
this.scopeNode.smoothingTimeConstant = 0.3;
|
||||||
|
|
||||||
|
this.audioNode.disconnect();
|
||||||
|
this.audioNode.connect(this.scopeNode);
|
||||||
|
this.scopeNode.connect(this.audioContext.destination);
|
||||||
|
|
||||||
|
this.log('Analyser node created and connected');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to setup analyser:', error);
|
||||||
|
this.log('Error setting up analyser: ' + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnalyserNode(): AnalyserNode | null {
|
||||||
|
return this.scopeNode;
|
||||||
|
}
|
||||||
|
|
||||||
private log(message: string): void {
|
private log(message: string): void {
|
||||||
this.options.onMessage?.(message);
|
this.options.onMessage?.(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,6 +92,20 @@ function createCsoundStore() {
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAudioContext(): AudioContext | null {
|
||||||
|
if (engine) {
|
||||||
|
return engine.getAudioContext();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnalyserNode(): AnalyserNode | null {
|
||||||
|
if (engine) {
|
||||||
|
return engine.getAnalyserNode();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
if (engine) {
|
if (engine) {
|
||||||
await engine.destroy();
|
await engine.destroy();
|
||||||
|
|||||||
1
web-ide
Submodule
1
web-ide
Submodule
Submodule web-ide added at d74138bb72
Reference in New Issue
Block a user