wip live coding mode
This commit is contained in:
0
REVIEW_LIVE_CODING_MODE
Normal file
0
REVIEW_LIVE_CODING_MODE
Normal file
@ -12,8 +12,10 @@
|
|||||||
import InputDialog from './lib/components/ui/InputDialog.svelte';
|
import InputDialog from './lib/components/ui/InputDialog.svelte';
|
||||||
import { createCsoundDerivedStores, type LogEntry } from './lib/csound';
|
import { createCsoundDerivedStores, type LogEntry } from './lib/csound';
|
||||||
import { type CsoundProject } from './lib/project-system';
|
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 { 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 {
|
import {
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
@ -35,6 +37,8 @@
|
|||||||
|
|
||||||
let analyserNode = $state<AnalyserNode | null>(null);
|
let analyserNode = $state<AnalyserNode | null>(null);
|
||||||
let interpreterLogs = $state<LogEntry[]>([]);
|
let interpreterLogs = $state<LogEntry[]>([]);
|
||||||
|
let currentStrategy = $state<ExecutionStrategy | null>(null);
|
||||||
|
let currentMode = $state<ProjectMode>('composition');
|
||||||
|
|
||||||
let logsUnsubscribe: (() => void) | undefined;
|
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) {
|
function handleFileSelect(project: CsoundProject | null) {
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
@ -96,9 +110,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExecute(code: string) {
|
async function handleExecute(code: string, source: 'selection' | 'block' | 'document') {
|
||||||
try {
|
try {
|
||||||
await csound.evaluate(code);
|
if (!currentStrategy) {
|
||||||
|
currentStrategy = createExecutionStrategy(currentMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullContent = projectEditor.content;
|
||||||
|
await currentStrategy.execute(csound, code, fullContent, source);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Execution error:', 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);
|
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 = [
|
const panelTabs = [
|
||||||
{
|
{
|
||||||
id: 'editor',
|
id: 'editor',
|
||||||
@ -191,6 +237,7 @@
|
|||||||
{projectManager}
|
{projectManager}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
onNewFile={handleNewFile}
|
onNewFile={handleNewFile}
|
||||||
|
onNewLiveCodingFile={handleNewLiveCodingFile}
|
||||||
onMetadataUpdate={handleMetadataUpdate}
|
onMetadataUpdate={handleMetadataUpdate}
|
||||||
selectedProjectId={projectEditor.currentProjectId}
|
selectedProjectId={projectEditor.currentProjectId}
|
||||||
/>
|
/>
|
||||||
@ -278,6 +325,7 @@
|
|||||||
onExecute={handleExecute}
|
onExecute={handleExecute}
|
||||||
logs={interpreterLogs}
|
logs={interpreterLogs}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
|
mode={currentMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -14,13 +14,21 @@
|
|||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import { vim } from '@replit/codemirror-vim';
|
||||||
import type { EditorSettingsStore } from '../../stores/editorSettings';
|
import type { EditorSettingsStore } from '../../stores/editorSettings';
|
||||||
|
import {
|
||||||
|
flashField,
|
||||||
|
flash,
|
||||||
|
getSelection,
|
||||||
|
getBlock,
|
||||||
|
getDocument
|
||||||
|
} from '../../editor/block-eval';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
language?: 'javascript' | 'html' | 'css';
|
language?: 'javascript' | 'html' | 'css';
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onExecute?: (code: string) => void;
|
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
|
mode?: 'composition' | 'livecoding';
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -28,7 +36,8 @@
|
|||||||
language = 'javascript',
|
language = 'javascript',
|
||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecute,
|
||||||
editorSettings
|
editorSettings,
|
||||||
|
mode = 'composition'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let editorContainer: HTMLDivElement;
|
let editorContainer: HTMLDivElement;
|
||||||
@ -44,14 +53,35 @@
|
|||||||
const lineWrappingCompartment = new Compartment();
|
const lineWrappingCompartment = new Compartment();
|
||||||
const vimCompartment = 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([
|
const evaluateKeymap = keymap.of([
|
||||||
{
|
{
|
||||||
key: 'Mod-e',
|
key: 'Mod-e',
|
||||||
run: (view) => {
|
run: () => {
|
||||||
if (onExecute) {
|
handleExecute();
|
||||||
const code = view.state.doc.toString();
|
|
||||||
onExecute(code);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +123,7 @@
|
|||||||
languageExtensions[language],
|
languageExtensions[language],
|
||||||
oneDark,
|
oneDark,
|
||||||
evaluateKeymap,
|
evaluateKeymap,
|
||||||
|
flashField(),
|
||||||
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
|
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
|
||||||
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||||
vimCompartment.of(initSettings.vimMode ? vim() : []),
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
|
|||||||
@ -8,9 +8,10 @@
|
|||||||
value: string;
|
value: string;
|
||||||
language?: 'javascript' | 'html' | 'css';
|
language?: 'javascript' | 'html' | 'css';
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onExecute?: (code: string) => void;
|
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||||
logs?: string[];
|
logs?: string[];
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
|
mode?: 'composition' | 'livecoding';
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -19,7 +20,8 @@
|
|||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecute,
|
||||||
logs = [],
|
logs = [],
|
||||||
editorSettings
|
editorSettings,
|
||||||
|
mode = 'composition'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let logPanelRef: LogPanel;
|
let logPanelRef: LogPanel;
|
||||||
@ -74,6 +76,7 @@
|
|||||||
{onChange}
|
{onChange}
|
||||||
{onExecute}
|
{onExecute}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
|
{mode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,18 @@
|
|||||||
import type { CsoundProject, ProjectManager } from '../../project-system';
|
import type { CsoundProject, ProjectManager } from '../../project-system';
|
||||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
|
|
||||||
|
import type { ProjectMode } from '../../project-system/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectManager: ProjectManager;
|
projectManager: ProjectManager;
|
||||||
onFileSelect?: (project: CsoundProject | null) => void;
|
onFileSelect?: (project: CsoundProject | null) => void;
|
||||||
onNewFile?: () => 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;
|
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<CsoundProject[]>([]);
|
let projects = $state<CsoundProject[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
@ -25,11 +28,13 @@
|
|||||||
|
|
||||||
let editTitle = $state('');
|
let editTitle = $state('');
|
||||||
let editAuthor = $state('');
|
let editAuthor = $state('');
|
||||||
|
let editMode = $state<ProjectMode>('composition');
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
editTitle = selectedProject.title;
|
editTitle = selectedProject.title;
|
||||||
editAuthor = selectedProject.author;
|
editAuthor = selectedProject.author;
|
||||||
|
editMode = selectedProject.mode;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,6 +72,10 @@
|
|||||||
onNewFile?.();
|
onNewFile?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNewLiveCodingFile() {
|
||||||
|
onNewLiveCodingFile?.();
|
||||||
|
}
|
||||||
|
|
||||||
function selectProject(project: CsoundProject) {
|
function selectProject(project: CsoundProject) {
|
||||||
onFileSelect?.(project);
|
onFileSelect?.(project);
|
||||||
}
|
}
|
||||||
@ -101,12 +110,14 @@
|
|||||||
|
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
editTitle !== selectedProject.title ||
|
editTitle !== selectedProject.title ||
|
||||||
editAuthor !== selectedProject.author;
|
editAuthor !== selectedProject.author ||
|
||||||
|
editMode !== selectedProject.mode;
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
onMetadataUpdate?.(selectedProject.id, {
|
onMetadataUpdate?.(selectedProject.id, {
|
||||||
title: editTitle,
|
title: editTitle,
|
||||||
author: editAuthor
|
author: editAuthor,
|
||||||
|
mode: editMode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,9 +126,16 @@
|
|||||||
<div class="file-browser">
|
<div class="file-browser">
|
||||||
<div class="browser-header">
|
<div class="browser-header">
|
||||||
<span class="browser-title">Files</span>
|
<span class="browser-title">Files</span>
|
||||||
<button class="action-button" onclick={handleNewFile} title="New file">
|
<div class="header-actions">
|
||||||
<Plus size={16} />
|
<button class="action-button" onclick={handleNewFile} title="New composition">
|
||||||
</button>
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
<button class="action-button live-coding" onclick={handleNewLiveCodingFile} title="New live coding template">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="browser-content">
|
<div class="browser-content">
|
||||||
@ -174,6 +192,17 @@
|
|||||||
onchange={handleMetadataChange}
|
onchange={handleMetadataChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
<div class="field readonly">
|
<div class="field readonly">
|
||||||
<label>Number of Saves</label>
|
<label>Number of Saves</label>
|
||||||
<div class="readonly-value">{selectedProject.saveCount}</div>
|
<div class="readonly-value">{selectedProject.saveCount}</div>
|
||||||
@ -228,6 +257,11 @@
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -245,6 +279,15 @@
|
|||||||
background-color: rgba(255, 255, 255, 0.1);
|
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 {
|
.browser-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -353,7 +396,8 @@
|
|||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input {
|
.field input,
|
||||||
|
.field select {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
border: 1px solid #3a3a3a;
|
border: 1px solid #3a3a3a;
|
||||||
@ -362,10 +406,15 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input:focus {
|
.field input:focus,
|
||||||
|
.field select:focus {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.field.readonly .readonly-value {
|
.field.readonly .readonly-value {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
|
|||||||
@ -31,3 +31,119 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
</CsScore>
|
</CsScore>
|
||||||
</CsoundSynthesizer>
|
</CsoundSynthesizer>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIVECODING_TEMPLATE = `<CsoundSynthesizer>
|
||||||
|
<CsOptions>
|
||||||
|
-odac
|
||||||
|
</CsOptions>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
<CsScore>
|
||||||
|
; Start reverb (always on)
|
||||||
|
i 99 0 -1
|
||||||
|
|
||||||
|
; Initial events will be sent during initialization
|
||||||
|
; After that, evaluate blocks below to trigger sounds
|
||||||
|
</CsScore>
|
||||||
|
</CsoundSynthesizer>
|
||||||
|
|
||||||
|
|
||||||
|
; 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
|
||||||
|
`;
|
||||||
|
|||||||
148
src/lib/csound/execution-strategies.ts
Normal file
148
src/lib/csound/execution-strategies.ts
Normal file
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CompositionStrategy implements ExecutionStrategy {
|
||||||
|
async execute(
|
||||||
|
csound: CsoundStore,
|
||||||
|
code: string,
|
||||||
|
fullContent: string,
|
||||||
|
source: 'selection' | 'block' | 'document'
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
const { header, instruments, score } = this.parseCSD(fullContent);
|
||||||
|
|
||||||
|
const fullOrchestra = header + '\n' + instruments;
|
||||||
|
const compileResult = await csound.compileOrchestra(fullOrchestra);
|
||||||
|
|
||||||
|
if (!compileResult.success) {
|
||||||
|
throw new Error(compileResult.errorMessage || 'Compilation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headerCompiled = true;
|
||||||
|
|
||||||
|
await csound.startPerformance();
|
||||||
|
|
||||||
|
if (score.trim()) {
|
||||||
|
await csound.readScore(score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async evaluateBlock(csound: CsoundStore, code: string): Promise<void> {
|
||||||
|
const trimmedCode = code.trim();
|
||||||
|
|
||||||
|
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(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
|
||||||
|
if (!orcMatch) {
|
||||||
|
return { header: '', instruments: '', score: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const orchestra = orcMatch[1].trim();
|
||||||
|
|
||||||
|
const scoMatch = content.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
|
||||||
|
const score = scoMatch ? scoMatch[1].trim() : '';
|
||||||
|
|
||||||
|
const instrMatch = orchestra.match(/([\s\S]*?)(instr\s+\d+[\s\S]*)/);
|
||||||
|
|
||||||
|
if (instrMatch) {
|
||||||
|
return {
|
||||||
|
header: instrMatch[1].trim(),
|
||||||
|
instruments: instrMatch[2].trim(),
|
||||||
|
score
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: orchestra,
|
||||||
|
instruments: '',
|
||||||
|
score
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
111
src/lib/editor/block-eval.ts
Normal file
111
src/lib/editor/block-eval.ts
Normal file
@ -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<FlashRange | null>();
|
||||||
|
|
||||||
|
const defaultStyle = {
|
||||||
|
'background-color': '#FFCA2880',
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleObjectToString = (styleObj: Record<string, string>): 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<string, string> = 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 };
|
||||||
|
}
|
||||||
@ -97,6 +97,7 @@ export class ProjectManager {
|
|||||||
content: data.content || '',
|
content: data.content || '',
|
||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
csoundVersion: CSOUND_VERSION,
|
csoundVersion: CSOUND_VERSION,
|
||||||
|
mode: data.mode || 'composition',
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.db.put(project);
|
await this.db.put(project);
|
||||||
@ -161,6 +162,7 @@ export class ProjectManager {
|
|||||||
...(data.author !== undefined && { author: data.author }),
|
...(data.author !== undefined && { author: data.author }),
|
||||||
...(data.content !== undefined && { content: data.content }),
|
...(data.content !== undefined && { content: data.content }),
|
||||||
...(data.tags !== undefined && { tags: data.tags }),
|
...(data.tags !== undefined && { tags: data.tags }),
|
||||||
|
...(data.mode !== undefined && { mode: data.mode }),
|
||||||
dateModified: getCurrentTimestamp(),
|
dateModified: getCurrentTimestamp(),
|
||||||
saveCount: existingProject.saveCount + 1,
|
saveCount: existingProject.saveCount + 1,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
export type ProjectMode = 'composition' | 'livecoding';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core data structure for a Csound project
|
* Core data structure for a Csound project
|
||||||
*/
|
*/
|
||||||
@ -28,6 +30,9 @@ export interface CsoundProject {
|
|||||||
|
|
||||||
/** Csound version used to create this project */
|
/** Csound version used to create this project */
|
||||||
csoundVersion: string;
|
csoundVersion: string;
|
||||||
|
|
||||||
|
/** Execution mode: composition (full document) or livecoding (block evaluation) */
|
||||||
|
mode: ProjectMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,6 +43,7 @@ export interface CreateProjectData {
|
|||||||
author: string;
|
author: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
mode?: ProjectMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +55,7 @@ export interface UpdateProjectData {
|
|||||||
author?: string;
|
author?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
mode?: ProjectMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -79,6 +79,9 @@ export class ProjectEditor {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.state.hasUnsavedChanges = false;
|
this.state.hasUnsavedChanges = false;
|
||||||
|
if (result.data) {
|
||||||
|
this.state.currentProject = result.data;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,11 +91,14 @@ export class ProjectEditor {
|
|||||||
async saveAs(title: string, author: string = 'Anonymous'): Promise<boolean> {
|
async saveAs(title: string, author: string = 'Anonymous'): Promise<boolean> {
|
||||||
const finalTitle = title.trim() || 'Untitled';
|
const finalTitle = title.trim() || 'Untitled';
|
||||||
|
|
||||||
|
const isLiveCodingTemplate = this.state.content.includes('Live Coding Template');
|
||||||
|
|
||||||
const result = await this.projectManager.createProject({
|
const result = await this.projectManager.createProject({
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
author,
|
author,
|
||||||
content: this.state.content,
|
content: this.state.content,
|
||||||
tags: []
|
tags: [],
|
||||||
|
mode: isLiveCodingTemplate ? 'livecoding' : 'composition'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -105,7 +111,7 @@ export class ProjectEditor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMetadata(updates: { title?: string; author?: string }): Promise<boolean> {
|
async updateMetadata(updates: { title?: string; author?: string; mode?: import('../project-system/types').ProjectMode }): Promise<boolean> {
|
||||||
if (!this.state.currentProject) {
|
if (!this.state.currentProject) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
1030
to_continue.md
Normal file
1030
to_continue.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user