view src/app/spectrogram/Spectrogram.ts @ 394:f45a916eb5b1

Use the cross hair layer for notes, tracks and curve. This involved bodging in unit to ShapedFeatureData, which isn't particularly easy to do because this isn't an encapsulated type. Need to come back to improving this, as I am monkey-patching a unit property onto Arrays etc.
author Lucas Thompson <dev@lucas.im>
date Thu, 01 Jun 2017 18:55:55 +0100
parents 6a83df5029fe
children c39df81c4dae
line wrap: on
line source
/**
 * Created by lucast on 16/03/2017.
 */
import {RealFft, KissRealFft} from 'piper/fft/RealFft';
import {hann} from 'piper/FftUtilities';
import {Framing} from 'piper';
import Waves from 'waves-ui-piper';

class SpectrogramEntity extends Waves.utils.MatrixEntity {

  private samples: Float32Array;
  private sampleRate: number;
  private framing: Framing;
  private fft: RealFft;
  private real: Float32Array;
  private nCols: number;
  private columnHeight: number;
  private window: Float32Array;

  constructor(samples: Float32Array, options: Framing & Object, sampleRate: number) {
    super();
    this.samples = samples;
    this.sampleRate = sampleRate;
    this.framing = options;
    this.real = new Float32Array(this.framing.blockSize);
    this.nCols = Math.floor(this.samples.length / this.framing.stepSize); // !!! not correct
    this.columnHeight = Math.round(this.framing.blockSize / 2) + 1;
    this.fft = new KissRealFft(this.framing.blockSize);
    this.window = hann(this.framing.blockSize);
  }

  dispose(): void {
    this.fft.dispose();
  }

  getColumnCount(): number {
    return this.nCols;
  }

  getColumnHeight(): number {
    return this.columnHeight;
  }

  getStepDuration(): number {
    return this.framing.stepSize / this.sampleRate;
  }

  getColumn(n: number): Float32Array {

    const startSample = n * this.framing.stepSize;
    const sz = this.framing.blockSize;

    this.real.fill(0);

    let available = sz;
    if (startSample + sz >= this.samples.length) {
      available = this.samples.length - startSample;
    }

    for (let i = 0; i < available; ++i) {
      this.real[i] = this.samples[startSample + i] * this.window[i];
    }

    const complex = this.fft.forward(this.real);

    const h = this.getColumnHeight();
    const col = new Float32Array(h);

    const scale = 1.0 / Math.sqrt(sz);
    for (let i = 0; i < h; ++i) {
      const re: number = complex[i * 2] * scale;
      const im: number = complex[i * 2 + 1] * scale;
      col[i] = Math.sqrt(re * re + im * im);
    }

    return col;
  }
}

export class WavesSpectrogramLayer extends Waves.core.Layer {
  constructor(bufferIn: AudioBuffer,
              options: Framing & Object) {

    const defaults = {
      normalise: 'hybrid',
      gain: 40.0,
      channel: -1,
      stepSize: 512,
      blockSize: 1024
    };

    const mergedOptions: Framing & Object & {channel: number} =
      Object.assign({}, defaults, options);

    const getSamples = ((buffer, channel) => {
      const nch = buffer.numberOfChannels;
      if (channel >= 0 || nch === 1) {
        if (channel < 0) {
          channel = 0;
        }
        return buffer.getChannelData(channel);
      } else {
        const before = performance.now();
        console.log('mixing down ' + nch + ' channels for spectrogram...');
        const mixed = Float32Array.from(buffer.getChannelData(0));
        const n = mixed.length;
        for (let ch = 1; ch < nch; ++ch) {
          const buf = buffer.getChannelData(ch);
          for (let i = 0; i < n; ++i) {
            mixed[i] += buf[i];
          }
        }
        const scale = 1.0 / nch;
        for (let i = 0; i < n; ++i) {
          mixed[i] *= scale;
        }
        console.log('done in ' + (performance.now() - before) + 'ms');
        return mixed;
      }
    });

    super(
      'entity',
      new SpectrogramEntity(getSamples(bufferIn, mergedOptions.channel),
        mergedOptions,
        bufferIn.sampleRate),
      mergedOptions
    );

    this.configureShape(Waves.shapes.Matrix, {}, mergedOptions);
  }
}