Trying to do better but it's hard

This commit is contained in:
2025-10-15 12:42:56 +02:00
parent 1dc9d1114e
commit ea8ecd4b2a
9 changed files with 200 additions and 163 deletions

1
csound-live-code Submodule

Submodule csound-live-code added at 71b9973520

View File

@ -31,6 +31,7 @@
FileStack, FileStack,
PanelLeftOpen, PanelLeftOpen,
PanelRightOpen, PanelRightOpen,
CircleStop,
} from "lucide-svelte"; } from "lucide-svelte";
const appContext = createAppContext(); const appContext = createAppContext();
@ -44,7 +45,7 @@
uiState, uiState,
executionContext, executionContext,
} = appContext; } = appContext;
const csoundDerived = createCsoundDerivedStores(csound); const { logs: csoundLogs, initialized, compiled, running } = createCsoundDerivedStores(csound);
let analyserNode = $state<AnalyserNode | null>(null); let analyserNode = $state<AnalyserNode | null>(null);
let interpreterLogs = $state<LogEntry[]>([]); let interpreterLogs = $state<LogEntry[]>([]);
@ -91,9 +92,27 @@
projectEditor.loadProject(projectToLoad); projectEditor.loadProject(projectToLoad);
} }
logsUnsubscribe = csoundDerived.logs.subscribe((logs) => { logsUnsubscribe = csoundLogs.subscribe((logs) => {
interpreterLogs = logs; interpreterLogs = logs;
}); });
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === '.') {
e.preventDefault();
handleStop();
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
handleSave();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}); });
onDestroy(async () => { onDestroy(async () => {
@ -220,6 +239,14 @@
} }
} }
async function handleStop() {
try {
await csound.stop();
} catch (error) {
console.error("Failed to stop:", error);
}
}
$effect(() => { $effect(() => {
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) { if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
analyserNode = csound.getAnalyserNode(); analyserNode = csound.getAnalyserNode();
@ -281,6 +308,14 @@
<div class="app-container"> <div class="app-container">
<TopBar title="OldBoy"> <TopBar title="OldBoy">
{#snippet leftActions()} {#snippet leftActions()}
<button
onclick={handleStop}
class="icon-button stop-button"
disabled={!$running}
title="Stop audio (Ctrl+.)"
>
<CircleStop size={18} />
</button>
<button <button
class="icon-button" class="icon-button"
onclick={handleNewFromTemplate} onclick={handleNewFromTemplate}
@ -292,8 +327,8 @@
class="icon-button" class="icon-button"
onclick={handleSave} onclick={handleSave}
disabled={!projectEditor.hasUnsavedChanges} disabled={!projectEditor.hasUnsavedChanges}
title="Save {projectEditor.hasUnsavedChanges title="Save (Ctrl+S){projectEditor.hasUnsavedChanges
? '(unsaved changes)' ? ' - unsaved changes'
: ''}" : ''}"
class:has-changes={projectEditor.hasUnsavedChanges} class:has-changes={projectEditor.hasUnsavedChanges}
> >
@ -537,6 +572,15 @@
color: #646cff; color: #646cff;
} }
.icon-button.stop-button:not(:disabled) {
color: #ff6b6b;
}
.icon-button.stop-button:hover:not(:disabled) {
color: #ff5252;
border-color: #ff5252;
}
h3 { h3 {
margin-top: 0; margin-top: 0;
color: rgba(255, 255, 255, 0.87); color: rgba(255, 255, 255, 0.87);

View File

@ -42,6 +42,7 @@
const lineNumbersCompartment = new Compartment(); const lineNumbersCompartment = new Compartment();
const lineWrappingCompartment = new Compartment(); const lineWrappingCompartment = new Compartment();
const vimCompartment = new Compartment(); const vimCompartment = new Compartment();
const languageCompartment = new Compartment();
function handleExecute() { function handleExecute() {
if (!editorView) return; if (!editorView) return;
@ -111,11 +112,13 @@
const initSettings = $editorSettings; const initSettings = $editorSettings;
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
editorView = new EditorView({ editorView = new EditorView({
doc: value, doc: value,
extensions: [ extensions: [
...baseExtensions, ...baseExtensions,
csoundMode({ fileType: 'csd' }), languageCompartment.of(csoundMode({ fileType })),
oneDark, oneDark,
evaluateKeymap, evaluateKeymap,
flashField(), flashField(),
@ -154,6 +157,15 @@
editorView.dom.style.fontFamily = settings.fontFamily; editorView.dom.style.fontFamily = settings.fontFamily;
}); });
$effect(() => {
if (!editorView) return;
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
editorView.dispatch({
effects: languageCompartment.reconfigure(csoundMode({ fileType }))
});
});
onDestroy(() => { onDestroy(() => {
if (editorView) { if (editorView) {
editorView.destroy(); editorView.destroy();

View File

@ -1,13 +1,10 @@
import { Csound } from '@csound/browser'; import { Csound } from '@csound/browser';
export type InstanceMode = 'persistent' | 'ephemeral';
export interface CsoundEngineOptions { export interface CsoundEngineOptions {
onMessage?: (message: string) => void; onMessage?: (message: string) => void;
onError?: (error: string) => void; onError?: (error: string) => void;
onPerformanceEnd?: () => void; onPerformanceEnd?: () => void;
onAnalyserNodeCreated?: (node: AnalyserNode) => void; onAnalyserNodeCreated?: (node: AnalyserNode) => void;
instanceMode?: InstanceMode;
} }
export interface PerformanceMetrics { export interface PerformanceMetrics {
@ -30,16 +27,15 @@ export class CsoundEngine {
private scopeNode: AnalyserNode | null = null; private scopeNode: AnalyserNode | null = null;
private audioNode: AudioNode | null = null; private audioNode: AudioNode | null = null;
private audioContext: AudioContext | null = null; private audioContext: AudioContext | null = null;
private instanceMode: InstanceMode;
constructor(options: CsoundEngineOptions = {}) { constructor(options: CsoundEngineOptions = {}) {
this.options = options; this.options = options;
this.instanceMode = options.instanceMode ?? 'ephemeral';
} }
async init(): Promise<void> { async init(): Promise<void> {
if (this.initialized) return; if (this.initialized) return;
await this.ensureCsoundInstance();
this.initialized = true; this.initialized = true;
this.log('Csound ready'); this.log('Csound ready');
} }
@ -49,10 +45,51 @@ export class CsoundEngine {
this.log('Creating Csound instance...'); this.log('Creating Csound instance...');
this.csound = await Csound(); this.csound = await Csound();
this.setupCallbacks(); this.setupCallbacks();
await this.csound.setOption('-odac');
} }
} }
async restart(): Promise<void> {
this.log('Restarting Csound...');
if (this.running) {
await this.stop();
}
if (this.csound) {
try {
await this.csound.reset();
} catch (error) {
this.log('Reset failed, creating new instance...');
await this.cleanupInstance();
await this.ensureCsoundInstance();
}
} else {
await this.ensureCsoundInstance();
}
await this.csound!.setOption('-m0');
await this.csound!.setOption('-odac');
await this.csound!.setOption('-+msg_color=false');
await this.csound!.setOption('--daemon');
const ac = await this.csound!.getAudioContext();
const sampleRate = ac?.sampleRate || 48000;
const header = `sr = ${sampleRate}\nksmps = 32\n0dbfs = 1\nnchnls = 2\nnchnls_i = 1`;
const compileResult = await this.csound!.compileOrc(header);
if (compileResult !== 0) {
throw new Error('Failed to compile base header');
}
await this.csound!.start();
this.compiled = true;
this.running = true;
this.setupAnalyser();
this.log('Csound restarted and ready');
}
private setupCallbacks(): void { private setupCallbacks(): void {
if (!this.csound) return; if (!this.csound) return;
@ -78,11 +115,13 @@ export class CsoundEngine {
throw new Error('Csound not initialized. Call init() first.'); throw new Error('Csound not initialized. Call init() first.');
} }
try { if (!this.csound) {
await this.ensureCsoundInstance(); throw new Error('No Csound instance available');
}
try {
this.log('Compiling orchestra...'); this.log('Compiling orchestra...');
const result = await this.csound!.compileOrc(orchestra); const result = await this.csound.compileOrc(orchestra);
if (result !== 0) { if (result !== 0) {
const errorMsg = 'Orchestra compilation failed'; const errorMsg = 'Orchestra compilation failed';
@ -135,7 +174,7 @@ export class CsoundEngine {
await this.csound!.readScore(event); await this.csound!.readScore(event);
} }
async evaluateCode(code: string, forceNewInstance = false): Promise<void> { async evaluateCode(code: string): Promise<void> {
if (!this.initialized) { if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.'); throw new Error('Csound not initialized. Call init() first.');
} }
@ -145,22 +184,10 @@ export class CsoundEngine {
await this.stop(); await this.stop();
} }
const needsNewInstance = forceNewInstance || await this.restart();
this.instanceMode === 'ephemeral' ||
!this.csound;
if (needsNewInstance && this.csound) {
this.log('Destroying existing instance...');
await this.cleanupInstance();
} else if (this.csound && this.compiled) {
this.log('Resetting Csound for new performance...');
await this.resetForNewPerformance();
}
this.scopeNode = null; this.scopeNode = null;
await this.ensureCsoundInstance();
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/); const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/); const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
@ -171,13 +198,12 @@ export class CsoundEngine {
const orc = orcMatch[1].trim(); const orc = orcMatch[1].trim();
const sco = scoMatch[1].trim(); const sco = scoMatch[1].trim();
const compileResult = await this.compileOrchestra(orc); await this.csound!.compileOrc(orc);
if (!compileResult.success) { this.compiled = true;
throw new Error(compileResult.errorMessage || 'Compilation failed');
}
if (sco) {
await this.readScore(sco); await this.readScore(sco);
await this.startPerformance(); }
} catch (error) { } catch (error) {
this.running = false; this.running = false;
@ -298,31 +324,6 @@ export class CsoundEngine {
} }
} }
async reset(): Promise<void> {
this.log('Resetting Csound...');
await this.stop();
await this.cleanupInstance();
this.compiled = false;
await this.ensureCsoundInstance();
this.log('Reset complete');
}
private async resetForNewPerformance(): Promise<void> {
if (!this.csound) return;
try {
await this.csound.reset();
await this.csound.setOption('-odac');
this.compiled = false;
this.running = false;
this.audioNode = null;
this.audioContext = null;
this.scopeNode = null;
} catch (error) {
this.log('Reset failed, creating new instance...');
await this.cleanupInstance();
}
}
private async cleanupInstance(): Promise<void> { private async cleanupInstance(): Promise<void> {
if (!this.csound) return; if (!this.csound) return;

View File

@ -78,6 +78,10 @@ export class ExecutionContext {
await this.endSession(); await this.endSession();
this.currentMode = newMode; this.currentMode = newMode;
this.strategy = this.createStrategyForMode(newMode); this.strategy = this.createStrategyForMode(newMode);
if (newMode === 'composition') {
await this.csound.stop();
}
} }
private createStrategyForMode(mode: ProjectMode): ExecutionStrategy { private createStrategyForMode(mode: ProjectMode): ExecutionStrategy {

View File

@ -25,7 +25,6 @@ export class CompositionStrategy implements ExecutionStrategy {
export class LiveCodingStrategy implements ExecutionStrategy { export class LiveCodingStrategy implements ExecutionStrategy {
private isInitialized = false; private isInitialized = false;
private headerCompiled = false;
async execute( async execute(
csound: CsoundStore, csound: CsoundStore,
@ -34,95 +33,40 @@ export class LiveCodingStrategy implements ExecutionStrategy {
source: EvalSource source: EvalSource
): Promise<void> { ): Promise<void> {
if (!this.isInitialized) { if (!this.isInitialized) {
await this.initializeFromDocument(csound, fullContent); await csound.restart();
this.isInitialized = true; this.isInitialized = true;
return;
} }
await this.evaluateBlock(csound, code); await this.evaluateBlock(csound, code);
} }
private async initializeFromDocument(
csound: CsoundStore,
fullContent: string
): Promise<void> {
const { header, instruments, score } = this.parseCSD(fullContent);
const fullOrchestra = header + '\n' + instruments;
const compileResult = await csound.compileOrchestra(fullOrchestra);
if (!compileResult.success) {
throw new Error(compileResult.errorMessage || 'Compilation failed');
}
this.headerCompiled = true;
await csound.startPerformance();
if (score.trim()) {
await csound.readScore(score);
}
}
private async evaluateBlock(csound: CsoundStore, code: string): Promise<void> { private async evaluateBlock(csound: CsoundStore, code: string): Promise<void> {
const trimmedCode = code.trim(); const filteredCode = this.stripCommentLines(code);
if (!trimmedCode) return; if (!filteredCode.trim()) return;
if (this.isScoreEvent(trimmedCode)) { if (this.isScoreEvent(filteredCode)) {
await csound.sendScoreEvent(trimmedCode); await csound.sendScoreEvent(filteredCode);
} }
else if (this.isInstrumentDefinition(trimmedCode)) { else if (this.isChannelSet(filteredCode)) {
await csound.compileOrchestra(trimmedCode); await this.handleChannelSet(csound, filteredCode);
}
else if (this.isChannelSet(trimmedCode)) {
await this.handleChannelSet(csound, trimmedCode);
} }
else { else {
await csound.compileOrchestra(trimmedCode); await csound.compileOrchestra(filteredCode);
} }
} }
private parseCSD(content: string): { private stripCommentLines(code: string): string {
header: string; return code
instruments: string; .split('\n')
score: string .filter(line => !line.trim().startsWith(';'))
} { .join('\n');
const orcMatch = content.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
if (!orcMatch) {
return { header: '', instruments: '', score: '' };
}
const orchestra = orcMatch[1].trim();
const scoMatch = content.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
const score = scoMatch ? scoMatch[1].trim() : '';
const instrMatch = orchestra.match(/([\s\S]*?)(instr\s+\d+[\s\S]*)/);
if (instrMatch) {
return {
header: instrMatch[1].trim(),
instruments: instrMatch[2].trim(),
score
};
}
return {
header: orchestra,
instruments: '',
score
};
} }
private isScoreEvent(code: string): boolean { private isScoreEvent(code: string): boolean {
return /^[ifea]\s+[\d\-]/.test(code); return /^[ifea]\s+[\d\-]/.test(code);
} }
private isInstrumentDefinition(code: string): boolean {
return /^\s*instr\s+/.test(code);
}
private isChannelSet(code: string): boolean { private isChannelSet(code: string): boolean {
return /^\w+\s*=\s*[\d\.\-]+/.test(code); return /^\w+\s*=\s*[\d\.\-]+/.test(code);
} }
@ -139,7 +83,6 @@ export class LiveCodingStrategy implements ExecutionStrategy {
reset(): void { reset(): void {
this.isInitialized = false; this.isInitialized = false;
this.headerCompiled = false;
} }
} }

View File

@ -1,5 +1,5 @@
import { writable, derived, get } from 'svelte/store'; import { writable, derived } from 'svelte/store';
import { CsoundEngine, type InstanceMode, type PerformanceMetrics, type CompilationResult } from './engine'; import { CsoundEngine, type PerformanceMetrics, type CompilationResult } from './engine';
export interface LogEntry { export interface LogEntry {
timestamp: Date; timestamp: Date;
@ -21,9 +21,9 @@ export interface CsoundStore {
startPerformance: () => Promise<void>; startPerformance: () => Promise<void>;
readScore: (score: string) => Promise<void>; readScore: (score: string) => Promise<void>;
sendScoreEvent: (event: string) => Promise<void>; sendScoreEvent: (event: string) => Promise<void>;
evaluate: (code: string, forceNewInstance?: boolean) => Promise<void>; evaluate: (code: string) => Promise<void>;
stop: () => Promise<void>; stop: () => Promise<void>;
reset: () => Promise<void>; restart: () => Promise<void>;
setControlChannel: (name: string, value: number) => Promise<void>; setControlChannel: (name: string, value: number) => Promise<void>;
getControlChannel: (name: string) => Promise<number>; getControlChannel: (name: string) => Promise<number>;
setStringChannel: (name: string, value: string) => Promise<void>; setStringChannel: (name: string, value: string) => Promise<void>;
@ -41,7 +41,7 @@ export interface CsoundStore {
destroy: () => Promise<void>; destroy: () => Promise<void>;
} }
export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore { export function createCsoundStore(): CsoundStore {
const initialState: CsoundState = { const initialState: CsoundState = {
initialized: false, initialized: false,
compiled: false, compiled: false,
@ -76,8 +76,7 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
}, },
onAnalyserNodeCreated: (node) => { onAnalyserNodeCreated: (node) => {
analyserNodeListeners.forEach(listener => listener(node)); analyserNodeListeners.forEach(listener => listener(node));
}, }
instanceMode
}); });
await engine.init(); await engine.init();
@ -140,13 +139,13 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
await engine.sendScoreEvent(event); await engine.sendScoreEvent(event);
}, },
async evaluate(code: string, forceNewInstance = false) { async evaluate(code: string) {
if (!engine) { if (!engine) {
throw new Error('Csound engine not initialized'); throw new Error('Csound engine not initialized');
} }
try { try {
await engine.evaluateCode(code, forceNewInstance); await engine.evaluateCode(code);
update(state => ({ update(state => ({
...state, ...state,
compiled: engine!.isCompiled(), compiled: engine!.isCompiled(),
@ -172,14 +171,14 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
} }
}, },
async reset() { async restart() {
if (!engine) return; if (!engine) return;
try { try {
await engine.reset(); await engine.restart();
update(state => ({ ...state, compiled: false, running: false })); update(state => ({ ...state, compiled: true, running: true }));
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Reset failed'; const errorMsg = error instanceof Error ? error.message : 'Restart failed';
addLog(errorMsg, 'error'); addLog(errorMsg, 'error');
} }
}, },

View File

@ -80,14 +80,57 @@ export function getLine(state: EditorState): EvalBlock {
return { text, from, to }; return { text, from, to };
} }
type BlockType = 'instr' | 'endin' | 'opcode' | 'endop' | null;
function startsWithBlockMarker(text: string): BlockType {
const trimmed = text.trim();
if (/^\s*instr\b/.test(text)) return 'instr';
if (/^\s*endin\b/.test(text)) return 'endin';
if (/^\s*opcode\b/.test(text)) return 'opcode';
if (/^\s*endop\b/.test(text)) return 'endop';
return null;
}
function findBlockMarker(
doc: any,
startLine: number,
direction: number,
limit: number
): [number, BlockType] | null {
for (let i = startLine; direction > 0 ? i <= limit : i >= limit; i += direction) {
if (i < 1 || i > doc.lines) break;
const lineText = doc.line(i).text;
const marker = startsWithBlockMarker(lineText);
if (marker) {
return [i, marker];
}
}
return null;
}
export function getBlock(state: EditorState): EvalBlock { export function getBlock(state: EditorState): EvalBlock {
let { doc, selection } = state; let { doc, selection } = state;
let { text, number } = state.doc.lineAt(selection.main.from); let { text, number } = state.doc.lineAt(selection.main.from);
if (text.trim().length === 0) return { text: '', from: null, to: null }; if (text.trim().length === 0) return { text: '', from: null, to: null };
let fromL, toL; const prevBlockMark = findBlockMarker(doc, number, -1, 1);
fromL = toL = number; const nextBlockMark = findBlockMarker(doc, number, 1, doc.lines);
if (
prevBlockMark &&
nextBlockMark &&
((prevBlockMark[1] === 'instr' && nextBlockMark[1] === 'endin') ||
(prevBlockMark[1] === 'opcode' && nextBlockMark[1] === 'endop'))
) {
const { from } = doc.line(prevBlockMark[0]);
const { to } = doc.line(nextBlockMark[0]);
text = state.doc.sliceString(from, to);
return { text, from, to };
}
let fromL = number;
let toL = number;
while (fromL > 1 && doc.line(fromL - 1).text.trim().length > 0) { while (fromL > 1 && doc.line(fromL - 1).text.trim().length > 0) {
fromL -= 1; fromL -= 1;

View File

@ -71,19 +71,11 @@ const LIVECODING_TEMPLATE: CsoundTemplate = {
id: 'livecoding', id: 'livecoding',
name: 'Live Coding', name: 'Live Coding',
mode: 'livecoding', mode: 'livecoding',
content: `<CsoundSynthesizer> content: `; LIVE CODING MODE
<CsOptions> ; Engine auto-initializes on first evaluation (Ctrl+E)
-odac ; Evaluate instruments/opcodes with Ctrl+E to define them
</CsOptions> ; Evaluate score events (i-statements) to trigger sounds
<CsInstruments> ; Press Ctrl+. to stop all audio
sr = 48000
ksmps = 32
nchnls = 2
0dbfs = 1
; Press Cmd/Ctrl+E on the full document first to initialize
; Then evaluate individual blocks to trigger sounds
gaReverb init 0 gaReverb init 0
@ -119,14 +111,12 @@ instr 99
gaReverb = 0 gaReverb = 0
endin endin
</CsInstruments> ; Start reverb (always on)
<CsScore>
i 99 0 -1 i 99 0 -1
</CsScore>
</CsoundSynthesizer>
; LIVE CODING EXAMPLES ; === LIVE CODING EXAMPLES ===
; Select a block and press Ctrl+E to evaluate
; Basic note ; Basic note
i 1 0 2 440 0.3 i 1 0 2 440 0.3
@ -146,7 +136,7 @@ i 2 1.5 0.5 130.81 0.4
; Long note for channel control ; Long note for channel control
i 1 0 30 440 0.3 i 1 0 30 440 0.3
; Change frequency while playing ; Change frequency while playing (select and evaluate)
freq = 440 freq = 440
freq = 554.37 freq = 554.37