mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-23 03:28:33 +00:00
commit
b818a02f76
@ -126,6 +126,19 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, 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;
|
export default Hap;
|
||||||
|
|||||||
@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof hap.value !== 'object') {
|
hap.ensureObjectValue();
|
||||||
throw new Error('csound only support objects as hap values');
|
|
||||||
}
|
|
||||||
let { gain = 0.8 } = hap.value;
|
let { gain = 0.8 } = hap.value;
|
||||||
gain *= 0.2;
|
gain *= 0.2;
|
||||||
|
|
||||||
|
|||||||
@ -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 * 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 { 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:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@ -63,7 +64,7 @@ function getDevice(output, outputs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
Pattern.prototype.midi = function (output, channel = 1) {
|
Pattern.prototype.midi = function (output) {
|
||||||
if (!supportsMidi()) {
|
if (!supportsMidi()) {
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
}
|
}
|
||||||
@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.onTrigger((time, hap) => {
|
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) {
|
if (!midiReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
.join(' | ')}`,
|
.join(' | ')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// console.log('midi', value, output);
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
|
// calculate time
|
||||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
time = time * 1000 + timingOffset;
|
time = time * 1000 + timingOffset;
|
||||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
|
||||||
// await enableWebMidi()
|
// destructure value
|
||||||
device.playNote(note, channel, {
|
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||||
time,
|
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||||
duration: hap.duration.valueOf() * 1000 - 5,
|
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||||
attack: velocity,
|
|
||||||
});
|
if (note) {
|
||||||
|
const midiNumber = toMidi(note);
|
||||||
|
device.playNote(midiNumber, midichan, {
|
||||||
|
time,
|
||||||
|
duration,
|
||||||
|
attack: velocity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -47,6 +47,7 @@ let startedAt = -1;
|
|||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||||
|
hap.ensureObjectValue();
|
||||||
const osc = await connect();
|
const osc = await connect();
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
|
|||||||
@ -193,21 +193,9 @@ function effectSend(input, effect, wet) {
|
|||||||
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||||
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
/* if (isNote(hap.value)) {
|
hap.ensureObjectValue();
|
||||||
// supports primitive hap values that look like notes
|
|
||||||
hap.value = { note: hap.value };
|
// calculate absolute time
|
||||||
} */
|
|
||||||
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)
|
|
||||||
let t = ac.currentTime + deadline;
|
let t = ac.currentTime + deadline;
|
||||||
// destructure value
|
// destructure value
|
||||||
let {
|
let {
|
||||||
|
|||||||
@ -22,12 +22,35 @@ If no outputName is given, it uses the first midi output it finds.
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>")
|
tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
|
||||||
.midi()`}
|
.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".`
|
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.
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} />
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`stack(
|
||||||
|
note("c a f e"),
|
||||||
|
ccv(sine.segment(16).slow(4)).ccn(74)
|
||||||
|
).midi()`}
|
||||||
|
/>
|
||||||
|
|
||||||
# SuperDirt API
|
# SuperDirt API
|
||||||
|
|
||||||
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
|
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user