Better architectural distinction between live coding mode and composition mode
This commit is contained in:
@ -10,11 +10,11 @@
|
|||||||
import Spectrogram from './lib/components/audio/Spectrogram.svelte';
|
import Spectrogram from './lib/components/audio/Spectrogram.svelte';
|
||||||
import ConfirmDialog from './lib/components/ui/ConfirmDialog.svelte';
|
import ConfirmDialog from './lib/components/ui/ConfirmDialog.svelte';
|
||||||
import InputDialog from './lib/components/ui/InputDialog.svelte';
|
import InputDialog from './lib/components/ui/InputDialog.svelte';
|
||||||
import { createCsoundDerivedStores, type LogEntry } from './lib/csound';
|
import TemplateDialog from './lib/components/ui/TemplateDialog.svelte';
|
||||||
|
import { createCsoundDerivedStores, type LogEntry, type EvalSource } from './lib/csound';
|
||||||
import { type CsoundProject } from './lib/project-system';
|
import { type CsoundProject } from './lib/project-system';
|
||||||
import { DEFAULT_CSOUND_TEMPLATE, LIVECODING_TEMPLATE } from './lib/config/templates';
|
import { templateRegistry, type CsoundTemplate } from './lib/templates/template-registry';
|
||||||
import { createAppContext, setAppContext } from './lib/contexts/app-context';
|
import { 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 type { ProjectMode } from './lib/project-system/types';
|
||||||
import {
|
import {
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
@ -32,13 +32,11 @@
|
|||||||
const appContext = createAppContext();
|
const appContext = createAppContext();
|
||||||
setAppContext(appContext);
|
setAppContext(appContext);
|
||||||
|
|
||||||
const { csound, projectManager, editorSettings, projectEditor, uiState } = appContext;
|
const { csound, projectManager, editorSettings, projectEditor, uiState, executionContext } = appContext;
|
||||||
const csoundDerived = createCsoundDerivedStores(csound);
|
const csoundDerived = createCsoundDerivedStores(csound);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -47,12 +45,16 @@
|
|||||||
|
|
||||||
const result = await projectManager.getAllProjects();
|
const result = await projectManager.getAllProjects();
|
||||||
if (result.success && result.data.length === 0) {
|
if (result.success && result.data.length === 0) {
|
||||||
await projectManager.createProject({
|
const classicTemplate = templateRegistry.getById('classic');
|
||||||
title: 'Template',
|
if (classicTemplate) {
|
||||||
author: 'System',
|
await projectManager.createProject({
|
||||||
content: DEFAULT_CSOUND_TEMPLATE,
|
title: 'Welcome',
|
||||||
tags: []
|
author: 'System',
|
||||||
});
|
content: classicTemplate.content,
|
||||||
|
tags: [],
|
||||||
|
mode: classicTemplate.mode
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logsUnsubscribe = csoundDerived.logs.subscribe(logs => {
|
logsUnsubscribe = csoundDerived.logs.subscribe(logs => {
|
||||||
@ -78,9 +80,10 @@
|
|||||||
projectEditor.setContent(value);
|
projectEditor.setContent(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewFile() {
|
function handleNewEmptyFile() {
|
||||||
|
const emptyTemplate = templateRegistry.getEmpty();
|
||||||
const result = projectEditor.requestSwitch(
|
const result = projectEditor.requestSwitch(
|
||||||
() => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE)
|
() => projectEditor.createNew(emptyTemplate.content)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result === 'confirm-unsaved') {
|
if (result === 'confirm-unsaved') {
|
||||||
@ -88,9 +91,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewLiveCodingFile() {
|
function handleNewFromTemplate() {
|
||||||
|
uiState.showTemplateDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTemplateSelect(template: CsoundTemplate) {
|
||||||
|
uiState.hideTemplateDialog();
|
||||||
|
|
||||||
const result = projectEditor.requestSwitch(
|
const result = projectEditor.requestSwitch(
|
||||||
() => projectEditor.createNew(LIVECODING_TEMPLATE)
|
() => projectEditor.createNew(template.content)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result === 'confirm-unsaved') {
|
if (result === 'confirm-unsaved') {
|
||||||
@ -110,14 +119,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExecute(code: string, source: 'selection' | 'block' | 'document') {
|
async function handleExecute(code: string, source: EvalSource) {
|
||||||
try {
|
try {
|
||||||
if (!currentStrategy) {
|
await executionContext.execute(code, source);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -189,28 +193,16 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const mode = projectEditor.currentProject?.mode || 'composition';
|
const mode = projectEditor.currentProject?.mode || 'composition';
|
||||||
|
const projectId = projectEditor.currentProjectId;
|
||||||
|
|
||||||
if (mode !== currentMode) {
|
if (mode !== executionContext.mode) {
|
||||||
const oldMode = currentMode;
|
executionContext.switchMode(mode).catch(console.error);
|
||||||
currentMode = mode;
|
}
|
||||||
|
|
||||||
// IMPORTANT: Only create new strategy if mode actually changed
|
executionContext.setContentProvider(() => projectEditor.content);
|
||||||
// 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 (projectId) {
|
||||||
|
executionContext.startSession(projectId).catch(console.error);
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -236,8 +228,8 @@
|
|||||||
<FileBrowser
|
<FileBrowser
|
||||||
{projectManager}
|
{projectManager}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
onNewFile={handleNewFile}
|
onNewEmptyFile={handleNewEmptyFile}
|
||||||
onNewLiveCodingFile={handleNewLiveCodingFile}
|
onNewFromTemplate={handleNewFromTemplate}
|
||||||
onMetadataUpdate={handleMetadataUpdate}
|
onMetadataUpdate={handleMetadataUpdate}
|
||||||
selectedProjectId={projectEditor.currentProjectId}
|
selectedProjectId={projectEditor.currentProjectId}
|
||||||
/>
|
/>
|
||||||
@ -325,7 +317,6 @@
|
|||||||
onExecute={handleExecute}
|
onExecute={handleExecute}
|
||||||
logs={interpreterLogs}
|
logs={interpreterLogs}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
mode={currentMode}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -448,6 +439,12 @@
|
|||||||
placeholder="Untitled"
|
placeholder="Untitled"
|
||||||
onConfirm={handleSaveAs}
|
onConfirm={handleSaveAs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TemplateDialog
|
||||||
|
bind:visible={uiState.templateDialogVisible}
|
||||||
|
onSelect={handleTemplateSelect}
|
||||||
|
onCancel={() => uiState.hideTemplateDialog()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -28,7 +28,6 @@
|
|||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
mode?: 'composition' | 'livecoding';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -36,8 +35,7 @@
|
|||||||
language = 'javascript',
|
language = 'javascript',
|
||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecute,
|
||||||
editorSettings,
|
editorSettings
|
||||||
mode = 'composition'
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let editorContainer: HTMLDivElement;
|
let editorContainer: HTMLDivElement;
|
||||||
@ -56,25 +54,23 @@
|
|||||||
function handleExecute() {
|
function handleExecute() {
|
||||||
if (!editorView) return;
|
if (!editorView) return;
|
||||||
|
|
||||||
if (mode === 'composition') {
|
const selection = getSelection(editorView.state);
|
||||||
// Composition mode: always evaluate entire document
|
if (selection.text) {
|
||||||
const doc = getDocument(editorView.state);
|
flash(editorView, selection.from, selection.to);
|
||||||
flash(editorView, doc.from, doc.to);
|
onExecute?.(selection.text, 'selection');
|
||||||
onExecute?.(doc.text, 'document');
|
return;
|
||||||
} 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 block = getBlock(editorView.state);
|
||||||
|
if (block.text) {
|
||||||
|
flash(editorView, block.from, block.to);
|
||||||
|
onExecute?.(block.text, 'block');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = getDocument(editorView.state);
|
||||||
|
flash(editorView, doc.from, doc.to);
|
||||||
|
onExecute?.(doc.text, 'document');
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluateKeymap = keymap.of([
|
const evaluateKeymap = keymap.of([
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||||
logs?: string[];
|
logs?: string[];
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
mode?: 'composition' | 'livecoding';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -20,8 +19,7 @@
|
|||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecute,
|
||||||
logs = [],
|
logs = [],
|
||||||
editorSettings,
|
editorSettings
|
||||||
mode = 'composition'
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let logPanelRef: LogPanel;
|
let logPanelRef: LogPanel;
|
||||||
@ -76,7 +74,6 @@
|
|||||||
{onChange}
|
{onChange}
|
||||||
{onExecute}
|
{onExecute}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
{mode}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { File, Plus, Trash2 } from 'lucide-svelte';
|
import { File, FilePlus, FileStack, Trash2 } from 'lucide-svelte';
|
||||||
import type { CsoundProject, ProjectManager } from '../../project-system';
|
import type { CsoundProject, ProjectManager } from '../../project-system';
|
||||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
|
|
||||||
@ -9,13 +9,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
projectManager: ProjectManager;
|
projectManager: ProjectManager;
|
||||||
onFileSelect?: (project: CsoundProject | null) => void;
|
onFileSelect?: (project: CsoundProject | null) => void;
|
||||||
onNewFile?: () => void;
|
onNewEmptyFile?: () => void;
|
||||||
onNewLiveCodingFile?: () => void;
|
onNewFromTemplate?: () => void;
|
||||||
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string; mode?: ProjectMode }) => void;
|
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string; mode?: ProjectMode }) => void;
|
||||||
selectedProjectId?: string | null;
|
selectedProjectId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { projectManager, onFileSelect, onNewFile, onNewLiveCodingFile, onMetadataUpdate, selectedProjectId = null }: Props = $props();
|
let { projectManager, onFileSelect, onNewEmptyFile, onNewFromTemplate, onMetadataUpdate, selectedProjectId = null }: Props = $props();
|
||||||
|
|
||||||
let projects = $state<CsoundProject[]>([]);
|
let projects = $state<CsoundProject[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
@ -68,12 +68,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewFile() {
|
function handleNewEmptyFile() {
|
||||||
onNewFile?.();
|
onNewEmptyFile?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewLiveCodingFile() {
|
function handleNewFromTemplate() {
|
||||||
onNewLiveCodingFile?.();
|
onNewFromTemplate?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectProject(project: CsoundProject) {
|
function selectProject(project: CsoundProject) {
|
||||||
@ -127,13 +127,11 @@
|
|||||||
<div class="browser-header">
|
<div class="browser-header">
|
||||||
<span class="browser-title">Files</span>
|
<span class="browser-title">Files</span>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="action-button" onclick={handleNewFile} title="New composition">
|
<button class="action-button" onclick={handleNewEmptyFile} title="New empty file">
|
||||||
<Plus size={16} />
|
<FilePlus size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button live-coding" onclick={handleNewLiveCodingFile} title="New live coding template">
|
<button class="action-button template-button" onclick={handleNewFromTemplate} title="New from template">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<FileStack size={18} />
|
||||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -263,9 +261,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
padding: 0.25rem;
|
padding: 0.375rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -275,17 +273,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-button:hover {
|
.action-button:hover {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 1);
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.live-coding {
|
.action-button.template-button {
|
||||||
color: rgba(100, 108, 255, 0.8);
|
color: rgba(100, 200, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.live-coding:hover {
|
.action-button.template-button:hover {
|
||||||
color: rgba(100, 108, 255, 1);
|
color: rgba(100, 200, 255, 1);
|
||||||
background-color: rgba(100, 108, 255, 0.15);
|
background-color: rgba(100, 200, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-content {
|
.browser-content {
|
||||||
|
|||||||
129
src/lib/components/ui/TemplateDialog.svelte
Normal file
129
src/lib/components/ui/TemplateDialog.svelte
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { templateRegistry, type CsoundTemplate } from '../../templates/template-registry';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
onSelect?: (template: CsoundTemplate) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { visible = false, onSelect, onCancel }: Props = $props();
|
||||||
|
|
||||||
|
const templates = templateRegistry.getAll();
|
||||||
|
|
||||||
|
function handleSelect(template: CsoundTemplate) {
|
||||||
|
onSelect?.(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackdropClick(e: MouseEvent) {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="modal-backdrop" onclick={handleBackdropClick}>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Choose Template</h2>
|
||||||
|
<button class="close-button" onclick={() => onCancel?.()}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-list">
|
||||||
|
{#each templates as template}
|
||||||
|
<button
|
||||||
|
class="template-item"
|
||||||
|
onclick={() => handleSelect(template)}
|
||||||
|
>
|
||||||
|
{template.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
width: 400px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item:hover {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -2,6 +2,7 @@ import { getContext, setContext } from 'svelte';
|
|||||||
import { ProjectManager } from '../project-system/project-manager';
|
import { ProjectManager } from '../project-system/project-manager';
|
||||||
import { ProjectDatabase } from '../project-system/db';
|
import { ProjectDatabase } from '../project-system/db';
|
||||||
import { createCsoundStore } from '../csound/store';
|
import { createCsoundStore } from '../csound/store';
|
||||||
|
import { ExecutionContext } from '../csound/execution-context';
|
||||||
import { createEditorSettingsStore } from '../stores/editorSettings';
|
import { createEditorSettingsStore } from '../stores/editorSettings';
|
||||||
import { ProjectEditor } from '../stores/projectEditor.svelte';
|
import { ProjectEditor } from '../stores/projectEditor.svelte';
|
||||||
import { UIState } from '../stores/uiState.svelte';
|
import { UIState } from '../stores/uiState.svelte';
|
||||||
@ -11,6 +12,7 @@ import type { EditorSettingsStore } from '../stores/editorSettings';
|
|||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
projectManager: ProjectManager;
|
projectManager: ProjectManager;
|
||||||
csound: CsoundStore;
|
csound: CsoundStore;
|
||||||
|
executionContext: ExecutionContext;
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
projectEditor: ProjectEditor;
|
projectEditor: ProjectEditor;
|
||||||
uiState: UIState;
|
uiState: UIState;
|
||||||
@ -21,9 +23,13 @@ const APP_CONTEXT_KEY = Symbol('app-context');
|
|||||||
export function createAppContext(): AppContext {
|
export function createAppContext(): AppContext {
|
||||||
const db = new ProjectDatabase();
|
const db = new ProjectDatabase();
|
||||||
const projectManager = new ProjectManager(db);
|
const projectManager = new ProjectManager(db);
|
||||||
|
const csound = createCsoundStore();
|
||||||
|
const executionContext = new ExecutionContext(csound, 'composition');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectManager,
|
projectManager,
|
||||||
csound: createCsoundStore(),
|
csound,
|
||||||
|
executionContext,
|
||||||
editorSettings: createEditorSettingsStore(),
|
editorSettings: createEditorSettingsStore(),
|
||||||
projectEditor: new ProjectEditor(projectManager),
|
projectEditor: new ProjectEditor(projectManager),
|
||||||
uiState: new UIState()
|
uiState: new UIState()
|
||||||
|
|||||||
98
src/lib/csound/execution-context.ts
Normal file
98
src/lib/csound/execution-context.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import type { CsoundStore } from './store';
|
||||||
|
import type { ProjectMode } from '../project-system/types';
|
||||||
|
import { CompositionStrategy, LiveCodingStrategy, type ExecutionStrategy } from './execution-strategies';
|
||||||
|
|
||||||
|
export type EvalSource = 'selection' | 'block' | 'document';
|
||||||
|
|
||||||
|
export interface ExecutionSession {
|
||||||
|
mode: ProjectMode;
|
||||||
|
projectId: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
startTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExecutionContext {
|
||||||
|
private session: ExecutionSession | null = null;
|
||||||
|
private strategy: ExecutionStrategy;
|
||||||
|
private currentMode: ProjectMode;
|
||||||
|
private contentProvider: (() => string) | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private csound: CsoundStore,
|
||||||
|
mode: ProjectMode
|
||||||
|
) {
|
||||||
|
this.currentMode = mode;
|
||||||
|
this.strategy = this.createStrategyForMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode(): ProjectMode {
|
||||||
|
return this.currentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeSession(): ExecutionSession | null {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentProvider(provider: () => string): void {
|
||||||
|
this.contentProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSession(projectId: string | null): Promise<void> {
|
||||||
|
await this.endSession();
|
||||||
|
|
||||||
|
this.session = {
|
||||||
|
mode: this.currentMode,
|
||||||
|
projectId,
|
||||||
|
isActive: true,
|
||||||
|
startTime: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.currentMode === 'livecoding') {
|
||||||
|
this.resetStrategy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async endSession(): Promise<void> {
|
||||||
|
if (!this.session?.isActive) return;
|
||||||
|
|
||||||
|
if (this.currentMode === 'livecoding') {
|
||||||
|
await this.csound.stop();
|
||||||
|
this.resetStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = { ...this.session, isActive: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(code: string, source: EvalSource): Promise<void> {
|
||||||
|
if (this.currentMode === 'livecoding' && (!this.session || !this.session.isActive)) {
|
||||||
|
await this.startSession(this.session?.projectId ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullContent = this.contentProvider?.() ?? '';
|
||||||
|
await this.strategy.execute(this.csound, code, fullContent, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchMode(newMode: ProjectMode): Promise<void> {
|
||||||
|
if (newMode === this.currentMode) return;
|
||||||
|
|
||||||
|
await this.endSession();
|
||||||
|
this.currentMode = newMode;
|
||||||
|
this.strategy = this.createStrategyForMode(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createStrategyForMode(mode: ProjectMode): ExecutionStrategy {
|
||||||
|
return mode === 'livecoding'
|
||||||
|
? new LiveCodingStrategy()
|
||||||
|
: new CompositionStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetStrategy(): void {
|
||||||
|
if (this.strategy instanceof LiveCodingStrategy) {
|
||||||
|
this.strategy.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
await this.endSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import type { CsoundStore } from './store';
|
import type { CsoundStore } from './store';
|
||||||
import type { ProjectMode } from '../project-system/types';
|
import type { ProjectMode } from '../project-system/types';
|
||||||
|
|
||||||
|
export type EvalSource = 'selection' | 'block' | 'document';
|
||||||
|
|
||||||
export interface ExecutionStrategy {
|
export interface ExecutionStrategy {
|
||||||
execute(
|
execute(
|
||||||
csound: CsoundStore,
|
csound: CsoundStore,
|
||||||
code: string,
|
code: string,
|
||||||
fullContent: string,
|
fullContent: string,
|
||||||
source: 'selection' | 'block' | 'document'
|
source: EvalSource
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +17,7 @@ export class CompositionStrategy implements ExecutionStrategy {
|
|||||||
csound: CsoundStore,
|
csound: CsoundStore,
|
||||||
code: string,
|
code: string,
|
||||||
fullContent: string,
|
fullContent: string,
|
||||||
source: 'selection' | 'block' | 'document'
|
source: EvalSource
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await csound.evaluate(fullContent);
|
await csound.evaluate(fullContent);
|
||||||
}
|
}
|
||||||
@ -29,7 +31,7 @@ export class LiveCodingStrategy implements ExecutionStrategy {
|
|||||||
csound: CsoundStore,
|
csound: CsoundStore,
|
||||||
code: string,
|
code: string,
|
||||||
fullContent: string,
|
fullContent: string,
|
||||||
source: 'selection' | 'block' | 'document'
|
source: EvalSource
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
await this.initializeFromDocument(csound, fullContent);
|
await this.initializeFromDocument(csound, fullContent);
|
||||||
|
|||||||
@ -7,3 +7,5 @@ export type {
|
|||||||
} from './engine';
|
} from './engine';
|
||||||
export { createCsoundStore, createCsoundDerivedStores } from './store';
|
export { createCsoundStore, createCsoundDerivedStores } from './store';
|
||||||
export type { LogEntry, CsoundStore } from './store';
|
export type { LogEntry, CsoundStore } from './store';
|
||||||
|
export { ExecutionContext } from './execution-context';
|
||||||
|
export type { ExecutionSession, EvalSource } from './execution-context';
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export class UIState {
|
|||||||
audioPermissionPopupVisible = $state(true);
|
audioPermissionPopupVisible = $state(true);
|
||||||
unsavedChangesDialogVisible = $state(false);
|
unsavedChangesDialogVisible = $state(false);
|
||||||
saveAsDialogVisible = $state(false);
|
saveAsDialogVisible = $state(false);
|
||||||
|
templateDialogVisible = $state(false);
|
||||||
|
|
||||||
shareUrl = $state('');
|
shareUrl = $state('');
|
||||||
|
|
||||||
@ -59,4 +60,12 @@ export class UIState {
|
|||||||
hideSaveAsDialog() {
|
hideSaveAsDialog() {
|
||||||
this.saveAsDialogVisible = false;
|
this.saveAsDialogVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showTemplateDialog() {
|
||||||
|
this.templateDialogVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTemplateDialog() {
|
||||||
|
this.templateDialogVisible = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,77 @@
|
|||||||
export const DEFAULT_CSOUND_TEMPLATE = `<CsoundSynthesizer>
|
import type { ProjectMode } from '../project-system/types';
|
||||||
<CsOptions>
|
|
||||||
</CsOptions>
|
|
||||||
<CsInstruments>
|
|
||||||
|
|
||||||
sr = 44100
|
export interface CsoundTemplate {
|
||||||
ksmps = 32
|
id: string;
|
||||||
nchnls = 2
|
name: string;
|
||||||
0dbfs = 1
|
mode: ProjectMode;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
instr 1
|
const EMPTY_TEMPLATE: CsoundTemplate = {
|
||||||
iFreq = p4
|
id: 'empty',
|
||||||
iAmp = p5
|
name: 'Empty',
|
||||||
|
mode: 'composition',
|
||||||
; ADSR envelope
|
content: `<CsoundSynthesizer>
|
||||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
<CsOptions>
|
||||||
|
-odac
|
||||||
; Sine wave oscillator
|
</CsOptions>
|
||||||
aOsc oscili iAmp * kEnv, iFreq
|
<CsInstruments>
|
||||||
|
|
||||||
outs aOsc, aOsc
|
sr = 48000
|
||||||
endin
|
ksmps = 32
|
||||||
|
nchnls = 2
|
||||||
</CsInstruments>
|
0dbfs = 1
|
||||||
<CsScore>
|
|
||||||
; Arpeggio: C4 E4 G4 C5
|
</CsInstruments>
|
||||||
i 1 0.0 0.5 261.63 0.3
|
<CsScore>
|
||||||
i 1 0.5 0.5 329.63 0.3
|
|
||||||
i 1 1.0 0.5 392.00 0.3
|
</CsScore>
|
||||||
i 1 1.5 0.5 523.25 0.3
|
</CsoundSynthesizer>
|
||||||
</CsScore>
|
`
|
||||||
</CsoundSynthesizer>
|
};
|
||||||
`;
|
|
||||||
|
const CLASSIC_TEMPLATE: CsoundTemplate = {
|
||||||
export const LIVECODING_TEMPLATE = `<CsoundSynthesizer>
|
id: 'classic',
|
||||||
|
name: 'Classic',
|
||||||
|
mode: 'composition',
|
||||||
|
content: `<CsoundSynthesizer>
|
||||||
|
<CsOptions>
|
||||||
|
-odac
|
||||||
|
</CsOptions>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
sr = 48000
|
||||||
|
ksmps = 32
|
||||||
|
nchnls = 2
|
||||||
|
0dbfs = 1
|
||||||
|
|
||||||
|
instr 1
|
||||||
|
iFreq = p4
|
||||||
|
iAmp = p5
|
||||||
|
|
||||||
|
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||||
|
|
||||||
|
aOsc oscili iAmp * kEnv, iFreq
|
||||||
|
|
||||||
|
outs aOsc, aOsc
|
||||||
|
endin
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
<CsScore>
|
||||||
|
i 1 0.0 0.5 261.63 0.3
|
||||||
|
i 1 0.5 0.5 329.63 0.3
|
||||||
|
i 1 1.0 0.5 392.00 0.3
|
||||||
|
i 1 1.5 0.5 523.25 0.3
|
||||||
|
</CsScore>
|
||||||
|
</CsoundSynthesizer>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIVECODING_TEMPLATE: CsoundTemplate = {
|
||||||
|
id: 'livecoding',
|
||||||
|
name: 'Live Coding',
|
||||||
|
mode: 'livecoding',
|
||||||
|
content: `<CsoundSynthesizer>
|
||||||
<CsOptions>
|
<CsOptions>
|
||||||
-odac
|
-odac
|
||||||
</CsOptions>
|
</CsOptions>
|
||||||
@ -43,18 +82,14 @@ ksmps = 32
|
|||||||
nchnls = 2
|
nchnls = 2
|
||||||
0dbfs = 1
|
0dbfs = 1
|
||||||
|
|
||||||
; Live Coding Template
|
|
||||||
; Press Cmd/Ctrl+E on the full document first to initialize
|
; Press Cmd/Ctrl+E on the full document first to initialize
|
||||||
; Then evaluate individual blocks to trigger sounds
|
; Then evaluate individual blocks to trigger sounds
|
||||||
|
|
||||||
; Global audio bus for effects
|
|
||||||
gaReverb init 0
|
gaReverb init 0
|
||||||
|
|
||||||
instr 1
|
instr 1
|
||||||
; Simple synth with channel control
|
|
||||||
kFreq chnget "freq"
|
kFreq chnget "freq"
|
||||||
kFreq = (kFreq == 0 ? p4 : kFreq)
|
kFreq = (kFreq == 0 ? p4 : kFreq)
|
||||||
|
|
||||||
kAmp = p5
|
kAmp = p5
|
||||||
|
|
||||||
kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0
|
kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0
|
||||||
@ -67,7 +102,6 @@ instr 1
|
|||||||
endin
|
endin
|
||||||
|
|
||||||
instr 2
|
instr 2
|
||||||
; Bass synth
|
|
||||||
iFreq = p4
|
iFreq = p4
|
||||||
iAmp = p5
|
iAmp = p5
|
||||||
|
|
||||||
@ -80,7 +114,6 @@ instr 2
|
|||||||
endin
|
endin
|
||||||
|
|
||||||
instr 99
|
instr 99
|
||||||
; Global reverb
|
|
||||||
aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5
|
aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5
|
||||||
outs aL, aR
|
outs aL, aR
|
||||||
gaReverb = 0
|
gaReverb = 0
|
||||||
@ -88,19 +121,14 @@ endin
|
|||||||
|
|
||||||
</CsInstruments>
|
</CsInstruments>
|
||||||
<CsScore>
|
<CsScore>
|
||||||
; Start reverb (always on)
|
|
||||||
i 99 0 -1
|
i 99 0 -1
|
||||||
|
|
||||||
; Initial events will be sent during initialization
|
|
||||||
; After that, evaluate blocks below to trigger sounds
|
|
||||||
</CsScore>
|
</CsScore>
|
||||||
</CsoundSynthesizer>
|
</CsoundSynthesizer>
|
||||||
|
|
||||||
|
|
||||||
; LIVE CODING EXAMPLES
|
; LIVE CODING EXAMPLES
|
||||||
; Evaluate each block separately (Cmd/Ctrl+E with cursor on the line)
|
|
||||||
|
|
||||||
; Basic note (instrument 1, start now, duration 2s, freq 440Hz, amp 0.3)
|
; Basic note
|
||||||
i 1 0 2 440 0.3
|
i 1 0 2 440 0.3
|
||||||
|
|
||||||
; Arpeggio
|
; Arpeggio
|
||||||
@ -115,35 +143,47 @@ i 2 0.5 0.5 146.83 0.4
|
|||||||
i 2 1.0 0.5 164.81 0.4
|
i 2 1.0 0.5 164.81 0.4
|
||||||
i 2 1.5 0.5 130.81 0.4
|
i 2 1.5 0.5 130.81 0.4
|
||||||
|
|
||||||
; Long note for testing channel control
|
; Long note for channel control
|
||||||
i 1 0 30 440 0.3
|
i 1 0 30 440 0.3
|
||||||
|
|
||||||
; While the long note plays, evaluate these to change frequency:
|
; Change frequency while playing
|
||||||
freq = 440
|
freq = 440
|
||||||
|
|
||||||
freq = 554.37
|
freq = 554.37
|
||||||
|
|
||||||
freq = 659.25
|
freq = 659.25
|
||||||
|
|
||||||
freq = 880
|
; Turn off instrument 1
|
||||||
|
|
||||||
; Turn off all instances of instrument 1
|
|
||||||
i -1 0 0
|
i -1 0 0
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
; Redefine instrument 1 with a different sound
|
const TEMPLATE_REGISTRY: CsoundTemplate[] = [
|
||||||
instr 1
|
EMPTY_TEMPLATE,
|
||||||
kFreq chnget "freq"
|
LIVECODING_TEMPLATE,
|
||||||
kFreq = (kFreq == 0 ? p4 : kFreq)
|
CLASSIC_TEMPLATE
|
||||||
kAmp = p5
|
];
|
||||||
|
|
||||||
kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0
|
export class TemplateRegistry {
|
||||||
aSaw vco2 kAmp * kEnv, kFreq, 2
|
private templates: Map<string, CsoundTemplate>;
|
||||||
aSquare vco2 kAmp * kEnv, kFreq * 1.01, 10
|
|
||||||
|
|
||||||
aSum = (aSaw + aSquare) * 0.5
|
constructor() {
|
||||||
aFilt moogladder aSum, 1500, 0.5
|
this.templates = new Map(
|
||||||
|
TEMPLATE_REGISTRY.map(template => [template.id, template])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
outs aFilt, aFilt
|
getAll(): CsoundTemplate[] {
|
||||||
gaReverb = gaReverb + aFilt * 0.3
|
return Array.from(this.templates.values());
|
||||||
endin
|
}
|
||||||
`;
|
|
||||||
|
getById(id: string): CsoundTemplate | undefined {
|
||||||
|
return this.templates.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmpty(): CsoundTemplate {
|
||||||
|
return EMPTY_TEMPLATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const templateRegistry = new TemplateRegistry();
|
||||||
Reference in New Issue
Block a user