diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2018-03-12 01:42:41 +0100 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2018-03-12 01:42:41 +0100 |
| commit | 6a83c9b84f49d541f39726712b8a0331590555d2 (patch) | |
| tree | 51f5f29e35fa3af1f483ce8b1bb9829d281c13f0 /client | |
| parent | 48f66e675f632d4ae837b3f8474569b3cfbceb56 (diff) | |
basic linear spectral manipulation!
Diffstat (limited to 'client')
| -rw-r--r-- | client/draw.js | 2 | ||||
| -rw-r--r-- | client/index.js | 184 | ||||
| -rw-r--r-- | client/lib/hall.js | 4 | ||||
| -rw-r--r-- | client/lib/mouse.js | 11 | ||||
| -rw-r--r-- | client/lib/sampler.js | 51 | ||||
| -rw-r--r-- | client/lib/spectrum.js | 184 | ||||
| -rw-r--r-- | client/lib/util.js | 3 |
7 files changed, 307 insertions, 132 deletions
diff --git a/client/draw.js b/client/draw.js index bed5f80..ef66e22 100644 --- a/client/draw.js +++ b/client/draw.js @@ -54,7 +54,7 @@ function triangle(px,py,r){ function tri(px, py, r) { ctx.save() ctx.globalCompositeOperation = 'multiply' - ctx.fillStyle = color.color((px+py)/(w+h), 0, 1, 1) + ctx.fillStyle = color.color((px+py)/(w+h), 0, 1, 0.2) function p(){ let theta = randrange(0, Math.PI*2) let x = px + Math.cos(theta) * r diff --git a/client/index.js b/client/index.js index e0f44bd..5ad002e 100644 --- a/client/index.js +++ b/client/index.js @@ -4,7 +4,6 @@ import Sampler from './lib/sampler' import draw from './draw' import keys from './lib/keys' -import color from './lib/color' import mouse from './lib/mouse' import output from './lib/output' import spectrum from './lib/spectrum' @@ -12,34 +11,27 @@ import spectrum from './lib/spectrum' import { Hall } from './lib/hall' import { - browser, requestAudioContext, - randint, randrange, choice, clamp, lerp, dist, shuffle, - isMobile, + requestAudioContext, + lerp, } from './lib/util' -const root = 440 -const s = 50 -const w = window.innerWidth -const h = window.innerHeight -const ws = w/s, hs = h/s +// const root = 440 const HALLWAY_LENGTH = 147 -const SPEAKER_COUNT = 16 +// const SPEAKER_COUNT = 16 -let notes = [299, 336, 374, 399, 449, 498, 561, 598].map(i => i/2) -notes = notes.concat(notes.map(i => i/2)) -notes = notes.concat(notes.map(i => i*2)) -notes = shuffle(notes) +// let notes = [299, 336, 374, 399, 449, 498, 561, 598].map(i => i/2) +// notes = notes.concat(notes.map(i => i/2)) let samplers = {} let sampler requestAudioContext( () => { - // samplers.smash = new Sampler('samples/smash/g{}.mp3', 12) - samplers.earth = new Sampler('samples/earth/earth{}.mp3', 20) - // samplers.glass = new Sampler('samples/glass/0{}Particle.mp3', 20) - // samplers.kalimba = new Sampler('samples/kalimba/380731__cabled-mess__sansula-08-c-raw.wav', 10) - sampler = samplers.earth + sampler = samplers.smash = new Sampler('samples/smash/g{}.mp3', 12) + // sampler = samplers.earth = new Sampler('samples/earth/earth{}.mp3', 20) + // sampler = samplers.glass = new Sampler('samples/glass/0{}Particle.mp3', 20) + // sampler = samplers.bong = new Sampler('samples/bong/bong{}.mp3', 10) + // sampler = samplers.kalimba = new Sampler('samples/kalimba/380731__cabled-mess__sansula-08-c-raw.wav', 10) samplers.choice = (m,n) => { const r = Math.random() if (r < m) return samplers.smash @@ -52,111 +44,103 @@ requestAudioContext( () => { }) }) - -const hall = new Hall ({ - length: HALLWAY_LENGTH, - speakers: SPEAKER_COUNT, -}) +// const hall = new Hall ({ +// length: HALLWAY_LENGTH, +// speakers: SPEAKER_COUNT, +// }) function redraw(){ draw.clear() } -function manipulate(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 +// let last_lin_bins, last_sam +let timeout +function manipulate(x, y, pcm, spec){ + if (timeout) return null + timeout = setTimeout( () => { timeout = null }, 20 ) - let i, j, u, v, _r, _i + // reverseSpectrum, + // shuffleSpectrum, + // invertSpectrum + // const pcm_rev = pcm.slice().reverse() + // const spec_rev = spectrum.toSpectrum(pcm_rev, spec.sr) + // return spec_rev + // return spectrum.reverseSpectrum(spec) + // const log_bins = spectrum.logarithmicBins(spec) - let aa = [] - for (i = 0; i < fft_size; i++) { - aa[i] = i - } - shuffle(aa) + // let lin_bins = (x * x * x * spec.fft_size/6)|0 + // let sam = (y * sampler.length)|0 + 1 + // if (lin_bins == last_lin_bins && sam == last_sam) return null + // last_lin_bins = lin_bins + // last_sam = sam + // const log_bins = spectrum.linearBins(spec, lin_bins) + // const new_bins = spectrum.reverseBins(log_bins) + // const concat_bins = spectrum.concatBins(new_bins) + // const new_spec = spectrum.reorderBins(spec, concat_bins) + // console.log(spec, new_spec) - let new_data = [], new_col, col - let bands = 2 << 4 - let band, band_index - let band_size = Math.floor(fft_size / bands) - for (i = 0; i < spec_len; i++) { - col = data[i] - new_col = new_data[i] = data[i].concat() - data[i][2] = 0 - for (j = 0; j < fft_size; j++) { - band = Math.floor(j / band_size) * band_size - band_index = j % band_size + // let new_spec = spectrum.cloneSpectrum(spec) + // new_spec = spectrum.rotatePhase(new_spec, x * Math.PI) - new_col[j] = col[ band + (band_size - band_index) ] + let new_spec = spectrum.rotateSpectrum(spec, x) - // spectrum inversion - // new_col[j] = data[i][ fft_size - j - 1] - - // erase mirrored half of fft - new_col[j + fft_size] = 0 - } - new_col[2] = 0 - } - - spec.data = new_data //.reverse() - // col = data[i] - // for (j = 0; j < fft_size; j++) { - // _r = j*2 - // _i = j*2+1 - // col[_r] = col[_r] / 2 - // col[_i] = col[_i] - // } - // } + return new_spec } -keys.listen(index => { - // trigger(Math.random(), ((index+7) % SPEAKER_COUNT) / SPEAKER_COUNT, 0, samplers.smash) - const sample = sampler.play(100, Tone.now(), output) - const buf = sample._buffer.get() - if (! buf) return - const pcm = buf.getChannelData(0) - const sr = buf.sampleRate - const duration = buf.duration - const spec = spectrum.toSpectrum(pcm, sr) - - draw.clear() - draw.waveform(pcm) - draw.spectrum(spec, 0, window.innerHeight/4 + 20) - - manipulate(spec) - - const audioBuffer = spectrum.fromSpectrum(spec) - const player = new Tone.Player(audioBuffer) - player.connect(output) - player.start(Tone.now() + pcm.length / sr) - - // const new_spec = spectrum.toSpectrum(audioBuffer.getChannelData(0), sr) - draw.spectrum(spec, 0, window.innerHeight * 1/2 + 40) +keys.listen(i => { + trigger(xx + i/50, yy, 0, sampler) }) +let xx = 0.5, yy = 0.5 mouse.register({ down: (x, y) => { redraw() + clearTimeout(timeout) + timeout = null + trigger(x, y, 0, sampler) }, - move: (x, y, dx, dy) => { + move: (x, y) => { + xx = x + yy = y }, - up: (x, y) => { + drag: (x, y) => { + trigger(x, y, 0, sampler) }, + // up: (x, y) => { + // }, }) -let timeout, px = 0, py = 0 -function play(x, y){ -} -function trigger(x, y, t, sampler){ +function trigger(x, y, t, source){ + x = x || 0 + y = y || 0 t = t || 0 t += Tone.now() - sampler = sampler || last_dist > 40 - ? samplers.choice(0.2, 0.2) - : samplers.choice((1-y) * 0.2, y*0.02) - const freq = notes[Math.floor(x * notes.length)] - const speaker = hall.play(sampler, y, freq, x, t) + source = source || sampler + // source = source || last_dist > 40 + // ? samplers.choice(0.2, 0.2) + // : samplers.choice((1-y) * 0.2, y*0.02) + // const freq = notes[Math.floor(x * notes.length)] + // const { speaker, player } = hall.play(source, y, freq, x, t) + + const { pcm, spec } = source.getWaveAndSpectrum(y) + if (! pcm) return + + const new_spec = manipulate(x, y, pcm, spec) + if (! new_spec) return + + draw.clear() + // draw.waveform(pcm) + draw.spectrum(spec, 0, window.innerHeight/4 + 20) + + const audioBuffer = spectrum.fromSpectrum(new_spec) + draw.waveform(audioBuffer.getChannelData(0)) + + const player = new Tone.Player(audioBuffer) + player.connect(output) + player.start(Tone.now()) + + // const new_spec = spectrum.toSpectrum(audioBuffer.getChannelData(0), sr) + draw.spectrum(new_spec, 0, window.innerHeight * 1/2 + 40) draw.triangle( lerp(x, 0, 1) * window.innerWidth, diff --git a/client/lib/hall.js b/client/lib/hall.js index e07db80..3fe390f 100644 --- a/client/lib/hall.js +++ b/client/lib/hall.js @@ -37,8 +37,8 @@ class Hall { } play(sound, i, freq, pan, time){ const speaker = this.getSpeaker(i) - sound.play(freq, time || 0, pan < 0.5 ? speaker.panLeft : speaker.panRight) - return speaker + const player = sound.play(freq, time || 0, pan < 0.5 ? speaker.panLeft : speaker.panRight) + return { speaker, player } } } diff --git a/client/lib/mouse.js b/client/lib/mouse.js index 990b8ef..809f209 100644 --- a/client/lib/mouse.js +++ b/client/lib/mouse.js @@ -15,6 +15,7 @@ mouse.register({ let fns = { down: [], move: [], + drag: [], up: [], } @@ -27,6 +28,7 @@ export default { register: (callbacks) => { callbacks.down && fns.down.push(callbacks.down) callbacks.move && fns.move.push(callbacks.move) + callbacks.drag && fns.drag.push(callbacks.drag) callbacks.up && fns.up.push(callbacks.up) } } @@ -36,17 +38,20 @@ let x, y, dragging = false function down(e){ x = e.pageX y = e.pageY + x /= window.innerWidth + y /= window.innerHeight dragging = true fns.down.map(f => f(x, y)) } function move(e){ - if (!dragging) return let dx = e.pageX - x let dy = e.pageY - y x = e.pageX y = e.pageY - dragging = true + x /= window.innerWidth + y /= window.innerHeight fns.move.map(f => f(x, y, dx, dy)) + dragging && fns.drag.map(f => f(x, y, dx, dy)) } function up(e){ dragging = false @@ -61,7 +66,7 @@ function touch(f){ } document.body.addEventListener("mousedown", down) document.body.addEventListener("mousemove", move) -document.body.addEventListener("mouseup", up) +window.addEventListener("mouseup", up) document.body.addEventListener("touchstart", touch(down)) document.body.addEventListener("touchmove", touch(move)) document.body.addEventListener("touchup", touch(up)) diff --git a/client/lib/sampler.js b/client/lib/sampler.js index 481a940..9a1b1fc 100644 --- a/client/lib/sampler.js +++ b/client/lib/sampler.js @@ -1,21 +1,20 @@ import Tone from 'tone' -import { lerp, choice } from './util' +import { choice, clamp } from './util' +import spectrum from './spectrum' -const player_count = 2 -const filter_count = 3 - -const crossfaders = [] +const player_count = 1 export default class Sampler { constructor(path, count){ this.samples = (() => { let s = '', a = [] for (let i = 1; i < count; i++) { - const s = i < 10 ? '0' + i : i; + s = i < 10 ? '0' + i : i; a.push({ root: 100, fn: path.replace(/{}/, s) }) } return a })() + this.length = this.samples.length this.samples.forEach((sample) => { sample.players = [] @@ -38,24 +37,52 @@ export default class Sampler { return choice(this.samples) } play(freq, time, output) { - const best = this.choice() - best.index = (best.index + 1) % player_count - - const player = best.players[ best.index ] + const { player, best } = this.getPlayer() freq = freq || best.root + player.playbackRate = freq / best.root + time = time || Tone.now() - player.playbackRate = freq / best.root if (player.loaded) { player.stop() player.disconnect() player.connect(output) player.start(time) } else { - console.log('loading') + // console.log('loading') } return player } + getRandomPlayer(){ + const best = this.choice() + best.index = (best.index + 1) % player_count + + const player = best.players[ best.index ] + return { player, best } + } + getPlayer(n){ + n = n || 0 + if (n < 1) n *= this.samples.length + const best = this.samples[clamp(n|0, 0, this.samples.length-1)] + best.index = (best.index + 1) % player_count + + const player = best.players[ best.index ] + return { player, best } + } + getWaveAndSpectrum(n){ + const { player } = this.getPlayer(n) + const buf = player._buffer.get() + if (! buf) return { pcm: null, spec: null } + const pcm = buf.getChannelData(0) + if (! player._spectrum) { + const sr = buf.sampleRate + player._spectrum = spectrum.toSpectrum(pcm, sr) + } + return { + pcm: pcm, + spec: player._spectrum, + } + } } diff --git a/client/lib/spectrum.js b/client/lib/spectrum.js index 2b30792..1dd9619 100644 --- a/client/lib/spectrum.js +++ b/client/lib/spectrum.js @@ -1,13 +1,12 @@ import Tone from 'tone' -import output from './output' +import { shuffle, quantize, mod } from './util' -const signalWindows = require('signal-windows').windows -const FFTJS = require('fft.js') +import { windows as signalWindows } from 'signal-windows' +import FFTJS from 'fft.js' -const fft_size = 2 << 10 +const fft_size = 1024 const fft_overlap = fft_size / 4 -const half_fft_size = fft_size / 2 const fft = new FFTJS(fft_size) @@ -49,15 +48,15 @@ function fromSpectrum(spec){ const audioBuffer = Tone.context.createBuffer(1, pcm_length, sr) const pcm = audioBuffer.getChannelData(0); - let i, j, u, v, _r, _i, col + 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 + // } fft.inverseTransform(out, col) u = i * (fft_overlap) - // for (j = fft_size * 1/4; j < fft_size * 3/4; j++) { - // pcm[u+j] = out[j*2] / ham[j] || 0 - // } for (j = 0; j < fft_size; j++) { pcm[u+j] += out[j*2] * ham[j] || 0 } @@ -78,12 +77,171 @@ function fadeInOut(pcm, fade_size){ pcm[pcm_length - i] *= fade } } -function fadeOut(pcm){ - +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 + toSpectrum, fromSpectrum, + fadeInOut, + cloneSpectrum, + reverseSpectrum, shuffleSpectrum, invertSpectrum, rotateSpectrum, + reorderBins, + linearBins, logarithmicBins, + concatBins, + reverseBins, minBins, maxBins, + rotatePhase, } diff --git a/client/lib/util.js b/client/lib/util.js index fd48768..1bbee9d 100644 --- a/client/lib/util.js +++ b/client/lib/util.js @@ -21,6 +21,7 @@ function lerp(n,a,b){ return (b-a)*n+a } function angle(x0,y0,x1,y1){ return Math.atan2(y1-y0,x1-x0) } function dist(x0,y0,x1,y1){ return Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2)) } function xor(a,b){ a=!!a; b=!!b; return (a||b) && !(a&&b) } +function quantize(a,b){ return Math.floor(a/b)*b } function shuffle(a){ for (var i = a.length; i > 0; i--){ var r = randint(i) @@ -104,7 +105,7 @@ function requestAudioContext (fn) { } export { - clamp, choice, mod, lerp, angle, dist, xor, + clamp, choice, mod, lerp, angle, dist, xor, quantize, randint, randrange, randsign, shuffle, gaussian, browser, requestAudioContext, } |
