mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-21 04:49:59 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b7ec7b403 | |||
| d23085e553 | |||
| d323b6843d | |||
| b2e927f4f0 | |||
| 3a271022b0 |
@@ -1287,7 +1287,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
// sets absolute duration of haps
|
||||
/* _duration(value) {
|
||||
_duration(value) {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
||||
}
|
||||
|
||||
@@ -1301,7 +1301,7 @@ export class Pattern {
|
||||
*/
|
||||
_legato(value) {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -1315,7 +1315,6 @@ export class Pattern {
|
||||
_velocity(velocity) {
|
||||
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Makes the sample fit the given number of cycles by changing the speed.
|
||||
@@ -1502,15 +1501,18 @@ Pattern.prototype.patternified = [
|
||||
'chop',
|
||||
'color',
|
||||
'cpm',
|
||||
'duration',
|
||||
'early',
|
||||
'fast',
|
||||
'jux',
|
||||
'late',
|
||||
'legato',
|
||||
'linger',
|
||||
'ply',
|
||||
'segment',
|
||||
'striate',
|
||||
'slow',
|
||||
'velocity',
|
||||
];
|
||||
|
||||
// aliases
|
||||
|
||||
@@ -130,10 +130,3 @@ export function curry(func, overload) {
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function objectify(value) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value; // do nothing if its already an object
|
||||
}
|
||||
return { value };
|
||||
}
|
||||
|
||||
+41
-44
@@ -4,8 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isNote } from 'tone';
|
||||
import _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, objectify } from '@strudel.cycles/core';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const WebMidi = _WebMidi;
|
||||
@@ -30,58 +32,53 @@ const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
if (output?.constructor?.name && isPattern(output?.constructor?.name)) {
|
||||
if (isPattern(output?.constructor?.name)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap, currentTime) => {
|
||||
let note;
|
||||
try {
|
||||
note = getPlayableNoteValue(hap);
|
||||
} catch (err) {
|
||||
// dont care if no note is found as we can still send cvs
|
||||
}
|
||||
const { velocity, nrpnn, nrpv, ccn, ccv, legato, duration: durationValue } = objectify(hap.value);
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const deadline = (time - currentTime) * 1000;
|
||||
time = WebMidi.time + deadline;
|
||||
/* const timingOffset = WebMidi.time - currentTime * 1000;
|
||||
time = time * 1000 + timingOffset; */
|
||||
const duration = (durationValue ?? hap.duration.valueOf()) * (legato ?? 1) * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
return this._withHap((hap) => {
|
||||
// const onTrigger = (time: number, hap: any) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
if (ccn && ccv) {
|
||||
device.sendControlChange(ccn, ccv, channel, { time });
|
||||
}
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import './tonal.mjs';
|
||||
import './voicings.mjs';
|
||||
export { addVoicings } from './voicings.mjs';
|
||||
|
||||
+23
-25
@@ -8,29 +8,30 @@ import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
|
||||
import _voicings from 'chord-voicings';
|
||||
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||
|
||||
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) =>
|
||||
dictionaryVoicing({
|
||||
let dictionaries = {
|
||||
lefthand,
|
||||
};
|
||||
|
||||
export const addVoicings = (name, dictionary) => {
|
||||
dictionaries = { ...dictionaries, [name]: dictionary };
|
||||
};
|
||||
|
||||
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4'], dictionaryName) => {
|
||||
const dictionary = dictionaries[dictionaryName];
|
||||
if (!dictionary) {
|
||||
throw new Error(`Dictionary ${dictionaryName} not found. Try adding it with addDictionary`);
|
||||
}
|
||||
return dictionaryVoicing({
|
||||
chord,
|
||||
dictionary: lefthand,
|
||||
dictionary,
|
||||
range,
|
||||
picker: minTopNoteDiff,
|
||||
lastVoicing,
|
||||
});
|
||||
};
|
||||
|
||||
const Pattern = _Pattern;
|
||||
|
||||
Pattern.prototype.fmapNested = function (func) {
|
||||
return new Pattern((span) =>
|
||||
this.query(span)
|
||||
.map((event) =>
|
||||
reify(func(event))
|
||||
.query(span)
|
||||
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context)),
|
||||
)
|
||||
.flat(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns chord symbols into voicings, using the smoothest voice leading possible.
|
||||
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
|
||||
@@ -43,18 +44,15 @@ Pattern.prototype.fmapNested = function (func) {
|
||||
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
|
||||
*/
|
||||
|
||||
Pattern.prototype.voicings = function (range) {
|
||||
Pattern.prototype.voicings = function (dictionaryName = 'lefthand') {
|
||||
let lastVoicing;
|
||||
if (!range?.length) {
|
||||
// allows to pass empty array, if too lazy to specify range
|
||||
range = ['F3', 'A4'];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing)._withContext(() => ({
|
||||
const range = ['F3', 'A4'];
|
||||
return this.fmap((value) => {
|
||||
lastVoicing = getVoicing(value, lastVoicing, range, dictionaryName);
|
||||
return stack(...lastVoicing) /* ._withContext(() => ({
|
||||
locations: event.context.locations || [],
|
||||
}));
|
||||
});
|
||||
})) */;
|
||||
}).outerJoin();
|
||||
};
|
||||
|
||||
Pattern.prototype._rootNotes = function (octave = 2) {
|
||||
|
||||
Reference in New Issue
Block a user