This commit is contained in:
2025-11-14 01:53:18 +01:00
parent e7ffcda096
commit 8941ee13bc
29 changed files with 3752 additions and 122 deletions

View File

@ -299,11 +299,11 @@
</button>
{/snippet}
<button
onclick={() => editorWithLogsRef?.triggerEvaluate()}
onclick={handleExecuteFile}
class="icon-button evaluate-button"
disabled={!$initialized}
class:is-running={$running}
title="Evaluate Block (Cmd-E)"
title="Run File (Cmd-R)"
>
<Play size={18} />
</button>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { getAppContext } from '../../contexts/app-context';
import { themes } from '../../themes';
import CsoundVersionToggle from '../settings/CsoundVersionToggle.svelte';
const { editorSettings } = getAppContext();
@ -18,6 +19,10 @@
</script>
<div class="editor-settings">
<div class="setting version-setting">
<CsoundVersionToggle />
</div>
<div class="setting">
<label>
<span class="label-text">Theme</span>
@ -245,4 +250,10 @@
width: 100%;
height: 100%;
}
.version-setting {
margin-bottom: var(--space-xl);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--border-color);
}
</style>

View File

@ -0,0 +1,77 @@
<script lang="ts">
import { getCsoundVersion, setCsoundVersion } from '../../utils/preferences';
let useCsound7 = $state(getCsoundVersion());
function handleVersionChange(version7: boolean) {
if (useCsound7 === version7) return;
const confirmed = confirm(
`Switch to Csound ${version7 ? '7' : '6'}?\n\n` +
`This will reload the page to apply the changes.`
);
if (confirmed) {
useCsound7 = version7;
setCsoundVersion(useCsound7);
if (typeof window !== 'undefined') {
window.location.reload();
}
}
}
</script>
<div class="csound-version-toggle">
<button
class="version-toggle-button"
class:active={!useCsound7}
onclick={() => handleVersionChange(false)}
title="Csound 6.18.7 (recommended for CCN)"
>
Csound 6
</button>
<button
class="version-toggle-button"
class:active={useCsound7}
onclick={() => handleVersionChange(true)}
title="Csound 7.0.0-beta11 (experimental)"
>
Csound 7
</button>
</div>
<style>
.csound-version-toggle {
display: flex;
gap: var(--space-xs);
padding: var(--space-xs);
}
.version-toggle-button {
flex: 1;
padding: var(--space-sm);
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-sm);
font-weight: 600;
transition: all var(--transition-base);
font-family: inherit;
}
.version-toggle-button:hover:not(.active) {
color: var(--text-color);
background-color: var(--editor-active-line);
border-color: var(--accent-color);
}
.version-toggle-button.active {
color: var(--accent-color);
background-color: rgba(100, 200, 255, 0.15);
border-color: var(--accent-color);
}
</style>

View File

@ -6,6 +6,7 @@ import { ExecutionContext } from '../csound/execution-context';
import { createEditorSettingsStore } from '../stores/editorSettings';
import { EditorState } from '../stores/editorState.svelte';
import { UIState } from '../stores/uiState.svelte';
import { getCsoundVersion } from '../utils/preferences';
import type { CsoundStore } from '../csound/store';
import type { EditorSettingsStore } from '../stores/editorSettings';
@ -23,7 +24,15 @@ const APP_CONTEXT_KEY = Symbol('app-context');
export function createAppContext(): AppContext {
const db = new FileDatabase();
const fileManager = new FileManager(db);
const csound = createCsoundStore();
const csound = createCsoundStore({
getProjectFiles: async () => {
const result = await fileManager.getAllFiles();
return result.success ? result.data : [];
},
useCsound7: getCsoundVersion()
});
const executionContext = new ExecutionContext(csound);
return {

View File

@ -1,10 +1,15 @@
import { Csound } from '@csound/browser';
import { Csound as Csound6 } from '@csound/browser';
import { Csound as Csound7 } from 'csound7';
import type { File } from '../project-system/types';
import { compileCSD, type CsoundObj } from './types';
export interface CsoundEngineOptions {
onMessage?: (message: string) => void;
onError?: (error: string) => void;
onPerformanceEnd?: () => void;
onAnalyserNodeCreated?: (node: AnalyserNode) => void;
getProjectFiles?: () => Promise<File[]>;
useCsound7?: boolean;
}
export interface PerformanceMetrics {
@ -19,7 +24,7 @@ export interface CompilationResult {
}
export class CsoundEngine {
private csound: Csound | null = null;
private csound: CsoundObj | null = null;
private initialized = false;
private compiled = false;
private running = false;
@ -27,9 +32,11 @@ export class CsoundEngine {
private scopeNode: AnalyserNode | null = null;
private audioNode: AudioNode | null = null;
private audioContext: AudioContext | null = null;
private useCsound7: boolean;
constructor(options: CsoundEngineOptions = {}) {
this.options = options;
this.useCsound7 = options.useCsound7 ?? false;
}
async init(): Promise<void> {
@ -42,7 +49,9 @@ export class CsoundEngine {
private async ensureCsoundInstance(): Promise<void> {
if (!this.csound) {
this.log('Creating Csound instance...');
const version = this.useCsound7 ? '7' : '6';
this.log(`Creating Csound ${version} instance...`);
const Csound = this.useCsound7 ? Csound7 : Csound6;
this.csound = await Csound();
this.setupCallbacks();
}
@ -68,6 +77,7 @@ export class CsoundEngine {
}
await this.csound!.setOption('-m0');
await this.csound!.setOption('-d');
await this.csound!.setOption('-odac');
await this.csound!.setOption('-+msg_color=false');
await this.csound!.setOption('--daemon');
@ -82,14 +92,43 @@ export class CsoundEngine {
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');
}
/**
* Sync all project files to Csound's virtual filesystem
* This enables #include directives to work properly
*/
private async syncProjectFilesToFS(): Promise<void> {
if (!this.csound) {
this.log('Warning: Cannot sync files - no Csound instance');
return;
}
if (!this.options.getProjectFiles) {
this.log('Warning: Cannot sync files - no getProjectFiles callback');
return;
}
const files = await this.options.getProjectFiles();
this.log(`Syncing ${files.length} files to virtual FS...`);
const encoder = new TextEncoder();
for (const file of files) {
try {
const content = encoder.encode(file.content);
await this.csound.fs.writeFile(file.title, content);
this.log(` - ${file.title}`);
} catch (error) {
this.log(`Warning: Failed to write ${file.title}: ${error}`);
}
}
this.log('File sync complete');
}
private setupCallbacks(): void {
if (!this.csound) return;
@ -174,7 +213,98 @@ export class CsoundEngine {
await this.csound!.readScore(event);
}
async evaluateCode(code: string): Promise<void> {
/**
* Run a .csd file (composition mode)
* Syncs all files to virtual FS, then uses native CSD compilation
*/
async playCSD(filename: string, content?: string): Promise<void> {
if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.');
}
try {
console.log('[Engine] playCSD() called with filename:', filename);
console.log('[Engine] Content provided:', !!content);
if (content) {
console.log('[Engine] Content preview:', content.substring(0, 100));
console.log('[Engine] Content length:', content.length);
}
if (this.running) {
await this.stop();
}
await this.restart();
this.scopeNode = null;
// Sync all files to virtual FS so #includes work
await this.syncProjectFilesToFS();
// If content is provided, write it to the filename
if (content) {
const encoder = new TextEncoder();
const encoded = encoder.encode(content);
console.log('[Engine] Encoded content length:', encoded.length);
await this.csound!.fs.writeFile(filename, encoded);
this.log(`Wrote ${filename} to virtual FS (${encoded.length} bytes)`);
console.log('[Engine] Successfully wrote to virtual FS');
// Verify the file was written correctly
try {
const readBack = await this.csound!.fs.readFile(filename);
const decoder = new TextDecoder();
const readContent = decoder.decode(readBack);
console.log('[Engine] Read back from FS:', readContent.substring(0, 100));
console.log('[Engine] Read back length:', readContent.length);
console.log('[Engine] Contains closing tag:', readContent.includes('</CsoundSynthesizer>'));
} catch (e) {
console.error('[Engine] Failed to read back file:', e);
}
}
this.log(`Compiling ${filename}...`);
// Debug: List files in virtual FS root to verify file exists
try {
const files = await this.csound!.fs.readdir('/');
console.log('[Engine] Files in virtual FS root:', files);
} catch (e) {
console.error('[Engine] Failed to list FS:', e);
}
// Use native CSD compilation (Csound handles #includes)
// Mode 0 = path (read from virtual FS), Mode 1 = text (parse as CSD content)
console.log('[Engine] Calling compileCSD with filename:', filename, 'mode: 0 (path)');
const result = await compileCSD(this.csound!, filename, 0);
console.log('[Engine] compileCSD returned:', result);
if (result !== 0) {
throw new Error(`Failed to compile ${filename}`);
}
this.compiled = true;
this.log('CSD compiled successfully');
// Start performance after successful compilation
this.log('Starting performance...');
await this.csound!.start();
this.running = true;
this.setupAnalyser();
this.log('Performance started');
} catch (error) {
this.running = false;
this.compiled = false;
const errorMsg = error instanceof Error ? error.message : 'Evaluation failed';
this.error(errorMsg);
throw error;
}
}
/**
* Run a .orc file (orchestra only, no score)
*/
async playORC(orchestraCode: string): Promise<void> {
if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.');
}
@ -185,25 +315,25 @@ export class CsoundEngine {
}
await this.restart();
this.scopeNode = null;
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
this.log('Compiling orchestra...');
if (!orcMatch || !scoMatch) {
throw new Error('Invalid CSD format. Must contain <CsInstruments> and <CsScore> sections.');
const result = await this.csound!.compileOrc(orchestraCode);
if (result !== 0) {
throw new Error('Failed to compile orchestra');
}
const orc = orcMatch[1].trim();
const sco = scoMatch[1].trim();
await this.csound!.compileOrc(orc);
this.compiled = true;
this.log('Orchestra compiled successfully');
if (sco) {
await this.readScore(sco);
}
// Start performance after successful compilation
this.log('Starting performance...');
await this.csound!.start();
this.running = true;
this.setupAnalyser();
this.log('Performance started');
} catch (error) {
this.running = false;
@ -214,6 +344,28 @@ export class CsoundEngine {
}
}
/**
* Incremental code evaluation (live coding mode)
* Requires Csound to be already running
*/
async evalCode(code: string): Promise<void> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
if (!this.running) {
throw new Error('Csound not running. Use playCSD() or playORC() first.');
}
this.log('Evaluating code...');
const result = await this.csound.evalCode(code);
if (result !== 0) {
this.error('Code evaluation failed');
throw new Error('Code evaluation failed');
}
}
async setControlChannel(name: string, value: number): Promise<void> {
if (!this.csound) {
throw new Error('No Csound instance available');

View File

@ -12,28 +12,46 @@ export class ExecutionContext {
this.contentProvider = provider;
}
/**
* Check if content is a complete CSD file
*/
private isCSDContent(code: string): boolean {
return code.includes('<CsoundSynthesizer>') &&
code.includes('<CsInstruments>');
}
/**
* Execute entire file (composition mode: full restart)
*/
async executeFile(): Promise<void> {
const content = this.contentProvider?.() ?? '';
console.log('[ExecutionContext] Content from provider:', content.substring(0, 100));
console.log('[ExecutionContext] Content length:', content.length);
if (!content.trim()) {
console.log('[ExecutionContext] Content is empty, aborting');
return;
}
this.initialized = false;
await this.csound.stop();
await this.csound.evaluate(content);
this.initialized = true;
}
/**
* Execute code block (live coding mode: incremental evaluation)
* If the code is a complete CSD file, routes to executeFile() instead
*/
async executeBlock(code: string): Promise<void> {
if (!code.trim()) {
return;
}
// If this is a complete CSD file, use composition mode
if (this.isCSDContent(code)) {
return await this.executeFile();
}
if (!this.initialized) {
await this.csound.restart();
this.initialized = true;
@ -81,6 +99,7 @@ export class ExecutionContext {
}
// Default: orchestra code (instrument definitions, opcodes, etc.)
await this.csound.compileOrchestra(trimmed);
// Use evalCode() to maintain macro context from initial CSD compilation
await this.csound.evalCode(trimmed);
}
}

View File

@ -52,7 +52,8 @@ export class LiveCodingStrategy implements ExecutionStrategy {
await this.handleChannelSet(csound, filteredCode);
}
else {
await csound.compileOrchestra(filteredCode);
// Use incremental evalCode for live coding
await csound.evalCode(filteredCode);
}
}

View File

@ -1,5 +1,6 @@
import { writable, derived } from 'svelte/store';
import { CsoundEngine, type PerformanceMetrics, type CompilationResult } from './engine';
import type { File } from '../project-system/types';
export interface LogEntry {
timestamp: Date;
@ -22,6 +23,9 @@ export interface CsoundStore {
readScore: (score: string) => Promise<void>;
sendScoreEvent: (event: string) => Promise<void>;
evaluate: (code: string) => Promise<void>;
playCSD: (filename: string) => Promise<void>;
playORC: (orchestraCode: string) => Promise<void>;
evalCode: (code: string) => Promise<void>;
stop: () => Promise<void>;
restart: () => Promise<void>;
setControlChannel: (name: string, value: number) => Promise<void>;
@ -41,7 +45,12 @@ export interface CsoundStore {
destroy: () => Promise<void>;
}
export function createCsoundStore(): CsoundStore {
export interface CsoundStoreOptions {
getProjectFiles?: () => Promise<File[]>;
useCsound7?: boolean;
}
export function createCsoundStore(options: CsoundStoreOptions = {}): CsoundStore {
const initialState: CsoundState = {
initialized: false,
compiled: false,
@ -76,7 +85,9 @@ export function createCsoundStore(): CsoundStore {
},
onAnalyserNodeCreated: (node) => {
analyserNodeListeners.forEach(listener => listener(node));
}
},
getProjectFiles: options.getProjectFiles,
useCsound7: options.useCsound7
});
await engine.init();
@ -145,7 +156,20 @@ export function createCsoundStore(): CsoundStore {
}
try {
await engine.evaluateCode(code);
console.log('[Store] evaluate() received code:', code.substring(0, 100));
console.log('[Store] Code length:', code.length);
console.log('[Store] Contains <CsoundSynthesizer>:', code.includes('<CsoundSynthesizer>'));
// Legacy method - detect file type and route appropriately
if (code.includes('<CsoundSynthesizer>')) {
console.log('[Store] Detected CSD file, calling playCSD');
// Pass the CSD content to playCSD which will write it after file sync
await engine.playCSD('temp.csd', code);
} else {
console.log('[Store] Detected ORC file, calling playORC');
// Assume it's orchestra code
await engine.playORC(code);
}
update(state => ({
...state,
compiled: engine!.isCompiled(),
@ -159,6 +183,60 @@ export function createCsoundStore(): CsoundStore {
}
},
async playCSD(filename: string) {
if (!engine) {
throw new Error('Csound engine not initialized');
}
try {
await engine.playCSD(filename);
update(state => ({
...state,
compiled: engine!.isCompiled(),
running: engine!.isRunning()
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'CSD playback failed';
addLog(errorMsg, 'error');
update(state => ({ ...state, compiled: false, running: false }));
throw error;
}
},
async playORC(orchestraCode: string) {
if (!engine) {
throw new Error('Csound engine not initialized');
}
try {
await engine.playORC(orchestraCode);
update(state => ({
...state,
compiled: engine!.isCompiled(),
running: engine!.isRunning()
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Orchestra playback failed';
addLog(errorMsg, 'error');
update(state => ({ ...state, compiled: false, running: false }));
throw error;
}
},
async evalCode(code: string) {
if (!engine) {
throw new Error('Csound engine not initialized');
}
try {
await engine.evalCode(code);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Code evaluation failed';
addLog(errorMsg, 'error');
throw error;
}
},
async stop() {
if (!engine) return;

22
src/lib/csound/types.ts Normal file
View File

@ -0,0 +1,22 @@
// CsoundObj is a type that represents the Csound instance
// returned by the Csound() function from either version
export type CsoundObj = any;
export function isCsound7(csound: CsoundObj): boolean {
// Csound 7 uses compileCSD (capital D), Csound 6 uses compileCsd (lowercase d)
return 'compileCSD' in csound && typeof csound.compileCSD === 'function';
}
export async function compileCSD(
csound: CsoundObj,
filename: string,
mode: number
): Promise<number> {
if (isCsound7(csound)) {
// Csound 7: compileCSD(filename, mode)
return await csound.compileCSD(filename, mode);
} else {
// Csound 6: compileCsd(filename) - no mode parameter
return await csound.compileCsd(filename);
}
}

View File

@ -9,6 +9,7 @@ const SYSTEM_FILE_NAMES = [
'globals.orc',
'lib.orc',
'livecode.orc',
'project.csd',
'scale.orc',
'synth.orc',
] as const;

View File

@ -0,0 +1,18 @@
const CSOUND_VERSION_KEY = 'useCsound7';
export function getCsoundVersion(): boolean {
if (typeof window === 'undefined') {
return false;
}
const stored = localStorage.getItem(CSOUND_VERSION_KEY);
return stored === 'true';
}
export function setCsoundVersion(useCsound7: boolean): void {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(CSOUND_VERSION_KEY, useCsound7.toString());
}