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 { 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<AnalyserNode | null>(null);
|
||||
let interpreterLogs = $state<LogEntry[]>([]);
|
||||
let currentStrategy = $state<ExecutionStrategy | null>(null);
|
||||
let currentMode = $state<ProjectMode>('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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper">
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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<CsoundProject[]>([]);
|
||||
let loading = $state(true);
|
||||
@ -25,11 +28,13 @@
|
||||
|
||||
let editTitle = $state('');
|
||||
let editAuthor = $state('');
|
||||
let editMode = $state<ProjectMode>('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 @@
|
||||
<div class="file-browser">
|
||||
<div class="browser-header">
|
||||
<span class="browser-title">Files</span>
|
||||
<button class="action-button" onclick={handleNewFile} title="New file">
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button class="action-button" onclick={handleNewFile} title="New composition">
|
||||
<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 class="browser-content">
|
||||
@ -174,6 +192,17 @@
|
||||
onchange={handleMetadataChange}
|
||||
/>
|
||||
</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">
|
||||
<label>Number of Saves</label>
|
||||
<div class="readonly-value">{selectedProject.saveCount}</div>
|
||||
@ -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;
|
||||
|
||||
@ -31,3 +31,119 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</CsScore>
|
||||
</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 || '',
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<boolean> {
|
||||
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<boolean> {
|
||||
async updateMetadata(updates: { title?: string; author?: string; mode?: import('../project-system/types').ProjectMode }): Promise<boolean> {
|
||||
if (!this.state.currentProject) {
|
||||
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