1031 lines
29 KiB
Markdown
1031 lines
29 KiB
Markdown
# 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<ProjectMode>('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
|
|
<div class="field">
|
|
<label for="file-mode">Mode</label>
|
|
<select
|
|
id="file-mode"
|
|
bind:value={editMode}
|
|
onchange={handleMetadataChange}
|
|
>
|
|
<option value="composition">Composition</option>
|
|
<option value="livecoding">Live Coding</option>
|
|
</select>
|
|
</div>
|
|
```
|
|
|
|
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<boolean>
|
|
```
|
|
|
|
**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<void>;
|
|
}
|
|
|
|
/**
|
|
* 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<void> {
|
|
// 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<void> {
|
|
// 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<void> {
|
|
// 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<void> {
|
|
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 <CsInstruments> section
|
|
const orcMatch = content.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
|
|
if (!orcMatch) {
|
|
return { header: '', instruments: '', score: '' };
|
|
}
|
|
|
|
const orchestra = orcMatch[1].trim();
|
|
|
|
// Extract <CsScore> section
|
|
const scoMatch = content.match(/<CsScore>([\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<void> {
|
|
// 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<ExecutionStrategy | null>(null);
|
|
let currentMode = $state<import('./lib/project-system/types').ProjectMode>('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
|
|
<EditorWithLogs
|
|
value={projectEditor.content}
|
|
language="javascript"
|
|
onChange={handleEditorChange}
|
|
onExecute={handleExecute} <!-- Already correct, just verify signature matches -->
|
|
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
|
|
<CsoundSynthesizer>
|
|
<CsOptions>
|
|
-odac
|
|
</CsOptions>
|
|
<CsInstruments>
|
|
sr = 48000
|
|
ksmps = 32
|
|
nchnls = 2
|
|
0dbfs = 1
|
|
|
|
instr 1
|
|
aOut poscil 0.2, 440
|
|
outs aOut, aOut
|
|
endin
|
|
</CsInstruments>
|
|
<CsScore>
|
|
i 1 0 2
|
|
</CsScore>
|
|
</CsoundSynthesizer>
|
|
```
|
|
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 `<CsScore>` 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
|