From 6283273d811cbc3aa944eb17fd9d6c03b6560656 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 24 Feb 2023 10:15:06 +0100 Subject: [PATCH 1/3] add helper hap.ensureObjectValue --- packages/core/hap.mjs | 13 +++++++++++++ packages/csound/index.mjs | 4 +--- packages/osc/osc.mjs | 1 + packages/webaudio/webaudio.mjs | 18 +++--------------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index 30251959..c9c91c3b 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -126,6 +126,19 @@ export class Hap { setContext(context) { return new Hap(this.whole, this.part, this.value, context); } + + ensureObjectValue() { + /* if (isNote(hap.value)) { + // supports primitive hap values that look like notes + hap.value = { note: hap.value }; + } */ + if (typeof this.value !== 'object') { + throw new Error( + `expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`, + 'error', + ); + } + } } export default Hap; diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index 3aabc22b..31ffa83a 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => { logger('[csound] not loaded yet', 'warning'); return; } - if (typeof hap.value !== 'object') { - throw new Error('csound only support objects as hap values'); - } + hap.ensureObjectValue(); let { gain = 0.8 } = hap.value; gain *= 0.2; diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 2176f61f..83e03dad 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -47,6 +47,7 @@ let startedAt = -1; */ Pattern.prototype.osc = function () { return this.onTrigger(async (time, hap, currentTime, cps = 1) => { + hap.ensureObjectValue(); const osc = await connect(); const cycle = hap.wholeOrPart().begin.valueOf(); const delta = hap.duration.valueOf(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 968cb996..526c4912 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -193,21 +193,9 @@ function effectSend(input, effect, wet) { // export const webaudioOutput = async (t, hap, ct, cps) => { export const webaudioOutput = async (hap, deadline, hapDuration) => { const ac = getAudioContext(); - /* if (isNote(hap.value)) { - // supports primitive hap values that look like notes - hap.value = { note: hap.value }; - } */ - if (typeof hap.value !== 'object') { - logger( - `hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`, - 'error', - ); - /* throw new Error( - `hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`, - ); */ - return; - } - // calculate correct time (tone.js workaround) + hap.ensureObjectValue(); + + // calculate absolute time let t = ac.currentTime + deadline; // destructure value let { From 5de6643604de789395fb556028de6ba91b81ddf0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 24 Feb 2023 10:15:21 +0100 Subject: [PATCH 2/3] midi: support ccn and ccv --- packages/midi/midi.mjs | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index de04f355..72fb58f6 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as _WebMidi from 'webmidi'; -import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core'; +import { Pattern, isPattern, logger } from '@strudel.cycles/core'; import { getAudioContext } from '@strudel.cycles/webaudio'; +import { toMidi } from '@strudel.cycles/core'; // if you use WebMidi from outside of this package, make sure to import that instance: export const { WebMidi } = _WebMidi; @@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) { ); } return this.onTrigger((time, hap) => { - let note = getPlayableNoteValue(hap); - const velocity = hap.context?.velocity ?? 0.9; - if (!isNote(note)) { - throw new Error('not a note: ' + note); - } if (!midiReady) { return; } @@ -106,15 +102,28 @@ Pattern.prototype.midi = function (output, channel = 1) { .join(' | ')}`, ); } - // console.log('midi', value, output); + hap.ensureObjectValue(); + + // calculate time const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000; time = time * 1000 + timingOffset; - // const inMs = '+' + (time - Tone.getContext().currentTime) * 1000; - // await enableWebMidi() - device.playNote(note, channel, { - time, - duration: hap.duration.valueOf() * 1000 - 5, - attack: velocity, - }); + + // destructure value + const { note, nrpnn, nrpv, ccn, ccv } = hap.value; + const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity + const duration = hap.duration.valueOf() * 1000 - 5; + + if (note) { + const midiNumber = toMidi(note); + console.log('midi number', midiNumber); + device.playNote(midiNumber, channel, { + time, + duration, + attack: velocity, + }); + } + if (ccn && ccv) { + device.sendControlChange(ccn, ccv, channel, { time }); + } }); }; From 886f8449fdd90e710bf9f120d4d5497e05080474 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 25 Feb 2023 12:23:01 +0100 Subject: [PATCH 3/3] add midichan + docs --- packages/midi/midi.mjs | 18 +++++++++++------ website/src/pages/learn/input-output.mdx | 25 +++++++++++++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 72fb58f6..789938d3 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -64,7 +64,7 @@ function getDevice(output, outputs) { } // Pattern.prototype.midi = function (output: string | number, channel = 1) { -Pattern.prototype.midi = function (output, channel = 1) { +Pattern.prototype.midi = function (output) { if (!supportsMidi()) { throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`); } @@ -109,21 +109,27 @@ Pattern.prototype.midi = function (output, channel = 1) { time = time * 1000 + timingOffset; // destructure value - const { note, nrpnn, nrpv, ccn, ccv } = hap.value; + const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value; const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity const duration = hap.duration.valueOf() * 1000 - 5; if (note) { const midiNumber = toMidi(note); - console.log('midi number', midiNumber); - device.playNote(midiNumber, channel, { + device.playNote(midiNumber, midichan, { time, duration, attack: velocity, }); } - if (ccn && ccv) { - device.sendControlChange(ccn, ccv, channel, { time }); + if (ccv && ccn) { + if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { + throw new Error('expected ccv to be a number between 0 and 1'); + } + if (!['string', 'number'].includes(typeof ccn)) { + throw new Error('expected ccn to be a number or a string'); + } + const scaled = Math.round(ccv * 127); + device.sendControlChange(ccn, scaled, midichan, { time }); } }); }; diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 4688cf25..e4c2a63f 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -22,12 +22,35 @@ If no outputName is given, it uses the first midi output it finds. ".voicings('lefthand'), "") + tune={`stack("".voicings('lefthand'), "").note() .midi()`} /> In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` +## midichan(number) + +Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. + +## ccn && ccv + +- `ccn` sets the cc number. Depends on your synths midi mapping +- `ccv` sets the cc value. normalized from 0 to 1. + + + +In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. +Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default. +But you can also control cc messages separately like this: + + + # SuperDirt API In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.