1import { getFrequency, logger, register } from '@strudel/core'; 2import { getAudioContext } from '@strudel/webaudio'; 3import csd from './project.csd?raw'; 4// import livecodeOrc from './livecode.orc?raw'; 5import presetsOrc from './presets.orc?raw'; 6 7let csoundLoader, _csound; 8 9// 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; 21 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} 126 127/** 128 * Sends notes to Csound for rendering with MIDI semantics. The hap value is 129 * translated to these Csound pfields: 130 * 131 * p1 -- Csound instrument either as a number (1-based, can be a fraction), 132 * or as a string name. 133 * p2 -- time in beats (usually seconds) from start of performance. 134 * p3 -- duration in beats (usually seconds). 135 * p4 -- MIDI key number (as a real number, not an integer but in [0, 127]. 136 * p5 -- MIDI velocity (as a real number, not an integer but in [0, 127]. 137 * p6 -- Strudel controls, as a string. 138 * 139 * @tags external_io 140 */ 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});