Chromatone logo
Theory
11
Back to main
Elementary synth
MIDI enabled synthesizer built with Elementary audio library

Modular synth

Built with JS, but operated by WASM lib in a separate audio thread. Construction started around June 2023 and is ongoing till then. Supposed to be gradually substituting the sitewide Tone.js synth used by now.

Resources

Snippets

Mic input

When you use el.in() you have to be specific about which channel you want to tap, i.e. el.in({channel: 0}), but then you need to make sure that your WebAudioNode running elementary's engine actually has input signals. So you would need to call core.initialize to declare that input channels are expected, and then you would need to actually connect your web audio input nodes to the node returned by core.initialize. Does that make sense?

js
let node = await core.initialize(audioContext, {
      numberOfInputs: 1,
      numberOfOutputs: 1,
      inputChannelCount: [1], // Notice, it's a mono input
      outputChannelCount: [2],
    }, 8);

    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      let micStream = await navigator.mediaDevices.getUserMedia({audio: true});
      let streamSource = audioContext.createMediaStreamSource(micStream);
      streamSource.connect(node);
    }
let node = await core.initialize(audioContext, {
      numberOfInputs: 1,
      numberOfOutputs: 1,
      inputChannelCount: [1], // Notice, it's a mono input
      outputChannelCount: [2],
    }, 8);

    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      let micStream = await navigator.mediaDevices.getUserMedia({audio: true});
      let streamSource = audioContext.createMediaStreamSource(micStream);
      streamSource.connect(node);
    }

Skew by upheaver

Sharing a useful snippet. JUCE's logskew function in Typescript, if anyone wants to scale their parameter values (for example convert 0-1 range to 20-20000 hz in a non linear way) :

js
const clamp = ( min: number, max: number, num: number ) => {
    return Math.min(Math.max(num, min), max);
}

const skew = ( min: number, max: number, centerPoint: number ) => {
    return Math.log(0.5) / Math.log((centerPoint - min) / (max - min));
}

const logskew = ( a: number, b: number, time: number, centerPoint: number) => {
    const proportion = clamp(0, 1, time);
    const sk = skew(a, b, centerPoint);
    const v = Math.exp (Math.log (proportion) / sk);
    return a + (b - a) * v;
}

console.log(logskew( 20, 20000, 0.5, 1000)) // will output 1000
const clamp = ( min: number, max: number, num: number ) => {
    return Math.min(Math.max(num, min), max);
}

const skew = ( min: number, max: number, centerPoint: number ) => {
    return Math.log(0.5) / Math.log((centerPoint - min) / (max - min));
}

const logskew = ( a: number, b: number, time: number, centerPoint: number) => {
    const proportion = clamp(0, 1, time);
    const sk = skew(a, b, centerPoint);
    const v = Math.exp (Math.log (proportion) / sk);
    return a + (b - a) * v;
}

console.log(logskew( 20, 20000, 0.5, 1000)) // will output 1000

Metronome

Yea long story short I think metro was maybe not a great idea and it's worth considering to deprecate it. More effective in my opinion is to take el.time() which monotonically increases, counting the number of elapsed samples, and derive a quarter-note phasor from el.time(). (From bpm you calculate seconds per beat, from which you calculate samples per beat, which you can use to convert el.time() into a phasor running at exactly quarter-note rate). Then of course if you want to change bpm you just change the numbers in your calculation but it will all be perfectly time-synced because it's derived from the actual time signal. And then you can turn your quarter-note phasor into a pulse train with el.le(phasor, 0.5)