From 46925f5c2e553b3babf43a5e7d9b4bf1bfd97e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Wed, 15 Oct 2025 02:53:48 +0200 Subject: [PATCH] wip live coding mode --- REVIEW_LIVE_CODING_MODE | 0 src/App.svelte | 56 +- src/lib/components/editor/Editor.svelte | 69 +- .../components/editor/EditorWithLogs.svelte | 7 +- src/lib/components/ui/FileBrowser.svelte | 67 +- src/lib/config/templates.ts | 116 ++ src/lib/csound/execution-strategies.ts | 148 +++ src/lib/editor/block-eval.ts | 111 ++ src/lib/project-system/project-manager.ts | 2 + src/lib/project-system/types.ts | 7 + src/lib/stores/projectEditor.svelte.ts | 10 +- to_continue.md | 1030 +++++++++++++++++ 12 files changed, 1599 insertions(+), 24 deletions(-) create mode 100644 REVIEW_LIVE_CODING_MODE create mode 100644 src/lib/csound/execution-strategies.ts create mode 100644 src/lib/editor/block-eval.ts create mode 100644 to_continue.md diff --git a/REVIEW_LIVE_CODING_MODE b/REVIEW_LIVE_CODING_MODE new file mode 100644 index 0000000..e69de29 diff --git a/src/App.svelte b/src/App.svelte index 5bd85f3..794befb 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -12,8 +12,10 @@ import InputDialog from './lib/components/ui/InputDialog.svelte'; import { createCsoundDerivedStores, type LogEntry } from './lib/csound'; import { type CsoundProject } from './lib/project-system'; - import { DEFAULT_CSOUND_TEMPLATE } from './lib/config/templates'; + import { DEFAULT_CSOUND_TEMPLATE, LIVECODING_TEMPLATE } from './lib/config/templates'; 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, PanelLeftOpen, @@ -35,6 +37,8 @@ let analyserNode = $state(null); let interpreterLogs = $state([]); + let currentStrategy = $state(null); + let currentMode = $state('composition'); let logsUnsubscribe: (() => void) | undefined; @@ -84,6 +88,16 @@ } } + function handleNewLiveCodingFile() { + const result = projectEditor.requestSwitch( + () => projectEditor.createNew(LIVECODING_TEMPLATE) + ); + + if (result === 'confirm-unsaved') { + uiState.showUnsavedChangesDialog(); + } + } + function handleFileSelect(project: CsoundProject | null) { if (!project) return; @@ -96,9 +110,14 @@ } } - async function handleExecute(code: string) { + async function handleExecute(code: string, source: 'selection' | 'block' | 'document') { try { - await csound.evaluate(code); + if (!currentStrategy) { + currentStrategy = createExecutionStrategy(currentMode); + } + + const fullContent = projectEditor.content; + await currentStrategy.execute(csound, code, fullContent, source); } catch (error) { console.error('Execution error:', error); } @@ -120,7 +139,7 @@ } } - async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) { + async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string; mode?: import('./lib/project-system/types').ProjectMode }) { await projectEditor.updateMetadata(updates); } @@ -168,6 +187,33 @@ } }); + $effect(() => { + const mode = projectEditor.currentProject?.mode || 'composition'; + + if (mode !== currentMode) { + const oldMode = currentMode; + currentMode = mode; + + // 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(); + } + } + + 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'); + } + } + }); + const panelTabs = [ { id: 'editor', @@ -191,6 +237,7 @@ {projectManager} onFileSelect={handleFileSelect} onNewFile={handleNewFile} + onNewLiveCodingFile={handleNewLiveCodingFile} onMetadataUpdate={handleMetadataUpdate} selectedProjectId={projectEditor.currentProjectId} /> @@ -278,6 +325,7 @@ onExecute={handleExecute} logs={interpreterLogs} {editorSettings} + mode={currentMode} /> diff --git a/src/lib/components/editor/Editor.svelte b/src/lib/components/editor/Editor.svelte index dcdc255..0a1dd33 100644 --- a/src/lib/components/editor/Editor.svelte +++ b/src/lib/components/editor/Editor.svelte @@ -14,13 +14,21 @@ import { oneDark } from '@codemirror/theme-one-dark'; import { vim } from '@replit/codemirror-vim'; import type { EditorSettingsStore } from '../../stores/editorSettings'; + import { + flashField, + flash, + getSelection, + getBlock, + getDocument + } from '../../editor/block-eval'; interface Props { value: string; language?: 'javascript' | 'html' | 'css'; onChange?: (value: string) => void; - onExecute?: (code: string) => void; + onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void; editorSettings: EditorSettingsStore; + mode?: 'composition' | 'livecoding'; } let { @@ -28,7 +36,8 @@ language = 'javascript', onChange, onExecute, - editorSettings + editorSettings, + mode = 'composition' }: Props = $props(); let editorContainer: HTMLDivElement; @@ -44,14 +53,35 @@ const lineWrappingCompartment = new Compartment(); const vimCompartment = new Compartment(); + function handleExecute() { + if (!editorView) return; + + if (mode === 'composition') { + // Composition mode: always evaluate entire document + const doc = getDocument(editorView.state); + flash(editorView, doc.from, doc.to); + onExecute?.(doc.text, 'document'); + } else { + // Live coding mode: evaluate selection or block + const selection = getSelection(editorView.state); + if (selection.text) { + flash(editorView, selection.from, selection.to); + onExecute?.(selection.text, 'selection'); + } else { + const block = getBlock(editorView.state); + if (block.text) { + flash(editorView, block.from, block.to); + onExecute?.(block.text, 'block'); + } + } + } + } + const evaluateKeymap = keymap.of([ { key: 'Mod-e', - run: (view) => { - if (onExecute) { - const code = view.state.doc.toString(); - onExecute(code); - } + run: () => { + handleExecute(); return true; } } @@ -93,6 +123,7 @@ languageExtensions[language], oneDark, evaluateKeymap, + flashField(), lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []), lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []), vimCompartment.of(initSettings.vimMode ? vim() : []), @@ -141,6 +172,30 @@ }); } }); + + export function getSelectedText(): string | null { + if (!editorView) return null; + const { text } = getSelection(editorView.state); + return text || null; + } + + export function getCurrentBlock(): string | null { + if (!editorView) return null; + const { text } = getBlock(editorView.state); + return text || null; + } + + export function getFullDocument(): string { + if (!editorView) return ''; + const { text } = getDocument(editorView.state); + return text; + } + + export function evaluateWithFlash(text: string, from: number | null, to: number | null) { + if (editorView && from !== null && to !== null) { + flash(editorView, from, to); + } + }
diff --git a/src/lib/components/editor/EditorWithLogs.svelte b/src/lib/components/editor/EditorWithLogs.svelte index 549c34e..1bb74a2 100644 --- a/src/lib/components/editor/EditorWithLogs.svelte +++ b/src/lib/components/editor/EditorWithLogs.svelte @@ -8,9 +8,10 @@ value: string; language?: 'javascript' | 'html' | 'css'; onChange?: (value: string) => void; - onExecute?: (code: string) => void; + onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void; logs?: string[]; editorSettings: EditorSettingsStore; + mode?: 'composition' | 'livecoding'; } let { @@ -19,7 +20,8 @@ onChange, onExecute, logs = [], - editorSettings + editorSettings, + mode = 'composition' }: Props = $props(); let logPanelRef: LogPanel; @@ -74,6 +76,7 @@ {onChange} {onExecute} {editorSettings} + {mode} />
diff --git a/src/lib/components/ui/FileBrowser.svelte b/src/lib/components/ui/FileBrowser.svelte index e85ec21..657eae6 100644 --- a/src/lib/components/ui/FileBrowser.svelte +++ b/src/lib/components/ui/FileBrowser.svelte @@ -4,15 +4,18 @@ import type { CsoundProject, ProjectManager } from '../../project-system'; import ConfirmDialog from './ConfirmDialog.svelte'; + import type { ProjectMode } from '../../project-system/types'; + interface Props { projectManager: ProjectManager; onFileSelect?: (project: CsoundProject | null) => void; onNewFile?: () => void; - onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string }) => void; + onNewLiveCodingFile?: () => void; + onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string; mode?: ProjectMode }) => void; selectedProjectId?: string | null; } - let { projectManager, onFileSelect, onNewFile, onMetadataUpdate, selectedProjectId = null }: Props = $props(); + let { projectManager, onFileSelect, onNewFile, onNewLiveCodingFile, onMetadataUpdate, selectedProjectId = null }: Props = $props(); let projects = $state([]); let loading = $state(true); @@ -25,11 +28,13 @@ let editTitle = $state(''); let editAuthor = $state(''); + let editMode = $state('composition'); $effect(() => { if (selectedProject) { editTitle = selectedProject.title; editAuthor = selectedProject.author; + editMode = selectedProject.mode; } }); @@ -67,6 +72,10 @@ onNewFile?.(); } + function handleNewLiveCodingFile() { + onNewLiveCodingFile?.(); + } + function selectProject(project: CsoundProject) { onFileSelect?.(project); } @@ -101,12 +110,14 @@ const hasChanges = editTitle !== selectedProject.title || - editAuthor !== selectedProject.author; + editAuthor !== selectedProject.author || + editMode !== selectedProject.mode; if (hasChanges) { onMetadataUpdate?.(selectedProject.id, { title: editTitle, - author: editAuthor + author: editAuthor, + mode: editMode }); } } @@ -115,9 +126,16 @@
Files - +
+ + +
@@ -174,6 +192,17 @@ onchange={handleMetadataChange} />
+
+ + +
{selectedProject.saveCount}
@@ -228,6 +257,11 @@ letter-spacing: 0.05em; } + .header-actions { + display: flex; + gap: 0.25rem; + } + .action-button { padding: 0.25rem; background-color: transparent; @@ -245,6 +279,15 @@ background-color: rgba(255, 255, 255, 0.1); } + .action-button.live-coding { + color: rgba(100, 108, 255, 0.8); + } + + .action-button.live-coding:hover { + color: rgba(100, 108, 255, 1); + background-color: rgba(100, 108, 255, 0.15); + } + .browser-content { flex: 1; overflow-y: auto; @@ -353,7 +396,8 @@ color: rgba(255, 255, 255, 0.6); } - .field input { + .field input, + .field select { padding: 0.5rem; background-color: #2a2a2a; border: 1px solid #3a3a3a; @@ -362,10 +406,15 @@ outline: none; } - .field input:focus { + .field input:focus, + .field select:focus { border-color: #646cff; } + .field select { + cursor: pointer; + } + .field.readonly .readonly-value { padding: 0.5rem; background-color: #1a1a1a; diff --git a/src/lib/config/templates.ts b/src/lib/config/templates.ts index 043b0aa..15b7028 100644 --- a/src/lib/config/templates.ts +++ b/src/lib/config/templates.ts @@ -31,3 +31,119 @@ i 1 1.5 0.5 523.25 0.3 `; + +export const LIVECODING_TEMPLATE = ` + +-odac + + + +sr = 48000 +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 + aOsc vco2 kAmp * kEnv, kFreq + + aFilt moogladder aOsc, 2000, 0.3 + + outs aFilt, aFilt + gaReverb = gaReverb + aFilt * 0.3 +endin + +instr 2 + ; Bass synth + iFreq = p4 + iAmp = p5 + + kEnv linsegr 0, 0.005, 1, 0.05, 0.5, 0.1, 0 + aOsc vco2 iAmp * kEnv, iFreq, 10 + + aFilt butterlp aOsc, 800 + + outs aFilt, aFilt +endin + +instr 99 + ; Global reverb + aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5 + outs aL, aR + gaReverb = 0 +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) +i 1 0 2 440 0.3 + +; Arpeggio +i 1 0 0.5 261.63 0.2 +i 1 0.5 0.5 329.63 0.2 +i 1 1.0 0.5 392.00 0.2 +i 1 1.5 0.5 523.25 0.2 + +; Bass line +i 2 0 0.5 130.81 0.4 +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 +i 1 0 30 440 0.3 + +; While the long note plays, evaluate these to change frequency: +freq = 440 + +freq = 554.37 + +freq = 659.25 + +freq = 880 + +; Turn off all instances of 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 + + 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 + + aSum = (aSaw + aSquare) * 0.5 + aFilt moogladder aSum, 1500, 0.5 + + outs aFilt, aFilt + gaReverb = gaReverb + aFilt * 0.3 +endin +`; diff --git a/src/lib/csound/execution-strategies.ts b/src/lib/csound/execution-strategies.ts new file mode 100644 index 0000000..398b40a --- /dev/null +++ b/src/lib/csound/execution-strategies.ts @@ -0,0 +1,148 @@ +import type { CsoundStore } from './store'; +import type { ProjectMode } from '../project-system/types'; + +export interface ExecutionStrategy { + execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise; +} + +export class CompositionStrategy implements ExecutionStrategy { + async execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise { + await csound.evaluate(fullContent); + } +} + +export class LiveCodingStrategy implements ExecutionStrategy { + private isInitialized = false; + private headerCompiled = false; + + async execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise { + if (!this.isInitialized) { + await this.initializeFromDocument(csound, fullContent); + this.isInitialized = true; + return; + } + + await this.evaluateBlock(csound, code); + } + + private async initializeFromDocument( + csound: CsoundStore, + fullContent: string + ): Promise { + 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 { + const trimmedCode = code.trim(); + + if (!trimmedCode) return; + + if (this.isScoreEvent(trimmedCode)) { + await csound.sendScoreEvent(trimmedCode); + } + else if (this.isInstrumentDefinition(trimmedCode)) { + await csound.compileOrchestra(trimmedCode); + } + else if (this.isChannelSet(trimmedCode)) { + await this.handleChannelSet(csound, trimmedCode); + } + else { + await csound.compileOrchestra(trimmedCode); + } + } + + private parseCSD(content: string): { + header: string; + instruments: string; + score: string + } { + const orcMatch = content.match(/([\s\S]*?)<\/CsInstruments>/); + if (!orcMatch) { + return { header: '', instruments: '', score: '' }; + } + + const orchestra = orcMatch[1].trim(); + + const scoMatch = content.match(/([\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 { + 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); + } + + private async handleChannelSet(csound: CsoundStore, code: string): Promise { + const match = code.match(/^(\w+)\s*=\s*([\d\.\-]+)/); + if (!match) return; + + const [, channelName, valueStr] = match; + const value = parseFloat(valueStr); + + await csound.setControlChannel(channelName, value); + } + + reset(): void { + this.isInitialized = false; + this.headerCompiled = false; + } +} + +export function createExecutionStrategy(mode: ProjectMode): ExecutionStrategy { + return mode === 'livecoding' + ? new LiveCodingStrategy() + : new CompositionStrategy(); +} diff --git a/src/lib/editor/block-eval.ts b/src/lib/editor/block-eval.ts new file mode 100644 index 0000000..a611ec8 --- /dev/null +++ b/src/lib/editor/block-eval.ts @@ -0,0 +1,111 @@ +import { EditorView, Decoration } from '@codemirror/view'; +import { EditorState, StateField, StateEffect } from '@codemirror/state'; + +interface EvalBlock { + text: string; + from: number | null; + to: number | null; +} + +type FlashRange = [number, number]; + +const setFlash = StateEffect.define(); + +const defaultStyle = { + 'background-color': '#FFCA2880', +}; + +const styleObjectToString = (styleObj: Record): string => + Object.entries(styleObj) + .map(([k, v]) => `${k}:${v}`) + .join(';'); + +export const flash = ( + view: EditorView, + from: number | null, + to: number | null, + timeout: number = 150, +) => { + if (from === null || to === null) return; + view.dispatch({ effects: setFlash.of([from, to]) }); + setTimeout(() => { + view.dispatch({ effects: setFlash.of(null) }); + }, timeout); +}; + +export const flashField = (style: Record = defaultStyle) => + StateField.define({ + create() { + return Decoration.none; + }, + update(flash, tr) { + try { + for (let e of tr.effects) { + if (e.is(setFlash)) { + if (e.value) { + const [from, to] = e.value; + const mark = Decoration.mark({ + attributes: { style: styleObjectToString(style) }, + }); + flash = Decoration.set([mark.range(from, to)]); + } else { + flash = Decoration.set([]); + } + } + } + return flash; + } catch (err) { + console.warn('flash error', err); + return flash; + } + }, + provide: (f) => EditorView.decorations.from(f), + }); + +export function getSelection(state: EditorState): EvalBlock { + if (state.selection.main.empty) return { text: '', from: null, to: null }; + + let { from, to } = state.selection.main; + + let text = state.doc.sliceString(from, to); + return { text, from, to }; +} + +export function getLine(state: EditorState): EvalBlock { + const line = state.doc.lineAt(state.selection.main.from); + + let { from, to } = line; + + let text = state.doc.sliceString(from, to); + return { text, from, to }; +} + +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; + + while (fromL > 1 && doc.line(fromL - 1).text.trim().length > 0) { + fromL -= 1; + } + while (toL < doc.lines && doc.line(toL + 1).text.trim().length > 0) { + toL += 1; + } + + let { from } = doc.line(fromL); + let { to } = doc.line(toL); + + text = state.doc.sliceString(from, to); + return { text, from, to }; +} + +export function getDocument(state: EditorState): EvalBlock { + const { from } = state.doc.line(1); + const { to } = state.doc.line(state.doc.lines); + const text = state.doc.sliceString(from, to); + return { text, from, to }; +} diff --git a/src/lib/project-system/project-manager.ts b/src/lib/project-system/project-manager.ts index 325da60..7e0c451 100644 --- a/src/lib/project-system/project-manager.ts +++ b/src/lib/project-system/project-manager.ts @@ -97,6 +97,7 @@ export class ProjectManager { content: data.content || '', tags: data.tags || [], csoundVersion: CSOUND_VERSION, + mode: data.mode || 'composition', }; await this.db.put(project); @@ -161,6 +162,7 @@ export class ProjectManager { ...(data.author !== undefined && { author: data.author }), ...(data.content !== undefined && { content: data.content }), ...(data.tags !== undefined && { tags: data.tags }), + ...(data.mode !== undefined && { mode: data.mode }), dateModified: getCurrentTimestamp(), saveCount: existingProject.saveCount + 1, }; diff --git a/src/lib/project-system/types.ts b/src/lib/project-system/types.ts index 35e931e..65966dd 100644 --- a/src/lib/project-system/types.ts +++ b/src/lib/project-system/types.ts @@ -1,3 +1,5 @@ +export type ProjectMode = 'composition' | 'livecoding'; + /** * Core data structure for a Csound project */ @@ -28,6 +30,9 @@ export interface CsoundProject { /** Csound version used to create this project */ csoundVersion: string; + + /** Execution mode: composition (full document) or livecoding (block evaluation) */ + mode: ProjectMode; } /** @@ -38,6 +43,7 @@ export interface CreateProjectData { author: string; content?: string; tags?: string[]; + mode?: ProjectMode; } /** @@ -49,6 +55,7 @@ export interface UpdateProjectData { author?: string; content?: string; tags?: string[]; + mode?: ProjectMode; } /** diff --git a/src/lib/stores/projectEditor.svelte.ts b/src/lib/stores/projectEditor.svelte.ts index d7e7fdc..25ce9a1 100644 --- a/src/lib/stores/projectEditor.svelte.ts +++ b/src/lib/stores/projectEditor.svelte.ts @@ -79,6 +79,9 @@ export class ProjectEditor { if (result.success) { this.state.hasUnsavedChanges = false; + if (result.data) { + this.state.currentProject = result.data; + } return true; } @@ -88,11 +91,14 @@ export class ProjectEditor { async saveAs(title: string, author: string = 'Anonymous'): Promise { const finalTitle = title.trim() || 'Untitled'; + const isLiveCodingTemplate = this.state.content.includes('Live Coding Template'); + const result = await this.projectManager.createProject({ title: finalTitle, author, content: this.state.content, - tags: [] + tags: [], + mode: isLiveCodingTemplate ? 'livecoding' : 'composition' }); if (result.success) { @@ -105,7 +111,7 @@ export class ProjectEditor { return false; } - async updateMetadata(updates: { title?: string; author?: string }): Promise { + async updateMetadata(updates: { title?: string; author?: string; mode?: import('../project-system/types').ProjectMode }): Promise { if (!this.state.currentProject) { return false; } diff --git a/to_continue.md b/to_continue.md new file mode 100644 index 0000000..b02c0af --- /dev/null +++ b/to_continue.md @@ -0,0 +1,1030 @@ +# Project Mode Implementation - Continuation Guide + +## Overview + +This document provides a complete roadmap for continuing the implementation of dual-mode support in OldBoy: **Composition Mode** (full document evaluation) and **Live Coding Mode** (block-based evaluation with persistent Csound instance). + +--- + +## What Has Been Completed + +### 1. Type System & Data Model ✅ + +**Files Modified:** +- `/src/lib/project-system/types.ts` +- `/src/lib/project-system/project-manager.ts` + +**Changes:** +1. Created `ProjectMode` type: + ```typescript + export type ProjectMode = 'composition' | 'livecoding'; + ``` + +2. Added `mode` field to `CsoundProject` interface: + ```typescript + export interface CsoundProject { + // ... existing fields + mode: ProjectMode; // NEW: Execution mode + } + ``` + +3. Updated `CreateProjectData` to accept optional mode: + ```typescript + export interface CreateProjectData { + // ... existing fields + mode?: ProjectMode; // Defaults to 'composition' if not provided + } + ``` + +4. Updated `UpdateProjectData` to allow mode updates: + ```typescript + export interface UpdateProjectData { + // ... existing fields + mode?: ProjectMode; // Can change mode after creation + } + ``` + +5. Modified `ProjectManager.createProject()`: + - Line 100: Added `mode: data.mode || 'composition'` to project initialization + - Ensures all new projects have a mode (defaults to composition) + +6. Modified `ProjectManager.updateProject()`: + - Line 165: Added `...(data.mode !== undefined && { mode: data.mode })` to update logic + - Allows mode to be updated without affecting other fields + +**Result:** All projects now have a mode field that persists in IndexedDB and is included in import/export operations. + +--- + +### 2. UI for Mode Selection ✅ + +**Files Modified:** +- `/src/lib/components/ui/FileBrowser.svelte` +- `/src/lib/stores/projectEditor.svelte.ts` +- `/src/App.svelte` + +**FileBrowser Changes:** + +1. **Imports (Line 7):** + ```typescript + import type { ProjectMode } from '../../project-system/types'; + ``` + +2. **Props Interface (Line 13):** + ```typescript + onMetadataUpdate?: (projectId: string, updates: { + title?: string; + author?: string; + mode?: ProjectMode // NEW + }) => void; + ``` + +3. **State (Line 30):** + ```typescript + let editMode = $state('composition'); + ``` + +4. **Effect Hook (Lines 32-38):** + ```typescript + $effect(() => { + if (selectedProject) { + editTitle = selectedProject.title; + editAuthor = selectedProject.author; + editMode = selectedProject.mode; // NEW: Sync mode from project + } + }); + ``` + +5. **Metadata Change Handler (Lines 103-118):** + ```typescript + function handleMetadataChange() { + if (!selectedProject) return; + + const hasChanges = + editTitle !== selectedProject.title || + editAuthor !== selectedProject.author || + editMode !== selectedProject.mode; // NEW: Include mode in change detection + + if (hasChanges) { + onMetadataUpdate?.(selectedProject.id, { + title: editTitle, + author: editAuthor, + mode: editMode // NEW: Send mode updates + }); + } + } + ``` + +6. **UI Element (Lines 183-193):** + ```svelte +
+ + +
+ ``` + +7. **CSS (Lines 373-390):** + ```css + .field input, + .field select { + padding: 0.5rem; + background-color: #2a2a2a; + border: 1px solid #3a3a3a; + color: rgba(255, 255, 255, 0.87); + font-size: 0.875rem; + outline: none; + } + + .field select { + cursor: pointer; + } + ``` + +**ProjectEditor Changes (Line 108):** +```typescript +async updateMetadata(updates: { + title?: string; + author?: string; + mode?: import('../project-system/types').ProjectMode // NEW +}): Promise +``` + +**App.svelte Changes (Line 123):** +```typescript +async function handleMetadataUpdate( + projectId: string, + updates: { + title?: string; + author?: string; + mode?: import('./lib/project-system/types').ProjectMode // NEW + } +) +``` + +**Result:** Users can now select project mode in the Files panel metadata editor. Mode changes are immediately persisted. + +--- + +### 3. Block Evaluation Infrastructure ✅ + +**File Created:** +- `/src/lib/editor/block-eval.ts` + +**Purpose:** Provides utilities for extracting code blocks and visual feedback, adapted from flok's cm-eval package. + +**Exports:** + +1. **`flash(view, from, to, timeout)`** + - Visually highlights evaluated code region + - Default timeout: 150ms + - Background color: `#FFCA2880` (yellow with transparency) + +2. **`flashField(style?)`** + - CodeMirror StateField for managing flash decorations + - Returns StateField to be added to editor extensions + - Handles flash effect lifecycle + +3. **`getSelection(state): EvalBlock`** + - Returns currently selected text with positions + - Returns `{ text: '', from: null, to: null }` if no selection + +4. **`getLine(state): EvalBlock`** + - Returns the entire line at cursor position + - Includes line's from/to positions + +5. **`getBlock(state): EvalBlock`** + - Returns paragraph block (text separated by blank lines) + - Searches up and down from cursor until blank lines found + - Core functionality for live coding mode + +6. **`getDocument(state): EvalBlock`** + - Returns entire document text + - Used for composition mode evaluation + +**EvalBlock Interface:** +```typescript +interface EvalBlock { + text: string; // The code to evaluate + from: number | null; // Start position in document + to: number | null; // End position in document +} +``` + +**Implementation Details:** + +- **Block Detection Algorithm:** + 1. Start at cursor line + 2. If line is blank, return empty block + 3. Search backwards until blank line or document start + 4. Search forwards until blank line or document end + 5. Extract text from start to end positions + +- **Flash Effect:** + - Uses CodeMirror StateEffect system + - Creates decoration with inline style + - Automatically clears after timeout + - Non-blocking (doesn't prevent editing) + +**Result:** Complete block evaluation infrastructure ready to integrate with Editor component. + +--- + +## What Needs to Be Done Next + +### Phase 1: Editor Integration with Block Evaluation + +**Goal:** Connect block-eval utilities to the Editor component and add visual feedback. + +**File to Modify:** `/src/lib/components/editor/Editor.svelte` + +**Steps:** + +1. **Import block-eval utilities (add to imports):** + ```typescript + import { + flashField, + flash, + getSelection, + getBlock, + getDocument + } from '../editor/block-eval'; + ``` + +2. **Add flashField to extensions:** + + Find where extensions are created (likely in a `$derived` or similar). It should look something like: + ```typescript + let extensions = $derived([ + // ... existing extensions + ]); + ``` + + Add flashField: + ```typescript + let extensions = $derived([ + // ... existing extensions + flashField(), // NEW: Add flash effect support + ]); + ``` + +3. **Expose block extraction methods:** + + Add these methods to the Editor component (after the component logic, before closing tag): + ```typescript + export function getSelectedText(): string | null { + if (!editorView) return null; + const { text } = getSelection(editorView.state); + return text || null; + } + + export function getCurrentBlock(): string | null { + if (!editorView) return null; + const { text } = getBlock(editorView.state); + return text || null; + } + + export function getFullDocument(): string { + if (!editorView) return ''; + const { text } = getDocument(editorView.state); + return text; + } + + export function evaluateWithFlash(text: string, from: number | null, to: number | null) { + if (editorView && from !== null && to !== null) { + flash(editorView, from, to); + } + } + ``` + +4. **Modify keyboard shortcut for execute:** + + Find the keyboard shortcut setup (search for "Ctrl-Enter" or "Cmd-Enter"). It might look like: + ```typescript + keymap.of([ + { + key: "Ctrl-Enter", + run: () => { + onExecute?.(value); + return true; + } + } + ]) + ``` + + Change it to call a new internal method: + ```typescript + keymap.of([ + { + key: "Ctrl-Enter", + mac: "Cmd-Enter", + run: () => { + handleExecute(); + return true; + } + } + ]) + ``` + +5. **Add internal execute handler:** + ```typescript + function handleExecute() { + if (!editorView) return; + + // Get selection or block + const selection = getSelection(editorView.state); + if (selection.text) { + // Has selection: evaluate it + flash(editorView, selection.from, selection.to); + onExecute?.(selection.text, 'selection'); + } else { + // No selection: evaluate block or document based on mode + // For now, always get block (mode logic will be in App.svelte) + const block = getBlock(editorView.state); + if (block.text) { + flash(editorView, block.from, block.to); + onExecute?.(block.text, 'block'); + } + } + } + ``` + +6. **Update onExecute prop type:** + + Change the prop signature from: + ```typescript + onExecute?: (code: string) => void; + ``` + + To: + ```typescript + onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void; + ``` + +**Expected Result:** +- Cmd/Ctrl+Enter will flash the code being evaluated +- Editor can distinguish between selection, block, and document evaluation +- Parent components can access block extraction methods + +--- + +### Phase 2: Execution Strategy Implementation + +**Goal:** Create strategy pattern for handling composition vs livecoding execution modes. + +**File to Create:** `/src/lib/csound/execution-strategies.ts` + +**Implementation:** + +```typescript +import type { CsoundStore } from './store'; +import type { ProjectMode } from '../project-system/types'; + +export interface ExecutionStrategy { + execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise; +} + +/** + * Composition Mode Strategy + * - Always evaluates full CSD document + * - Uses ephemeral instances (fresh instance each time) + * - Standard workflow: compile → start → play → cleanup + */ +export class CompositionStrategy implements ExecutionStrategy { + async execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise { + // Always evaluate full document in composition mode + await csound.evaluate(fullContent); + } +} + +/** + * Live Coding Mode Strategy + * - Evaluates blocks incrementally + * - Uses persistent instance (maintains state) + * - Supports: score events, channel updates, instrument redefinition + */ +export class LiveCodingStrategy implements ExecutionStrategy { + private isInitialized = false; + private headerCompiled = false; + + async execute( + csound: CsoundStore, + code: string, + fullContent: string, + source: 'selection' | 'block' | 'document' + ): Promise { + // First time: initialize with full document + if (!this.isInitialized) { + await this.initializeFromDocument(csound, fullContent); + this.isInitialized = true; + return; + } + + // Subsequent evaluations: handle blocks + await this.evaluateBlock(csound, code); + } + + private async initializeFromDocument( + csound: CsoundStore, + fullContent: string + ): Promise { + // Parse CSD to extract orchestra and initial score + const { header, instruments, score } = this.parseCSD(fullContent); + + // Compile header + instruments + const fullOrchestra = header + '\n' + instruments; + const compileResult = await csound.compileOrchestra(fullOrchestra); + + if (!compileResult.success) { + throw new Error(compileResult.errorMessage || 'Compilation failed'); + } + + this.headerCompiled = true; + + // Start performance + await csound.startPerformance(); + + // If score has events, send them + if (score.trim()) { + await csound.readScore(score); + } + } + + private async evaluateBlock(csound: CsoundStore, code: string): Promise { + const trimmedCode = code.trim(); + + if (!trimmedCode) return; + + // Detect what kind of code this is + if (this.isScoreEvent(trimmedCode)) { + // Send score event (e.g., "i 1 0 2 0.5") + await csound.sendScoreEvent(trimmedCode); + } + else if (this.isInstrumentDefinition(trimmedCode)) { + // Recompile instrument + await csound.compileOrchestra(trimmedCode); + } + else if (this.isChannelSet(trimmedCode)) { + // Set channel value (e.g., "freq = 440") + await this.handleChannelSet(csound, trimmedCode); + } + else { + // Default: try to compile as orchestra code + await csound.compileOrchestra(trimmedCode); + } + } + + private parseCSD(content: string): { + header: string; + instruments: string; + score: string + } { + // Extract section + const orcMatch = content.match(/([\s\S]*?)<\/CsInstruments>/); + if (!orcMatch) { + return { header: '', instruments: '', score: '' }; + } + + const orchestra = orcMatch[1].trim(); + + // Extract section + const scoMatch = content.match(/([\s\S]*?)<\/CsScore>/); + const score = scoMatch ? scoMatch[1].trim() : ''; + + // Split orchestra into header (sr, ksmps, nchnls, etc.) and instruments + const instrMatch = orchestra.match(/([\s\S]*?)(instr\s+\d+[\s\S]*)/); + + if (instrMatch) { + return { + header: instrMatch[1].trim(), + instruments: instrMatch[2].trim(), + score + }; + } + + // If no instruments found, treat entire orchestra as header + return { + header: orchestra, + instruments: '', + score + }; + } + + private isScoreEvent(code: string): boolean { + // Check for score event syntax: i, f, e, a followed by space/number + return /^[ifea]\s+[\d\-]/.test(code); + } + + private isInstrumentDefinition(code: string): boolean { + // Check for instrument definition + return /^\s*instr\s+/.test(code); + } + + private isChannelSet(code: string): boolean { + // Simple channel set syntax: varname = value + // e.g., "freq = 440" or "cutoff = 2000" + return /^\w+\s*=\s*[\d\.\-]+/.test(code); + } + + private async handleChannelSet(csound: CsoundStore, code: string): Promise { + // Parse: varname = value + const match = code.match(/^(\w+)\s*=\s*([\d\.\-]+)/); + if (!match) return; + + const [, channelName, valueStr] = match; + const value = parseFloat(valueStr); + + await csound.setControlChannel(channelName, value); + } + + /** + * Reset the strategy state (called when switching documents or resetting) + */ + reset(): void { + this.isInitialized = false; + this.headerCompiled = false; + } +} + +/** + * Factory function to create appropriate strategy based on mode + */ +export function createExecutionStrategy(mode: ProjectMode): ExecutionStrategy { + return mode === 'livecoding' + ? new LiveCodingStrategy() + : new CompositionStrategy(); +} +``` + +**Expected Result:** +- Strategy pattern cleanly separates composition and livecoding behaviors +- LiveCodingStrategy maintains state across evaluations +- Supports score events, instrument redefinition, and channel control + +--- + +### Phase 3: Update App.svelte to Use Strategies + +**File to Modify:** `/src/App.svelte` + +**Steps:** + +1. **Import strategy utilities:** + ```typescript + import { createExecutionStrategy, type ExecutionStrategy } from './lib/csound/execution-strategies'; + ``` + +2. **Track current strategy:** + ```typescript + let currentStrategy = $state(null); + let currentMode = $state('composition'); + ``` + +3. **Update strategy when project changes:** + + Add effect to watch for project mode changes: + ```typescript + $effect(() => { + const mode = projectEditor.currentProject?.mode || 'composition'; + + // Only recreate strategy if mode changed + if (mode !== currentMode) { + currentMode = mode; + currentStrategy = createExecutionStrategy(mode); + + // If switching to livecoding, need to reset csound + if (mode === 'livecoding') { + // Reset to ensure clean state + csound.reset().catch(console.error); + } + } + }); + ``` + +4. **Update handleExecute function:** + + Replace existing handleExecute (around line 99): + ```typescript + async function handleExecute(code: string, source: 'selection' | 'block' | 'document') { + try { + if (!currentStrategy) { + currentStrategy = createExecutionStrategy(currentMode); + } + + const fullContent = projectEditor.content; + await currentStrategy.execute(csound, code, fullContent, source); + } catch (error) { + console.error('Execution error:', error); + } + } + ``` + +5. **Update EditorWithLogs onExecute prop:** + + Find where EditorWithLogs is used (around line 274): + ```svelte + + logs={interpreterLogs} + {editorSettings} + /> + ``` + +**Expected Result:** +- App automatically uses correct strategy based on project mode +- Strategy instance persists across evaluations +- Mode switches trigger proper cleanup/reset + +--- + +### Phase 4: Update Csound Store for Mode-Aware Instance Management + +**Goal:** Make Csound store use persistent vs ephemeral instances based on project mode. + +**File to Modify:** `/src/lib/contexts/app-context.ts` + +**Current Code (likely around line 10-15):** +```typescript +const csound = createCsoundStore(); +``` + +**Change to:** +```typescript +const csound = createCsoundStore('ephemeral'); // Default to ephemeral +``` + +**Then in App.svelte, add mode-based reset logic:** + +In the `$effect` that watches mode changes (from Phase 3, Step 3), enhance it: + +```typescript +$effect(() => { + const mode = projectEditor.currentProject?.mode || 'composition'; + + if (mode !== currentMode) { + const oldMode = currentMode; + currentMode = mode; + currentStrategy = createExecutionStrategy(mode); + + // Handle Csound instance mode switching + if (mode === 'livecoding' && oldMode === 'composition') { + // Switching TO livecoding: need persistent instance + // Reset will be handled by first LiveCodingStrategy execution + console.log('Switched to live coding mode'); + } else if (mode === 'composition' && oldMode === 'livecoding') { + // Switching FROM livecoding: stop and cleanup + csound.stop().catch(console.error); + console.log('Switched to composition mode'); + } + } +}); +``` + +**Note:** The actual instance mode (ephemeral/persistent) is now implicitly handled by the strategy: +- **CompositionStrategy**: calls `csound.evaluate()` which uses ephemeral mode (current default) +- **LiveCodingStrategy**: calls `compileOrchestra()` + `startPerformance()` which uses the same instance repeatedly + +The key insight is that **persistence is achieved by NOT calling `evaluate()`** (which destroys/recreates), but instead using the low-level API (`compileOrchestra`, `startPerformance`, `sendScoreEvent`). + +**Expected Result:** +- Composition mode: Each evaluation gets fresh instance +- Live coding mode: Single instance persists across block evaluations +- Switching modes properly cleans up old instances + +--- + +### Phase 5: Testing & Verification + +**Test Plan:** + +#### Test 1: Composition Mode (Baseline) +1. Create new project (defaults to composition mode) +2. Paste this CSD code: + ```csound + + + -odac + + + sr = 48000 + ksmps = 32 + nchnls = 2 + 0dbfs = 1 + + instr 1 + aOut poscil 0.2, 440 + outs aOut, aOut + endin + + + i 1 0 2 + + + ``` +3. Press Cmd/Ctrl+Enter +4. **Expected:** Entire document flashes yellow, sound plays for 2 seconds +5. Modify frequency to 880, press Cmd/Ctrl+Enter again +6. **Expected:** New instance created, new sound plays + +#### Test 2: Live Coding Mode - Initial Setup +1. Open FileBrowser, select the project +2. In metadata, change Mode from "Composition" to "Live Coding" +3. Press Cmd/Ctrl+Enter on the entire document +4. **Expected:** + - Document flashes + - Csound initializes (check logs for "Starting performance...") + - Sound plays if score has events + +#### Test 3: Live Coding Mode - Block Evaluation +1. Clear the `` section (or comment out with semicolons) +2. Ensure document is initialized (press Cmd/Ctrl+Enter on full document) +3. Add a new line at the bottom of the document: + ```csound + i 1 0 2 0.5 + ``` +4. Place cursor on that line, press Cmd/Ctrl+Enter +5. **Expected:** + - Only that line flashes + - Sound plays immediately (instrument 1 triggered) + - Logs show score event sent, NOT full recompilation + +#### Test 4: Live Coding Mode - Channel Control +1. Modify instrument 1 to use a channel: + ```csound + instr 1 + kFreq chnget "freq" + aOut poscil 0.2, kFreq + outs aOut, aOut + endin + ``` +2. Reinitialize (Cmd+Enter on full document) +3. Start a long note: + ```csound + i 1 0 60 + ``` +4. While playing, evaluate this block: + ```csound + freq = 440 + ``` +5. Then evaluate: + ```csound + freq = 880 + ``` +6. **Expected:** + - Frequency changes in real-time while note plays + - No audio interruption + +#### Test 5: Live Coding Mode - Instrument Redefinition +1. While note is playing from Test 4, modify instrument 1: + ```csound + instr 1 + kFreq chnget "freq" + aOut vco2 0.2, kFreq + outs aOut, aOut + endin + ``` +2. Select only the instrument definition, press Cmd+Enter +3. **Expected:** + - Instrument recompiles + - Next triggered note uses new definition + - Currently playing notes might continue with old definition (Csound behavior) + +#### Test 6: Mode Switching +1. In live coding mode with performance running +2. Switch mode to "Composition" in FileBrowser +3. **Expected:** + - Performance stops + - Logs show "Stopped" +4. Press Cmd+Enter +5. **Expected:** + - Full document evaluation (composition mode behavior) + +#### Test 7: Persistence Across Sessions +1. Create project in live coding mode +2. Close browser tab +3. Reopen, load project +4. **Expected:** + - Mode is still "Live Coding" in metadata + - First evaluation initializes correctly + +--- + +## Troubleshooting Guide + +### Issue: "Cannot find module '@flok-editor/session'" + +**Cause:** The block-eval code references types from flok's session package. + +**Solution:** +- Our implementation in `/src/lib/editor/block-eval.ts` is standalone and doesn't need this +- If TypeScript complains, we don't use the `Document` type from flok +- Our editor integration passes callbacks, not Document objects + +### Issue: Flashing doesn't appear + +**Cause:** flashField not added to editor extensions + +**Solution:** +1. Verify `import { flashField } from '../editor/block-eval'` in Editor.svelte +2. Verify `flashField()` is in the extensions array +3. Check browser console for errors + +### Issue: Block evaluation evaluates wrong text + +**Cause:** Block detection algorithm confused by comment syntax + +**Solution:** +- Csound uses `;` for line comments +- Blank line detection: `line.text.trim().length === 0` +- If commented lines interfere, update `getBlock()` to treat `;`-only lines as blank + +### Issue: Live coding mode doesn't persist + +**Cause:** Strategy state lost between evaluations + +**Solution:** +- Verify `currentStrategy` is stored in `$state()`, not recreated each time +- Check that `$effect` only recreates strategy when mode actually changes +- Ensure LiveCodingStrategy instance is reused + +### Issue: Performance doesn't start in live coding mode + +**Cause:** `startPerformance()` not called or called before compilation + +**Solution:** +- Check logs for compilation errors +- Verify `compileOrchestra()` returns `success: true` +- Ensure `startPerformance()` is awaited + +### Issue: Mode changes don't take effect + +**Cause:** Project not reloaded after metadata update + +**Solution:** +- `handleMetadataUpdate()` should call `projectEditor.updateMetadata()` +- This should update `projectEditor.currentProject` +- `$effect` watching `currentProject?.mode` should trigger +- Verify effect dependencies are correct + +--- + +## File Structure Reference + +### New Files Created +``` +/src/lib/editor/block-eval.ts - Block evaluation utilities (✅ COMPLETE) +/src/lib/csound/execution-strategies.ts - Strategy pattern (❌ TODO) +``` + +### Modified Files +``` +/src/lib/project-system/types.ts - Added ProjectMode type (✅ COMPLETE) +/src/lib/project-system/project-manager.ts - Added mode handling (✅ COMPLETE) +/src/lib/components/ui/FileBrowser.svelte - Added mode selector UI (✅ COMPLETE) +/src/lib/stores/projectEditor.svelte.ts - Updated metadata types (✅ COMPLETE) +/src/App.svelte - Updated handlers (✅ PARTIAL - needs strategy integration) +/src/lib/components/editor/Editor.svelte - Needs block eval integration (❌ TODO) +/src/lib/contexts/app-context.ts - Might need mode-aware csound init (❌ TODO) +``` + +--- + +## Key Design Principles to Remember + +1. **No Fallbacks**: If livecoding fails, it fails. Don't silently fall back to composition mode. + +2. **Mode Per Project**: Mode is project metadata, not global state. Different tabs could have different modes. + +3. **Persistent = Reuse Low-Level API**: Persistence isn't a csound setting, it's about calling `compileOrchestra()` + `startPerformance()` instead of `evaluate()`. + +4. **Block = Paragraph**: A block is text separated by blank lines. This is the standard live coding convention. + +5. **Flash for Feedback**: Always flash evaluated code so user knows what executed. + +6. **Strategy Owns State**: LiveCodingStrategy tracks initialization state, not the store or app. + +--- + +## Implementation Order + +Follow this exact order to minimize issues: + +1. ✅ **Phase 1**: Editor integration (visual feedback works first) +2. ✅ **Phase 2**: Create execution strategies (logic isolated and testable) +3. ✅ **Phase 3**: Wire strategies to App.svelte (connect pieces) +4. ✅ **Phase 4**: Verify instance management (make sure persistence works) +5. ✅ **Phase 5**: Test thoroughly (catch edge cases) + +--- + +## Success Criteria + +### Composition Mode +- [✅] Full document evaluation on Cmd+Enter +- [✅] Flash effect shows what was evaluated +- [✅] Fresh instance every time (no state leakage) + +### Live Coding Mode +- [❌] First evaluation initializes from full document +- [❌] Subsequent evaluations process blocks incrementally +- [❌] Score events trigger sounds immediately +- [❌] Channel updates affect running performance +- [❌] Instrument redefinition works +- [❌] Instance persists across evaluations +- [❌] Logs show block evaluations, not full recompilations + +### Both Modes +- [✅] Mode selection UI works +- [✅] Mode persists in database +- [❌] Mode switching cleans up properly +- [❌] Keyboard shortcuts work consistently + +--- + +## Additional Notes + +### Why Ephemeral Default Makes Sense Now + +The original implementation used ephemeral mode by default because Web Audio reconnection after `csound.reset()` was unreliable. However, in live coding mode: + +- We **don't call `reset()`** between evaluations +- We **don't call `evaluate()`** which destroys the instance +- We use **low-level API** (`compileOrchestra`, `sendScoreEvent`) which reuses the instance + +So "persistent mode" is actually achieved by **avoiding the methods that destroy/recreate**. + +### Why We Adapted cm-eval Instead of Using It + +The flok cm-eval package: +- Assumes a `Document` object with an `evaluate()` method +- Is tightly coupled to flok's session architecture +- Uses their specific remote evaluation system + +Our needs: +- Evaluate blocks locally (no remote) +- Different document format (CSD vs raw code) +- Integrate with existing Csound store + +Solution: Extract the core block detection and flash logic, adapt for our architecture. + +### Csound Score Event Syntax Quick Reference + +For testing live coding mode: + +```csound +; Start instrument 1 at time 0, duration 2 seconds, amplitude 0.5 +i 1 0 2 0.5 + +; Start instrument 1 now (time 0), indefinite duration (-1) +i 1 0 -1 0.5 + +; Turn off all instances of instrument 1 +i -1 0 0 + +; Function table: create table 1, size 8192, sine wave +f 1 0 8192 10 1 +``` + +--- + +## Next Session Checklist + +When you resume implementation: + +- [ ] Read this document thoroughly +- [ ] Verify all completed work with `pnpm build` +- [ ] Start with Phase 1 (Editor integration) +- [ ] Test each phase before moving to next +- [ ] Update this document if you deviate from the plan +- [ ] Document any issues found and solutions + +--- + +## Questions to Resolve + +None at this time. The architecture is well-defined and ready for implementation. + +--- + +**Document Version:** 1.0 +**Last Updated:** 2025-01-15 +**Status:** Ready for Phase 1 implementation