mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-17 19:14:45 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f046ae883 | |||
| 2b575d50b9 | |||
| 7b92874909 | |||
| 10bfe32a2d | |||
| a41b07c67a | |||
| 20027d7208 | |||
| f38792c569 | |||
| 2c2bfd7070 | |||
| 484c5e6cdd |
@@ -1,8 +1,10 @@
|
||||
import jsdoc from '../../doc.json';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
import { Scale } from '@tonaljs/tonal';
|
||||
import { soundMap } from 'superdough';
|
||||
//TODO: fix tonal scale import
|
||||
// import { Scale } from '@tonaljs/tonal';
|
||||
// import { soundMap } from '@strudel/webaudio';
|
||||
let soundMap = undefined;
|
||||
import { complex } from '@strudel/tonal';
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
@@ -79,7 +81,9 @@ const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
export function bankCompletions() {
|
||||
const soundDict = soundMap.get();
|
||||
// TODO: FIX IMPORT
|
||||
const soundDict = soundMap?.get() ?? {};
|
||||
|
||||
const banks = new Set();
|
||||
for (const key of Object.keys(soundDict)) {
|
||||
const [bank, suffix] = key.split('_');
|
||||
@@ -90,13 +94,13 @@ export function bankCompletions() {
|
||||
.map((name) => ({ label: name, type: 'bank' }));
|
||||
}
|
||||
|
||||
// Attempt to get all scale names from Tonal
|
||||
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
|
||||
let scaleCompletions = [];
|
||||
try {
|
||||
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||
} catch (e) {
|
||||
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
}
|
||||
// try {
|
||||
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||
// } catch (e) {
|
||||
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
// }
|
||||
|
||||
// Valid mode values for voicing
|
||||
const modeCompletions = [
|
||||
@@ -268,7 +272,7 @@ function soundHandler(context) {
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
const soundNames = Object.keys(soundMap.get()).sort();
|
||||
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
|
||||
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
||||
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
||||
const from = soundContext.to - fragment.length;
|
||||
|
||||
@@ -72,15 +72,6 @@ export function registerControl(names, ...aliases) {
|
||||
return bag;
|
||||
}
|
||||
|
||||
// Constructs a superdirt effect bus control
|
||||
const bus = register('bus', function (busid, control_name, busval, pat) {
|
||||
return pat[control_name]('c' + busid).withValue((val) => {
|
||||
const newval = { ...val };
|
||||
newval['^' + control_name] = [busid, busval];
|
||||
return newval;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
||||
* separated by ':'.
|
||||
|
||||
@@ -58,10 +58,11 @@ export class Cyclist {
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset() || hap.context.processParts) {
|
||||
if (hap.hasOnset()) {
|
||||
const targetTime =
|
||||
(hap.part.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
// the following line is dumb and only here for backwards compatibility
|
||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
|
||||
@@ -16,7 +16,7 @@ export class Hap {
|
||||
then the whole will be returned as None, in which case the given
|
||||
value will have been sampled from the point halfway between the
|
||||
start and end of the 'part' timespan.
|
||||
The context is to store metadata, including a list of source code locations causing the event.
|
||||
The context is to store a list of source code locations causing the event.
|
||||
|
||||
The word 'Event' is more or less a reserved word in javascript, hence this
|
||||
class is named called 'Hap'.
|
||||
@@ -161,10 +161,6 @@ export class Hap {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
|
||||
withContext(f) {
|
||||
return new Hap(this.whole, this.part, this.value, f(this.context));
|
||||
}
|
||||
|
||||
ensureObjectValue() {
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
|
||||
@@ -62,12 +62,7 @@ export class Pattern {
|
||||
this.__steps = steps === undefined ? undefined : Fraction(steps);
|
||||
}
|
||||
|
||||
_processParts() {
|
||||
return this.withHap((hap) => hap.withContext((c) => ({ ...c, processParts: true })));
|
||||
}
|
||||
|
||||
setSteps(steps) {
|
||||
// TODO shouldn't this be pure and return a new pattern?
|
||||
this._steps = steps;
|
||||
return this;
|
||||
}
|
||||
|
||||
+7
-35
@@ -52,7 +52,6 @@ export function parseControlsFromHap(hap, cps) {
|
||||
controls.unit === 'c' && controls.speed != null && (controls.speed = controls.speed / cps);
|
||||
const channels = controls.channels;
|
||||
channels != undefined && (controls.channels = JSON.stringify(channels));
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
@@ -64,42 +63,16 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
||||
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
||||
|
||||
if ('oschost' in hap.value) {
|
||||
msg['host'] = hap.value['oschost'];
|
||||
}
|
||||
if ('oscport' in hap.value) {
|
||||
msg['port'] = hap.value['oscport'];
|
||||
}
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
export async function superdirtTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
const ws = await connect();
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
||||
|
||||
let oschost = '127.0.0.1';
|
||||
let oscport = 57120;
|
||||
let bushost = oschost;
|
||||
let busport = 57110;
|
||||
|
||||
for (const [k, v] of Object.entries(controls)) {
|
||||
if (k.startsWith('^')) {
|
||||
const bus_idval = v;
|
||||
const msg = JSON.stringify({
|
||||
oschost: bushost,
|
||||
oscport: busport,
|
||||
address: '/c_set',
|
||||
args: bus_idval,
|
||||
timestamp: ts,
|
||||
});
|
||||
// console.log('bus message', msg);
|
||||
ws.send(msg);
|
||||
// So they aren't sent with trigger messages
|
||||
delete controls[k];
|
||||
}
|
||||
}
|
||||
|
||||
if (hap.hasOnset()) {
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
ws.send(JSON.stringify({ oschost, oscport, address: '/dirt/play', args: keyvals, timestamp: ts }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||
@@ -110,4 +83,3 @@ export async function superdirtTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
* @returns Pattern
|
||||
*/
|
||||
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
||||
export const superdirt = register('superdirt', (pat) => pat._processParts().onTrigger(superdirtTrigger));
|
||||
|
||||
+13
-9
@@ -12,13 +12,18 @@ import { WebSocketServer } from 'ws';
|
||||
import osc from 'osc';
|
||||
|
||||
const WS_PORT = 8080; // WebSocket server port
|
||||
const OSC_REMOTE_IP = '127.0.0.1';
|
||||
const OSC_REMOTE_PORT = 57120;
|
||||
|
||||
const udpPort = new osc.UDPPort({
|
||||
localAddress: '0.0.0.0',
|
||||
localPort: 0,
|
||||
remoteAddress: OSC_REMOTE_IP,
|
||||
remotePort: OSC_REMOTE_PORT,
|
||||
});
|
||||
|
||||
udpPort.open();
|
||||
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
|
||||
|
||||
udpPort.on('error', (e) => {
|
||||
console.log('Error: ', e);
|
||||
@@ -31,24 +36,23 @@ wss.on('connection', (ws) => {
|
||||
console.log('New WebSocket connection');
|
||||
|
||||
ws.on('message', (message) => {
|
||||
let oschost = '127.0.0.1';
|
||||
let oscport = 57120;
|
||||
let osc_host = '127.0.0.1';
|
||||
let osc_port = 57120;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
if ('oschost' in data) {
|
||||
oschost = data['oschost'];
|
||||
delete data['oschost'];
|
||||
if ('host' in data) {
|
||||
osc_host = data['host'];
|
||||
}
|
||||
if ('oscport' in data) {
|
||||
oscport = data['oscport'];
|
||||
delete data['oscport'];
|
||||
if ('port' in data) {
|
||||
osc_port = data['port'];
|
||||
}
|
||||
let msg = { address: data['address'], args: data['args'] };
|
||||
if ('timestamp' in data) {
|
||||
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
|
||||
}
|
||||
udpPort.send(msg, oschost, oscport);
|
||||
|
||||
udpPort.send(msg, osc_host, osc_port);
|
||||
} catch (err) {
|
||||
console.error('Error parsing message:', err);
|
||||
}
|
||||
|
||||
@@ -92,11 +92,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
return new Promise((resolve) => {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
resolve(URL.createObjectURL(blob));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user