Skip to content

Spike/colored waveforms #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
Expand All @@ -28,7 +28,7 @@
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"esversion": 6,
"unused": false,

/* do not expect assignment call */
Expand Down
11 changes: 6 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
language: node_js
node_js:
- "0.12"
- "4"

sudo: false

Expand All @@ -10,12 +10,13 @@ cache:
- node_modules

before_install:
- export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
- "npm config set spin false"
- "npm install -g npm@^2"
- npm config set spin false
- npm install -g bower
- bower --version
- npm install phantomjs-prebuilt
- node_modules/phantomjs-prebuilt/bin/phantomjs --version

install:
- npm install -g bower
- npm install
- bower install

Expand Down
2 changes: 1 addition & 1 deletion .watchmanconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"ignore_dirs": ["tmp"]
"ignore_dirs": ["tmp", "dist"]
}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ You will need the following things properly installed on your computer.
* [Git](http://git-scm.com/)
* [Node.js](http://nodejs.org/) (with NPM)
* [Bower](http://bower.io/)
* [Ember CLI](http://www.ember-cli.com/)
* [Ember CLI](http://ember-cli.com/)
* [PhantomJS](http://phantomjs.org/)

## Installation

* `git clone <repository-url>` this repository
* change into the new directory
* `cd linx`
* `npm install`
* `bower install`

## Running / Development

* `ember server`
* `ember serve`
* Visit your app at [http://localhost:4200](http://localhost:4200).

### Code Generators
Expand All @@ -46,7 +46,7 @@ Specify what it takes to deploy your app.
## Further Reading / Useful Links

* [ember.js](http://emberjs.com/)
* [ember-cli](http://www.ember-cli.com/)
* [ember-cli](http://ember-cli.com/)
* Development Browser Extensions
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
Expand Down
4 changes: 2 additions & 2 deletions app/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ember from 'ember';
import Resolver from 'ember/resolver';
import loadInitializers from 'ember/load-initializers';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';

//
Expand Down
41 changes: 25 additions & 16 deletions app/components/arrangement-visual/track-clip/wave.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import Ember from 'ember';

import d3 from 'd3';
Expand All @@ -8,7 +7,7 @@ import { join } from 'ember-cli-d3/utils/d3';
import multiply from 'linx/lib/computed/multiply';

export default Ember.Component.extend(
GraphicSupport('peaks.[]', 'waveColor', 'height'), {
new GraphicSupport('peaks.[]', 'waveColor', 'height'), {

// required params
peaks: null,
Expand All @@ -26,27 +25,37 @@ export default Ember.Component.extend(
this.drawWaveform(selection);
},

drawWaveform: join([0], 'path.TrackClipWave-waveform', {
drawWaveform: join('peaks', 'path.TrackClipWave-waveform', {
update(selection) {
const median = this.get('height') / 2.0;
const peaksLength = this.get('peaks.length');

const peaks = this.get('peaks');
const peaksLength = peaks.get('length');

const area = d3.svg.area()
.x((peak, i) => {
const percent = i / peaksLength;
const beat = percent * peaksLength;
return beat;
const freqScale = d3.scale.log()
.domain([50, 22050])
.range([0, 1])
.clamp(true);

selection
.style('fill', ([ ymin, ymax, dominantFreq ], i) => {
return (d3.hsl(freqScale(dominantFreq) * 360, 0.8, 0.5) + '') || this.get('waveColor');
})
.y0(([ ymin, ymax ]) => median + ymin * median)
.y1(([ ymin, ymax ]) => median + ymax * median);
.attr('d', (peak, i) => {
const prevPeak = peaks[i - 1];
const nextPeak = peaks[i + 1];

const localPeaks = [prevPeak, peak, nextPeak].without(undefined);

if (peaks.length) {
selection
.style('fill', this.get('waveColor'))
.attr('d', area(peaks));
}
const area = d3.svg.area()
// .x((peak, j) => (i + j) - (localPeaks.length / 2.0))
.x((peak, j) => (i + j))
.y0(([ ymin, ymax, dominantFreq ]) => median + ymin * median)
.y1(([ ymin, ymax, dominantFreq ]) => median + ymax * median);

return area(localPeaks);
})
}
}),
});

16 changes: 8 additions & 8 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">

{{content-for 'head'}}
{{content-for "head"}}

<link rel="stylesheet" href="assets/vendor.css">
<link rel="stylesheet" href="assets/linx.css">
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link rel="stylesheet" href="{{rootURL}}assets/linx.css">

{{content-for 'head-footer'}}
{{content-for "head-footer"}}
</head>
<body>
{{content-for 'body'}}
{{content-for "body"}}

<script src="assets/vendor.js"></script>
<script src="assets/linx.js"></script>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/linx.js"></script>

{{content-for 'body-footer'}}
{{content-for "body-footer"}}
</body>
</html>
92 changes: 69 additions & 23 deletions app/models/track/audio-binary.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global Parallel:true */
/* global DSPJS:true */
import Ember from 'ember';
import DS from 'ember-data';

Expand All @@ -10,9 +11,11 @@ import { isValidNumber, asResolvedPromise } from 'linx/lib/utils';
import ReadinessMixin from 'linx/mixins/readiness';
import Ajax from 'linx/lib/ajax';

const FFT_BUFFER_SIZE = 2048;

export default Ember.Object.extend(
ReadinessMixin('isArrayBufferLoadedAndDecoded'),
RequireAttributes('track'), {
new ReadinessMixin('isArrayBufferLoadedAndDecoded'),
new RequireAttributes('track'), {

file: Ember.computed.reads('track.file'),
isLoading: Ember.computed.or('arrayBuffer.isPending', 'decodedArrayBuffer.isPending'),
Expand Down Expand Up @@ -103,6 +106,7 @@ export default Ember.Object.extend(

// TODO(COMPUTEDPROMISE): use that?
decodedArrayBuffer: Ember.computed('audioContext', 'arrayBuffer', function() {
console.log('decodedArrayBuffer');
let { arrayBuffer, audioContext } = this.getProperties('arrayBuffer', 'audioContext');

if (arrayBuffer) {
Expand All @@ -121,11 +125,10 @@ export default Ember.Object.extend(

_peaksCache: Ember.computed(() => Ember.Map.create()),

// returns a promise of an array of arrays of [ymin, ymax] values of the waveform
// returns a promise of an array of arrays of [ymin, ymax, frequency] values of the waveform
// from startTime to endTime when broken into length subranges
// returns a promise
getPeaks({ startTime, endTime, length }) {
// Ember.Logger.log('AudioBinary.getPeaks', startTime, endTime, length);
Ember.Logger.log('AudioBinary.getPeaks', startTime, endTime, length);

const cacheKey = `startTime:${startTime},endTime:${endTime},length:${length}`;
const peaksCache = this.get('_peaksCache');
Expand All @@ -137,17 +140,18 @@ export default Ember.Object.extend(
}

const audioBuffer = this.get('audioBuffer');
if (!audioBuffer) { return asResolvedPromise([]); }
if (!audioBuffer || !isValidNumber(length)) { return asResolvedPromise([]); }

Ember.assert('Cannot AudioBinary.getPeaks without length', isValidNumber(length));
startTime = isValidNumber(startTime) ? startTime : 0;
endTime = isValidNumber(endTime) ? endTime : 0;

// TODO(REFACTOR): update to use multiple channels
const samples = audioBuffer.getChannelData(0);
const sampleRate = audioBuffer.sampleRate;
const startSample = startTime * sampleRate;
const endSample = endTime * sampleRate;
const startSample = ~~(startTime * sampleRate);
const endSample = ~~(endTime * sampleRate);



const job = new Parallel({
samples,
Expand All @@ -156,42 +160,84 @@ export default Ember.Object.extend(
length,
});

const cacheValue = job.spawn(({ samples, startSample, endSample, length }) => {
const sampleSize = (endSample - startSample) / length;
const sampleStep = ~~(sampleSize / 10) || 1; // reduce granularity with small length
const peaks = [];

// const cacheValue = job.spawn(({ samples, startSample, endSample, length }) => {

const sampleSize = ~~((endSample - startSample) / length); // number of samples per pixel
const sampleStep = ~~(sampleSize / 100) || 1; // reduce granularity with small length
const peaks = new Array(~~length);

// compute largest frameSize which is a power of 2
let frameSize = 2;
while (frameSize < sampleSize) {
frameSize *= 2;
}
const fft = new DSPJS.FFT(frameSize, sampleRate);

console.log({ sampleSize, sampleStep, frameSize })


for (let i = 0; i < length; i++) {
const start = ~~(startSample + i * sampleSize);
const end = ~~(start + sampleSize);
let min = samples[start] || 0;
let max = samples[start] || 0;
let dominantFrequency = 0;

// calculate max and min in this sample
// calculate min and max for this peak
for (let j = start; j < end; j += sampleStep) {
const value = samples[j] || 0;

if (value > max) {
max = value;
}
if (value < min) {
min = value;
}
min = Math.min(value, min);
max = Math.max(value, max);
}

// calculate dominant frequency for this peak
// using as many samples from given sampleSize as possible
const frame = new Float32Array(frameSize);
frame.fill(0);
for (let k = 0; k < sampleSize; k++) {
frame[k] = samples[start + k];
}

fft.forward(frame);
const spectrum = fft.spectrum.slice(0, ~~(frameSize / 2) - 1); // ignore beyond N/2, Nyquist Frequency
const dominantFrequencyIndex = indexOfMax(spectrum);
dominantFrequency = dominantFrequencyIndex * sampleRate / frameSize;

// add to peaks
peaks[i] = [min, max];
peaks[i] = [min, max, dominantFrequency];
}

return peaks;
});
const cacheValue = asResolvedPromise(peaks);
// return peaks;
// });

peaksCache.set(cacheKey, cacheValue);

return cacheValue;
},


toString() {
return '<linx@object:track/audio-binary>';
},
});

function indexOfMax(arr) {
if (arr.length === 0) {
return -1;
}

let max = arr[0];
let maxIndex = 0;

for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
maxIndex = i;
max = arr[i];
}
}

return maxIndex;
}
3 changes: 3 additions & 0 deletions app/resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Resolver from 'ember-resolver';

export default Resolver;
3 changes: 2 additions & 1 deletion app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
location: config.locationType
location: config.locationType,
rootURL: config.rootURL
});

Router.map(function() {
Expand Down
2 changes: 1 addition & 1 deletion config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = function(environment) {
// namespaced directory where resolver will look for resource files
podModulePrefix: 'linx/pods',
environment: environment,
baseURL: '/',
rootURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
Expand Down
2 changes: 2 additions & 0 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global require, module */
/* jshint node:true */

var EmberApp = require('ember-cli/lib/broccoli/ember-app');

Expand Down Expand Up @@ -34,6 +35,7 @@ module.exports = function(defaults) {

app.import('vendor/js/soundtouch.min.js');
app.import('vendor/js/parallel.js'); // TODO(TECHDEBT): minify
app.import('vendor/js/dsp.js'); // TODO(TECHDEBT): minify

// WebAudioTestApi overrides native globals; only import when testing
if (app.env === 'test') {
Expand Down
Loading