draw vizualizations/colour changes based on the shape of an oscillator wave in a tone.js react project?

485 Views Asked by At

I'd like to animate/change colour based on the current section of an oscillator wave, to create a pulsing colour effect that matches the wave? The waves can be any of the types e.g. sine, triangle etc. so this would create different pulsing depending on the wave type and the frequency can be changed for the oscillator too (which I would then like to also change the pulsing timings).

Presumably I need to get the amplitude and wavelength (are these even the right terms for what i need?) from the oscillator object and use it in correlation with the Draw object while using some css animation but I am a bit stuck as to where to start? Specifically around the tone.js part of how I would get the values needed and how I would morph from colour A to B and back etc over time. Would I need to include an external library like p5 or can I do this all through tone.js with draw alone?

cheers david

EDIT - @paulwheeler - Thanks so much for all your comments, that is a big help. To be clearer, and to answer your queries, here is what I have and need, should you have any more input -

i) I have two oscillators continuously playing from Tone.js. They are 'playing' at frequency A and B, to create a binaural sound. The difference between the two i will call the range. So for example, Osc1 might be 100hz, osc2 might be 104hz and the range is 4hz. These oscillators can be of any type allowed by tone.js (sawtooth, sine, triangle..)

ii) When the sound is playing i want to take the range freqency and attach it to two colours. So at the peak of the frequency colour A will be showing as a background colour and at the trough of the frequency colour B will be showing. Inbetween those times the colour will be morphing between the two i.e. at any time t the colour will be a representation of the Y position on the wave, that is reflective of the distance from 0. This will then make it appear that the colour is changing to match the shape of the waves. kind of like here in css (but just using keyframes alone in this example and just to give an example of what i mean visually) -

https://jsfiddle.net/CF3Np/4/

@keyframes animation {
0%     {background-color:red;}
50.0%  {background-color:green;}
100.0%  {background-color:red;}
}
1

There are 1 best solutions below

0
On

From a cursory scan of the tone.js documentation it looks like it has everything you could want to synthesize and analyze sounds. However it doesn't appear have any facility for graphics. As far as drawing graphics goes, p5.js is certainly one option. However, the reference to "css animation" is a bit out of place here, because libraries like p5.js aren't really designed with css styling in mind. Instead you would be doing all of the animation yourself with calls to drawing functions for each frame.

The thing you are looking for when it comes to analyzing sounds in order to process them as data, and potentially visualize them, is a Fast Fourier Transform. This is an algorithm that decomposes a sample of a sound wave into separate sine wave components, and you can use the amplitudes of those sine waves to measure the sound in terms of component frequencies. Both p5.js (via the p5.sound add-in) and tone.js have FFT classes. If you're already using tone.js you'll probably want to stick with that for analyzing sounds, but you can certainly create visualizations for them using p5.js. Just as a conceptual example, here's a pure p5.js example:

const notes = [{
    name: 'c',
    freq: 261.6
  },
  {
    name: 'c#',
    freq: 277.2,
    sharp: true
  },
  {
    name: 'd',
    freq: 293.7
  },
  {
    name: 'd#',
    freq: 311.1,
    sharp: true
  },
  {
    name: 'e',
    freq: 329.6
  },
  {
    name: 'f',
    freq: 349.2
  },
  {
    name: 'f#',
    freq: 370.0,
    sharp: true
  },
  {
    name: 'g',
    freq: 392.0
  },
  {
    name: 'g#',
    freq: 415.3,
    sharp: true
  },
  {
    name: 'a',
    freq: 440.0
  },
  {
    name: 'a#',
    freq: 466.2,
    sharp: true
  },
  {
    name: 'b',
    freq: 493.9
  },
];

let playing = {};
let fft;
let bg;

function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB);
  bg = color('lightgray');

  for (let note of notes) {
    note.osc = new p5.Oscillator();
    note.osc.freq(note.freq);
    note.osc.amp(0);
  }

  fft = new p5.FFT();
}

function toggleNote(name) {
  let note = notes.filter(n => n.name === name)[0];
  if (playing[name] === undefined) {
    // First play
    note.osc.start();
  }
  if (playing[name] = !playing[name]) {
    // fade in a little
    note.osc.amp(0.2, 0.2);
  } else {
    // fade out a little
    note.osc.amp(0, 0.4);
  }
}

function playNote(name) {
  let note = notes.filter(n => n.name === name)[0];
  if (playing[name] === undefined) {
    // First play
    note.osc.start();
  }
  playing[name] = true;
  note.osc.amp(0.2, 0.2);
}

function releaseNote(name) {
  let note = notes.filter(n => n.name === name)[0];
  playing[name] = false;
  note.osc.amp(0, 0.4);
}

function draw() {
  background(bg);
  let w = width / 3;
  let h = min(height, w * 0.8);
  drawSpectrumGraph(w, 0, w, h);
  drawWaveformGraph(w * 2, 0, w, h);
  drawKeyboard();
  bg = color((fft.getCentroid() * 1.379) % 360, 30, 50);
}

function drawSpectrumGraph(left, top, w, h) {
  let spectrum = fft.analyze();

  stroke('limegreen');
  fill('darkgreen');
  strokeWeight(1);

  beginShape();
  vertex(left, top + h);

  for (let i = 0; i < spectrum.length; i++) {
    vertex(
      left + map(log(i), 0, log(spectrum.length), 0, w),
      top + map(spectrum[i], 0, 255, h, 0)
    );
  }

  vertex(left + w, top + h);
  endShape(CLOSE);
}

function drawWaveformGraph(left, top, w, h) {
  let waveform = fft.waveform();

  stroke('limegreen');
  noFill();
  strokeWeight(1);

  beginShape();

  for (let i = 0; i < waveform.length; i++) {
    let x = map(i * 5, 0, waveform.length, 0, w);
    let y = map(waveform[i], -1, 2, h / 10 * 8, 0);
    vertex(left + x, top + y);
  }

  endShape();
}

function drawKeyboard() {
  let w = width / 3;
  let h = min(height, w * 0.8);
  let x = 1;
  let keyWidth = (w - 8) / 7;
  let sharpWidth = keyWidth * 0.8;
  noStroke();
  let sharpKeys = [];
  for (let note of notes) {
    fill(playing[note.name] ? 'beige' : 'ivory');
    if (note.sharp) {
      sharpKeys.push({
        fill: playing[note.name] ? 'black' : 'dimgray',
        rect: [x - sharpWidth / 2, 0, sharpWidth, h / 2, 0, 0, 4, 4]
      });
    } else {
      rect(x, 0, keyWidth, h - 1, 0, 0, 4, 4);
      x += keyWidth + 1;
    }
  }
  for (let key of sharpKeys) {
    fill(key.fill);
    rect(...key.rect);
  }
}

let keymap = {
  'z': 'c',
  's': 'c#',
  'x': 'd',
  'd': 'd#',
  'c': 'e',
  'v': 'f',
  'g': 'f#',
  'b': 'g',
  'h': 'g#',
  'n': 'a',
  'j': 'a#',
  'm': 'b',
}

function keyPressed(e) {
  let note = keymap[e.key];
  if (note) {
    playNote(note);
  }
}

function keyReleased(e) {
  let note = keymap[e.key];
  if (note) {
    releaseNote(note);
  }
}

function mouseClicked() {
  if (mouseX < width / 3) {
    let w = width / 3;
    let h = w * 0.8;
    let x = 1;
    let keyWidth = (w - 8) / 7;
    let sharpWidth = keyWidth * 0.8;
    let naturalKeys = [];
    let sharpKeys = [];
    for (let note of notes) {
      if (note.sharp) {
        sharpKeys.push({
          name: note.name,
          bounds: {
            left: x - sharpWidth / 2,
            top: 0,
            right: x - sharpWidth / 2 + sharpWidth,
            bottom: h / 2
          }
        });
      } else {
        naturalKeys.push({
          name: note.name,
          bounds: {
            left: x,
            top: 0,
            right: x + keyWidth,
            bottom: h - 1
          }
        });
        x += keyWidth + 1;
      }
    }

    for (let {
        bounds,
        name
      } of sharpKeys.concat(naturalKeys)) {
      if (mouseX > bounds.left && mouseX < bounds.right &&
        mouseY > bounds.top && mouseY < bounds.bottom) {
        toggleNote(name);
        break;
      }
    }
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>