diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 17dceb81..9ebccee3 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -10,6 +10,7 @@ import { tokenizeNote, toMidi, fromMidi, + freqToMidi, _mod, compose, getFrequency, @@ -94,6 +95,12 @@ describe('fromMidi', () => { expect(fromMidi(57)).toEqual(220); }); }); +describe('freqToMidi', () => { + it('should turn frequency into midi', () => { + expect(freqToMidi(440)).toEqual(69); + expect(freqToMidi(220)).toEqual(57); + }); +}); describe('getFrequency', () => { const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context); it('should turn note into frequency', () => { diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b1997977..d8a7a233 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -31,6 +31,30 @@ export const fromMidi = (n) => { return Math.pow(2, (n - 69) / 12) * 440; }; +export const freqToMidi = (freq) => { + return (12 * Math.log(freq / 440)) / Math.LN2 + 69; +}; + +export const valueToMidi = (value, fallbackValue) => { + if (typeof value !== 'object') { + throw new Error('valueToMidi: expected object value'); + } + let { freq, note } = value; + if (typeof freq === 'number') { + return freqToMidi(freq); + } + if (typeof note === 'string') { + return toMidi(note); + } + if (typeof note === 'number') { + return note; + } + if (!fallbackValue) { + throw new Error('valueToMidi: expected freq or note to be set'); + } + return fallbackValue; +}; + /** * @deprecated does not appear to be referenced or invoked anywhere in the codebase */ diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 094af87f..7b437ae3 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -1,4 +1,4 @@ -import { logger, toMidi } from '@strudel.cycles/core'; +import { logger, toMidi, valueToMidi } from '@strudel.cycles/core'; import { getAudioContext } from './index.mjs'; const bufferCache = {}; // string: Promise @@ -20,9 +20,12 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export const getSampleBufferSource = async (s, n, note, speed) => { +export const getSampleBufferSource = async (s, n, note, speed, freq) => { let transpose = 0; - let midi = typeof note === 'string' ? toMidi(note) : note || 36; + if (freq !== undefined && note !== undefined) { + logger('[sampler] hap has note and freq. ignoring note', 'warning'); + } + let midi = valueToMidi({ freq, note }, 36); transpose = midi - 36; // C3 is middle C const ac = getAudioContext(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 532957fa..4cac9083 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -288,10 +288,10 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => { if (soundfont) { // is soundfont - bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); + bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq); } else { // is sample from loaded samples(..) - bufferSource = await getSampleBufferSource(s, n, note, speed); + bufferSource = await getSampleBufferSource(s, n, note, speed, freq); } // asny stuff above took too long? if (ac.currentTime > t) { diff --git a/repl/src/prebake.mjs b/repl/src/prebake.mjs index 97443dcf..a2e97062 100644 --- a/repl/src/prebake.mjs +++ b/repl/src/prebake.mjs @@ -1,4 +1,4 @@ -import { Pattern, toMidi } from '@strudel.cycles/core'; +import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core'; import { samples } from '@strudel.cycles/webaudio'; export async function prebake({ isMock = false, baseDir = '.' } = {}) { @@ -25,9 +25,9 @@ Pattern.prototype.piano = function () { .s('piano') .release(0.1) .fmap((value) => { - const midi = typeof value.note === 'string' ? toMidi(value.note) : value.note; + const midi = valueToMidi(value); // pan by pitch - const pan = panwidth(Math.min(midi / maxPan, 1), 0.5); + const pan = panwidth(Math.min(Math.round(midi) / maxPan, 1), 0.5); return { ...value, pan: (value.pan || 1) * pan }; }); };