From ba35a81e9b4f6dfc4cb610a9ae8ea64065465cb1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 23 Mar 2023 10:18:24 +0100 Subject: [PATCH] - feat: add freq support to gm soundfonts - refactor: toMidi -> noteToMidi - refactor: fromMidi -> midiToFreq --- packages/core/pianoroll.mjs | 4 ++-- packages/core/test/util.test.mjs | 34 +++++++++++++++--------------- packages/core/util.mjs | 18 ++++++++-------- packages/midi/midi.mjs | 4 ++-- packages/soundfonts/fontloader.mjs | 25 +++++++++++++++------- packages/soundfonts/sfumato.mjs | 6 +++--- packages/webaudio/sampler.mjs | 4 ++-- packages/webaudio/synth.mjs | 6 +++--- undocumented.json | 4 ++-- website/src/repl/prebake.mjs | 4 ++-- 10 files changed, 59 insertions(+), 50 deletions(-) diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index eecb274f..635b7fac 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -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 getValue = (e) => { @@ -18,7 +18,7 @@ const getValue = (e) => { } note = note ?? n; if (typeof note === 'string') { - return toMidi(note); + return noteToMidi(note); } if (typeof note === 'number') { return note; diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 4a79192b..6b1053a3 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs'; import { isNote, tokenizeNote, - toMidi, - fromMidi, + noteToMidi, + midiToFreq, freqToMidi, _mod, compose, @@ -75,27 +75,27 @@ describe('isNote', () => { expect(tokenizeNote(123)).toStrictEqual([]); }); }); -describe('toMidi', () => { +describe('noteToMidi', () => { it('should turn notes into midi', () => { - expect(toMidi('A4')).toEqual(69); - expect(toMidi('C4')).toEqual(60); - expect(toMidi('Db4')).toEqual(61); - expect(toMidi('C3')).toEqual(48); - expect(toMidi('Cb3')).toEqual(47); - expect(toMidi('Cbb3')).toEqual(46); - expect(toMidi('C#3')).toEqual(49); - expect(toMidi('C#3')).toEqual(49); - expect(toMidi('C##3')).toEqual(50); + expect(noteToMidi('A4')).toEqual(69); + expect(noteToMidi('C4')).toEqual(60); + expect(noteToMidi('Db4')).toEqual(61); + expect(noteToMidi('C3')).toEqual(48); + expect(noteToMidi('Cb3')).toEqual(47); + expect(noteToMidi('Cbb3')).toEqual(46); + expect(noteToMidi('C#3')).toEqual(49); + expect(noteToMidi('C#3')).toEqual(49); + expect(noteToMidi('C##3')).toEqual(50); }); it('should throw an error when given a non-note', () => { - expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`); - expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`); + expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`); + expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`); }); }); -describe('fromMidi', () => { +describe('midiToFreq', () => { it('should turn midi into frequency', () => { - expect(fromMidi(69)).toEqual(440); - expect(fromMidi(57)).toEqual(220); + expect(midiToFreq(69)).toEqual(440); + expect(midiToFreq(57)).toEqual(220); }); }); describe('freqToMidi', () => { diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 27256d68..2b43cf0b 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -19,7 +19,7 @@ export const tokenizeNote = (note) => { }; // turns the given note into its midi number representation -export const toMidi = (note) => { +export const noteToMidi = (note) => { const [pc, acc, oct = 3] = tokenizeNote(note); if (!pc) { 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; return (Number(oct) + 1) * 12 + chroma + offset; }; -export const fromMidi = (n) => { +export const midiToFreq = (n) => { return Math.pow(2, (n - 69) / 12) * 440; }; @@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => { return freqToMidi(freq); } if (typeof note === 'string') { - return toMidi(note); + return noteToMidi(note); } if (typeof note === 'number') { return note; @@ -62,9 +62,9 @@ export const valueToMidi = (value, fallbackValue) => { */ export const getFreq = (noteOrMidi) => { 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 (typeof note === 'number' && context.type !== 'frequency') { - note = fromMidi(hap.value); + note = midiToFreq(hap.value); } else if (typeof note === 'number' && context.type === 'frequency') { note = hap.value; // legacy workaround.. will be removed in the future } else if (typeof note !== 'string' || !isNote(note)) { @@ -110,9 +110,9 @@ export const getFrequency = (hap) => { return getFreq(value.note || value.n || value.value); } if (typeof value === 'number' && context.type !== 'frequency') { - value = fromMidi(hap.value); + value = midiToFreq(hap.value); } else if (typeof value === 'string' && isNote(value)) { - value = fromMidi(toMidi(hap.value)); + value = midiToFreq(noteToMidi(hap.value)); } else if (typeof value !== 'number') { throw new Error('not a note or frequency: ' + value); } @@ -170,7 +170,7 @@ export function parseNumeral(numOrString) { return asNumber; } if (isNote(numOrString)) { - return toMidi(numOrString); + return noteToMidi(numOrString); } throw new Error(`cannot parse as numeral: "${numOrString}"`); } diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 789938d3..ab242d10 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -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 { Pattern, isPattern, logger } from '@strudel.cycles/core'; 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: export const { WebMidi } = _WebMidi; @@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) { const duration = hap.duration.valueOf() * 1000 - 5; if (note) { - const midiNumber = toMidi(note); + const midiNumber = noteToMidi(note); device.playNote(midiNumber, midichan, { time, duration, diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index c28dfb2f..86e6fc6e 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -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 gm from './gm.mjs'; @@ -18,15 +18,24 @@ async function loadFont(name) { return loadCache[name]; } -export async function getFontBufferSource(name, pitch, ac) { - if (typeof pitch === 'string') { - pitch = toMidi(pitch); +export async function getFontBufferSource(name, value, ac) { + let { note = 'c3', freq } = value; + 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(); src.buffer = buffer; 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.playbackRate.value = playbackRate; const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd; @@ -121,11 +130,11 @@ export function registerSoundfonts() { registerSound( name, 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 font = fonts[n % fonts.length]; const ctx = getAudioContext(); - const bufferSource = await getFontBufferSource(font, note, ctx); + const bufferSource = await getFontBufferSource(font, value, ctx); bufferSource.start(time); const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time); bufferSource.connect(envelope); diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 926999f3..c03bfef5 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -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 { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; @@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) { const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; const deadline = ctx.currentTime + t - ct; - const args = [ctx, preset, toMidi(note), deadline]; + const args = [ctx, preset, noteToMidi(note), deadline]; const stop = startPresetNote(...args); stop(deadline + h.duration); }); @@ -36,7 +36,7 @@ export function loadSoundfont(url) { throw new Error('preset not found'); } const deadline = time; // - ctx.currentTime; - const args = [ctx, p, toMidi(note), deadline]; + const args = [ctx, p, noteToMidi(note), deadline]; const stop = startPresetNote(...args); return { node: undefined, stop }; }, diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 48c45e11..cda4415e 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -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 { getEnvelope } from './helpers.mjs'; @@ -34,7 +34,7 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => { if (Array.isArray(bank)) { sampleUrl = bank[n % bank.length]; } else { - const midiDiff = (noteA) => toMidi(noteA) - midi; + const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes const closest = Object.keys(bank) .filter((k) => !k.startsWith('_')) diff --git a/packages/webaudio/synth.mjs b/packages/webaudio/synth.mjs index 5af0a692..242c8fb4 100644 --- a/packages/webaudio/synth.mjs +++ b/packages/webaudio/synth.mjs @@ -1,4 +1,4 @@ -import { fromMidi, toMidi } from '@strudel.cycles/core'; +import { midiToFreq, noteToMidi } from '@strudel.cycles/core'; import { registerSound } from './webaudio.mjs'; import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; @@ -13,11 +13,11 @@ export function registerSynthSounds() { // with synths, n and note are the same thing n = note || n || 36; if (typeof n === 'string') { - n = toMidi(n); // e.g. c3 => 48 + n = noteToMidi(n); // e.g. c3 => 48 } // get frequency 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) // make oscillator diff --git a/undocumented.json b/undocumented.json index e7095fe1..7735b34d 100644 --- a/undocumented.json +++ b/undocumented.json @@ -15,8 +15,8 @@ "isNoteWithOctave", "isNote", "tokenizeNote", - "toMidi", - "fromMidi", + "noteToMidi", + "midiToFreq", "freqToMidi", "valueToMidi", "_mod", diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index b7b848d0..9203ec98 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -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 { 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; Pattern.prototype.piano = function () {