在tone.js react项目中根据振荡器波形的形状绘制可视化/颜色变化?

问题描述

我想根据振荡器波形的当前部分设置动画/更改颜色,以创建与波形匹配的脉冲颜色效果?波浪可以是任何类型,例如正弦、三角等,因此这会根据波形类型产生不同的脉冲,并且振荡器的频率也可以改变(然后我想也改变脉冲时序)。

大概我需要从振荡器对象中获取振幅和波长(这些甚至是我需要的正确术语吗?),并在使用一些 css 动画时将其与 Draw 对象相关联使用,但我有点卡住了从哪里开始?特别是在tone.js 部分,我将如何获得所需的值,以及我将如何随着时间的推移从颜色 A 变为 B 并返回等。我是否需要包含一个像 p5 这样的外部库,或者我可以通过tone.js 单独使用draw 来完成这一切吗?

干杯 大卫

编辑 - @paulwheeler - 非常感谢您的所有评论,这是一个很大的帮助。为了更清楚,并回答您的疑问,这是我所拥有和需要的,如果您有更多输入 -

i) 我有两个振荡器从 Tone.js 连续播放。它们在频率 A 和 B 下“演奏”,以产生双耳声音。两者之间的差异我将称之为范围。例如,Osc1 可能是 100hz,osc2 可能是 104hz,范围是 4hz。这些振荡器可以是tone.js允许的任何类型(锯齿、正弦、三角形..)

ii) 播放声音时,我想获取范围频率并将其附加到两种颜色。因此,在频率的峰值处,颜色 A 将显示为背景颜色,而在频率的低谷处,颜色 B 将显示为背景色。在这些时间之间,颜色将在两者之间变形,即在任何时间 t 颜色将是波浪上 Y 位置的表示,即反射距 0 的距离。这将使它看起来颜色正在变化以匹配波浪的形状。有点像这里的 css(但在这个例子中只使用关键帧,只是为了举例说明我的视觉意思)-

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

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

解决方法

tone.js documentation 的粗略扫描来看,它似乎拥有您想要合成和分析声音的一切。但是,它似乎没有任何图形功能。就绘制图形而言,p5.js 当然是一种选择。然而,这里对“css 动画”的引用有点不合适,因为像 p5.js 这样的库在设计时并没有真正考虑 css 样式。相反,您将自己完成所有动画,并为每一帧调用绘图函数。

在分析声音以便将它们作为数据进行处理并可能将它们可视化时,您正在寻找的东西是 Fast Fourier Transform。这是一种将声波样本分解为单独的正弦波分量的算法,您可以使用这些正弦波的幅度根据分量频率来测量声音。 p5.js(通过 p5.sound 插件)和 tone.js 都有 FFT 类。如果您已经在使用tone.js,您可能希望坚持使用它来分析声音,但您当然可以使用p5.js 为它们创建可视化。作为一个概念示例,这里是一个纯 p5.js 示例:

const notes = [{
    name: 'c',freq: 261.6
  },{
    name: 'c#',freq: 277.2,sharp: true
  },{
    name: 'd',freq: 293.7
  },{
    name: 'd#',freq: 311.1,{
    name: 'e',freq: 329.6
  },{
    name: 'f',freq: 349.2
  },{
    name: 'f#',freq: 370.0,{
    name: 'g',freq: 392.0
  },{
    name: 'g#',freq: 415.3,{
    name: 'a',freq: 440.0
  },{
    name: 'a#',freq: 466.2,{
    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,w,h);
  drawWaveformGraph(w * 2,h);
  drawKeyboard();
  bg = color((fft.getCentroid() * 1.379) % 360,30,50);
}

function drawSpectrumGraph(left,top,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),log(spectrum.length),w),top + map(spectrum[i],255,h,0)
    );
  }

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

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

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

  beginShape();

  for (let i = 0; i < waveform.length; i++) {
    let x = map(i * 5,waveform.length,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,sharpWidth,h / 2,4,4]
      });
    } else {
      rect(x,keyWidth,h - 1,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,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>