diff --git a/src/App.svelte b/src/App.svelte index 794befb..74dee1a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -10,11 +10,11 @@ import Spectrogram from './lib/components/audio/Spectrogram.svelte'; import ConfirmDialog from './lib/components/ui/ConfirmDialog.svelte'; import InputDialog from './lib/components/ui/InputDialog.svelte'; - import { createCsoundDerivedStores, type LogEntry } from './lib/csound'; + import TemplateDialog from './lib/components/ui/TemplateDialog.svelte'; + import { createCsoundDerivedStores, type LogEntry, type EvalSource } from './lib/csound'; import { type CsoundProject } from './lib/project-system'; - import { DEFAULT_CSOUND_TEMPLATE, LIVECODING_TEMPLATE } from './lib/config/templates'; + import { templateRegistry, type CsoundTemplate } from './lib/templates/template-registry'; import { createAppContext, setAppContext } from './lib/contexts/app-context'; - import { createExecutionStrategy, type ExecutionStrategy } from './lib/csound/execution-strategies'; import type { ProjectMode } from './lib/project-system/types'; import { PanelLeftClose, @@ -32,13 +32,11 @@ const appContext = createAppContext(); setAppContext(appContext); - const { csound, projectManager, editorSettings, projectEditor, uiState } = appContext; + const { csound, projectManager, editorSettings, projectEditor, uiState, executionContext } = appContext; const csoundDerived = createCsoundDerivedStores(csound); let analyserNode = $state(null); let interpreterLogs = $state([]); - let currentStrategy = $state(null); - let currentMode = $state('composition'); let logsUnsubscribe: (() => void) | undefined; @@ -47,12 +45,16 @@ const result = await projectManager.getAllProjects(); if (result.success && result.data.length === 0) { - await projectManager.createProject({ - title: 'Template', - author: 'System', - content: DEFAULT_CSOUND_TEMPLATE, - tags: [] - }); + const classicTemplate = templateRegistry.getById('classic'); + if (classicTemplate) { + await projectManager.createProject({ + title: 'Welcome', + author: 'System', + content: classicTemplate.content, + tags: [], + mode: classicTemplate.mode + }); + } } logsUnsubscribe = csoundDerived.logs.subscribe(logs => { @@ -78,9 +80,10 @@ projectEditor.setContent(value); } - function handleNewFile() { + function handleNewEmptyFile() { + const emptyTemplate = templateRegistry.getEmpty(); const result = projectEditor.requestSwitch( - () => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE) + () => projectEditor.createNew(emptyTemplate.content) ); if (result === 'confirm-unsaved') { @@ -88,9 +91,15 @@ } } - function handleNewLiveCodingFile() { + function handleNewFromTemplate() { + uiState.showTemplateDialog(); + } + + function handleTemplateSelect(template: CsoundTemplate) { + uiState.hideTemplateDialog(); + const result = projectEditor.requestSwitch( - () => projectEditor.createNew(LIVECODING_TEMPLATE) + () => projectEditor.createNew(template.content) ); if (result === 'confirm-unsaved') { @@ -110,14 +119,9 @@ } } - async function handleExecute(code: string, source: 'selection' | 'block' | 'document') { + async function handleExecute(code: string, source: EvalSource) { try { - if (!currentStrategy) { - currentStrategy = createExecutionStrategy(currentMode); - } - - const fullContent = projectEditor.content; - await currentStrategy.execute(csound, code, fullContent, source); + await executionContext.execute(code, source); } catch (error) { console.error('Execution error:', error); } @@ -189,28 +193,16 @@ $effect(() => { const mode = projectEditor.currentProject?.mode || 'composition'; + const projectId = projectEditor.currentProjectId; - if (mode !== currentMode) { - const oldMode = currentMode; - currentMode = mode; + if (mode !== executionContext.mode) { + executionContext.switchMode(mode).catch(console.error); + } - // IMPORTANT: Only create new strategy if mode actually changed - // Reset the old strategy if switching away from livecoding - if (oldMode === 'livecoding' && currentStrategy) { - const liveCodingStrategy = currentStrategy as any; - if (liveCodingStrategy.reset) { - liveCodingStrategy.reset(); - } - } + executionContext.setContentProvider(() => projectEditor.content); - currentStrategy = createExecutionStrategy(mode); - - if (mode === 'livecoding' && oldMode === 'composition') { - console.log('Switched to live coding mode'); - } else if (mode === 'composition' && oldMode === 'livecoding') { - csound.stop().catch(console.error); - console.log('Switched to composition mode'); - } + if (projectId) { + executionContext.startSession(projectId).catch(console.error); } }); @@ -236,8 +228,8 @@ @@ -325,7 +317,6 @@ onExecute={handleExecute} logs={interpreterLogs} {editorSettings} - mode={currentMode} /> @@ -448,6 +439,12 @@ placeholder="Untitled" onConfirm={handleSaveAs} /> + + uiState.hideTemplateDialog()} + /> diff --git a/src/lib/contexts/app-context.ts b/src/lib/contexts/app-context.ts index 8e11f33..0806115 100644 --- a/src/lib/contexts/app-context.ts +++ b/src/lib/contexts/app-context.ts @@ -2,6 +2,7 @@ import { getContext, setContext } from 'svelte'; import { ProjectManager } from '../project-system/project-manager'; import { ProjectDatabase } from '../project-system/db'; import { createCsoundStore } from '../csound/store'; +import { ExecutionContext } from '../csound/execution-context'; import { createEditorSettingsStore } from '../stores/editorSettings'; import { ProjectEditor } from '../stores/projectEditor.svelte'; import { UIState } from '../stores/uiState.svelte'; @@ -11,6 +12,7 @@ import type { EditorSettingsStore } from '../stores/editorSettings'; export interface AppContext { projectManager: ProjectManager; csound: CsoundStore; + executionContext: ExecutionContext; editorSettings: EditorSettingsStore; projectEditor: ProjectEditor; uiState: UIState; @@ -21,9 +23,13 @@ const APP_CONTEXT_KEY = Symbol('app-context'); export function createAppContext(): AppContext { const db = new ProjectDatabase(); const projectManager = new ProjectManager(db); + const csound = createCsoundStore(); + const executionContext = new ExecutionContext(csound, 'composition'); + return { projectManager, - csound: createCsoundStore(), + csound, + executionContext, editorSettings: createEditorSettingsStore(), projectEditor: new ProjectEditor(projectManager), uiState: new UIState() diff --git a/src/lib/csound/execution-context.ts b/src/lib/csound/execution-context.ts new file mode 100644 index 0000000..ef9ed61 --- /dev/null +++ b/src/lib/csound/execution-context.ts @@ -0,0 +1,98 @@ +import type { CsoundStore } from './store'; +import type { ProjectMode } from '../project-system/types'; +import { CompositionStrategy, LiveCodingStrategy, type ExecutionStrategy } from './execution-strategies'; + +export type EvalSource = 'selection' | 'block' | 'document'; + +export interface ExecutionSession { + mode: ProjectMode; + projectId: string | null; + isActive: boolean; + startTime: Date; +} + +export class ExecutionContext { + private session: ExecutionSession | null = null; + private strategy: ExecutionStrategy; + private currentMode: ProjectMode; + private contentProvider: (() => string) | null = null; + + constructor( + private csound: CsoundStore, + mode: ProjectMode + ) { + this.currentMode = mode; + this.strategy = this.createStrategyForMode(mode); + } + + get mode(): ProjectMode { + return this.currentMode; + } + + get activeSession(): ExecutionSession | null { + return this.session; + } + + setContentProvider(provider: () => string): void { + this.contentProvider = provider; + } + + async startSession(projectId: string | null): Promise { + await this.endSession(); + + this.session = { + mode: this.currentMode, + projectId, + isActive: true, + startTime: new Date() + }; + + if (this.currentMode === 'livecoding') { + this.resetStrategy(); + } + } + + async endSession(): Promise { + if (!this.session?.isActive) return; + + if (this.currentMode === 'livecoding') { + await this.csound.stop(); + this.resetStrategy(); + } + + this.session = { ...this.session, isActive: false }; + } + + async execute(code: string, source: EvalSource): Promise { + if (this.currentMode === 'livecoding' && (!this.session || !this.session.isActive)) { + await this.startSession(this.session?.projectId ?? null); + } + + const fullContent = this.contentProvider?.() ?? ''; + await this.strategy.execute(this.csound, code, fullContent, source); + } + + async switchMode(newMode: ProjectMode): Promise { + if (newMode === this.currentMode) return; + + await this.endSession(); + this.currentMode = newMode; + this.strategy = this.createStrategyForMode(newMode); + } + + private createStrategyForMode(mode: ProjectMode): ExecutionStrategy { + return mode === 'livecoding' + ? new LiveCodingStrategy() + : new CompositionStrategy(); + } + + private resetStrategy(): void { + if (this.strategy instanceof LiveCodingStrategy) { + this.strategy.reset(); + } + } + + async destroy(): Promise { + await this.endSession(); + } +} diff --git a/src/lib/csound/execution-strategies.ts b/src/lib/csound/execution-strategies.ts index 398b40a..91bf71e 100644 --- a/src/lib/csound/execution-strategies.ts +++ b/src/lib/csound/execution-strategies.ts @@ -1,12 +1,14 @@ import type { CsoundStore } from './store'; import type { ProjectMode } from '../project-system/types'; +export type EvalSource = 'selection' | 'block' | 'document'; + export interface ExecutionStrategy { execute( csound: CsoundStore, code: string, fullContent: string, - source: 'selection' | 'block' | 'document' + source: EvalSource ): Promise; } @@ -15,7 +17,7 @@ export class CompositionStrategy implements ExecutionStrategy { csound: CsoundStore, code: string, fullContent: string, - source: 'selection' | 'block' | 'document' + source: EvalSource ): Promise { await csound.evaluate(fullContent); } @@ -29,7 +31,7 @@ export class LiveCodingStrategy implements ExecutionStrategy { csound: CsoundStore, code: string, fullContent: string, - source: 'selection' | 'block' | 'document' + source: EvalSource ): Promise { if (!this.isInitialized) { await this.initializeFromDocument(csound, fullContent); diff --git a/src/lib/csound/index.ts b/src/lib/csound/index.ts index eaae68b..7c73c60 100644 --- a/src/lib/csound/index.ts +++ b/src/lib/csound/index.ts @@ -7,3 +7,5 @@ export type { } from './engine'; export { createCsoundStore, createCsoundDerivedStores } from './store'; export type { LogEntry, CsoundStore } from './store'; +export { ExecutionContext } from './execution-context'; +export type { ExecutionSession, EvalSource } from './execution-context'; diff --git a/src/lib/stores/uiState.svelte.ts b/src/lib/stores/uiState.svelte.ts index f608509..d50721b 100644 --- a/src/lib/stores/uiState.svelte.ts +++ b/src/lib/stores/uiState.svelte.ts @@ -10,6 +10,7 @@ export class UIState { audioPermissionPopupVisible = $state(true); unsavedChangesDialogVisible = $state(false); saveAsDialogVisible = $state(false); + templateDialogVisible = $state(false); shareUrl = $state(''); @@ -59,4 +60,12 @@ export class UIState { hideSaveAsDialog() { this.saveAsDialogVisible = false; } + + showTemplateDialog() { + this.templateDialogVisible = true; + } + + hideTemplateDialog() { + this.templateDialogVisible = false; + } } diff --git a/src/lib/config/templates.ts b/src/lib/templates/template-registry.ts similarity index 50% rename from src/lib/config/templates.ts rename to src/lib/templates/template-registry.ts index 15b7028..fa9c34c 100644 --- a/src/lib/config/templates.ts +++ b/src/lib/templates/template-registry.ts @@ -1,38 +1,77 @@ -export const DEFAULT_CSOUND_TEMPLATE = ` - - - +import type { ProjectMode } from '../project-system/types'; -sr = 44100 -ksmps = 32 -nchnls = 2 -0dbfs = 1 +export interface CsoundTemplate { + id: string; + name: string; + mode: ProjectMode; + content: string; +} -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 - - - -; 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 - - -`; - -export const LIVECODING_TEMPLATE = ` +const EMPTY_TEMPLATE: CsoundTemplate = { + id: 'empty', + name: 'Empty', + mode: 'composition', + content: ` + +-odac + + + +sr = 48000 +ksmps = 32 +nchnls = 2 +0dbfs = 1 + + + + + + +` +}; + +const CLASSIC_TEMPLATE: CsoundTemplate = { + id: 'classic', + name: 'Classic', + mode: 'composition', + content: ` + +-odac + + + +sr = 48000 +ksmps = 32 +nchnls = 2 +0dbfs = 1 + +instr 1 + iFreq = p4 + iAmp = p5 + + kEnv madsr 0.01, 0.1, 0.6, 0.2 + + aOsc oscili iAmp * kEnv, iFreq + + outs aOsc, aOsc +endin + + + +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 + + +` +}; + +const LIVECODING_TEMPLATE: CsoundTemplate = { + id: 'livecoding', + name: 'Live Coding', + mode: 'livecoding', + content: ` -odac @@ -43,18 +82,14 @@ ksmps = 32 nchnls = 2 0dbfs = 1 -; Live Coding Template ; Press Cmd/Ctrl+E on the full document first to initialize ; Then evaluate individual blocks to trigger sounds -; Global audio bus for effects gaReverb init 0 instr 1 - ; Simple synth with channel control kFreq chnget "freq" kFreq = (kFreq == 0 ? p4 : kFreq) - kAmp = p5 kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0 @@ -67,7 +102,6 @@ instr 1 endin instr 2 - ; Bass synth iFreq = p4 iAmp = p5 @@ -80,7 +114,6 @@ instr 2 endin instr 99 - ; Global reverb aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5 outs aL, aR gaReverb = 0 @@ -88,19 +121,14 @@ endin -; Start reverb (always on) i 99 0 -1 - -; Initial events will be sent during initialization -; After that, evaluate blocks below to trigger sounds ; LIVE CODING EXAMPLES -; Evaluate each block separately (Cmd/Ctrl+E with cursor on the line) -; Basic note (instrument 1, start now, duration 2s, freq 440Hz, amp 0.3) +; Basic note i 1 0 2 440 0.3 ; Arpeggio @@ -115,35 +143,47 @@ i 2 0.5 0.5 146.83 0.4 i 2 1.0 0.5 164.81 0.4 i 2 1.5 0.5 130.81 0.4 -; Long note for testing channel control +; Long note for channel control i 1 0 30 440 0.3 -; While the long note plays, evaluate these to change frequency: +; Change frequency while playing freq = 440 freq = 554.37 freq = 659.25 -freq = 880 - -; Turn off all instances of instrument 1 +; Turn off instrument 1 i -1 0 0 +` +}; -; Redefine instrument 1 with a different sound -instr 1 - kFreq chnget "freq" - kFreq = (kFreq == 0 ? p4 : kFreq) - kAmp = p5 +const TEMPLATE_REGISTRY: CsoundTemplate[] = [ + EMPTY_TEMPLATE, + LIVECODING_TEMPLATE, + CLASSIC_TEMPLATE +]; - kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0 - aSaw vco2 kAmp * kEnv, kFreq, 2 - aSquare vco2 kAmp * kEnv, kFreq * 1.01, 10 +export class TemplateRegistry { + private templates: Map; - aSum = (aSaw + aSquare) * 0.5 - aFilt moogladder aSum, 1500, 0.5 + constructor() { + this.templates = new Map( + TEMPLATE_REGISTRY.map(template => [template.id, template]) + ); + } - outs aFilt, aFilt - gaReverb = gaReverb + aFilt * 0.3 -endin -`; + getAll(): CsoundTemplate[] { + return Array.from(this.templates.values()); + } + + getById(id: string): CsoundTemplate | undefined { + return this.templates.get(id); + } + + getEmpty(): CsoundTemplate { + return EMPTY_TEMPLATE; + } +} + +export const templateRegistry = new TemplateRegistry();