1import { getFrequency, logger, register } from '@strudel/core';
2import { getAudioContext } from '@strudel/webaudio';
3import csd from './project.csd?raw';

import livecodeOrc from './livecode.orc?raw';

5import presetsOrc from './presets.orc?raw';
7let csoundLoader, _csound;

initializes csound + can be used to reevaluate given instrument code

10export async function loadCSound(code = '') {
11  await init();
12  if (code) {
13    code = `${code}`;
14    //     ^       ^
15    // wrapping in backticks makes sure it works when calling as templated function
16    await _csound?.evalCode(code);
17  }
18}
19export const loadcsound = loadCSound;
20export const loadCsound = loadCSound;
22export const csound = register('csound', (instrument, pat) => {
23  instrument = instrument || 'triangle';
24  init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
25  // TODO: find a alternative way to wait for csound to load (to wait with first time playback)
26  return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
27    if (!_csound) {
28      logger('[csound] not loaded yet', 'warning');
29      return;
30    }
31    hap.ensureObjectValue();
32    let { gain = 0.8 } = hap.value;
33    gain *= 0.2;
34
35    const freq = Math.round(getFrequency(hap));
36    const controls = Object.entries({ ...hap.value, freq })
37      .flat()
38      .join('/');
39    // TODO: find out how to send a precise ctx based time
40    // http://www.csounds.com/manual/html/i.html
41    const timeOffset = targetTime - currentTime; // latency ?
42    //const timeOffset = time_deprecate - getAudioContext().currentTime
43    const params = [
44      `"${instrument}"`, // p1: instrument name
45      timeOffset, // p2: starting time in arbitrary unit called beats
46      hap.duration + 0, // p3: duration in beats
47      // instrument specific params:
48      freq, //.toFixed(precision), // p4: frequency
49      gain, // p5: gain
50      `"${controls}"`, // p6 controls as string (like superdirt osc message)
51    ];
52    const msg = `i ${params.join(' ')}`;
53    _csound.inputMessage(msg);
54  });
55});
56
57function eventLogger(type, args) {
58  const [msg] = args;
59  if (
60    type === 'message' &&
61    (['[commit: HEAD]'].includes(msg) ||
62      msg.startsWith('--Csound version') ||
63      msg.startsWith('libsndfile') ||
64      msg.startsWith('sr =') ||
65      msg.startsWith('0dBFS') ||
66      msg.startsWith('audio buffered') ||
67      msg.startsWith('writing') ||
68      msg.startsWith('SECTION 1:'))
69  ) {
70    // ignore
71    return;
72  }
73  let logType = 'info';
74  if (msg.startsWith('error:')) {
75    logType = 'error';
76  }
77  logger(`[csound] ${msg || ''}`, logType);
78}
79
80async function load() {
81  if (window.__csound__) {
82    // Allows using some other csound instance.
83    // In that case, the external Csound is responsible
84    // for compiling an orchestra and starting to perform.
85    logger('[load] Using external Csound', 'warning');
86    _csound = window.__csound__;
87    return _csound;
88  } else {
89    const { Csound } = await import('@csound/browser');
90    _csound = await Csound({ audioContext: getAudioContext() });
91    _csound.removeAllListeners('message');
92    ['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
93    await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
94    await _csound.setOption('--sample-accurate');
95    await _csound.setOption('-odac');
96    await _csound.compileCsdText(csd);
97    // await _csound.compileOrc(livecodeOrc);
98    await _csound.compileOrc(presetsOrc);
99    await _csound.start();
100    return _csound;
101  }
102}
103
104async function init() {
105  csoundLoader = csoundLoader || load();
106  return csoundLoader;
107}
108
109let orcCache = {};
110export async function loadOrc(url) {
111  await init();
112  if (typeof url !== 'string') {
113    throw new Error('loadOrc: expected url string');
114  }
115  if (url.startsWith('github:')) {
116    const [_, path] = url.split('github:');
117    url = `https://raw.githubusercontent.com/${path}`;
118  }
119  if (!orcCache[url]) {
120    orcCache[url] = fetch(url)
121      .then((res) => res.text())
122      .then((code) => _csound.compileOrc(code));
123  }
124  await orcCache[url];
125}

Sends notes to Csound for rendering with MIDI semantics. The hap value is translated to these Csound pfields:

p1 -- Csound instrument either as a number (1-based, can be a fraction), or as a string name. p2 -- time in beats (usually seconds) from start of performance. p3 -- duration in beats (usually seconds). p4 -- MIDI key number (as a real number, not an integer but in [0, 127]. p5 -- MIDI velocity (as a real number, not an integer but in [0, 127]. p6 -- Strudel controls, as a string.

@tags external_io

141export const csoundm = register('csoundm', (instrument, pat) => {
142  let p1 = instrument;
143  if (typeof instrument === 'string') {
144    p1 = `"${instrument}"`;
145  }
146  init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
147  return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
148    if (!_csound) {
149      logger('[csound] not loaded yet', 'warning');
150      return;
151    }
152    if (typeof hap.value !== 'object') {
153      throw new Error('csound only support objects as hap values');
154    }
155    // Time in seconds counting from now.
156    const p2 = targetTime - currentTime;
157    const p3 = hap.duration.valueOf() + 0;
158    const frequency = getFrequency(hap);
159    let { gain = 1, velocity = 0.9 } = hap.value;
160    velocity = gain * velocity;
161    // Translate frequency to MIDI key number _without_ rounding.
162    const C4 = 261.62558;
163    let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
164    const p4 = octave * 12.0 - 36.0;
165    // We prefer floating point precision, but over the MIDI range [0, 127].
166    const p5 = 127 * velocity;
167    // The Strudel controls as a string.
168    const p6 = Object.entries({ ...hap.value, frequency })
169      .flat()
170      .join('/');
171    const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
172    console.log('[csoundm]:', i_statement);
173    _csound.inputMessage(i_statement);
174  });
175});