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 ConfirmDialog from './lib/components/ui/ConfirmDialog.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 { 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 { createExecutionStrategy, type ExecutionStrategy } from './lib/csound/execution-strategies';
|
||||
import type { ProjectMode } from './lib/project-system/types';
|
||||
import {
|
||||
PanelLeftClose,
|
||||
@ -32,13 +32,11 @@
|
||||
const appContext = createAppContext();
|
||||
setAppContext(appContext);
|
||||
|
||||
const { csound, projectManager, editorSettings, projectEditor, uiState } = appContext;
|
||||
const { csound, projectManager, editorSettings, projectEditor, uiState, executionContext } = appContext;
|
||||
const csoundDerived = createCsoundDerivedStores(csound);
|
||||
|
||||
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;
|
||||
|
||||
@ -47,13 +45,17 @@
|
||||
|
||||
const result = await projectManager.getAllProjects();
|
||||
if (result.success && result.data.length === 0) {
|
||||
const classicTemplate = templateRegistry.getById('classic');
|
||||
if (classicTemplate) {
|
||||
await projectManager.createProject({
|
||||
title: 'Template',
|
||||
title: 'Welcome',
|
||||
author: 'System',
|
||||
content: DEFAULT_CSOUND_TEMPLATE,
|
||||
tags: []
|
||||
content: classicTemplate.content,
|
||||
tags: [],
|
||||
mode: classicTemplate.mode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logsUnsubscribe = csoundDerived.logs.subscribe(logs => {
|
||||
interpreterLogs = logs;
|
||||
@ -78,9 +80,10 @@
|
||||
projectEditor.setContent(value);
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
function handleNewEmptyFile() {
|
||||
const emptyTemplate = templateRegistry.getEmpty();
|
||||
const result = projectEditor.requestSwitch(
|
||||
() => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE)
|
||||
() => projectEditor.createNew(emptyTemplate.content)
|
||||
);
|
||||
|
||||
if (result === 'confirm-unsaved') {
|
||||
@ -88,9 +91,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleNewLiveCodingFile() {
|
||||
function handleNewFromTemplate() {
|
||||
uiState.showTemplateDialog();
|
||||
}
|
||||
|
||||
function handleTemplateSelect(template: CsoundTemplate) {
|
||||
uiState.hideTemplateDialog();
|
||||
|
||||
const result = projectEditor.requestSwitch(
|
||||
() => projectEditor.createNew(LIVECODING_TEMPLATE)
|
||||
() => projectEditor.createNew(template.content)
|
||||
);
|
||||
|
||||
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 {
|
||||
if (!currentStrategy) {
|
||||
currentStrategy = createExecutionStrategy(currentMode);
|
||||
}
|
||||
|
||||
const fullContent = projectEditor.content;
|
||||
await currentStrategy.execute(csound, code, fullContent, source);
|
||||
await executionContext.execute(code, source);
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
}
|
||||
@ -189,28 +193,16 @@
|
||||
|
||||
$effect(() => {
|
||||
const mode = projectEditor.currentProject?.mode || 'composition';
|
||||
const projectId = projectEditor.currentProjectId;
|
||||
|
||||
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();
|
||||
}
|
||||
if (mode !== executionContext.mode) {
|
||||
executionContext.switchMode(mode).catch(console.error);
|
||||
}
|
||||
|
||||
currentStrategy = createExecutionStrategy(mode);
|
||||
executionContext.setContentProvider(() => projectEditor.content);
|
||||
|
||||
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');
|
||||
}
|
||||
if (projectId) {
|
||||
executionContext.startSession(projectId).catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
@ -236,8 +228,8 @@
|
||||
<FileBrowser
|
||||
{projectManager}
|
||||
onFileSelect={handleFileSelect}
|
||||
onNewFile={handleNewFile}
|
||||
onNewLiveCodingFile={handleNewLiveCodingFile}
|
||||
onNewEmptyFile={handleNewEmptyFile}
|
||||
onNewFromTemplate={handleNewFromTemplate}
|
||||
onMetadataUpdate={handleMetadataUpdate}
|
||||
selectedProjectId={projectEditor.currentProjectId}
|
||||
/>
|
||||
@ -325,7 +317,6 @@
|
||||
onExecute={handleExecute}
|
||||
logs={interpreterLogs}
|
||||
{editorSettings}
|
||||
mode={currentMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -448,6 +439,12 @@
|
||||
placeholder="Untitled"
|
||||
onConfirm={handleSaveAs}
|
||||
/>
|
||||
|
||||
<TemplateDialog
|
||||
bind:visible={uiState.templateDialogVisible}
|
||||
onSelect={handleTemplateSelect}
|
||||
onCancel={() => uiState.hideTemplateDialog()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
onChange?: (value: string) => void;
|
||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||
editorSettings: EditorSettingsStore;
|
||||
mode?: 'composition' | 'livecoding';
|
||||
}
|
||||
|
||||
let {
|
||||
@ -36,8 +35,7 @@
|
||||
language = 'javascript',
|
||||
onChange,
|
||||
onExecute,
|
||||
editorSettings,
|
||||
mode = 'composition'
|
||||
editorSettings
|
||||
}: Props = $props();
|
||||
|
||||
let editorContainer: HTMLDivElement;
|
||||
@ -56,25 +54,23 @@
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
|
||||
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([
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
||||
logs?: string[];
|
||||
editorSettings: EditorSettingsStore;
|
||||
mode?: 'composition' | 'livecoding';
|
||||
}
|
||||
|
||||
let {
|
||||
@ -20,8 +19,7 @@
|
||||
onChange,
|
||||
onExecute,
|
||||
logs = [],
|
||||
editorSettings,
|
||||
mode = 'composition'
|
||||
editorSettings
|
||||
}: Props = $props();
|
||||
|
||||
let logPanelRef: LogPanel;
|
||||
@ -76,7 +74,6 @@
|
||||
{onChange}
|
||||
{onExecute}
|
||||
{editorSettings}
|
||||
{mode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 ConfirmDialog from './ConfirmDialog.svelte';
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
interface Props {
|
||||
projectManager: ProjectManager;
|
||||
onFileSelect?: (project: CsoundProject | null) => void;
|
||||
onNewFile?: () => void;
|
||||
onNewLiveCodingFile?: () => void;
|
||||
onNewEmptyFile?: () => void;
|
||||
onNewFromTemplate?: () => void;
|
||||
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string; mode?: ProjectMode }) => void;
|
||||
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 loading = $state(true);
|
||||
@ -68,12 +68,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
onNewFile?.();
|
||||
function handleNewEmptyFile() {
|
||||
onNewEmptyFile?.();
|
||||
}
|
||||
|
||||
function handleNewLiveCodingFile() {
|
||||
onNewLiveCodingFile?.();
|
||||
function handleNewFromTemplate() {
|
||||
onNewFromTemplate?.();
|
||||
}
|
||||
|
||||
function selectProject(project: CsoundProject) {
|
||||
@ -127,13 +127,11 @@
|
||||
<div class="browser-header">
|
||||
<span class="browser-title">Files</span>
|
||||
<div class="header-actions">
|
||||
<button class="action-button" onclick={handleNewFile} title="New composition">
|
||||
<Plus size={16} />
|
||||
<button class="action-button" onclick={handleNewEmptyFile} title="New empty file">
|
||||
<FilePlus size={18} />
|
||||
</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 class="action-button template-button" onclick={handleNewFromTemplate} title="New from template">
|
||||
<FileStack size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -263,9 +261,9 @@
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 0.25rem;
|
||||
padding: 0.375rem;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@ -275,17 +273,17 @@
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-button.live-coding {
|
||||
color: rgba(100, 108, 255, 0.8);
|
||||
.action-button.template-button {
|
||||
color: rgba(100, 200, 255, 0.8);
|
||||
}
|
||||
|
||||
.action-button.live-coding:hover {
|
||||
color: rgba(100, 108, 255, 1);
|
||||
background-color: rgba(100, 108, 255, 0.15);
|
||||
.action-button.template-button:hover {
|
||||
color: rgba(100, 200, 255, 1);
|
||||
background-color: rgba(100, 200, 255, 0.15);
|
||||
}
|
||||
|
||||
.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 { ProjectDatabase } from '../project-system/db';
|
||||
import { createCsoundStore } from '../csound/store';
|
||||
import { ExecutionContext } from '../csound/execution-context';
|
||||
import { createEditorSettingsStore } from '../stores/editorSettings';
|
||||
import { ProjectEditor } from '../stores/projectEditor.svelte';
|
||||
import { UIState } from '../stores/uiState.svelte';
|
||||
@ -11,6 +12,7 @@ import type { EditorSettingsStore } from '../stores/editorSettings';
|
||||
export interface AppContext {
|
||||
projectManager: ProjectManager;
|
||||
csound: CsoundStore;
|
||||
executionContext: ExecutionContext;
|
||||
editorSettings: EditorSettingsStore;
|
||||
projectEditor: ProjectEditor;
|
||||
uiState: UIState;
|
||||
@ -21,9 +23,13 @@ const APP_CONTEXT_KEY = Symbol('app-context');
|
||||
export function createAppContext(): AppContext {
|
||||
const db = new ProjectDatabase();
|
||||
const projectManager = new ProjectManager(db);
|
||||
const csound = createCsoundStore();
|
||||
const executionContext = new ExecutionContext(csound, 'composition');
|
||||
|
||||
return {
|
||||
projectManager,
|
||||
csound: createCsoundStore(),
|
||||
csound,
|
||||
executionContext,
|
||||
editorSettings: createEditorSettingsStore(),
|
||||
projectEditor: new ProjectEditor(projectManager),
|
||||
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 { ProjectMode } from '../project-system/types';
|
||||
|
||||
export type EvalSource = 'selection' | 'block' | 'document';
|
||||
|
||||
export interface ExecutionStrategy {
|
||||
execute(
|
||||
csound: CsoundStore,
|
||||
code: string,
|
||||
fullContent: string,
|
||||
source: 'selection' | 'block' | 'document'
|
||||
source: EvalSource
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@ -15,7 +17,7 @@ export class CompositionStrategy implements ExecutionStrategy {
|
||||
csound: CsoundStore,
|
||||
code: string,
|
||||
fullContent: string,
|
||||
source: 'selection' | 'block' | 'document'
|
||||
source: EvalSource
|
||||
): Promise<void> {
|
||||
await csound.evaluate(fullContent);
|
||||
}
|
||||
@ -29,7 +31,7 @@ export class LiveCodingStrategy implements ExecutionStrategy {
|
||||
csound: CsoundStore,
|
||||
code: string,
|
||||
fullContent: string,
|
||||
source: 'selection' | 'block' | 'document'
|
||||
source: EvalSource
|
||||
): Promise<void> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initializeFromDocument(csound, fullContent);
|
||||
|
||||
@ -7,3 +7,5 @@ export type {
|
||||
} from './engine';
|
||||
export { createCsoundStore, createCsoundDerivedStores } 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);
|
||||
unsavedChangesDialogVisible = $state(false);
|
||||
saveAsDialogVisible = $state(false);
|
||||
templateDialogVisible = $state(false);
|
||||
|
||||
shareUrl = $state('');
|
||||
|
||||
@ -59,4 +60,12 @@ export class UIState {
|
||||
hideSaveAsDialog() {
|
||||
this.saveAsDialogVisible = false;
|
||||
}
|
||||
|
||||
showTemplateDialog() {
|
||||
this.templateDialogVisible = true;
|
||||
}
|
||||
|
||||
hideTemplateDialog() {
|
||||
this.templateDialogVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +1,77 @@
|
||||
export const DEFAULT_CSOUND_TEMPLATE = `<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
import type { ProjectMode } from '../project-system/types';
|
||||
|
||||
sr = 44100
|
||||
ksmps = 32
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
export interface CsoundTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
mode: ProjectMode;
|
||||
content: string;
|
||||
}
|
||||
|
||||
instr 1
|
||||
iFreq = p4
|
||||
iAmp = p5
|
||||
|
||||
; ADSR envelope
|
||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||
|
||||
; Sine wave oscillator
|
||||
aOsc oscili iAmp * kEnv, iFreq
|
||||
|
||||
outs aOsc, aOsc
|
||||
endin
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
; Arpeggio: C4 E4 G4 C5
|
||||
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>
|
||||
`;
|
||||
|
||||
export const LIVECODING_TEMPLATE = `<CsoundSynthesizer>
|
||||
const EMPTY_TEMPLATE: CsoundTemplate = {
|
||||
id: 'empty',
|
||||
name: 'Empty',
|
||||
mode: 'composition',
|
||||
content: `<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
-odac
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
|
||||
sr = 48000
|
||||
ksmps = 32
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>
|
||||
`
|
||||
};
|
||||
|
||||
const CLASSIC_TEMPLATE: CsoundTemplate = {
|
||||
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>
|
||||
-odac
|
||||
</CsOptions>
|
||||
@ -43,18 +82,14 @@ 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
|
||||
@ -67,7 +102,6 @@ instr 1
|
||||
endin
|
||||
|
||||
instr 2
|
||||
; Bass synth
|
||||
iFreq = p4
|
||||
iAmp = p5
|
||||
|
||||
@ -80,7 +114,6 @@ instr 2
|
||||
endin
|
||||
|
||||
instr 99
|
||||
; Global reverb
|
||||
aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5
|
||||
outs aL, aR
|
||||
gaReverb = 0
|
||||
@ -88,19 +121,14 @@ 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)
|
||||
; Basic note
|
||||
i 1 0 2 440 0.3
|
||||
|
||||
; 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.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
|
||||
|
||||
; While the long note plays, evaluate these to change frequency:
|
||||
; Change frequency while playing
|
||||
freq = 440
|
||||
|
||||
freq = 554.37
|
||||
|
||||
freq = 659.25
|
||||
|
||||
freq = 880
|
||||
|
||||
; Turn off all instances of instrument 1
|
||||
; Turn off 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
|
||||
const TEMPLATE_REGISTRY: CsoundTemplate[] = [
|
||||
EMPTY_TEMPLATE,
|
||||
LIVECODING_TEMPLATE,
|
||||
CLASSIC_TEMPLATE
|
||||
];
|
||||
|
||||
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
|
||||
export class TemplateRegistry {
|
||||
private templates: Map<string, CsoundTemplate>;
|
||||
|
||||
aSum = (aSaw + aSquare) * 0.5
|
||||
aFilt moogladder aSum, 1500, 0.5
|
||||
constructor() {
|
||||
this.templates = new Map(
|
||||
TEMPLATE_REGISTRY.map(template => [template.id, template])
|
||||
);
|
||||
}
|
||||
|
||||
outs aFilt, aFilt
|
||||
gaReverb = gaReverb + aFilt * 0.3
|
||||
endin
|
||||
`;
|
||||
getAll(): CsoundTemplate[] {
|
||||
return Array.from(this.templates.values());
|
||||
}
|
||||
|
||||
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