Skip to content

Commit

Permalink
Merge pull request #534 from tidalcycles/soundfont-freq-support
Browse files Browse the repository at this point in the history
feat: add freq support to gm soundfonts
  • Loading branch information
felixroos authored Mar 23, 2023
2 parents ddf61a6 + ba35a81 commit 9bbc048
Show file tree
Hide file tree
Showing 10 changed files with 59 additions and 50 deletions.
4 changes: 2 additions & 2 deletions packages/core/pianoroll.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/>.
*/

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) => {
Expand All @@ -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;
Expand Down
34 changes: 17 additions & 17 deletions packages/core/test/util.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
Expand Down Expand Up @@ -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', () => {
Expand Down
18 changes: 9 additions & 9 deletions packages/core/util.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '"');
Expand All @@ -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;
};

Expand All @@ -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;
Expand All @@ -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));
};

/**
Expand All @@ -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)) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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}"`);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/midi/midi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 17 additions & 8 deletions packages/soundfonts/fontloader.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions packages/soundfonts/sfumato.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
});
Expand Down Expand Up @@ -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 };
},
Expand Down
4 changes: 2 additions & 2 deletions packages/webaudio/sampler.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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('_'))
Expand Down
6 changes: 3 additions & 3 deletions packages/webaudio/synth.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions undocumented.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"isNoteWithOctave",
"isNote",
"tokenizeNote",
"toMidi",
"fromMidi",
"noteToMidi",
"midiToFreq",
"freqToMidi",
"valueToMidi",
"_mod",
Expand Down
4 changes: 2 additions & 2 deletions website/src/repl/prebake.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 () {
Expand Down

0 comments on commit 9bbc048

Please sign in to comment.