Let's fucking go

This commit is contained in:
2025-10-14 23:21:00 +02:00
parent cec7ed8bd4
commit 267323abf2
10 changed files with 994 additions and 45 deletions

141
src/lib/AudioScope.svelte Normal file
View 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>

View File

@ -169,6 +169,22 @@
onchange={handleMetadataChange}
/>
</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>
{/if}
@ -344,4 +360,13 @@
.field input:focus {
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>

View 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
View 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>

View File

@ -3,16 +3,20 @@
interface Props {
title?: string;
leftActions?: import('svelte').Snippet;
children?: import('svelte').Snippet;
}
let { title = 'oldboy', children }: Props = $props();
let { title = 'OldBoy', leftActions, children }: Props = $props();
</script>
<div class="top-bar">
<div class="title-section">
<Code2 size={20} />
<span class="title">{title}</span>
<div class="left-section">
<div class="title-section">
<Code2 size={20} />
<span class="title">{title}</span>
</div>
{@render leftActions?.()}
</div>
<div class="actions">
{@render children?.()}
@ -31,6 +35,12 @@
box-sizing: border-box;
}
.left-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.title-section {
display: flex;
align-items: center;

View File

@ -3,6 +3,7 @@ import { Csound } from '@csound/browser';
export interface CsoundEngineOptions {
onMessage?: (message: string) => void;
onError?: (error: string) => void;
onPerformanceEnd?: () => void;
}
export class CsoundEngine {
@ -10,6 +11,9 @@ export class CsoundEngine {
private initialized = false;
private running = false;
private options: CsoundEngineOptions;
private scopeNode: AnalyserNode | null = null;
private audioNode: AudioNode | null = null;
private audioContext: AudioContext | null = null;
constructor(options: CsoundEngineOptions = {}) {
this.options = options;
@ -18,26 +22,12 @@ export class CsoundEngine {
async init(): Promise<void> {
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.log('Csound initialized successfully');
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to initialize Csound';
this.error(errorMsg);
throw error;
}
this.initialized = true;
this.log('Csound ready');
}
async evaluateCode(code: string): Promise<void> {
if (!this.initialized || !this.csound) {
if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.');
}
@ -46,8 +36,29 @@ export class CsoundEngine {
await this.stop();
}
this.log('Resetting Csound...');
await this.csound.reset();
this.scopeNode = null;
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...');
await this.csound.setOption('-odac');
@ -63,22 +74,28 @@ export class CsoundEngine {
const sco = scoMatch[1].trim();
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...');
await this.csound.readScore(sco);
this.log('Starting...');
this.log('Starting performance...');
this.running = true;
await this.csound.start();
this.log('Performing...');
this.running = true;
await this.csound.perform();
this.setupAnalyser();
this.log('Performance complete');
this.running = false;
} catch (error) {
this.running = false;
if (this.csound) {
try {
await this.csound.cleanup();
} catch {}
}
const errorMsg = error instanceof Error ? error.message : 'Evaluation failed';
this.error(errorMsg);
throw error;
@ -89,8 +106,9 @@ export class CsoundEngine {
if (!this.csound || !this.running) return;
try {
this.log('Stopping...');
await this.csound.stop();
await this.csound.reset();
await this.csound.cleanup();
this.running = false;
this.log('Stopped');
} catch (error) {
@ -107,6 +125,36 @@ export class CsoundEngine {
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 {
this.options.onMessage?.(message);
}

View File

@ -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() {
if (engine) {
await engine.destroy();