view src/app/spectrogram/Spectrogram.ts @ 509:041468f553e1 tip master

Merge pull request #57 from LucasThompson/fix/session-stack-max-call-stack Fix accidental recursion in PersistentStack
author Lucas Thompson <LucasThompson@users.noreply.github.com>
date Mon, 27 Nov 2017 11:04:30 +0000
parents c39df81c4dae
children
line wrap: on
line source
/**
 * Created by lucast on 16/03/2017.
 */
import {RealFft, KissRealFft, hann} from 'piper-js/fft';
import {KissFft} from 'piper-js/fft/KissFftModule';
import {Framing} from 'piper-js/core';
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, KissFft);
    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);
  }
}