Browse Source

Fix merge

Noah 8 years ago
parent
commit
6d54939d01

+ 1 - 2
Dockerfile View File

3
 # Install dependencies
3
 # Install dependencies
4
 RUN apt-get update --yes && apt-get upgrade --yes
4
 RUN apt-get update --yes && apt-get upgrade --yes
5
 RUN apt-get install git nodejs npm \
5
 RUN apt-get install git nodejs npm \
6
-libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ \
6
+libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev libpng-dev build-essential g++ \
7
 ffmpeg \
7
 ffmpeg \
8
-libgroove-dev zlib1g-dev libpng-dev \
9
 redis-server --yes
8
 redis-server --yes
10
 
9
 
11
 RUN ln -s `which nodejs` /usr/bin/node
10
 RUN ln -s `which nodejs` /usr/bin/node

+ 6 - 24
INSTALL.md View File

5
 * [Node.js/NPM](https://nodejs.org/) v0.11.2 or greater
5
 * [Node.js/NPM](https://nodejs.org/) v0.11.2 or greater
6
 * [node-canvas dependencies](https://github.com/Automattic/node-canvas#installation)
6
 * [node-canvas dependencies](https://github.com/Automattic/node-canvas#installation)
7
 * [FFmpeg](https://www.ffmpeg.org/)
7
 * [FFmpeg](https://www.ffmpeg.org/)
8
-* [libgroove](https://github.com/andrewrk/libgroove)
9
 
8
 
10
 If you're using a particularly fancy distributed setup you'll also need to install [Redis](http://redis.io/).
9
 If you're using a particularly fancy distributed setup you'll also need to install [Redis](http://redis.io/).
11
 
10
 
22
 An example bootstrap script for installing Audiogram on Ubuntu looks like this:
21
 An example bootstrap script for installing Audiogram on Ubuntu looks like this:
23
 
22
 
24
 ```sh
23
 ```sh
25
-# 14.04 only: add PPAs for FFmpeg and Libgroove
24
+# 14.04 only: add PPA for FFmpeg
26
 # Not required for 15.04+
25
 # Not required for 15.04+
27
 sudo add-apt-repository ppa:mc3man/trusty-media --yes
26
 sudo add-apt-repository ppa:mc3man/trusty-media --yes
28
-sudo apt-add-repository ppa:andrewrk/libgroove --yes
29
 
27
 
30
 # Update/upgrade
28
 # Update/upgrade
31
 sudo apt-get update --yes && sudo apt-get upgrade --yes
29
 sudo apt-get update --yes && sudo apt-get upgrade --yes
35
 # Git
33
 # Git
36
 # node-canvas dependencies (Cairo, Pango, libgif, libjpeg)
34
 # node-canvas dependencies (Cairo, Pango, libgif, libjpeg)
37
 # FFmpeg
35
 # FFmpeg
38
-# node-waveform dependencies (libgroove, zlib, libpng)
39
 sudo apt-get install git nodejs npm \
36
 sudo apt-get install git nodejs npm \
40
-libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ \
37
+libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev libpng-dev build-essential g++ \
41
 ffmpeg \
38
 ffmpeg \
42
-libgroove-dev zlib1g-dev libpng-dev \
43
 --yes
39
 --yes
44
 
40
 
45
 # Install Redis if you plan to use it to share rendering among multiple processes/servers
41
 # Install Redis if you plan to use it to share rendering among multiple processes/servers
82
 
78
 
83
 1. [Node.js/NPM](https://nodejs.org/)
79
 1. [Node.js/NPM](https://nodejs.org/)
84
 2. [node-canvas dependencies](https://github.com/Automattic/node-canvas#installation)
80
 2. [node-canvas dependencies](https://github.com/Automattic/node-canvas#installation)
85
-4. [libgroove](https://github.com/andrewrk/libgroove)
86
-
87
-You probably don't need to install FFmpeg separately because libgroove depends on it.
81
+4. [FFmpeg](https://www.ffmpeg.org/)
88
 
82
 
89
 You can install Node.js by [downloading it from the website](https://nodejs.org/).
83
 You can install Node.js by [downloading it from the website](https://nodejs.org/).
90
 
84
 
91
-Installation of node-canvas dependencies and libgroove might look like the following with [Homebrew](http://brew.sh/) (you'll want to make sure [XCode](https://developer.apple.com/xcode/) is installed and up-to-date too):
85
+Installation of node-canvas dependencies and FFmpeg might look like the following with [Homebrew](http://brew.sh/) (you'll want to make sure [XCode](https://developer.apple.com/xcode/) is installed and up-to-date too):
92
 
86
 
93
 ```sh
87
 ```sh
94
 # Install Git if you haven't already
88
 # Install Git if you haven't already
95
 brew install git
89
 brew install git
96
 
90
 
97
-# Install Cairo, Pango, libgif, libjpeg, libgroove, and FFmpeg
91
+# Install Cairo, Pango, libgif, libjpeg, libpng, and FFmpeg
98
 # You may not need to install zlib
92
 # You may not need to install zlib
99
-brew install pkg-config cairo pango libpng jpeg giflib libgroove ffmpeg
93
+brew install pkg-config cairo pango libpng jpeg giflib ffmpeg
100
 
94
 
101
 # Go to the directory where you want the audiogram directory
95
 # Go to the directory where you want the audiogram directory
102
 cd /where/to/put/this/
96
 cd /where/to/put/this/
147
 
141
 
148
 Follow the [Homebrew troubleshooting guide](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/Troubleshooting.md#troubleshooting), particularly making sure that XCode is up to date.
142
 Follow the [Homebrew troubleshooting guide](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/Troubleshooting.md#troubleshooting), particularly making sure that XCode is up to date.
149
 
143
 
150
-### Installing libgroove manually
151
-
152
-If you're on a Mac and installing [libgroove](https://github.com/andrewrk/libgroove) with Homebrew failed for some reason, you can try following the instructions in the repo to install it more manually.
153
-
154
-### Installing XCode command line tools
155
-
156
-Audiogram uses [waveform](https://github.com/andrewrk/waveform), which requires the development version of `zlib`. Your Mac might already have this, but if it doesn't, installing XCode command line tools might help:
157
-
158
-```sh
159
-xcode-select --install
160
-```
161
-
162
 ### Updating node-gyp
144
 ### Updating node-gyp
163
 
145
 
164
 Updating node-gyp to a current version with:
146
 Updating node-gyp to a current version with:

+ 0 - 35
audiogram/duration.js View File

1
-var probe = require("node-ffprobe");
2
-
3
-module.exports = function(filename, cb){
4
-
5
-  probe(filename, function(err, probeData) {
6
-
7
-    if (err) {
8
-      return cb(err);
9
-    }
10
-
11
-    var duration = getDuration(probeData);
12
-
13
-    if (!duration || duration === "N/A" || !(duration > 0)) {
14
-      return cb("Couldn't probe audio duration.");
15
-    }
16
-
17
-    return cb(null, duration);
18
-
19
-  });
20
-
21
-};
22
-
23
-function getDuration(probeData) {
24
-
25
-  if (probeData.format && probeData.format.duration) {
26
-    return probeData.format.duration;
27
-  }
28
-
29
-  if (Array.isArray(probeData.streams) && probeData.streams.length && probeData.streams[0].duration) {
30
-    return probeData.streams[0].duration;
31
-  }
32
-
33
-  return null;
34
-
35
-}

+ 23 - 27
audiogram/index.js View File

5
     serverSettings = require("../settings/"),
5
     serverSettings = require("../settings/"),
6
     transports = require("../lib/transports/"),
6
     transports = require("../lib/transports/"),
7
     logger = require("../lib/logger/"),
7
     logger = require("../lib/logger/"),
8
-    getDuration = require("./duration.js"),
8
+    Profiler = require("../lib/profiler.js"),
9
+    probe = require("../lib/probe.js"),
9
     getWaveform = require("./waveform.js"),
10
     getWaveform = require("./waveform.js"),
10
     initializeCanvas = require("./initialize-canvas.js"),
11
     initializeCanvas = require("./initialize-canvas.js"),
11
     drawFrames = require("./draw-frames.js"),
12
     drawFrames = require("./draw-frames.js"),
24
   this.videoPath = path.join(this.dir, "video.mp4");
25
   this.videoPath = path.join(this.dir, "video.mp4");
25
   this.frameDir = path.join(this.dir, "frames");
26
   this.frameDir = path.join(this.dir, "frames");
26
 
27
 
28
+  this.profiler = new Profiler();
29
+
27
   return this;
30
   return this;
28
 
31
 
29
 }
32
 }
30
 
33
 
31
-// Probe an audio file for its duration, compute the number of frames required
32
-Audiogram.prototype.getDuration = function(cb) {
34
+// Get the waveform data from the audio file, split into frames
35
+Audiogram.prototype.getWaveform = function(cb) {
33
 
36
 
34
   var self = this;
37
   var self = this;
35
 
38
 
36
-  this.status("duration");
39
+  this.status("probing");
37
 
40
 
38
-  getDuration(this.audioPath, function(err, duration){
41
+  probe(this.audioPath, function(err, data){
39
 
42
 
40
     if (err) {
43
     if (err) {
41
       return cb(err);
44
       return cb(err);
42
     }
45
     }
43
 
46
 
44
-    if (self.settings.theme.maxDuration && self.settings.theme.maxDuration < duration) {
45
-      cb("Exceeds max duration of " + self.settings.theme.maxDuration + "s");
47
+    if (self.settings.theme.maxDuration && self.settings.theme.maxDuration < data.duration) {
48
+      return cb("Exceeds max duration of " + self.settings.theme.maxDuration + "s");
46
     }
49
     }
47
 
50
 
48
-    self.set("numFrames", self.numFrames = Math.floor(duration * self.settings.theme.framesPerSecond));
49
-
50
-    cb(null);
51
-
52
-  });
53
-
54
-};
51
+    self.profiler.size(data.duration);
52
+    self.set("numFrames", self.numFrames = Math.floor(data.duration * self.settings.theme.framesPerSecond));
53
+    self.status("waveform");
55
 
54
 
56
-// Get the waveform data from the audio file, split into frames
57
-Audiogram.prototype.getWaveform = function(cb) {
58
-
59
-  var self = this;
55
+    getWaveform(self.audioPath, {
56
+      numFrames: self.numFrames,
57
+      samplesPerFrame: self.settings.theme.samplesPerFrame,
58
+      channels: data.channels
59
+    }, function(waveformErr, waveform){
60
 
60
 
61
-  this.status("waveform");
61
+      return cb(waveformErr, self.waveform = waveform);
62
 
62
 
63
-  getWaveform(this.audioPath, {
64
-    numFrames: this.numFrames,
65
-    samplesPerFrame: this.settings.theme.samplesPerFrame
66
-  }, function(err, waveform){
63
+    });
67
 
64
 
68
-    return cb(err, self.waveform = waveform);
69
 
65
 
70
   });
66
   });
71
 
67
 
162
     q.defer(this.trimAudio.bind(this), this.settings.start || 0, this.settings.end || null);
158
     q.defer(this.trimAudio.bind(this), this.settings.start || 0, this.settings.end || null);
163
   }
159
   }
164
 
160
 
165
-  // Get the audio's duration for computing number of frames
166
-  q.defer(this.getDuration.bind(this));
167
-
168
   // Get the audio waveform data
161
   // Get the audio waveform data
169
   q.defer(this.getWaveform.bind(this));
162
   q.defer(this.getWaveform.bind(this));
170
 
163
 
187
       self.set("url", transports.getURL(self.id));
180
       self.set("url", transports.getURL(self.id));
188
     }
181
     }
189
 
182
 
183
+    logger.debug(self.profiler.print());
184
+
190
     return cb(err);
185
     return cb(err);
191
 
186
 
192
   });
187
   });
203
 
198
 
204
 // Convenience method for .set("status")
199
 // Convenience method for .set("status")
205
 Audiogram.prototype.status = function(value) {
200
 Audiogram.prototype.status = function(value) {
201
+  this.profiler.start(value);
206
   return this.set("status", value);
202
   return this.set("status", value);
207
 };
203
 };
208
 
204
 

+ 3 - 3
audiogram/trim.js View File

1
 var exec = require("child_process").exec,
1
 var exec = require("child_process").exec,
2
-    getDuration = require("./duration.js");
2
+    probe = require("../lib/probe.js");
3
 
3
 
4
 function trimAudio(options, cb) {
4
 function trimAudio(options, cb) {
5
 
5
 
6
   if (!options.endTime) {
6
   if (!options.endTime) {
7
 
7
 
8
-    return getDuration(options.origin, function(err, duration){
8
+    return probe(options.origin, function(err, data){
9
       if (err) {
9
       if (err) {
10
         return cb(err);
10
         return cb(err);
11
       }
11
       }
12
 
12
 
13
-      options.endTime = duration;
13
+      options.endTime = data.duration;
14
       trimAudio(options, cb);
14
       trimAudio(options, cb);
15
 
15
 
16
     });
16
     });

+ 67 - 37
audiogram/waveform.js View File

1
-var waveform = require("waveform"),
2
-    d3 = require("d3");
1
+var probe = require("../lib/probe.js"),
2
+    d3 = require("d3"),
3
+    pcmStream = require("../lib/pcm.js");
3
 
4
 
4
 function getWaveform(filename, options, cb) {
5
 function getWaveform(filename, options, cb) {
5
 
6
 
6
-  var numSamples = options.numFrames * options.samplesPerFrame;
7
+  var stream = pcmStream(filename, {
8
+        channels: options.channels
9
+      }),
10
+      samples = [];
7
 
11
 
8
-  var waveformOptions = {
9
-    "scan": false,
10
-    "waveformjs": "-",
11
-    "wjs-width": numSamples,
12
-    "wjs-precision": 2,
13
-    "wjs-plain": true,
14
-    "encoding": "utf8"
15
-  };
12
+  stream.on("data",function(sample, channel){
16
 
13
 
17
-  waveform(filename, waveformOptions, function(err, buf) {
18
-
19
-    if (err) {
20
-      return cb(err);
14
+    // Average multiple channels
15
+    if (channel > 0) {
16
+      samples[samples.length - 1] = ((samples[samples.length - 1] * channel) + sample) / (channel + 1);
17
+    } else {
18
+      samples.push(sample);
21
     }
19
     }
22
 
20
 
23
-    cb(null, processWaveform(JSON.parse(buf)));
21
+  });
22
+
23
+  stream.on("error", cb);
24
 
24
 
25
+  stream.on("end", function(output){
26
+    var processed = processSamples(samples, options.numFrames, options.samplesPerFrame);
27
+    return cb(null, processed);
25
   });
28
   });
26
 
29
 
27
-  // Slice one-dimensional waveform data into array of arrays, one array per frame
28
-  function processWaveform(waveformData) {
30
+}
29
 
31
 
30
-    var max = -Infinity,
31
-        maxFrame;
32
+function processSamples(samples, numFrames, samplesPerFrame) {
32
 
33
 
33
-    waveformData.forEach(function(d, i){
34
-      if (d > max) {
35
-        max = d;
36
-        maxFrame = Math.floor(i / options.samplesPerFrame);
37
-      }
38
-    });
34
+  // TODO spread out slop across frames
35
+  var perFrame = Math.floor(samples.length / numFrames),
36
+      perPoint = Math.floor(perFrame / samplesPerFrame),
37
+      range = d3.range(samplesPerFrame),
38
+      maxFrame,
39
+      maxMedian = min = max = 0;
39
 
40
 
40
-    // Scale peaks to 1
41
-    var scaled = d3.scaleLinear()
42
-      .domain([0, max])
43
-      .range([0, 1]);
41
+  var unadjusted = d3.range(numFrames).map(function(frame){
44
 
42
 
45
-    var waveformFrames = d3.range(options.numFrames).map(function getFrame(frameNumber) {
43
+    var frameSamples = samples.slice(frame * perFrame, (frame + 1) * perFrame),
44
+        points = range.map(function(point){
46
 
45
 
47
-      return waveformData.slice(options.samplesPerFrame * frameNumber, options.samplesPerFrame * (frameNumber + 1)).map(scaled);
46
+          var pointSamples = frameSamples.slice(point * perPoint, (point + 1) * perPoint),
47
+              localMin = localMax = 0;
48
 
48
 
49
-    });
49
+          for (var i = 0, l = pointSamples.length; i < l; i++) {
50
+            if (pointSamples[i] < localMin) localMin = pointSamples[i];
51
+            if (pointSamples[i] > localMax) localMax = pointSamples[i];
52
+          }
53
+
54
+          if (localMin < min) min = localMin;
55
+          if (localMax > max) max = localMax;
56
+
57
+          // Min value, max value, and midpoint value
58
+          return [localMin, localMax, pointSamples[Math.floor(pointSamples.length / 2)]];
59
+
60
+        }),
61
+        median = d3.median(points.map(function(point){
62
+          return point[1] - point[0];
63
+        }));
50
 
64
 
51
-    // Set the first and last frame's waveforms to something peak-y for better thumbnails
52
-    waveformFrames[0] = waveformFrames[waveformFrames.length - 1] = waveformFrames[maxFrame];
65
+    if (median > maxMedian) {
66
+      maxMedian = median;
67
+      maxFrame = frame;
68
+    }
69
+
70
+    return points;
71
+
72
+  });
73
+
74
+  // Scale up to -1 / 1
75
+  var adjustment = 1 / Math.max(Math.abs(min), Math.abs(max));
76
+
77
+  var adjusted = unadjusted.map(function(frame){
78
+    return frame.map(function(point){
79
+      return [adjustment * point[0], adjustment * point[1]];
80
+    });
81
+  });
53
 
82
 
54
-    return waveformFrames;
83
+  // Make first and last frame peaky
84
+  adjusted[0] = adjusted[numFrames - 1] = adjusted[maxFrame];
55
 
85
 
56
-  }
86
+  return adjusted;
57
 
87
 
58
 }
88
 }
59
 
89
 

+ 2 - 2
client/index.js View File

263
       return "Downloading audio for processing";
263
       return "Downloading audio for processing";
264
     case "trim":
264
     case "trim":
265
       return "Trimming audio";
265
       return "Trimming audio";
266
-    case "duration":
267
-      return "Checking duration";
266
+    case "probing":
267
+      return "Probing audio file";
268
     case "waveform":
268
     case "waveform":
269
       return "Analyzing waveform";
269
       return "Analyzing waveform";
270
     case "renderer":
270
     case "renderer":

File diff suppressed because it is too large
+ 1 - 1
client/sample-wave.js


+ 1 - 0
client/waveform.js View File

38
   var fileReader = new FileReader();
38
   var fileReader = new FileReader();
39
 
39
 
40
   var close = function(err, data) {
40
   var close = function(err, data) {
41
+    console.warn(err);
41
     ctx.close();
42
     ctx.close();
42
     cb(err, data);
43
     cb(err, data);
43
   };
44
   };

+ 55 - 0
lib/pcm.js View File

1
+var spawn = require("child_process").spawn,
2
+    stream = require("stream");
3
+
4
+// Based on https://github.com/jhurliman/node-pcm
5
+// Modified to respect channels, use fewer vars, and return a stream
6
+module.exports = function(filename, options) {
7
+  var sampleStream = new stream.Stream(),
8
+      channels = 2,
9
+      output = "",
10
+      channel = 0,
11
+      oddByte;
12
+
13
+  sampleStream.readable = true;
14
+
15
+  if (options && options.channels) channels = options.channels;
16
+
17
+  var args = ["-i", filename, "-f", "s16le", "-ac", channels, "-acodec", "pcm_s16le"];
18
+
19
+  if (options && options.sampleRate) {
20
+    args.push("-ar", options.sampleRate);
21
+  }
22
+
23
+  args.push("-y", "pipe:1");
24
+
25
+  var spawned = spawn("ffmpeg", args);
26
+
27
+  spawned.stdout.on("data", function(data) {
28
+
29
+    var len = data.length;
30
+
31
+    if (oddByte != null) {
32
+      sampleStream.emit("data", ((data.readInt8(i++, true) << 8) | oddByte) / 32767.0, channel);
33
+      channel = ++channel % channels;
34
+    }
35
+
36
+    for (var i = 0; i < len; i += 2) {
37
+      sampleStream.emit("data", data.readInt16LE(i, true) / 32767.0, channel);
38
+      channel = ++channel % channels;
39
+    }
40
+
41
+    oddByte = (i < len) ? data.readUInt8(i, true) : null;
42
+
43
+  });
44
+
45
+  spawned.stderr.on("data", function(data) {
46
+    output += data.toString();
47
+  });
48
+
49
+  spawned.stderr.on("end", function() {
50
+    sampleStream.emit(oddByte !== undefined ? "end" : "error", output);
51
+  });
52
+
53
+  return sampleStream;
54
+
55
+};

+ 43 - 0
lib/probe.js View File

1
+var probe = require("node-ffprobe");
2
+
3
+module.exports = function(filename, cb){
4
+
5
+  probe(filename, function(err, probeData) {
6
+
7
+    if (err) {
8
+      return cb(err);
9
+    }
10
+
11
+    var duration = getProperty(probeData, "duration"),
12
+        channels = getProperty(probeData, "channels");
13
+
14
+    if (!duration || duration === "N/A" || !(duration > 0)) {
15
+      return cb("Couldn't probe audio duration.");
16
+    }
17
+
18
+    if (typeof channels !== "number" || channels < 1 || channels > 2) {
19
+      return cb("Couldn't detect mono/stereo channels");
20
+    }
21
+
22
+    return cb(null, {
23
+      duration: duration,
24
+      channels: channels
25
+    });
26
+
27
+  });
28
+
29
+};
30
+
31
+function getProperty(probeData, property) {
32
+
33
+  if (probeData.format && probeData.format[property]) {
34
+    return probeData.format[property];
35
+  }
36
+
37
+  if (Array.isArray(probeData.streams) && probeData.streams.length && probeData.streams[0][property]) {
38
+    return probeData.streams[0][property];
39
+  }
40
+
41
+  return null;
42
+
43
+}

+ 42 - 0
lib/profiler.js View File

1
+function Profiler() {
2
+  this._times = {};
3
+  return this;
4
+};
5
+
6
+Profiler.prototype.start = function(key) {
7
+  this.end(this._current);
8
+  this._current = key;
9
+  this._times[this._current] = { start: Date.now() };
10
+  return this;
11
+};
12
+
13
+Profiler.prototype.size = function(size) {
14
+  if (!arguments.length) return this._size;
15
+  this._size = size;
16
+  return this;
17
+};
18
+
19
+Profiler.prototype.end = function(key) {
20
+  if (key in this._times) this._times[key].end = Date.now();
21
+  return this;
22
+};
23
+
24
+Profiler.prototype.print = function(size) {
25
+  var rows = [],
26
+      row;
27
+
28
+  this.end(this._current);
29
+
30
+  for (var key in this._times) {
31
+    row = { key: key, time: this._times[key].end - this._times[key].start };
32
+    if (this._size) row.per = row.time / this._size;
33
+    rows.push(row);
34
+  }
35
+
36
+  return rows.map(function(row){
37
+    return row.key + ": " + Math.round(row.time) + "ms total" + (row.per ? ", " + Math.round(row.per) + "ms per" : "");
38
+  }).join("\n");
39
+
40
+};
41
+
42
+module.exports = Profiler;

+ 1 - 2
package.json View File

25
   "dependencies": {
25
   "dependencies": {
26
     "aws-sdk": "^2.2.39",
26
     "aws-sdk": "^2.2.39",
27
     "browserify": "^13.0.0",
27
     "browserify": "^13.0.0",
28
+    "canvas": "git+https://github.com/chearon/node-canvas.git#b62dd3a9fa",
28
     "compression": "^1.6.1",
29
     "compression": "^1.6.1",
29
     "d3": "^4.1.1",
30
     "d3": "^4.1.1",
30
     "dotenv": "^2.0.0",
31
     "dotenv": "^2.0.0",
39
     "rimraf": "^2.5.0",
40
     "rimraf": "^2.5.0",
40
     "smartquotes": "^1.0.0",
41
     "smartquotes": "^1.0.0",
41
     "underscore": "^1.8.3",
42
     "underscore": "^1.8.3",
42
-    "canvas": "git+https://github.com/chearon/node-canvas.git#b62dd3a9fa",
43
-    "waveform": "git+https://github.com/veltman/node-waveform.git#8b1a22f109",
44
     "webaudio-peaks": "0.0.5",
43
     "webaudio-peaks": "0.0.5",
45
     "winston": "^2.2.0"
44
     "winston": "^2.2.0"
46
   },
45
   },

+ 10 - 9
renderer/patterns.js View File

36
 
36
 
37
     var top = data.map(function(d,i){
37
     var top = data.map(function(d,i){
38
 
38
 
39
-      return [x(i), baseline - height(d)];
39
+      return [x(i), baseline - height(d[1])];
40
 
40
 
41
     });
41
     });
42
 
42
 
43
     var bottom = data.map(function(d,i){
43
     var bottom = data.map(function(d,i){
44
 
44
 
45
-      return [x(i), baseline + height(d)];
45
+      return [x(i), baseline + height(-d[0])];
46
 
46
 
47
     }).reverse();
47
     }).reverse();
48
 
48
 
90
 
90
 
91
     data.forEach(function(val, i){
91
     data.forEach(function(val, i){
92
 
92
 
93
-      var h = height(val),
94
-          x = barX(i);
93
+      var h = height(-val[0]) + height(val[1]),
94
+          x = barX(i),
95
+          y = baseline - height(val[1]);
95
 
96
 
96
-      context.fillRect(x, baseline - h, barWidth, h * 2);
97
+      context.fillRect(x, y, barWidth, h);
97
 
98
 
98
       if (round) {
99
       if (round) {
99
         context.beginPath();
100
         context.beginPath();
100
-        context.arc(x + barWidth / 2, baseline - h, barWidth / 2, 0, 2 * Math.PI);
101
-        context.moveTo(x + barWidth / 2, baseline + h);
102
-        context.arc(x + barWidth / 2, baseline + h, barWidth / 2, 0, 2 * Math.PI);
101
+        context.arc(x + barWidth / 2, y, barWidth / 2, 0, 2 * Math.PI);
102
+        context.moveTo(x + barWidth / 2, y + h);
103
+        context.arc(x + barWidth / 2, y + h, barWidth / 2, 0, 2 * Math.PI);
103
         context.fill();
104
         context.fill();
104
       }
105
       }
105
 
106
 
132
 
133
 
133
     data.forEach(function(val, i){
134
     data.forEach(function(val, i){
134
 
135
 
135
-      var bricks = Math.max(1, Math.floor(height(val) / (brickHeight + brickGap))),
136
+      var bricks = Math.max(1, Math.floor(height(val[1]) / (brickHeight + brickGap))),
136
           x = barX(i);
137
           x = barX(i);
137
 
138
 
138
       d3.range(bricks).forEach(function(b){
139
       d3.range(bricks).forEach(function(b){

+ 1 - 1
server/status.js View File

29
         hash = { status: "unknown" };
29
         hash = { status: "unknown" };
30
       }
30
       }
31
 
31
 
32
-      ["duration","numFrames","framesComplete"].forEach(function(key) {
32
+      ["numFrames", "framesComplete"].forEach(function(key) {
33
         if (key in hash) {
33
         if (key in hash) {
34
           hash[key] = +hash[key];
34
           hash[key] = +hash[key];
35
         }
35
         }

BIN
test/data/glazed-donut-mono.mp3 View File


BIN
test/data/glazed-donut-mono.wav View File


BIN
test/data/long-beeps.mp3 View File


BIN
test/data/short.wav View File


+ 8 - 1
test/frame-test.js View File

10
 
10
 
11
 var frameDir = path.join(__dirname, "tmp", "frames");
11
 var frameDir = path.join(__dirname, "tmp", "frames");
12
 
12
 
13
-var waveform = [[0, 1, 0], [1, 0.1, 1]];
13
+var waveform = [
14
+  [
15
+    [0, 0], [-1, 1], [0, 0]
16
+  ],
17
+  [
18
+    [-1, 1], [-0.1, 0.1], [-1, 1]
19
+  ]
20
+];
14
 
21
 
15
 function tester(options) {
22
 function tester(options) {
16
 
23
 

test/duration-test.js → test/probe-test.js View File

5
 
5
 
6
 require("mkdirp").sync(path.join(__dirname, "tmp"));
6
 require("mkdirp").sync(path.join(__dirname, "tmp"));
7
 
7
 
8
-var getDuration = require("../audiogram/duration.js"),
8
+var probe = require("../lib/probe.js"),
9
     trimAudio = require("../audiogram/trim.js");
9
     trimAudio = require("../audiogram/trim.js");
10
 
10
 
11
-tape("MP3 duration", function(test) {
11
+tape("MP3 probe", function(test) {
12
 
12
 
13
-  getDuration(path.join(__dirname, "data/glazed-donut.mp3"), function(err, duration){
13
+  probe(path.join(__dirname, "data/glazed-donut.mp3"), function(err, data){
14
 
14
 
15
     test.error(err);
15
     test.error(err);
16
-    test.equal(typeof duration, "number");
17
-    test.assert(Math.abs(duration - 26.67) < 0.1);
16
+    test.equal(typeof data.duration, "number");
17
+    test.equal(data.channels, 2);
18
+    test.assert(Math.abs(data.duration - 26.67) < 0.1);
18
     test.end();
19
     test.end();
19
 
20
 
20
   });
21
   });
21
 
22
 
22
 });
23
 });
23
 
24
 
24
-tape("WAV duration", function(test) {
25
+tape("WAV probe", function(test) {
25
 
26
 
26
-  getDuration(path.join(__dirname, "data/glazed-donut.wav"), function(err, duration){
27
+  probe(path.join(__dirname, "data/glazed-donut.wav"), function(err, data){
27
 
28
 
28
     test.error(err);
29
     test.error(err);
29
-    test.equal(typeof duration, "number");
30
-    test.assert(Math.abs(duration - 1.83) < 0.1);
30
+    test.equal(typeof data.duration, "number");
31
+    test.equal(data.channels, 2);
32
+    test.assert(Math.abs(data.duration - 1.83) < 0.1);
31
     test.end();
33
     test.end();
32
 
34
 
33
   });
35
   });
34
 
36
 
35
 });
37
 });
36
 
38
 
37
-tape("Duration error", function(test) {
39
+tape("Mono probe", function(test) {
38
 
40
 
39
-  getDuration(path.join(__dirname, "..", "README.md"), function(err){
41
+  probe(path.join(__dirname, "data/short.wav"), function(err, data){
42
+
43
+    test.error(err);
44
+    test.equal(typeof data.duration, "number");
45
+    test.equal(data.channels, 1);
46
+    test.assert(Math.abs(data.duration - 0.01) < 0.01);
47
+    test.end();
48
+
49
+  });
50
+
51
+});
52
+
53
+tape("Probe error", function(test) {
54
+
55
+  probe(path.join(__dirname, "..", "README.md"), function(err){
40
 
56
 
41
     test.ok(err);
57
     test.ok(err);
42
     test.end();
58
     test.end();
55
 
71
 
56
   queue(1)
72
   queue(1)
57
     .defer(trimAudio, options)
73
     .defer(trimAudio, options)
58
-    .defer(getDuration, options.destination)
59
-    .await(function(err, _, duration){
74
+    .defer(probe, options.destination)
75
+    .await(function(err, _, data){
60
 
76
 
61
       test.error(err);
77
       test.error(err);
62
-      test.equal(typeof duration, "number");
63
-      test.assert(Math.abs(duration - 20) < 0.1);
78
+      test.equal(typeof data.duration, "number");
79
+      test.assert(Math.abs(data.duration - 20) < 0.1);
64
       test.end();
80
       test.end();
65
 
81
 
66
     });
82
     });
77
 
93
 
78
   queue(1)
94
   queue(1)
79
     .defer(trimAudio, options)
95
     .defer(trimAudio, options)
80
-    .defer(getDuration, options.destination)
81
-    .await(function(err, _, duration){
96
+    .defer(probe, options.destination)
97
+    .await(function(err, _, data){
82
 
98
 
83
       test.error(err);
99
       test.error(err);
84
-      test.equal(typeof duration, "number");
85
-      test.assert(Math.abs(duration - 20) < 0.1);
100
+      test.equal(typeof data.duration, "number");
101
+      test.assert(Math.abs(data.duration - 20) < 0.1);
86
       test.end();
102
       test.end();
87
 
103
 
88
     });
104
     });
100
 
116
 
101
   queue(1)
117
   queue(1)
102
     .defer(trimAudio, options)
118
     .defer(trimAudio, options)
103
-    .defer(getDuration, options.destination)
104
-    .await(function(err, _, duration){
119
+    .defer(probe, options.destination)
120
+    .await(function(err, _, data){
105
 
121
 
106
       test.error(err);
122
       test.error(err);
107
-      test.equal(typeof duration, "number");
108
-      test.assert(Math.abs(duration - 5) < 0.1);
123
+      test.equal(typeof data.duration, "number");
124
+      test.assert(Math.abs(data.duration - 5) < 0.1);
109
       test.end();
125
       test.end();
110
 
126
 
111
     });
127
     });
122
     endTime: 4
138
     endTime: 4
123
   };
139
   };
124
 
140
 
125
-  queue(1)
126
-    .defer(trimAudio, options)
127
-    .defer(getDuration, options.destination)
128
-    .await(function(err, _, duration){
129
-
130
-      test.ok(err);
131
-      test.end();
132
-
133
-    });
134
-
135
-});
136
-
137
-// Cleanup
138
-tape.onFinish(function(){
139
-  require("rimraf")(path.join(__dirname, "tmp"), function(err){
140
-    if (err) {
141
-      throw err;
142
-    }
141
+  trimAudio(options, function(err){
142
+    test.ok(err);
143
+    test.end();
143
   });
144
   });
145
+
144
 });
146
 });
145
 
147
 
146
 // Cleanup
148
 // Cleanup

+ 29 - 22
test/waveform-test.js View File

1
 var tape = require("tape"),
1
 var tape = require("tape"),
2
     path = require("path");
2
     path = require("path");
3
 
3
 
4
-var getWaveform = require("../audiogram/waveform.js");
4
+var getWaveform = require("../audiogram/waveform.js"),
5
+    probe = require("../lib/probe.js");
5
 
6
 
6
 var sample = path.join(__dirname, "data/glazed-donut.mp3");
7
 var sample = path.join(__dirname, "data/glazed-donut.mp3");
7
 
8
 
8
 tape("Waveform", function(test) {
9
 tape("Waveform", function(test) {
9
 
10
 
10
   var options = {
11
   var options = {
11
-    numFrames: 500,
12
+    framesPerSecond: 20,
12
     samplesPerFrame: 10
13
     samplesPerFrame: 10
13
   };
14
   };
14
 
15
 
15
-  getWaveform(sample, options, function(err, waveform){
16
+  probe(sample, function(e1, data){
16
 
17
 
17
-    test.error(err);
18
-    test.assert(Array.isArray(waveform) && waveform.length === options.numFrames);
18
+    test.error(e1);
19
 
19
 
20
-    var firstMax = Math.max.apply(null, waveform[0]);
20
+    getWaveform(sample, options, function(e2, waveform){
21
 
21
 
22
-    test.assert(firstMax <= 1);
22
+      test.error(e2);
23
+      test.assert(Array.isArray(waveform) && waveform.length === Math.floor(data.duration * options.framesPerSecond));
23
 
24
 
24
-    test.assert(waveform.every(function(frame){
25
-      return frame.length === options.samplesPerFrame;
26
-    }));
25
+      var firstMax = Math.max.apply(null, waveform[0].map(function(d){ return d[1]; }));
27
 
26
 
28
-    test.assert(waveform.every(function(frame){
29
-      return frame.every(function(val){
30
-        return typeof val === "number" && val >= 0 && val <= firstMax;
31
-      });
32
-    }));
27
+      test.assert(firstMax <= 1);
33
 
28
 
34
-    test.end();
29
+      test.assert(waveform.every(function(frame){
30
+        return frame.length === options.samplesPerFrame && frame.every(function(f){
31
+          return f.length === 2 && typeof f[0] === "number" && typeof f[1] === "number" && f[0] <= 0 && f[0] >= -1 && f[1] >= 0 && f[1] <= firstMax;
32
+        });
33
+      }));
34
+
35
+      test.end();
36
+
37
+    });
35
 
38
 
36
   });
39
   });
37
 
40
 
38
 });
41
 });
39
 
42
 
40
-tape("Waveform missing numFrames", function(test) {
43
+tape("Max Duration Error", function(test) {
41
 
44
 
42
   var options = {
45
   var options = {
43
-    samplesPerFrame: 10
46
+    framesPerSecond: 20,
47
+    samplesPerFrame: 10,
48
+    maxDuration: 20
44
   };
49
   };
45
 
50
 
46
   getWaveform(sample, options, function(err, waveform){
51
   getWaveform(sample, options, function(err, waveform){
47
 
52
 
48
-    test.ok(err);
53
+    test.assert(err);
49
     test.end();
54
     test.end();
50
 
55
 
51
   });
56
   });
52
 
57
 
53
 });
58
 });
54
 
59
 
55
-tape("Waveform missing samplesPerFrame", function(test) {
60
+tape("Max Duration OK", function(test) {
56
 
61
 
57
   var options = {
62
   var options = {
58
-    numFrames: 500,
63
+    framesPerSecond: 20,
64
+    samplesPerFrame: 10,
65
+    maxDuration: 30
59
   };
66
   };
60
 
67
 
61
   getWaveform(sample, options, function(err, waveform){
68
   getWaveform(sample, options, function(err, waveform){
62
 
69
 
63
-    test.ok(err);
70
+    test.error(err);
64
     test.end();
71
     test.end();
65
 
72
 
66
   });
73
   });