summaryrefslogtreecommitdiff
path: root/app/client/audio/lib/spectrum.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/client/audio/lib/spectrum.js')
-rw-r--r--app/client/audio/lib/spectrum.js278
1 files changed, 278 insertions, 0 deletions
diff --git a/app/client/audio/lib/spectrum.js b/app/client/audio/lib/spectrum.js
new file mode 100644
index 0000000..f4a5444
--- /dev/null
+++ b/app/client/audio/lib/spectrum.js
@@ -0,0 +1,278 @@
+import Tone from 'tone'
+
+import { shuffle, quantize, mod } from '../util'
+
+import { windows as signalWindows } from 'signal-windows'
+import FFTJS from 'fft.js'
+
+const fft_size = 512
+const fft_overlap = fft_size / 4
+
+const fft = new FFTJS(fft_size)
+
+function toSpectrum(pcm, sr){
+ sr = sr || 44100
+ const ham = signalWindows.construct('ham', fft_size)
+ const pcm_in = new Array(fft_size)
+ const pcm_length = pcm.length
+ const pcm_q_length = Math.ceil(pcm_length / fft_size) * fft_size
+ let i, j, fft_out, data = [];
+ for (i = -fft_size; i < pcm_q_length; i += fft_overlap) {
+ for (j = 0; j < fft_size; j++) {
+ pcm_in[j] = pcm[i+j] * ham[j] || 0
+ }
+ fft_out = fft.createComplexArray()
+ fft.realTransform(fft_out, pcm_in)
+ fft.completeSpectrum(fft_out)
+ data.push(fft_out)
+ }
+ return {
+ data,
+ sr,
+ fft_size,
+ fft_overlap,
+ }
+}
+
+function fromSpectrum(spec){
+ const data = spec.data
+ const sr = spec.sr
+ const fft_size = spec.fft_size
+ const fft_overlap = spec.fft_overlap
+ const spec_len = data.length
+
+ const ham = signalWindows.construct('ham', fft_size)
+ const out = fft.createComplexArray()
+ const pcm_length = fft_overlap * spec_len
+
+ const audioBuffer = Tone.context.createBuffer(1, pcm_length, sr)
+ const pcm = audioBuffer.getChannelData(0);
+
+ let i, j, u, col
+
+ for (i = 0; i < spec_len; i++) {
+ col = data[i]
+ // for (j = fft_size; j < fft_size << 1; j++) {
+ // col[j] = 0
+ // }
+ // if (i == 0) console.log(col)
+ fft.inverseTransform(out, col)
+ u = i * (fft_overlap)
+ for (j = 0; j < fft_size; j++) {
+ pcm[u+j] += out[j*2] * ham[j] || 0
+ }
+ }
+
+ fadeInOut(pcm, fft_size)
+ // console.log(pcm)
+ return audioBuffer
+}
+
+function fromImageData(imageData, sr, _r, _i) {
+ const pixels = imageData.data
+ const w = imageData.width
+ const h = imageData.height
+ let data = new Array(w)
+ let x, y, u, v, v2
+ for (y = 0; y < h; y++) {
+ let col = data[y] = new Float32Array(h * 4)
+ for (x = 0; x < w; x++) {
+ u = (x * (w) + y) * 4
+ v = x * 2
+ col[v] = (pixels[u] / 255 - 0.5) * _r
+ col[v+1] = (pixels[u+1] / 255 - 0.5) * _i
+ v2 = (h-y + h) * 2
+ col[v2] = col[v]
+ col[v2+1] = 0 // col[v+1]
+ }
+ col[h*2] = col[h*2+1] = col[h*2-1] = col[h*2-2] = 0
+ }
+ const spec = {
+ data,
+ sr,
+ fft_size, fft_overlap
+ }
+ return spec
+}
+
+function binToHz(spec, i){
+ return (i / spec.fft_size) * spec.sr
+}
+
+function fadeInOut(pcm, fade_size){
+ const pcm_length = pcm.length
+ let fade = 0, i
+ for (i = 0; i < fade_size; i++) {
+ fade = i / (fade_size)
+ fade *= fade
+ pcm[i] *= fade
+ pcm[pcm_length - i] *= fade
+ }
+}
+function rotatePhase(spec, theta){
+ let { data, fft_size } = spec
+ let i, j, col, len = data.length
+ for (i = 0; i < len; i++) {
+ col = data[i]
+ for (j = 0; j < fft_size; j++) {
+ col[j*2+1] += theta
+ }
+ }
+ return spec
+}
+
+function linearBins(spec, n){
+ n = n || 1
+
+ let bins = [], i, q_i
+ for (q_i = 0; q_i < n; q_i++) {
+ bins[q_i] = []
+ }
+ const step = Math.floor(spec.fft_size / n)
+ const len_quantize_n = quantize(spec.fft_size, n)
+ for (i = 0; i < len_quantize_n; i++) {
+ q_i = Math.floor(i/step)
+ bins[q_i] = bins[q_i] || []
+ bins[q_i].push(i)
+ }
+ // leftover bins get put at end
+ for (; i < spec.fft_size; i++) {
+ bins[q_i].push(i)
+ }
+ return bins
+}
+function logarithmicBins(spec){
+ let bins = [], i, j, q_i
+ let binCount = Math.log2(spec.fft_size) - 1
+ for (i = 0, q_i = 0, j = 0; i < binCount; i++) {
+ j += 1 << i
+ bins[i] = []
+ for (; q_i < j; q_i++) {
+ bins[i].push(q_i)
+ }
+ }
+ return bins
+}
+function concatBins(bins){
+ return bins.reduce((acc, cv) => acc.concat(cv), [])
+}
+function reverseBins(bins){
+ return bins.map( bin => bin.reverse() )
+}
+function minBins(bins){
+ return bins.map( bin => {
+ const b = bin[0]
+ return bin.map(() => b)
+ })
+}
+function maxBins(bins){
+ return bins.map( bin => {
+ const b = bin[bin.length-1]
+ return bin.map(() => b)
+ })
+}
+function rotateSpectrum(spec, n){
+ const { fft_size } = spec
+ if (n && n < 1) {
+ n -= 0.5
+ n *= fft_size
+ }
+ n = Math.floor(n)
+ let order = new Array(fft_size), i
+ for (i = 0; i < fft_size; i++) {
+ order[i] = mod(i + n, fft_size/2)
+ }
+ return reorderBins(spec, order)
+}
+function cloneSpectrum(spec){
+ const {
+ data,
+ fft_size,
+ sr, fft_overlap
+ } = spec
+ const spec_len = data.length
+
+ let new_data = new Array(spec_len)
+ let i
+ for (i = 0; i < spec_len; i++) {
+ new_data[i] = data[i].concat()
+ new_data[i][2] = 0
+ }
+
+ return {
+ data: new_data,
+ fft_size,
+ sr, fft_overlap,
+ }
+}
+function reverseSpectrum(spec){
+ let new_spec = cloneSpectrum(spec)
+ new_spec.data = new_spec.data.reverse()
+ return new_spec
+}
+function shuffleSpectrum(spec){
+ const { fft_size } = spec
+ let order = new Array(fft_size), i
+ for (i = 0; i < fft_size; i++) {
+ order[i] = i
+ }
+ shuffle(order)
+ return reorderBins(spec, order)
+}
+function invertSpectrum(spec){
+ const { fft_size } = spec
+ let order = new Array(fft_size), i
+ for (i = 0; i < fft_size; i++) {
+ order[i] = fft_size - i - 1
+ }
+ return reorderBins(spec, order)
+}
+function reorderBins(spec, order){
+ let new_spec = cloneSpectrum(spec)
+ const {
+ data,
+ sr,
+ fft_size,
+ fft_overlap,
+ } = spec
+ const spec_len = data.length
+ const { data: new_data } = new_spec
+
+ let i, j, col, new_col
+ for (j = order.length; j < fft_size; j++) {
+ order[j] = j
+ }
+
+ for (i = 0; i < spec_len; i++) {
+ col = data[i]
+ new_col = new_data[i] = data[i].concat()
+ col[0] = 0
+ col[2] = 0
+ col[4] = 0
+ for (j = 0; j < fft_size/2; j++) {
+ new_col[j*2] = col[order[j]*2]
+ new_col[j*2+1] = col[order[j]*2+1]
+ }
+ for (; j < fft_size; j++) {
+ new_col[j*2] = 0
+ new_col[j*2+1] = 0
+ }
+ }
+
+ return {
+ data: new_data,
+ sr, fft_size, fft_overlap,
+ }
+}
+
+export default {
+ toSpectrum, fromSpectrum, fromImageData, binToHz,
+ fadeInOut,
+ cloneSpectrum,
+ reverseSpectrum, shuffleSpectrum, invertSpectrum, rotateSpectrum,
+ reorderBins,
+ linearBins, logarithmicBins,
+ concatBins,
+ reverseBins, minBins, maxBins,
+ rotatePhase,
+}