Better architectural distinction between live coding mode and composition mode

This commit is contained in:
2025-10-15 11:04:27 +02:00
parent 46925f5c2e
commit bbdb01200e
11 changed files with 434 additions and 160 deletions

View File

@ -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>

View File

@ -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([

View File

@ -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>

View File

@ -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 {

View 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>

View File

@ -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()

View 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();
}
}

View File

@ -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);

View File

@ -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';

View File

@ -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;
}
} }

View File

@ -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();