view src/app/spectrogram/Spectrogram.ts @ 456:7bb0bac6f8dc

Add export button for recordings and option to remove audio item (also removes all related analyses atm). Revokes associated object url for audio on removal. Will be problematic if the history is used for undo / redo.
author Lucas Thompson <dev@lucas.im>
date Thu, 29 Jun 2017 20:11:14 +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);
  }
}