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

View File

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

View File

@ -1,13 +1,10 @@
import { Csound } from '@csound/browser';
export type InstanceMode = 'persistent' | 'ephemeral';
export interface CsoundEngineOptions {
onMessage?: (message: string) => void;
onError?: (error: string) => void;
onPerformanceEnd?: () => void;
onAnalyserNodeCreated?: (node: AnalyserNode) => void;
instanceMode?: InstanceMode;
}
export interface PerformanceMetrics {
@ -30,16 +27,15 @@ export class CsoundEngine {
private scopeNode: AnalyserNode | null = null;
private audioNode: AudioNode | null = null;
private audioContext: AudioContext | null = null;
private instanceMode: InstanceMode;
constructor(options: CsoundEngineOptions = {}) {
this.options = options;
this.instanceMode = options.instanceMode ?? 'ephemeral';
}
async init(): Promise<void> {
if (this.initialized) return;
await this.ensureCsoundInstance();
this.initialized = true;
this.log('Csound ready');
}
@ -49,10 +45,51 @@ export class CsoundEngine {
this.log('Creating Csound instance...');
this.csound = await Csound();
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 {
if (!this.csound) return;
@ -78,11 +115,13 @@ export class CsoundEngine {
throw new Error('Csound not initialized. Call init() first.');
}
try {
await this.ensureCsoundInstance();
if (!this.csound) {
throw new Error('No Csound instance available');
}
try {
this.log('Compiling orchestra...');
const result = await this.csound!.compileOrc(orchestra);
const result = await this.csound.compileOrc(orchestra);
if (result !== 0) {
const errorMsg = 'Orchestra compilation failed';
@ -135,7 +174,7 @@ export class CsoundEngine {
await this.csound!.readScore(event);
}
async evaluateCode(code: string, forceNewInstance = false): Promise<void> {
async evaluateCode(code: string): Promise<void> {
if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.');
}
@ -145,22 +184,10 @@ export class CsoundEngine {
await this.stop();
}
const needsNewInstance = forceNewInstance ||
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();
}
await this.restart();
this.scopeNode = null;
await this.ensureCsoundInstance();
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
@ -171,13 +198,12 @@ export class CsoundEngine {
const orc = orcMatch[1].trim();
const sco = scoMatch[1].trim();
const compileResult = await this.compileOrchestra(orc);
if (!compileResult.success) {
throw new Error(compileResult.errorMessage || 'Compilation failed');
}
await this.csound!.compileOrc(orc);
this.compiled = true;
if (sco) {
await this.readScore(sco);
await this.startPerformance();
}
} catch (error) {
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> {
if (!this.csound) return;

View File

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

View File

@ -25,7 +25,6 @@ export class CompositionStrategy implements ExecutionStrategy {
export class LiveCodingStrategy implements ExecutionStrategy {
private isInitialized = false;
private headerCompiled = false;
async execute(
csound: CsoundStore,
@ -34,95 +33,40 @@ export class LiveCodingStrategy implements ExecutionStrategy {
source: EvalSource
): Promise<void> {
if (!this.isInitialized) {
await this.initializeFromDocument(csound, fullContent);
await csound.restart();
this.isInitialized = true;
return;
}
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> {
const trimmedCode = code.trim();
const filteredCode = this.stripCommentLines(code);
if (!trimmedCode) return;
if (!filteredCode.trim()) return;
if (this.isScoreEvent(trimmedCode)) {
await csound.sendScoreEvent(trimmedCode);
if (this.isScoreEvent(filteredCode)) {
await csound.sendScoreEvent(filteredCode);
}
else if (this.isInstrumentDefinition(trimmedCode)) {
await csound.compileOrchestra(trimmedCode);
}
else if (this.isChannelSet(trimmedCode)) {
await this.handleChannelSet(csound, trimmedCode);
else if (this.isChannelSet(filteredCode)) {
await this.handleChannelSet(csound, filteredCode);
}
else {
await csound.compileOrchestra(trimmedCode);
await csound.compileOrchestra(filteredCode);
}
}
private parseCSD(content: string): {
header: string;
instruments: string;
score: string
} {
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 stripCommentLines(code: string): string {
return code
.split('\n')
.filter(line => !line.trim().startsWith(';'))
.join('\n');
}
private isScoreEvent(code: string): boolean {
return /^[ifea]\s+[\d\-]/.test(code);
}
private isInstrumentDefinition(code: string): boolean {
return /^\s*instr\s+/.test(code);
}
private isChannelSet(code: string): boolean {
return /^\w+\s*=\s*[\d\.\-]+/.test(code);
}
@ -139,7 +83,6 @@ export class LiveCodingStrategy implements ExecutionStrategy {
reset(): void {
this.isInitialized = false;
this.headerCompiled = false;
}
}

View File

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

View File

@ -80,14 +80,57 @@ export function getLine(state: EditorState): EvalBlock {
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 {
let { doc, selection } = state;
let { text, number } = state.doc.lineAt(selection.main.from);
if (text.trim().length === 0) return { text: '', from: null, to: null };
let fromL, toL;
fromL = toL = number;
const prevBlockMark = findBlockMarker(doc, number, -1, 1);
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) {
fromL -= 1;

View File

@ -71,19 +71,11 @@ const LIVECODING_TEMPLATE: CsoundTemplate = {
id: 'livecoding',
name: 'Live Coding',
mode: 'livecoding',
content: `<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>
<CsInstruments>
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
content: `; LIVE CODING MODE
; Engine auto-initializes on first evaluation (Ctrl+E)
; Evaluate instruments/opcodes with Ctrl+E to define them
; Evaluate score events (i-statements) to trigger sounds
; Press Ctrl+. to stop all audio
gaReverb init 0
@ -119,14 +111,12 @@ instr 99
gaReverb = 0
endin
</CsInstruments>
<CsScore>
; Start reverb (always on)
i 99 0 -1
</CsScore>
</CsoundSynthesizer>
; LIVE CODING EXAMPLES
; === LIVE CODING EXAMPLES ===
; Select a block and press Ctrl+E to evaluate
; Basic note
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
i 1 0 30 440 0.3
; Change frequency while playing
; Change frequency while playing (select and evaluate)
freq = 440
freq = 554.37