Trying to do better but it's hard
This commit is contained in:
1
csound-live-code
Submodule
1
csound-live-code
Submodule
Submodule csound-live-code added at 71b9973520
@ -31,6 +31,7 @@
|
|||||||
FileStack,
|
FileStack,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
|
CircleStop,
|
||||||
} from "lucide-svelte";
|
} from "lucide-svelte";
|
||||||
|
|
||||||
const appContext = createAppContext();
|
const appContext = createAppContext();
|
||||||
@ -44,7 +45,7 @@
|
|||||||
uiState,
|
uiState,
|
||||||
executionContext,
|
executionContext,
|
||||||
} = appContext;
|
} = appContext;
|
||||||
const csoundDerived = createCsoundDerivedStores(csound);
|
const { logs: csoundLogs, initialized, compiled, running } = createCsoundDerivedStores(csound);
|
||||||
|
|
||||||
let analyserNode = $state<AnalyserNode | null>(null);
|
let analyserNode = $state<AnalyserNode | null>(null);
|
||||||
let interpreterLogs = $state<LogEntry[]>([]);
|
let interpreterLogs = $state<LogEntry[]>([]);
|
||||||
@ -91,9 +92,27 @@
|
|||||||
projectEditor.loadProject(projectToLoad);
|
projectEditor.loadProject(projectToLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
logsUnsubscribe = csoundDerived.logs.subscribe((logs) => {
|
logsUnsubscribe = csoundLogs.subscribe((logs) => {
|
||||||
interpreterLogs = logs;
|
interpreterLogs = logs;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === '.') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
@ -220,6 +239,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleStop() {
|
||||||
|
try {
|
||||||
|
await csound.stop();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to stop:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
|
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
|
||||||
analyserNode = csound.getAnalyserNode();
|
analyserNode = csound.getAnalyserNode();
|
||||||
@ -281,6 +308,14 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<TopBar title="OldBoy">
|
<TopBar title="OldBoy">
|
||||||
{#snippet leftActions()}
|
{#snippet leftActions()}
|
||||||
|
<button
|
||||||
|
onclick={handleStop}
|
||||||
|
class="icon-button stop-button"
|
||||||
|
disabled={!$running}
|
||||||
|
title="Stop audio (Ctrl+.)"
|
||||||
|
>
|
||||||
|
<CircleStop size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={handleNewFromTemplate}
|
onclick={handleNewFromTemplate}
|
||||||
@ -292,8 +327,8 @@
|
|||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={handleSave}
|
onclick={handleSave}
|
||||||
disabled={!projectEditor.hasUnsavedChanges}
|
disabled={!projectEditor.hasUnsavedChanges}
|
||||||
title="Save {projectEditor.hasUnsavedChanges
|
title="Save (Ctrl+S){projectEditor.hasUnsavedChanges
|
||||||
? '(unsaved changes)'
|
? ' - unsaved changes'
|
||||||
: ''}"
|
: ''}"
|
||||||
class:has-changes={projectEditor.hasUnsavedChanges}
|
class:has-changes={projectEditor.hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
@ -537,6 +572,15 @@
|
|||||||
color: #646cff;
|
color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button.stop-button:not(:disabled) {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button.stop-button:hover:not(:disabled) {
|
||||||
|
color: #ff5252;
|
||||||
|
border-color: #ff5252;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
const lineNumbersCompartment = new Compartment();
|
const lineNumbersCompartment = new Compartment();
|
||||||
const lineWrappingCompartment = new Compartment();
|
const lineWrappingCompartment = new Compartment();
|
||||||
const vimCompartment = new Compartment();
|
const vimCompartment = new Compartment();
|
||||||
|
const languageCompartment = new Compartment();
|
||||||
|
|
||||||
function handleExecute() {
|
function handleExecute() {
|
||||||
if (!editorView) return;
|
if (!editorView) return;
|
||||||
@ -111,11 +112,13 @@
|
|||||||
|
|
||||||
const initSettings = $editorSettings;
|
const initSettings = $editorSettings;
|
||||||
|
|
||||||
|
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
|
||||||
|
|
||||||
editorView = new EditorView({
|
editorView = new EditorView({
|
||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
...baseExtensions,
|
...baseExtensions,
|
||||||
csoundMode({ fileType: 'csd' }),
|
languageCompartment.of(csoundMode({ fileType })),
|
||||||
oneDark,
|
oneDark,
|
||||||
evaluateKeymap,
|
evaluateKeymap,
|
||||||
flashField(),
|
flashField(),
|
||||||
@ -154,6 +157,15 @@
|
|||||||
editorView.dom.style.fontFamily = settings.fontFamily;
|
editorView.dom.style.fontFamily = settings.fontFamily;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!editorView) return;
|
||||||
|
|
||||||
|
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
|
||||||
|
editorView.dispatch({
|
||||||
|
effects: languageCompartment.reconfigure(csoundMode({ fileType }))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (editorView) {
|
if (editorView) {
|
||||||
editorView.destroy();
|
editorView.destroy();
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
import { Csound } from '@csound/browser';
|
import { Csound } from '@csound/browser';
|
||||||
|
|
||||||
export type InstanceMode = 'persistent' | 'ephemeral';
|
|
||||||
|
|
||||||
export interface CsoundEngineOptions {
|
export interface CsoundEngineOptions {
|
||||||
onMessage?: (message: string) => void;
|
onMessage?: (message: string) => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
onPerformanceEnd?: () => void;
|
onPerformanceEnd?: () => void;
|
||||||
onAnalyserNodeCreated?: (node: AnalyserNode) => void;
|
onAnalyserNodeCreated?: (node: AnalyserNode) => void;
|
||||||
instanceMode?: InstanceMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PerformanceMetrics {
|
export interface PerformanceMetrics {
|
||||||
@ -30,16 +27,15 @@ export class CsoundEngine {
|
|||||||
private scopeNode: AnalyserNode | null = null;
|
private scopeNode: AnalyserNode | null = null;
|
||||||
private audioNode: AudioNode | null = null;
|
private audioNode: AudioNode | null = null;
|
||||||
private audioContext: AudioContext | null = null;
|
private audioContext: AudioContext | null = null;
|
||||||
private instanceMode: InstanceMode;
|
|
||||||
|
|
||||||
constructor(options: CsoundEngineOptions = {}) {
|
constructor(options: CsoundEngineOptions = {}) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.instanceMode = options.instanceMode ?? 'ephemeral';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await this.ensureCsoundInstance();
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
this.log('Csound ready');
|
this.log('Csound ready');
|
||||||
}
|
}
|
||||||
@ -49,10 +45,51 @@ export class CsoundEngine {
|
|||||||
this.log('Creating Csound instance...');
|
this.log('Creating Csound instance...');
|
||||||
this.csound = await Csound();
|
this.csound = await Csound();
|
||||||
this.setupCallbacks();
|
this.setupCallbacks();
|
||||||
await this.csound.setOption('-odac');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
this.log('Restarting Csound...');
|
||||||
|
|
||||||
|
if (this.running) {
|
||||||
|
await this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.csound) {
|
||||||
|
try {
|
||||||
|
await this.csound.reset();
|
||||||
|
} catch (error) {
|
||||||
|
this.log('Reset failed, creating new instance...');
|
||||||
|
await this.cleanupInstance();
|
||||||
|
await this.ensureCsoundInstance();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.ensureCsoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.csound!.setOption('-m0');
|
||||||
|
await this.csound!.setOption('-odac');
|
||||||
|
await this.csound!.setOption('-+msg_color=false');
|
||||||
|
await this.csound!.setOption('--daemon');
|
||||||
|
|
||||||
|
const ac = await this.csound!.getAudioContext();
|
||||||
|
const sampleRate = ac?.sampleRate || 48000;
|
||||||
|
|
||||||
|
const header = `sr = ${sampleRate}\nksmps = 32\n0dbfs = 1\nnchnls = 2\nnchnls_i = 1`;
|
||||||
|
|
||||||
|
const compileResult = await this.csound!.compileOrc(header);
|
||||||
|
if (compileResult !== 0) {
|
||||||
|
throw new Error('Failed to compile base header');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.csound!.start();
|
||||||
|
|
||||||
|
this.compiled = true;
|
||||||
|
this.running = true;
|
||||||
|
this.setupAnalyser();
|
||||||
|
this.log('Csound restarted and ready');
|
||||||
|
}
|
||||||
|
|
||||||
private setupCallbacks(): void {
|
private setupCallbacks(): void {
|
||||||
if (!this.csound) return;
|
if (!this.csound) return;
|
||||||
|
|
||||||
@ -78,11 +115,13 @@ export class CsoundEngine {
|
|||||||
throw new Error('Csound not initialized. Call init() first.');
|
throw new Error('Csound not initialized. Call init() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!this.csound) {
|
||||||
await this.ensureCsoundInstance();
|
throw new Error('No Csound instance available');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
this.log('Compiling orchestra...');
|
this.log('Compiling orchestra...');
|
||||||
const result = await this.csound!.compileOrc(orchestra);
|
const result = await this.csound.compileOrc(orchestra);
|
||||||
|
|
||||||
if (result !== 0) {
|
if (result !== 0) {
|
||||||
const errorMsg = 'Orchestra compilation failed';
|
const errorMsg = 'Orchestra compilation failed';
|
||||||
@ -135,7 +174,7 @@ export class CsoundEngine {
|
|||||||
await this.csound!.readScore(event);
|
await this.csound!.readScore(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateCode(code: string, forceNewInstance = false): Promise<void> {
|
async evaluateCode(code: string): Promise<void> {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
throw new Error('Csound not initialized. Call init() first.');
|
throw new Error('Csound not initialized. Call init() first.');
|
||||||
}
|
}
|
||||||
@ -145,22 +184,10 @@ export class CsoundEngine {
|
|||||||
await this.stop();
|
await this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsNewInstance = forceNewInstance ||
|
await this.restart();
|
||||||
this.instanceMode === 'ephemeral' ||
|
|
||||||
!this.csound;
|
|
||||||
|
|
||||||
if (needsNewInstance && this.csound) {
|
|
||||||
this.log('Destroying existing instance...');
|
|
||||||
await this.cleanupInstance();
|
|
||||||
} else if (this.csound && this.compiled) {
|
|
||||||
this.log('Resetting Csound for new performance...');
|
|
||||||
await this.resetForNewPerformance();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scopeNode = null;
|
this.scopeNode = null;
|
||||||
|
|
||||||
await this.ensureCsoundInstance();
|
|
||||||
|
|
||||||
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
|
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
|
||||||
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
|
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
|
||||||
|
|
||||||
@ -171,13 +198,12 @@ export class CsoundEngine {
|
|||||||
const orc = orcMatch[1].trim();
|
const orc = orcMatch[1].trim();
|
||||||
const sco = scoMatch[1].trim();
|
const sco = scoMatch[1].trim();
|
||||||
|
|
||||||
const compileResult = await this.compileOrchestra(orc);
|
await this.csound!.compileOrc(orc);
|
||||||
if (!compileResult.success) {
|
this.compiled = true;
|
||||||
throw new Error(compileResult.errorMessage || 'Compilation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (sco) {
|
||||||
await this.readScore(sco);
|
await this.readScore(sco);
|
||||||
await this.startPerformance();
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
@ -298,31 +324,6 @@ export class CsoundEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reset(): Promise<void> {
|
|
||||||
this.log('Resetting Csound...');
|
|
||||||
await this.stop();
|
|
||||||
await this.cleanupInstance();
|
|
||||||
this.compiled = false;
|
|
||||||
await this.ensureCsoundInstance();
|
|
||||||
this.log('Reset complete');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetForNewPerformance(): Promise<void> {
|
|
||||||
if (!this.csound) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.csound.reset();
|
|
||||||
await this.csound.setOption('-odac');
|
|
||||||
this.compiled = false;
|
|
||||||
this.running = false;
|
|
||||||
this.audioNode = null;
|
|
||||||
this.audioContext = null;
|
|
||||||
this.scopeNode = null;
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Reset failed, creating new instance...');
|
|
||||||
await this.cleanupInstance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async cleanupInstance(): Promise<void> {
|
private async cleanupInstance(): Promise<void> {
|
||||||
if (!this.csound) return;
|
if (!this.csound) return;
|
||||||
|
|||||||
@ -78,6 +78,10 @@ export class ExecutionContext {
|
|||||||
await this.endSession();
|
await this.endSession();
|
||||||
this.currentMode = newMode;
|
this.currentMode = newMode;
|
||||||
this.strategy = this.createStrategyForMode(newMode);
|
this.strategy = this.createStrategyForMode(newMode);
|
||||||
|
|
||||||
|
if (newMode === 'composition') {
|
||||||
|
await this.csound.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createStrategyForMode(mode: ProjectMode): ExecutionStrategy {
|
private createStrategyForMode(mode: ProjectMode): ExecutionStrategy {
|
||||||
|
|||||||
@ -25,7 +25,6 @@ export class CompositionStrategy implements ExecutionStrategy {
|
|||||||
|
|
||||||
export class LiveCodingStrategy implements ExecutionStrategy {
|
export class LiveCodingStrategy implements ExecutionStrategy {
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
private headerCompiled = false;
|
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
csound: CsoundStore,
|
csound: CsoundStore,
|
||||||
@ -34,95 +33,40 @@ export class LiveCodingStrategy implements ExecutionStrategy {
|
|||||||
source: EvalSource
|
source: EvalSource
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
await this.initializeFromDocument(csound, fullContent);
|
await csound.restart();
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.evaluateBlock(csound, code);
|
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> {
|
private async evaluateBlock(csound: CsoundStore, code: string): Promise<void> {
|
||||||
const trimmedCode = code.trim();
|
const filteredCode = this.stripCommentLines(code);
|
||||||
|
|
||||||
if (!trimmedCode) return;
|
if (!filteredCode.trim()) return;
|
||||||
|
|
||||||
if (this.isScoreEvent(trimmedCode)) {
|
if (this.isScoreEvent(filteredCode)) {
|
||||||
await csound.sendScoreEvent(trimmedCode);
|
await csound.sendScoreEvent(filteredCode);
|
||||||
}
|
}
|
||||||
else if (this.isInstrumentDefinition(trimmedCode)) {
|
else if (this.isChannelSet(filteredCode)) {
|
||||||
await csound.compileOrchestra(trimmedCode);
|
await this.handleChannelSet(csound, filteredCode);
|
||||||
}
|
|
||||||
else if (this.isChannelSet(trimmedCode)) {
|
|
||||||
await this.handleChannelSet(csound, trimmedCode);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await csound.compileOrchestra(trimmedCode);
|
await csound.compileOrchestra(filteredCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseCSD(content: string): {
|
private stripCommentLines(code: string): string {
|
||||||
header: string;
|
return code
|
||||||
instruments: string;
|
.split('\n')
|
||||||
score: string
|
.filter(line => !line.trim().startsWith(';'))
|
||||||
} {
|
.join('\n');
|
||||||
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 {
|
private isScoreEvent(code: string): boolean {
|
||||||
return /^[ifea]\s+[\d\-]/.test(code);
|
return /^[ifea]\s+[\d\-]/.test(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isInstrumentDefinition(code: string): boolean {
|
|
||||||
return /^\s*instr\s+/.test(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isChannelSet(code: string): boolean {
|
private isChannelSet(code: string): boolean {
|
||||||
return /^\w+\s*=\s*[\d\.\-]+/.test(code);
|
return /^\w+\s*=\s*[\d\.\-]+/.test(code);
|
||||||
}
|
}
|
||||||
@ -139,7 +83,6 @@ export class LiveCodingStrategy implements ExecutionStrategy {
|
|||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.headerCompiled = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { writable, derived, get } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
import { CsoundEngine, type InstanceMode, type PerformanceMetrics, type CompilationResult } from './engine';
|
import { CsoundEngine, type PerformanceMetrics, type CompilationResult } from './engine';
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
@ -21,9 +21,9 @@ export interface CsoundStore {
|
|||||||
startPerformance: () => Promise<void>;
|
startPerformance: () => Promise<void>;
|
||||||
readScore: (score: string) => Promise<void>;
|
readScore: (score: string) => Promise<void>;
|
||||||
sendScoreEvent: (event: string) => Promise<void>;
|
sendScoreEvent: (event: string) => Promise<void>;
|
||||||
evaluate: (code: string, forceNewInstance?: boolean) => Promise<void>;
|
evaluate: (code: string) => Promise<void>;
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
reset: () => Promise<void>;
|
restart: () => Promise<void>;
|
||||||
setControlChannel: (name: string, value: number) => Promise<void>;
|
setControlChannel: (name: string, value: number) => Promise<void>;
|
||||||
getControlChannel: (name: string) => Promise<number>;
|
getControlChannel: (name: string) => Promise<number>;
|
||||||
setStringChannel: (name: string, value: string) => Promise<void>;
|
setStringChannel: (name: string, value: string) => Promise<void>;
|
||||||
@ -41,7 +41,7 @@ export interface CsoundStore {
|
|||||||
destroy: () => Promise<void>;
|
destroy: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
|
export function createCsoundStore(): CsoundStore {
|
||||||
const initialState: CsoundState = {
|
const initialState: CsoundState = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
compiled: false,
|
compiled: false,
|
||||||
@ -76,8 +76,7 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
|
|||||||
},
|
},
|
||||||
onAnalyserNodeCreated: (node) => {
|
onAnalyserNodeCreated: (node) => {
|
||||||
analyserNodeListeners.forEach(listener => listener(node));
|
analyserNodeListeners.forEach(listener => listener(node));
|
||||||
},
|
}
|
||||||
instanceMode
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await engine.init();
|
await engine.init();
|
||||||
@ -140,13 +139,13 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
|
|||||||
await engine.sendScoreEvent(event);
|
await engine.sendScoreEvent(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
async evaluate(code: string, forceNewInstance = false) {
|
async evaluate(code: string) {
|
||||||
if (!engine) {
|
if (!engine) {
|
||||||
throw new Error('Csound engine not initialized');
|
throw new Error('Csound engine not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await engine.evaluateCode(code, forceNewInstance);
|
await engine.evaluateCode(code);
|
||||||
update(state => ({
|
update(state => ({
|
||||||
...state,
|
...state,
|
||||||
compiled: engine!.isCompiled(),
|
compiled: engine!.isCompiled(),
|
||||||
@ -172,14 +171,14 @@ export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async reset() {
|
async restart() {
|
||||||
if (!engine) return;
|
if (!engine) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await engine.reset();
|
await engine.restart();
|
||||||
update(state => ({ ...state, compiled: false, running: false }));
|
update(state => ({ ...state, compiled: true, running: true }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : 'Reset failed';
|
const errorMsg = error instanceof Error ? error.message : 'Restart failed';
|
||||||
addLog(errorMsg, 'error');
|
addLog(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -80,14 +80,57 @@ export function getLine(state: EditorState): EvalBlock {
|
|||||||
return { text, from, to };
|
return { text, from, to };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BlockType = 'instr' | 'endin' | 'opcode' | 'endop' | null;
|
||||||
|
|
||||||
|
function startsWithBlockMarker(text: string): BlockType {
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (/^\s*instr\b/.test(text)) return 'instr';
|
||||||
|
if (/^\s*endin\b/.test(text)) return 'endin';
|
||||||
|
if (/^\s*opcode\b/.test(text)) return 'opcode';
|
||||||
|
if (/^\s*endop\b/.test(text)) return 'endop';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBlockMarker(
|
||||||
|
doc: any,
|
||||||
|
startLine: number,
|
||||||
|
direction: number,
|
||||||
|
limit: number
|
||||||
|
): [number, BlockType] | null {
|
||||||
|
for (let i = startLine; direction > 0 ? i <= limit : i >= limit; i += direction) {
|
||||||
|
if (i < 1 || i > doc.lines) break;
|
||||||
|
const lineText = doc.line(i).text;
|
||||||
|
const marker = startsWithBlockMarker(lineText);
|
||||||
|
if (marker) {
|
||||||
|
return [i, marker];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getBlock(state: EditorState): EvalBlock {
|
export function getBlock(state: EditorState): EvalBlock {
|
||||||
let { doc, selection } = state;
|
let { doc, selection } = state;
|
||||||
let { text, number } = state.doc.lineAt(selection.main.from);
|
let { text, number } = state.doc.lineAt(selection.main.from);
|
||||||
|
|
||||||
if (text.trim().length === 0) return { text: '', from: null, to: null };
|
if (text.trim().length === 0) return { text: '', from: null, to: null };
|
||||||
|
|
||||||
let fromL, toL;
|
const prevBlockMark = findBlockMarker(doc, number, -1, 1);
|
||||||
fromL = toL = number;
|
const nextBlockMark = findBlockMarker(doc, number, 1, doc.lines);
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevBlockMark &&
|
||||||
|
nextBlockMark &&
|
||||||
|
((prevBlockMark[1] === 'instr' && nextBlockMark[1] === 'endin') ||
|
||||||
|
(prevBlockMark[1] === 'opcode' && nextBlockMark[1] === 'endop'))
|
||||||
|
) {
|
||||||
|
const { from } = doc.line(prevBlockMark[0]);
|
||||||
|
const { to } = doc.line(nextBlockMark[0]);
|
||||||
|
text = state.doc.sliceString(from, to);
|
||||||
|
return { text, from, to };
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromL = number;
|
||||||
|
let toL = number;
|
||||||
|
|
||||||
while (fromL > 1 && doc.line(fromL - 1).text.trim().length > 0) {
|
while (fromL > 1 && doc.line(fromL - 1).text.trim().length > 0) {
|
||||||
fromL -= 1;
|
fromL -= 1;
|
||||||
|
|||||||
@ -71,19 +71,11 @@ const LIVECODING_TEMPLATE: CsoundTemplate = {
|
|||||||
id: 'livecoding',
|
id: 'livecoding',
|
||||||
name: 'Live Coding',
|
name: 'Live Coding',
|
||||||
mode: 'livecoding',
|
mode: 'livecoding',
|
||||||
content: `<CsoundSynthesizer>
|
content: `; LIVE CODING MODE
|
||||||
<CsOptions>
|
; Engine auto-initializes on first evaluation (Ctrl+E)
|
||||||
-odac
|
; Evaluate instruments/opcodes with Ctrl+E to define them
|
||||||
</CsOptions>
|
; Evaluate score events (i-statements) to trigger sounds
|
||||||
<CsInstruments>
|
; Press Ctrl+. to stop all audio
|
||||||
|
|
||||||
sr = 48000
|
|
||||||
ksmps = 32
|
|
||||||
nchnls = 2
|
|
||||||
0dbfs = 1
|
|
||||||
|
|
||||||
; Press Cmd/Ctrl+E on the full document first to initialize
|
|
||||||
; Then evaluate individual blocks to trigger sounds
|
|
||||||
|
|
||||||
gaReverb init 0
|
gaReverb init 0
|
||||||
|
|
||||||
@ -119,14 +111,12 @@ instr 99
|
|||||||
gaReverb = 0
|
gaReverb = 0
|
||||||
endin
|
endin
|
||||||
|
|
||||||
</CsInstruments>
|
; Start reverb (always on)
|
||||||
<CsScore>
|
|
||||||
i 99 0 -1
|
i 99 0 -1
|
||||||
</CsScore>
|
|
||||||
</CsoundSynthesizer>
|
|
||||||
|
|
||||||
|
|
||||||
; LIVE CODING EXAMPLES
|
; === LIVE CODING EXAMPLES ===
|
||||||
|
; Select a block and press Ctrl+E to evaluate
|
||||||
|
|
||||||
; Basic note
|
; Basic note
|
||||||
i 1 0 2 440 0.3
|
i 1 0 2 440 0.3
|
||||||
@ -146,7 +136,7 @@ i 2 1.5 0.5 130.81 0.4
|
|||||||
; Long note for channel control
|
; Long note for channel control
|
||||||
i 1 0 30 440 0.3
|
i 1 0 30 440 0.3
|
||||||
|
|
||||||
; Change frequency while playing
|
; Change frequency while playing (select and evaluate)
|
||||||
freq = 440
|
freq = 440
|
||||||
|
|
||||||
freq = 554.37
|
freq = 554.37
|
||||||
|
|||||||
Reference in New Issue
Block a user