- feat: add freq support to gm soundfonts

- refactor: toMidi -> noteToMidi
- refactor: fromMidi -> midiToFreq
This commit is contained in:
Felix Roos 2023-03-23 10:18:24 +01:00
parent 13133583ca
commit ba35a81e9b
10 changed files with 59 additions and 50 deletions

View File

@ -4,7 +4,7 @@ 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/>. 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 { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min; const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => { const getValue = (e) => {
@ -18,7 +18,7 @@ const getValue = (e) => {
} }
note = note ?? n; note = note ?? n;
if (typeof note === 'string') { if (typeof note === 'string') {
return toMidi(note); return noteToMidi(note);
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;

View File

@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import { import {
isNote, isNote,
tokenizeNote, tokenizeNote,
toMidi, noteToMidi,
fromMidi, midiToFreq,
freqToMidi, freqToMidi,
_mod, _mod,
compose, compose,
@ -75,27 +75,27 @@ describe('isNote', () => {
expect(tokenizeNote(123)).toStrictEqual([]); expect(tokenizeNote(123)).toStrictEqual([]);
}); });
}); });
describe('toMidi', () => { describe('noteToMidi', () => {
it('should turn notes into midi', () => { it('should turn notes into midi', () => {
expect(toMidi('A4')).toEqual(69); expect(noteToMidi('A4')).toEqual(69);
expect(toMidi('C4')).toEqual(60); expect(noteToMidi('C4')).toEqual(60);
expect(toMidi('Db4')).toEqual(61); expect(noteToMidi('Db4')).toEqual(61);
expect(toMidi('C3')).toEqual(48); expect(noteToMidi('C3')).toEqual(48);
expect(toMidi('Cb3')).toEqual(47); expect(noteToMidi('Cb3')).toEqual(47);
expect(toMidi('Cbb3')).toEqual(46); expect(noteToMidi('Cbb3')).toEqual(46);
expect(toMidi('C#3')).toEqual(49); expect(noteToMidi('C#3')).toEqual(49);
expect(toMidi('C#3')).toEqual(49); expect(noteToMidi('C#3')).toEqual(49);
expect(toMidi('C##3')).toEqual(50); expect(noteToMidi('C##3')).toEqual(50);
}); });
it('should throw an error when given a non-note', () => { it('should throw an error when given a non-note', () => {
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`); expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`); expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
}); });
}); });
describe('fromMidi', () => { describe('midiToFreq', () => {
it('should turn midi into frequency', () => { it('should turn midi into frequency', () => {
expect(fromMidi(69)).toEqual(440); expect(midiToFreq(69)).toEqual(440);
expect(fromMidi(57)).toEqual(220); expect(midiToFreq(57)).toEqual(220);
}); });
}); });
describe('freqToMidi', () => { describe('freqToMidi', () => {

View File

@ -19,7 +19,7 @@ export const tokenizeNote = (note) => {
}; };
// turns the given note into its midi number representation // turns the given note into its midi number representation
export const toMidi = (note) => { export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note); const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) { if (!pc) {
throw new Error('not a note: "' + note + '"'); throw new Error('not a note: "' + note + '"');
@ -28,7 +28,7 @@ export const toMidi = (note) => {
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0; const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
export const fromMidi = (n) => { export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440; return Math.pow(2, (n - 69) / 12) * 440;
}; };
@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
return freqToMidi(freq); return freqToMidi(freq);
} }
if (typeof note === 'string') { if (typeof note === 'string') {
return toMidi(note); return noteToMidi(note);
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
@ -62,9 +62,9 @@ export const valueToMidi = (value, fallbackValue) => {
*/ */
export const getFreq = (noteOrMidi) => { export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') { if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi); return midiToFreq(noteOrMidi);
} }
return fromMidi(toMidi(noteOrMidi)); return midiToFreq(noteToMidi(noteOrMidi));
}; };
/** /**
@ -91,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
} }
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') { if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(hap.value); note = midiToFreq(hap.value);
} else if (typeof note === 'number' && context.type === 'frequency') { } else if (typeof note === 'number' && context.type === 'frequency') {
note = hap.value; // legacy workaround.. will be removed in the future note = hap.value; // legacy workaround.. will be removed in the future
} else if (typeof note !== 'string' || !isNote(note)) { } else if (typeof note !== 'string' || !isNote(note)) {
@ -110,9 +110,9 @@ export const getFrequency = (hap) => {
return getFreq(value.note || value.n || value.value); return getFreq(value.note || value.n || value.value);
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value); value = midiToFreq(hap.value);
} else if (typeof value === 'string' && isNote(value)) { } else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(hap.value)); value = midiToFreq(noteToMidi(hap.value));
} else if (typeof value !== 'number') { } else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value); throw new Error('not a note or frequency: ' + value);
} }
@ -170,7 +170,7 @@ export function parseNumeral(numOrString) {
return asNumber; return asNumber;
} }
if (isNote(numOrString)) { if (isNote(numOrString)) {
return toMidi(numOrString); return noteToMidi(numOrString);
} }
throw new Error(`cannot parse as numeral: "${numOrString}"`); throw new Error(`cannot parse as numeral: "${numOrString}"`);
} }

View File

@ -7,7 +7,7 @@ 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, 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'; import { noteToMidi } 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;
@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
const duration = hap.duration.valueOf() * 1000 - 5; const duration = hap.duration.valueOf() * 1000 - 5;
if (note) { if (note) {
const midiNumber = toMidi(note); const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, { device.playNote(midiNumber, midichan, {
time, time,
duration, duration,

View File

@ -1,4 +1,4 @@
import { toMidi } from '@strudel.cycles/core'; import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio'; import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import gm from './gm.mjs'; import gm from './gm.mjs';
@ -18,15 +18,24 @@ async function loadFont(name) {
return loadCache[name]; return loadCache[name];
} }
export async function getFontBufferSource(name, pitch, ac) { export async function getFontBufferSource(name, value, ac) {
if (typeof pitch === 'string') { let { note = 'c3', freq } = value;
pitch = toMidi(pitch); let midi;
if (freq) {
midi = freqToMidi(freq);
} else if (typeof note === 'string') {
midi = noteToMidi(note);
} else if (typeof note === 'number') {
midi = note;
} else {
throw new Error(`unexpected "note" type "${typeof note}"`);
} }
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const { buffer, zone } = await getFontPitch(name, midi, ac);
const src = ac.createBufferSource(); const src = ac.createBufferSource();
src.buffer = buffer; src.buffer = buffer;
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune; const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0); const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
// src detune? // src detune?
src.playbackRate.value = playbackRate; src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd; const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
@ -121,11 +130,11 @@ export function registerSoundfonts() {
registerSound( registerSound(
name, name,
async (time, value, onended) => { async (time, value, onended) => {
const { note = 'c3', n = 0 } = value; const { n = 0 } = value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value; const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n % fonts.length]; const font = fonts[n % fonts.length];
const ctx = getAudioContext(); const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, note, ctx); const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time); bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time); const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope); bufferSource.connect(envelope);

View File

@ -1,4 +1,4 @@
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core'; import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio'; import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
const note = getPlayableNoteValue(h); const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length]; const preset = sf.presets[n % sf.presets.length];
const deadline = ctx.currentTime + t - ct; const deadline = ctx.currentTime + t - ct;
const args = [ctx, preset, toMidi(note), deadline]; const args = [ctx, preset, noteToMidi(note), deadline];
const stop = startPresetNote(...args); const stop = startPresetNote(...args);
stop(deadline + h.duration); stop(deadline + h.duration);
}); });
@ -36,7 +36,7 @@ export function loadSoundfont(url) {
throw new Error('preset not found'); throw new Error('preset not found');
} }
const deadline = time; // - ctx.currentTime; const deadline = time; // - ctx.currentTime;
const args = [ctx, p, toMidi(note), deadline]; const args = [ctx, p, noteToMidi(note), deadline];
const stop = startPresetNote(...args); const stop = startPresetNote(...args);
return { node: undefined, stop }; return { node: undefined, stop };
}, },

View File

@ -1,4 +1,4 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core'; import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs'; import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs'; import { getEnvelope } from './helpers.mjs';
@ -34,7 +34,7 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
if (Array.isArray(bank)) { if (Array.isArray(bank)) {
sampleUrl = bank[n % bank.length]; sampleUrl = bank[n % bank.length];
} else { } else {
const midiDiff = (noteA) => toMidi(noteA) - midi; const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes // object format will expect keys as notes
const closest = Object.keys(bank) const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_')) .filter((k) => !k.startsWith('_'))

View File

@ -1,4 +1,4 @@
import { fromMidi, toMidi } from '@strudel.cycles/core'; import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs'; import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
@ -13,11 +13,11 @@ export function registerSynthSounds() {
// with synths, n and note are the same thing // with synths, n and note are the same thing
n = note || n || 36; n = note || n || 36;
if (typeof n === 'string') { if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48 n = noteToMidi(n); // e.g. c3 => 48
} }
// get frequency // get frequency
if (!freq && typeof n === 'number') { if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48); freq = midiToFreq(n); // + 48);
} }
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator // make oscillator

View File

@ -15,8 +15,8 @@
"isNoteWithOctave", "isNoteWithOctave",
"isNote", "isNote",
"tokenizeNote", "tokenizeNote",
"toMidi", "noteToMidi",
"fromMidi", "midiToFreq",
"freqToMidi", "freqToMidi",
"valueToMidi", "valueToMidi",
"_mod", "_mod",

View File

@ -1,4 +1,4 @@
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core'; import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core';
//import { registerSoundfonts } from '@strudel.cycles/soundfonts'; //import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio'; import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
@ -22,7 +22,7 @@ export async function prebake() {
]); ]);
} }
const maxPan = toMidi('C8'); const maxPan = noteToMidi('C8');
const panwidth = (pan, width) => pan * width + (1 - width) / 2; const panwidth = (pan, width) => pan * width + (1 - width) / 2;
Pattern.prototype.piano = function () { Pattern.prototype.piano = function () {