[Pkg-javascript-commits] [node-groove] 01/02: Imported Upstream version 2.2.2
Andrew Kelley
andrewrk-guest at moszumanska.debian.org
Mon Jun 30 15:33:42 UTC 2014
This is an automated email from the git hooks/post-receive script.
andrewrk-guest pushed a commit to branch master
in repository node-groove.
commit a3bbecfd909b697d4c1f55dadc5a94c38bad16cc
Author: Andrew Kelley <superjoe30 at gmail.com>
Date: Mon Jun 30 15:25:49 2014 +0000
Imported Upstream version 2.2.2
---
.gitignore | 2 +
README.md | 513 ++++++++++++++++++++++++++++++++++++++++++++
binding.gyp | 23 ++
example/fingerprint.js | 64 ++++++
example/metadata.js | 69 ++++++
example/playlist.js | 62 ++++++
example/replaygain.js | 67 ++++++
example/transcode.js | 53 +++++
lib/index.js | 108 ++++++++++
package.json | 35 +++
src/gn_encoder.cc | 416 +++++++++++++++++++++++++++++++++++
src/gn_encoder.h | 39 ++++
src/gn_file.cc | 332 ++++++++++++++++++++++++++++
src/gn_file.h | 39 ++++
src/gn_fingerprinter.cc | 373 ++++++++++++++++++++++++++++++++
src/gn_fingerprinter.h | 43 ++++
src/gn_loudness_detector.cc | 320 +++++++++++++++++++++++++++
src/gn_loudness_detector.h | 41 ++++
src/gn_player.cc | 357 ++++++++++++++++++++++++++++++
src/gn_player.h | 47 ++++
src/gn_playlist.cc | 240 +++++++++++++++++++++
src/gn_playlist.h | 47 ++++
src/gn_playlist_item.cc | 71 ++++++
src/gn_playlist_item.h | 32 +++
src/groove.cc | 93 ++++++++
test/danse.ogg | Bin 0 -> 88407 bytes
test/test.js | 146 +++++++++++++
27 files changed, 3632 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d5b7a9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/build
+/node_modules
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c8fd409
--- /dev/null
+++ b/README.md
@@ -0,0 +1,513 @@
+# node-groove
+
+Node.js bindings to [libgroove](https://github.com/andrewrk/libgroove) -
+generic music player backend library.
+
+Live discussion in #libgroove IRC channel on irc.freenode.org.
+
+## Usage
+
+1. Install libgroove to your system.
+2. `npm install --save groove`
+
+### Get Metadata from File
+
+```js
+var groove = require('groove');
+
+groove.open("danse-macabre.ogg", function(err, file) {
+ if (err) throw err;
+ console.log(file.metadata());
+ console.log("duration:", file.duration());
+ file.close(function(err) {
+ if (err) throw err;
+ });
+});
+```
+
+#### More Examples
+
+ * example/metadata.js - read or update metadata in a media file
+ * example/playlist.js - play several files in a row and then exit
+ * example/replaygain.js - compute replaygain values for media files
+ * example/transcode.js - convert and splice several files together
+ * example/fingerprint.js - create an acoustid fingerprint for media files
+
+## API Documentation
+
+### globals
+
+#### groove.setLogging(level)
+
+`level` can be:
+
+ * `groove.LOG_QUIET`
+ * `groove.LOG_ERROR`
+ * `groove.LOG_WARNING`
+ * `groove.LOG_INFO`
+
+#### groove.loudnessToReplayGain(loudness)
+
+Converts a loudness value which is in LUFS to the ReplayGain-suggested dB
+adjustment.
+
+#### groove.dBToFloat(dB)
+
+Converts dB format volume adjustment to a floating point gain format.
+
+#### groove.getVersion()
+
+Returns an object with these properties:
+
+ * `major`
+ * `minor`
+ * `patch`
+
+### GrooveFile
+
+#### groove.open(filename, callback)
+
+`callback(err, file)`
+
+#### file.close(callback)
+
+`callback(err)`
+
+#### file.duration()
+
+In seconds.
+
+#### file.shortNames()
+
+A comma-separated list of short names for the format.
+
+#### file.getMetadata(key, [flags])
+
+Flags:
+
+ * `groove.TAG_MATCH_CASE`
+ * `groove.TAG_DONT_OVERWRITE`
+ * `groove.TAG_APPEND`
+
+#### file.setMetadata(key, value, [flags])
+
+See `getMetadata` for flags.
+
+Pass `null` for `value` to delete a key.
+
+#### file.metadata()
+
+This returns an object populated with all the metadata.
+Updating the object does nothing. Use `setMetadata` to
+update metadata and then `save` to write changes to disk.
+
+#### file.dirty
+
+Boolean whether `save` will do anything.
+
+#### file.filename
+
+The string that was passed to `groove.open`
+
+#### file.save(callback)
+
+`callback(err)`
+
+### GroovePlaylist
+
+#### groove.createPlaylist()
+
+A playlist managers keeping an audio buffer full. To send the buffer
+to your speakers, use `playlist.createPlayer()`.
+
+#### playlist.items()
+
+Returns a read-only array of playlist items.
+Use `playlist.insert` and `playlist.remove` to modify.
+
+`[playlistItem1, playlistItem2, ...]`
+
+#### playlist.play()
+
+#### playlist.pause()
+
+#### playlist.seek(playlistItem, position)
+
+Seek to `playlistItem`, `position` seconds into the song.
+
+#### playlist.insert(file, gain, peak, nextPlaylistItem)
+
+Creates a new playlist item with file and puts it in the playlist before
+`nextPlaylistItem`. If `nextPlaylistItem` is `null`, appends the new
+item to the playlist.
+
+`gain` is a float format volume adjustment that applies only to this item.
+defaults to 1.0
+
+`peak` is float format, see `item.peak`.
+defaults to 1.0
+
+Returns the newly added playlist item.
+
+Once you add a file to the playlist, you must not `file.close()` it until
+you first remove it from the playlist.
+
+#### playlist.remove(playlistItem)
+
+Remove `playlistItem` from the playlist.
+
+Note that you are responsible for calling `file.close()` on every file
+that you open with `groove.open`. `playlist.remove` will not close files.
+
+#### playlist.position()
+
+Returns `{item, pos}` where `item` is the playlist item currently being
+decoded and `pos` is how many seconds into the song the decode head is.
+
+Note that typically you are more interested in the position of the play head,
+not the decode head. Example methods which return the play head are
+`player.position()` and `encoder.position()`.
+
+#### playlist.playing()
+
+Returns `true` or `false`.
+
+#### playlist.clear()
+
+Remove all playlist items.
+
+#### playlist.count()
+
+How many items are on the playlist.
+
+#### playlist.gain
+
+#### playlist.setGain(value)
+
+Between 0.0 and 1.0. You probably want to leave this at 1.0, since using
+replaygain will typically lower your volume a significant amount.
+
+#### playlist.setItemGain(playlistItem, gain)
+
+`gain` is a float that affects the volume of the specified playlist item only.
+To convert from dB to float, use exp(log(10) * 0.05 * dBValue).
+
+#### playlist.setItemPeak(playlistItem, peak)
+
+See `item.peak`
+
+#### playlist.setFillMode(mode)
+
+`mode` can be:
+
+ * `groove.EVERY_SINK_FULL`
+
+ This is the default behavior. The playlist will decode audio if any sinks
+ are not full. If any sinks do not drain fast enough the data will buffer up
+ in the playlist.
+
+ * `groove.ANY_SINK_FULL`
+
+ With this behavior, the playlist will stop decoding audio when any attached
+ sink is full, and then resume decoding audio every sink is not full.
+
+Defaults to `groove.EVERY_SINK_FULL`.
+
+### GroovePlaylistItem
+
+These are not instantiated directly; instead they are returned from
+`playlist.items()`.
+
+#### item.file
+
+#### item.gain
+
+A volume adjustment in float format to apply to the file when it plays.
+This is typically used for loudness compensation, for example ReplayGain.
+To convert from dB to float, use `groove.dBToFloat`
+
+#### item.peak
+
+The sample peak of this playlist item is assumed to be 1.0 in float
+format. If you know for certain that the peak is less than 1.0, you
+may set this value which may allow the volume adjustment to use
+a pure amplifier rather than a compressor. This results in slightly
+better audio quality.
+
+#### item.id
+
+Every time you obtain a playlist item from groove, you will get a fresh
+JavaScript object, but it might point to the same underlying libgroove pointer
+as another. The `id` field is a way to check if two playlist items reference
+the same one.
+
+### GroovePlayer
+
+#### groove.getDevices()
+
+Returns an array of device names which are the devices you can send audio
+to.
+
+#### groove.createPlayer()
+
+Creates a GroovePlayer instance which you can then configure by setting
+properties.
+
+#### player.deviceIndex
+
+Before calling `attach()`, set this to the index of one of the devices
+returned from `groove.getDevices()` or `null` to represent the default device.
+Use `groove.DUMMY_DEVICE` to represent a dummy audio player.
+
+#### player.targetAudioFormat
+
+The desired audio format settings with which to open the device.
+`groove.createPlayer()` defaults these to 44100 Hz,
+signed 16-bit int, stereo.
+These are preferences; if a setting cannot be used, a substitute will
+be used instead. In this case, actualAudioFormat will be updated to reflect
+the substituted values.
+
+Properties:
+
+ * `sampleRate`
+ * `channelLayout`
+ * `sampleFormat`
+
+#### player.actualAudioFormat
+
+groove sets this to the actual format you get when you open the device.
+Ideally will be the same as targetAudioFormat but might not be.
+
+Properties:
+
+ * `sampleRate`
+ * `channelLayout`
+ * `sampleFormat`
+
+#### player.deviceBufferSize
+
+how big the device buffer should be, in sample frames.
+must be a power of 2.
+`groove.createPlayer()` defaults this to 1024
+
+#### player.sinkBufferSize
+
+How big the sink buffer should be, in sample frames.
+`groove.createPlayer()` defaults this to 8192
+
+#### player.attach(playlist, callback)
+
+Sends audio to sound device.
+
+`callback(err)`
+
+#### player.detach(callback)
+
+`callback(err)`
+
+#### player.position()
+
+Returns `{item, pos}` where `item` is the playlist item currently being
+played and `pos` is how many seconds into the song the play head is.
+
+#### player.on('nowplaying', handler)
+
+Fires when the item that is now playing changes. It can be `null`.
+
+`handler()`
+
+#### player.on('bufferunderrun', handler)
+
+Fires when a buffer underrun occurs. Ideally you'll never see this.
+
+`handler()`
+
+### GrooveEncoder
+
+#### groove.createEncoder()
+
+#### encoder.bitRate
+
+select encoding quality by choosing a target bit rate
+
+#### encoder.formatShortName
+
+optional - help libgroove guess which format to use.
+`avconv -formats` to get a list of possibilities.
+
+#### encoder.codecShortName
+
+optional - help libgroove guess which codec to use.
+`avconv-codecs` to get a list of possibilities.
+
+#### encoder.filename
+
+optional - provide an example filename to help libgroove guess
+which format/codec to use.
+
+#### encoder.mimeType
+
+optional - provide a mime type string to help libgrooove guess
+which format/codec to use.
+
+#### encoder.targetAudioFormat
+
+The desired audio format settings with which to encode.
+`groove.createEncoder()` defaults these to 44100 Hz,
+signed 16-bit int, stereo.
+These are preferences; if a setting cannot be used, a substitute will
+be used instead. In this case, actualAudioFormat will be updated to reflect
+the substituted values.
+
+Properties:
+
+ * `sampleRate`
+ * `channelLayout`
+ * `sampleFormat`
+
+#### encoder.actualAudioFormat
+
+groove sets this to the actual format you get when you attach the encoder.
+Ideally will be the same as targetAudioFormat but might not be.
+
+Properties:
+
+ * `sampleRate`
+ * `channelLayout`
+ * `sampleFormat`
+
+
+#### encoder.attach(playlist, callback)
+
+`callback(err)`
+
+#### encoder.detach(callback)
+
+`callback(err)`
+
+#### encoder.getBuffer()
+
+Returns `null` if no buffer available, or an object with these properties:
+
+ * `buffer` - a node `Buffer` instance which is the encoded data for this chunk
+ this can be `null` in which case this buffer is actually the end of
+ playlist sentinel.
+ * `item` - the GroovePlaylistItem of which this buffer is encoded data for
+ * `pos` - position in seconds that this buffer represents in into the item
+
+#### encoder.on('buffer', handler)
+
+`handler()`
+
+Emitted when there is a buffer available to get. You still need to get the
+buffer with `getBuffer()`.
+
+#### encoder.position()
+
+Returns `{item, pos}` where `item` is the playlist item currently being
+encoded and `pos` is how many seconds into the song the encode head is.
+
+### GrooveLoudnessDetector
+
+#### groove.createLoudnessDetector()
+
+returns a GrooveLoudnessDetector
+
+#### detector.infoQueueSize
+
+Set this to determine how far ahead into the playlist to look.
+
+#### detector.sinkBufferSize
+
+How big the sink buffer should be, in sample frames.
+`groove.createLoudnessDetector()` defaults this to 8192
+
+#### detector.disableAlbum
+
+Set to `true` to only compute track loudness. This is faster and requires less
+memory than computing both.
+
+#### detector.attach(playlist, callback)
+
+`callback(err)`
+
+#### detector.detach(callback)
+
+`callback(err)`
+
+#### detector.getInfo()
+
+Returns `null` if no info available, or an object with these properties:
+
+ * `loudness` - loudness in LUFS
+ * `peak` - sample peak in float format of the file
+ * `duration` - duration in seconds of the track
+ * `item` - the GroovePlaylistItem that this applies to, or `null` if it applies
+ to the entire album.
+
+#### detector.position()
+
+Returns `{item, pos}` where `item` is the playlist item currently being
+detected and `pos` is how many seconds into the song the detect head is.
+
+#### detector.on('info', handler)
+
+`handler()`
+
+Emitted when there is info available to get. You still need to get the info
+with `getInfo()`.
+
+### GrooveFingerprinter
+
+#### groove.createFingerprinter()
+
+returns a GrooveFingerprinter
+
+#### groove.encodeFingerprint(rawFingerprint)
+
+Given an Array of integers which is the raw fingerprint, encode it into a
+string which can be submitted to acoustid.org.
+
+#### groove.decodeFingerprint(fingerprint)
+
+Given the fingerprint string, returns a list of integers which is the raw
+fingerprint data.
+
+#### printer.infoQueueSize
+
+Set this to determine how far ahead into the playlist to look.
+
+#### printer.sinkBufferSize
+
+How big the sink buffer should be, in sample frames.
+`groove.createFingerprinter()` defaults this to 8192
+
+#### printer.attach(playlist, callback)
+
+`callback(err)`
+
+#### printer.detach(callback)
+
+`callback(err)`
+
+#### printer.getInfo()
+
+Returns `null` if no info available, or an object with these properties:
+
+ * `fingerprint` - integer array which is the raw fingerprint
+ * `duration` - duration in seconds of the track
+ * `item` - the GroovePlaylistItem that this applies to, or `null` if it applies
+ to the entire album.
+
+#### printer.position()
+
+Returns `{item, pos}` where `item` is the playlist item currently being
+fingerprinted and `pos` is how many seconds into the song the printer head is.
+
+#### printer.on('info', handler)
+
+`handler()`
+
+Emitted when there is info available to get. You still need to get the info
+with `getInfo()`.
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 0000000..b1c647c
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,23 @@
+{
+ "targets": [
+ {
+ "target_name": "groove",
+ "sources": [
+ "src/groove.cc",
+ "src/gn_file.cc",
+ "src/gn_playlist.cc",
+ "src/gn_player.cc",
+ "src/gn_playlist_item.cc",
+ "src/gn_loudness_detector.cc",
+ "src/gn_fingerprinter.cc",
+ "src/gn_encoder.cc"
+ ],
+ "libraries": [
+ "-lgroove",
+ "-lgrooveplayer",
+ "-lgrooveloudness",
+ "-lgroovefingerprinter"
+ ]
+ }
+ ]
+}
diff --git a/example/fingerprint.js b/example/fingerprint.js
new file mode 100644
index 0000000..c8598cd
--- /dev/null
+++ b/example/fingerprint.js
@@ -0,0 +1,64 @@
+/* generate the acoustid fingerprint of songs */
+
+var groove = require('../');
+var assert = require('assert');
+var Batch = require('batch'); // npm install batch
+
+if (process.argv.length < 3) usage();
+
+var playlist = groove.createPlaylist();
+var printer = groove.createFingerprinter();
+
+printer.on('info', function() {
+ var info = printer.getInfo();
+ if (info.item) {
+ console.log(info.item.file.filename, "fingerprint:");
+ console.log(info.fingerprint);
+ } else {
+ cleanup();
+ }
+});
+
+printer.attach(playlist, function(err) {
+ assert.ifError(err);
+
+ var batch = new Batch();
+ for (var i = 2; i < process.argv.length; i += 1) {
+ batch.push(openFileFn(process.argv[i]));
+ }
+ batch.end(function(err, files) {
+ files.forEach(function(file) {
+ if (file) {
+ playlist.insert(file, null);
+ }
+ });
+ });
+});
+
+function openFileFn(filename) {
+ return function(cb) {
+ groove.open(filename, cb);
+ };
+}
+
+function cleanup() {
+ var batch = new Batch();
+ var files = playlist.items().map(function(item) { return item.file; });
+ playlist.clear();
+ files.forEach(function(file) {
+ batch.push(function(cb) {
+ file.close(cb);
+ });
+ });
+ batch.end(function(err) {
+ printer.detach(function(err) {
+ if (err) console.error(err.stack);
+ });
+ });
+}
+
+function usage() {
+ console.error("Usage: node fingerprint.js file1 file2 ...");
+ process.exit(1);
+}
+
diff --git a/example/metadata.js b/example/metadata.js
new file mode 100644
index 0000000..1d3f1b0
--- /dev/null
+++ b/example/metadata.js
@@ -0,0 +1,69 @@
+/* read or update metadata in a media file */
+
+var groove = require('../');
+
+if (process.argv.length < 3) usage();
+
+groove.setLogging(groove.LOG_INFO);
+
+var filename = process.argv[2];
+groove.open(filename, function(err, file) {
+ if (err) {
+ console.error("error opening file:", err.stack);
+ process.exit(1);
+ }
+ var key, value;
+ for (var i = 3; i < process.argv.length; i += 1) {
+ var arg = process.argv[i];
+ if (arg === '--update') {
+ if (i + 2 >= process.argv.length) {
+ console.error("--update requires 2 arguments");
+ cleanup(file, usage);
+ return;
+ }
+ key = process.argv[++i];
+ value = process.argv[++i];
+ file.setMetadata(key, value);
+ } else if (arg === '--delete') {
+ if (i + 1 >= process.argv.length) {
+ console.error("--delete requires 1 argument");
+ cleanup(file, usage);
+ return;
+ }
+ key = process.argv[++i];
+ file.setMetadata(key, null);
+ } else {
+ cleanup(file, usage);
+ return;
+ }
+ }
+
+ console.log("duration", "=", file.duration());
+ var metadata = file.metadata();
+ for (key in metadata) {
+ value = metadata[key];
+ console.log(key, "=", value);
+ }
+ if (file.dirty) {
+ file.save(handleSaveErr);
+ } else {
+ cleanup(file);
+ }
+ function handleSaveErr(err) {
+ if (err) console.error("Error saving:", err.stack);
+ cleanup(file);
+ }
+});
+
+function usage() {
+ console.error("Usage:", process.argv[0], process.argv[1],
+ "<file> [--update key value] [--delete key]");
+ process.exit(1);
+}
+
+function cleanup(file, cb) {
+ file.close(function(err) {
+ if (err) console.error("Error closing file:", err.stack);
+ if (cb) cb();
+ });
+}
diff --git a/example/playlist.js b/example/playlist.js
new file mode 100644
index 0000000..db269af
--- /dev/null
+++ b/example/playlist.js
@@ -0,0 +1,62 @@
+/* play several files in a row and then exit */
+
+var groove = require('../');
+var assert = require('assert');
+var Batch = require('batch'); // npm install batch
+
+if (process.argv.length < 3) usage();
+
+var playlist = groove.createPlaylist();
+var player = groove.createPlayer();
+
+player.on('nowplaying', function() {
+ var current = player.position();
+ if (!current.item) {
+ cleanup();
+ return;
+ }
+ var artist = current.item.file.getMetadata('artist');
+ var title = current.item.file.getMetadata('title');
+ console.log("Now playing:", artist, "-", title);
+});
+
+var batch = new Batch();
+for (var i = 2; i < process.argv.length; i += 1) {
+ batch.push(openFileFn(process.argv[i]));
+}
+batch.end(function(err, files) {
+ files.forEach(function(file) {
+ if (file) {
+ playlist.insert(file);
+ }
+ });
+ player.attach(playlist, function(err) {
+ assert.ifError(err);
+ });
+});
+function openFileFn(filename) {
+ return function(cb) {
+ groove.open(filename, cb);
+ };
+}
+
+function cleanup() {
+ var batch = new Batch();
+ var files = playlist.items().map(function(item) { return item.file; });
+ playlist.clear();
+ files.forEach(function(file) {
+ batch.push(function(cb) {
+ file.close(cb);
+ });
+ });
+ batch.end(function(err) {
+ player.detach(function(err) {
+ if (err) console.error(err.stack);
+ });
+ });
+}
+
+function usage() {
+ console.error("Usage: playlist file1 file2 ...");
+ process.exit(1);
+}
diff --git a/example/replaygain.js b/example/replaygain.js
new file mode 100644
index 0000000..2bf84b5
--- /dev/null
+++ b/example/replaygain.js
@@ -0,0 +1,67 @@
+/* replaygain scanner */
+
+var groove = require('../');
+var assert = require('assert');
+var Batch = require('batch'); // npm install batch
+
+if (process.argv.length < 3) usage();
+
+var playlist = groove.createPlaylist();
+var detector = groove.createLoudnessDetector();
+
+detector.on('info', function() {
+ var info = detector.getInfo();
+ if (info.item) {
+ console.log(info.item.file.filename, "gain:",
+ groove.loudnessToReplayGain(info.loudness), "peak:", info.peak,
+ "duration:", info.duration);
+ } else {
+ console.log("all files gain:",
+ groove.loudnessToReplayGain(info.loudness), "peak:", info.peak,
+ "duration:", info.duration);
+ cleanup();
+ }
+});
+
+detector.attach(playlist, function(err) {
+ assert.ifError(err);
+
+ var batch = new Batch();
+ for (var i = 2; i < process.argv.length; i += 1) {
+ batch.push(openFileFn(process.argv[i]));
+ }
+ batch.end(function(err, files) {
+ files.forEach(function(file) {
+ if (file) {
+ playlist.insert(file, null);
+ }
+ });
+ });
+});
+
+function openFileFn(filename) {
+ return function(cb) {
+ groove.open(filename, cb);
+ };
+}
+
+function cleanup() {
+ var batch = new Batch();
+ var files = playlist.items().map(function(item) { return item.file; });
+ playlist.clear();
+ files.forEach(function(file) {
+ batch.push(function(cb) {
+ file.close(cb);
+ });
+ });
+ batch.end(function(err) {
+ detector.detach(function(err) {
+ if (err) console.error(err.stack);
+ });
+ });
+}
+
+function usage() {
+ console.error("Usage: node replaygain.js file1 file2 ...");
+ process.exit(1);
+}
diff --git a/example/transcode.js b/example/transcode.js
new file mode 100644
index 0000000..b0fe64e
--- /dev/null
+++ b/example/transcode.js
@@ -0,0 +1,53 @@
+/* transcode a file into ogg vorbis */
+
+var groove = require('../');
+var assert = require('assert');
+var fs = require('fs');
+
+if (process.argv.length < 4) usage();
+
+groove.setLogging(groove.LOG_INFO);
+
+var playlist = groove.createPlaylist();
+var encoder = groove.createEncoder();
+encoder.formatShortName = "ogg";
+encoder.codecShortName = "vorbis";
+
+var outStream = fs.createWriteStream(process.argv[3]);
+
+encoder.on('buffer', function() {
+ var buffer;
+ while (buffer = encoder.getBuffer()) {
+ if (buffer.buffer) {
+ outStream.write(buffer.buffer);
+ } else {
+ cleanup();
+ return;
+ }
+ }
+});
+
+encoder.attach(playlist, function(err) {
+ assert.ifError(err);
+
+ groove.open(process.argv[2], function(err, file) {
+ assert.ifError(err);
+ playlist.insert(file, null);
+ });
+});
+
+function cleanup() {
+ var file = playlist.items()[0].file;
+ playlist.clear();
+ file.close(function(err) {
+ assert.ifError(err);
+ encoder.detach(function(err) {
+ assert.ifError(err);
+ });
+ });
+}
+
+function usage() {
+ console.error("Usage: node transcode.js inputfile outputfile");
+ process.exit(1);
+}
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..d28641b
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,108 @@
+var bindings = require('bindings')('groove.node');
+var EventEmitter = require('events').EventEmitter;
+var util = require('util');
+
+var DB_SCALE = Math.log(10.0) * 0.05;
+
+/* "C++ modules aren't really for doing complex things that need to be
+ * strewn across multiple modules. Just get your binding done as quick
+ * as possible, get out of there, and then wrap it in JS for all the fancy stuff
+ *
+ * -isaacs
+ */
+
+// hi-jack some of the native methods
+var bindingsCreatePlayer = bindings.createPlayer;
+var bindingsCreateLoudnessDetector = bindings.createLoudnessDetector;
+var bindingsCreateFingerprinter = bindings.createFingerprinter;
+var bindingsCreateEncoder = bindings.createEncoder;
+
+bindings.createPlayer = jsCreatePlayer;
+bindings.createEncoder = jsCreateEncoder;
+bindings.createLoudnessDetector = jsCreateLoudnessDetector;
+bindings.createFingerprinter = jsCreateFingerprinter;
+bindings.loudnessToReplayGain = loudnessToReplayGain;
+bindings.dBToFloat = dBToFloat;
+bindings.DUMMY_DEVICE = -2;
+
+module.exports = bindings;
+
+function jsCreateEncoder() {
+ var encoder = bindingsCreateEncoder(eventCb);
+
+ postHocInherit(encoder, EventEmitter);
+ EventEmitter.call(encoder);
+
+ return encoder;
+
+ function eventCb() {
+ encoder.emit('buffer');
+ }
+}
+
+function jsCreatePlayer() {
+ var player = bindingsCreatePlayer(eventCb);
+
+ postHocInherit(player, EventEmitter);
+ EventEmitter.call(player);
+
+ return player;
+
+ function eventCb(id) {
+ switch (id) {
+ case bindings._EVENT_NOWPLAYING:
+ player.emit('nowplaying');
+ break;
+ case bindings._EVENT_BUFFERUNDERRUN:
+ player.emit('bufferunderrun');
+ break;
+ }
+ }
+}
+
+function jsCreateLoudnessDetector() {
+ var detector = bindingsCreateLoudnessDetector(eventCb);
+
+ postHocInherit(detector, EventEmitter);
+ EventEmitter.call(detector);
+
+ return detector;
+
+ function eventCb() {
+ detector.emit('info');
+ }
+}
+
+function jsCreateFingerprinter() {
+ var printer = bindingsCreateFingerprinter(eventCb);
+ postHocInherit(printer, EventEmitter);
+ EventEmitter.call(printer);
+
+ return printer;
+
+ function eventCb() {
+ printer.emit('info');
+ }
+}
+
+function postHocInherit(baseInstance, Super) {
+ var baseProto = Object.getPrototypeOf(baseInstance);
+ var superProto = Super.prototype;
+ Object.keys(superProto).forEach(function(method) {
+ if (!baseProto[method]) baseProto[method] = superProto[method];
+ });
+}
+
+function clamp_rg(x) {
+ if (x > 51.0) return 51.0;
+ else if (x < -51.0) return -51.0;
+ else return x;
+}
+
+function loudnessToReplayGain(loudness) {
+ return clamp_rg(-18.0 - loudness);
+}
+
+function dBToFloat(dB) {
+ return Math.exp(dB * DB_SCALE);
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c52c358
--- /dev/null
+++ b/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "groove",
+ "version": "2.2.2",
+ "description": "bindings to libgroove - generic music player library",
+ "main": "lib/index.js",
+ "author": "Andrew Kelley <superjoe30 at gmail.com>",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/andrewrk/node-groove"
+ },
+ "scripts": {
+ "test": "node test/test.js",
+ "install": "node-gyp rebuild"
+ },
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.18"
+ },
+ "devDependencies": {
+ "ncp": "~0.5.1",
+ "tap": "~0.4.11"
+ },
+ "dependencies": {
+ "bindings": "~1.2.1"
+ },
+ "gypfile": true,
+ "bugs": {
+ "url": "https://github.com/andrewrk/node-groove/issues"
+ },
+ "homepage": "https://github.com/andrewrk/node-groove",
+ "directories": {
+ "example": "example",
+ "test": "test"
+ }
+}
diff --git a/src/gn_encoder.cc b/src/gn_encoder.cc
new file mode 100644
index 0000000..1439347
--- /dev/null
+++ b/src/gn_encoder.cc
@@ -0,0 +1,416 @@
+#include <node.h>
+#include <node_buffer.h>
+#include "gn_encoder.h"
+#include "gn_playlist.h"
+#include "gn_playlist_item.h"
+
+using namespace v8;
+
+GNEncoder::GNEncoder() {};
+GNEncoder::~GNEncoder() {
+ groove_encoder_destroy(encoder);
+ delete event_context;
+};
+
+Persistent<Function> GNEncoder::constructor;
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNEncoder::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GrooveEncoder"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(2);
+ // Methods
+ AddMethod(tpl, "attach", Attach);
+ AddMethod(tpl, "detach", Detach);
+ AddMethod(tpl, "getBuffer", GetBuffer);
+ AddMethod(tpl, "position", Position);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNEncoder::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNEncoder *obj = new GNEncoder();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNEncoder::NewInstance(GrooveEncoder *encoder) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(instance);
+ gn_encoder->encoder = encoder;
+
+ return scope.Close(instance);
+}
+
+struct AttachReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GrooveEncoder *encoder;
+ GroovePlaylist *playlist;
+ int errcode;
+ Persistent<Object> instance;
+ String::Utf8Value *format_short_name;
+ String::Utf8Value *codec_short_name;
+ String::Utf8Value *filename;
+ String::Utf8Value *mime_type;
+ GNEncoder::EventContext *event_context;
+};
+
+static void EventAsyncCb(uv_async_t *handle, int status) {
+ HandleScope scope;
+
+ GNEncoder::EventContext *context = reinterpret_cast<GNEncoder::EventContext *>(handle->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ argv[0] = Undefined();
+
+ TryCatch try_catch;
+ context->event_cb->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+
+ uv_mutex_lock(&context->mutex);
+ uv_cond_signal(&context->cond);
+ uv_mutex_unlock(&context->mutex);
+}
+
+static void EventThreadEntry(void *arg) {
+ GNEncoder::EventContext *context = reinterpret_cast<GNEncoder::EventContext *>(arg);
+ while (groove_encoder_buffer_peek(context->encoder, 1) > 0) {
+ uv_mutex_lock(&context->mutex);
+ uv_async_send(&context->event_async);
+ uv_cond_wait(&context->cond, &context->mutex);
+ uv_mutex_unlock(&context->mutex);
+ }
+}
+
+static void AttachAsync(uv_work_t *req) {
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ r->encoder->format_short_name = r->format_short_name ? **r->format_short_name : NULL;
+ r->encoder->codec_short_name = r->codec_short_name ? **r->codec_short_name : NULL;
+ r->encoder->filename = r->filename ? **r->filename : NULL;
+ r->encoder->mime_type = r->mime_type ? **r->mime_type : NULL;
+
+ r->errcode = groove_encoder_attach(r->encoder, r->playlist);
+ if (r->format_short_name) {
+ delete r->format_short_name;
+ r->format_short_name = NULL;
+ }
+ if (r->codec_short_name) {
+ delete r->codec_short_name;
+ r->codec_short_name = NULL;
+ }
+ if (r->filename) {
+ delete r->filename;
+ r->filename = NULL;
+ }
+ if (r->mime_type) {
+ delete r->mime_type;
+ r->mime_type = NULL;
+ }
+
+ GNEncoder::EventContext *context = r->event_context;
+ uv_cond_init(&context->cond);
+ uv_mutex_init(&context->mutex);
+
+ uv_async_init(uv_default_loop(), &context->event_async, EventAsyncCb);
+ context->event_async.data = context;
+
+ uv_thread_create(&context->event_thread, EventThreadEntry, context);
+}
+
+static void AttachAfter(uv_work_t *req) {
+ HandleScope scope;
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("encoder attach failed"));
+ } else {
+ argv[0] = Null();
+
+ Local<Object> actualAudioFormat = Object::New();
+ actualAudioFormat->Set(String::NewSymbol("sampleRate"),
+ Number::New(r->encoder->actual_audio_format.sample_rate));
+ actualAudioFormat->Set(String::NewSymbol("channelLayout"),
+ Number::New(r->encoder->actual_audio_format.channel_layout));
+ actualAudioFormat->Set(String::NewSymbol("sampleFormat"),
+ Number::New(r->encoder->actual_audio_format.sample_fmt));
+
+ r->instance->Set(String::NewSymbol("actualAudioFormat"), actualAudioFormat);
+ }
+
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNEncoder::Create(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ GrooveEncoder *encoder = groove_encoder_create();
+ Handle<Object> instance = NewInstance(encoder)->ToObject();
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(instance);
+ EventContext *context = new EventContext;
+ gn_encoder->event_context = context;
+ context->event_cb = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ context->encoder = encoder;
+
+ // set properties on the instance with default values from
+ // GrooveEncoder struct
+ Local<Object> targetAudioFormat = Object::New();
+ targetAudioFormat->Set(String::NewSymbol("sampleRate"),
+ Number::New(encoder->target_audio_format.sample_rate));
+ targetAudioFormat->Set(String::NewSymbol("channelLayout"),
+ Number::New(encoder->target_audio_format.channel_layout));
+ targetAudioFormat->Set(String::NewSymbol("sampleFormat"),
+ Number::New(encoder->target_audio_format.sample_fmt));
+
+ instance->Set(String::NewSymbol("bitRate"), Number::New(encoder->bit_rate));
+ instance->Set(String::NewSymbol("actualAudioFormat"), Null());
+ instance->Set(String::NewSymbol("targetAudioFormat"), targetAudioFormat);
+ instance->Set(String::NewSymbol("formatShortName"), Null());
+ instance->Set(String::NewSymbol("codecShortName"), Null());
+ instance->Set(String::NewSymbol("filename"), Null());
+ instance->Set(String::NewSymbol("mimeType"), Null());
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNEncoder::Attach(const Arguments& args) {
+ HandleScope scope;
+
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected object arg[0]")));
+ return scope.Close(Undefined());
+ }
+ if (args.Length() < 2 || !args[1]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[1]")));
+ return scope.Close(Undefined());
+ }
+
+ Local<Object> instance = args.This();
+ Local<Value> targetAudioFormatValue = instance->Get(String::NewSymbol("targetAudioFormat"));
+ if (!targetAudioFormatValue->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected targetAudioFormat to be an object")));
+ return scope.Close(Undefined());
+ }
+
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args[0]->ToObject());
+
+ AttachReq *request = new AttachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[1]));
+ request->instance = Persistent<Object>::New(args.This());
+ request->playlist = gn_playlist->playlist;
+ request->event_context = gn_encoder->event_context;
+ GrooveEncoder *encoder = gn_encoder->encoder;
+ request->encoder = encoder;
+
+ // copy the properties from our instance to the encoder
+ Local<Value> formatShortName = instance->Get(String::NewSymbol("formatShortName"));
+ if (formatShortName->IsNull() || formatShortName->IsUndefined()) {
+ request->format_short_name = NULL;
+ } else {
+ request->format_short_name = new String::Utf8Value(formatShortName->ToString());
+ }
+ Local<Value> codecShortName = instance->Get(String::NewSymbol("codecShortName"));
+ if (codecShortName->IsNull() || codecShortName->IsUndefined()) {
+ request->codec_short_name = NULL;
+ } else {
+ request->codec_short_name = new String::Utf8Value(codecShortName->ToString());
+ }
+ Local<Value> filenameStr = instance->Get(String::NewSymbol("filename"));
+ if (filenameStr->IsNull() || filenameStr->IsUndefined()) {
+ request->filename = NULL;
+ } else {
+ request->filename = new String::Utf8Value(filenameStr->ToString());
+ }
+ Local<Value> mimeType = instance->Get(String::NewSymbol("mimeType"));
+ if (mimeType->IsNull() || mimeType->IsUndefined()) {
+ request->mime_type = NULL;
+ } else {
+ request->mime_type = new String::Utf8Value(mimeType->ToString());
+ }
+
+ Local<Object> targetAudioFormat = targetAudioFormatValue->ToObject();
+ Local<Value> sampleRate = targetAudioFormat->Get(String::NewSymbol("sampleRate"));
+ double sample_rate = sampleRate->NumberValue();
+ double channel_layout = targetAudioFormat->Get(String::NewSymbol("channelLayout"))->NumberValue();
+ double sample_fmt = targetAudioFormat->Get(String::NewSymbol("sampleFormat"))->NumberValue();
+ encoder->target_audio_format.sample_rate = (int)sample_rate;
+ encoder->target_audio_format.channel_layout = (int)channel_layout;
+ encoder->target_audio_format.sample_fmt = (enum GrooveSampleFormat)(int)sample_fmt;
+
+ double bit_rate = instance->Get(String::NewSymbol("bitRate"))->NumberValue();
+ encoder->bit_rate = (int)bit_rate;
+
+ uv_queue_work(uv_default_loop(), &request->req, AttachAsync,
+ (uv_after_work_cb)AttachAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct DetachReq {
+ uv_work_t req;
+ GrooveEncoder *encoder;
+ Persistent<Function> callback;
+ int errcode;
+ GNEncoder::EventContext *event_context;
+};
+
+static void DetachAsyncFree(uv_handle_t *handle) {
+}
+
+static void DetachAsync(uv_work_t *req) {
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+ r->errcode = groove_encoder_detach(r->encoder);
+
+ uv_cond_signal(&r->event_context->cond);
+ uv_thread_join(&r->event_context->event_thread);
+ uv_cond_destroy(&r->event_context->cond);
+ uv_mutex_destroy(&r->event_context->mutex);
+ uv_close(reinterpret_cast<uv_handle_t*>(&r->event_context->event_async), DetachAsyncFree);
+}
+
+static void DetachAfter(uv_work_t *req) {
+ HandleScope scope;
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("encoder detach failed"));
+ } else {
+ argv[0] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNEncoder::Detach(const Arguments& args) {
+ HandleScope scope;
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ GrooveEncoder *encoder = gn_encoder->encoder;
+
+ if (!encoder->playlist) {
+ ThrowException(Exception::Error(String::New("detach: not attached")));
+ return scope.Close(Undefined());
+ }
+
+ DetachReq *request = new DetachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->encoder = encoder;
+ request->event_context = gn_encoder->event_context;
+
+ uv_queue_work(uv_default_loop(), &request->req, DetachAsync,
+ (uv_after_work_cb)DetachAfter);
+
+ return scope.Close(Undefined());
+}
+
+static void buffer_free(char *data, void *hint) {
+ GrooveBuffer *buffer = reinterpret_cast<GrooveBuffer*>(hint);
+ groove_buffer_unref(buffer);
+}
+
+Handle<Value> GNEncoder::GetBuffer(const Arguments& args) {
+ HandleScope scope;
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(args.This());
+ GrooveEncoder *encoder = gn_encoder->encoder;
+
+ GrooveBuffer *buffer;
+ switch (groove_encoder_buffer_get(encoder, &buffer, 0)) {
+ case GROOVE_BUFFER_YES: {
+ Local<Object> object = Object::New();
+
+ Handle<Object> bufferObject = node::Buffer::New(
+ reinterpret_cast<char*>(buffer->data[0]), buffer->size,
+ buffer_free, buffer)->handle_;
+ object->Set(String::NewSymbol("buffer"), bufferObject);
+
+ if (buffer->item) {
+ object->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(buffer->item));
+ } else {
+ object->Set(String::NewSymbol("item"), Null());
+ }
+ object->Set(String::NewSymbol("pos"), Number::New(buffer->pos));
+ object->Set(String::NewSymbol("pts"), Number::New(buffer->pts));
+ return scope.Close(object);
+ }
+ case GROOVE_BUFFER_END: {
+ Local<Object> object = Object::New();
+ object->Set(String::NewSymbol("buffer"), Null());
+ object->Set(String::NewSymbol("item"), Null());
+ object->Set(String::NewSymbol("pos"), Null());
+ object->Set(String::NewSymbol("pts"), Null());
+ return scope.Close(object);
+ }
+ default:
+ return scope.Close(Null());
+ }
+}
+
+Handle<Value> GNEncoder::Position(const Arguments& args) {
+ HandleScope scope;
+
+ GNEncoder *gn_encoder = node::ObjectWrap::Unwrap<GNEncoder>(args.This());
+ GrooveEncoder *encoder = gn_encoder->encoder;
+
+ GroovePlaylistItem *item;
+ double pos;
+ groove_encoder_position(encoder, &item, &pos);
+
+ Local<Object> obj = Object::New();
+ obj->Set(String::NewSymbol("pos"), Number::New(pos));
+ if (item) {
+ obj->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(item));
+ } else {
+ obj->Set(String::NewSymbol("item"), Null());
+ }
+ return scope.Close(obj);
+}
diff --git a/src/gn_encoder.h b/src/gn_encoder.h
new file mode 100644
index 0000000..063bf60
--- /dev/null
+++ b/src/gn_encoder.h
@@ -0,0 +1,39 @@
+#ifndef GN_ENCODER_H
+#define GN_ENCODER_H
+
+#include <node.h>
+
+#include <groove/encoder.h>
+
+class GNEncoder : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GrooveEncoder *encoder);
+
+ static v8::Handle<v8::Value> Create(const v8::Arguments& args);
+
+ struct EventContext {
+ uv_thread_t event_thread;
+ uv_async_t event_async;
+ uv_cond_t cond;
+ uv_mutex_t mutex;
+ GrooveEncoder *encoder;
+ v8::Persistent<v8::Function> event_cb;
+ };
+
+ GrooveEncoder *encoder;
+ EventContext *event_context;
+ private:
+ GNEncoder();
+ ~GNEncoder();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> Attach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Detach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> GetBuffer(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Position(const v8::Arguments& args);
+};
+
+#endif
diff --git a/src/gn_file.cc b/src/gn_file.cc
new file mode 100644
index 0000000..ef38f50
--- /dev/null
+++ b/src/gn_file.cc
@@ -0,0 +1,332 @@
+#include <node.h>
+#include "gn_file.h"
+
+using namespace v8;
+
+GNFile::GNFile() {};
+GNFile::~GNFile() {};
+
+Persistent<Function> GNFile::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::New(name), fn);
+}
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNFile::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GrooveFile"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
+ // Fields
+ AddGetter(tpl, "filename", GetFilename);
+ AddGetter(tpl, "dirty", GetDirty);
+ AddGetter(tpl, "id", GetId);
+ // Methods
+ AddMethod(tpl, "close", Close);
+ AddMethod(tpl, "getMetadata", GetMetadata);
+ AddMethod(tpl, "setMetadata", SetMetadata);
+ AddMethod(tpl, "metadata", Metadata);
+ AddMethod(tpl, "shortNames", ShortNames);
+ AddMethod(tpl, "save", Save);
+ AddMethod(tpl, "duration", Duration);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNFile::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNFile *obj = new GNFile();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNFile::NewInstance(GrooveFile *file) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(instance);
+ gn_file->file = file;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNFile::GetDirty(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(info.This());
+ return scope.Close(Boolean::New(gn_file->file->dirty));
+}
+
+Handle<Value> GNFile::GetId(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(info.This());
+ char buf[64];
+ snprintf(buf, sizeof(buf), "%p", gn_file->file);
+ return scope.Close(String::New(buf));
+}
+
+Handle<Value> GNFile::GetFilename(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(info.This());
+ return scope.Close(String::New(gn_file->file->filename));
+}
+
+Handle<Value> GNFile::GetMetadata(const Arguments& args) {
+ HandleScope scope;
+
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsString()) {
+ ThrowException(Exception::TypeError(String::New("Expected string arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ int flags = 0;
+ if (args.Length() >= 2) {
+ if (!args[1]->IsNumber()) {
+ ThrowException(Exception::TypeError(String::New("Expected number arg[1]")));
+ return scope.Close(Undefined());
+ }
+ flags = (int)args[1]->NumberValue();
+ }
+
+ String::Utf8Value key_str(args[0]->ToString());
+ GrooveTag *tag = groove_file_metadata_get(gn_file->file, *key_str, NULL, flags);
+ if (tag)
+ return scope.Close(String::New(groove_tag_value(tag)));
+ return scope.Close(Null());
+}
+
+Handle<Value> GNFile::SetMetadata(const Arguments& args) {
+ HandleScope scope;
+
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsString()) {
+ ThrowException(Exception::TypeError(String::New("Expected string arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ if (args.Length() < 2 || !args[0]->IsString()) {
+ ThrowException(Exception::TypeError(String::New("Expected string arg[1]")));
+ return scope.Close(Undefined());
+ }
+
+ int flags = 0;
+ if (args.Length() >= 3) {
+ if (!args[2]->IsNumber()) {
+ ThrowException(Exception::TypeError(String::New("Expected number arg[2]")));
+ return scope.Close(Undefined());
+ }
+ flags = (int)args[2]->NumberValue();
+ }
+
+ String::Utf8Value key_str(args[0]->ToString());
+ String::Utf8Value val_str(args[1]->ToString());
+ int err = groove_file_metadata_set(gn_file->file, *key_str, *val_str, flags);
+ if (err < 0) {
+ ThrowException(Exception::Error(String::New("set metadata failed")));
+ return scope.Close(Undefined());
+ }
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNFile::Metadata(const Arguments& args) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+ Local<Object> metadata = Object::New();
+
+ GrooveTag *tag = NULL;
+ while ((tag = groove_file_metadata_get(gn_file->file, "", tag, 0)))
+ metadata->Set(String::New(groove_tag_key(tag)), String::New(groove_tag_value(tag)));
+
+ return scope.Close(metadata);
+}
+
+Handle<Value> GNFile::ShortNames(const Arguments& args) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+ return scope.Close(String::New(groove_file_short_names(gn_file->file)));
+}
+
+Handle<Value> GNFile::Duration(const Arguments& args) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+ return scope.Close(Number::New(groove_file_duration(gn_file->file)));
+}
+
+struct CloseReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GrooveFile *file;
+};
+
+static void CloseAsync(uv_work_t *req) {
+ CloseReq *r = reinterpret_cast<CloseReq *>(req->data);
+ if (r->file) {
+ groove_file_close(r->file);
+ }
+}
+
+static void CloseAfter(uv_work_t *req) {
+ HandleScope scope;
+ CloseReq *r = reinterpret_cast<CloseReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->file) {
+ argv[0] = Null();
+ } else {
+ argv[0] = Exception::Error(String::New("file already closed"));
+ }
+
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNFile::Close(const Arguments& args) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ CloseReq *request = new CloseReq;
+
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->file = gn_file->file;
+ request->req.data = request;
+
+ gn_file->file = NULL;
+ uv_queue_work(uv_default_loop(), &request->req, CloseAsync, (uv_after_work_cb)CloseAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct OpenReq {
+ uv_work_t req;
+ GrooveFile *file;
+ String::Utf8Value *filename;
+ Persistent<Function> callback;
+};
+
+static void OpenAsync(uv_work_t *req) {
+ OpenReq *r = reinterpret_cast<OpenReq *>(req->data);
+ r->file = groove_file_open(**r->filename);
+}
+
+static void OpenAfter(uv_work_t *req) {
+ HandleScope scope;
+ OpenReq *r = reinterpret_cast<OpenReq *>(req->data);
+
+ Handle<Value> argv[2];
+ if (r->file) {
+ argv[0] = Null();
+ argv[1] = GNFile::NewInstance(r->file);
+ } else {
+ argv[0] = Exception::Error(String::New("open file failed"));
+ argv[1] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), 2, argv);
+
+ // cleanup
+ delete r->filename;
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNFile::Open(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsString()) {
+ ThrowException(Exception::TypeError(String::New("Expected string arg[0]")));
+ return scope.Close(Undefined());
+ }
+ if (args.Length() < 2 || !args[1]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[1]")));
+ return scope.Close(Undefined());
+ }
+ OpenReq *request = new OpenReq;
+
+ request->filename = new String::Utf8Value(args[0]->ToString());
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[1]));
+ request->req.data = request;
+
+ uv_queue_work(uv_default_loop(), &request->req, OpenAsync, (uv_after_work_cb)OpenAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct SaveReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GrooveFile *file;
+ int ret;
+};
+
+static void SaveAsync(uv_work_t *req) {
+ SaveReq *r = reinterpret_cast<SaveReq *>(req->data);
+ r->ret = groove_file_save(r->file);
+}
+
+static void SaveAfter(uv_work_t *req) {
+ HandleScope scope;
+ SaveReq *r = reinterpret_cast<SaveReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->ret < 0) {
+ argv[0] = Exception::Error(String::New("save failed"));
+ } else {
+ argv[0] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNFile::Save(const Arguments& args) {
+ HandleScope scope;
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ SaveReq *request = new SaveReq;
+
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->file = gn_file->file;
+ request->req.data = request;
+
+ uv_queue_work(uv_default_loop(), &request->req, SaveAsync, (uv_after_work_cb)SaveAfter);
+
+ return scope.Close(Undefined());
+}
diff --git a/src/gn_file.h b/src/gn_file.h
new file mode 100644
index 0000000..bc4b6c2
--- /dev/null
+++ b/src/gn_file.h
@@ -0,0 +1,39 @@
+#ifndef GN_FILE_H
+#define GN_FILE_H
+
+#include <node.h>
+
+#include <groove/groove.h>
+
+class GNFile : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GrooveFile *file);
+
+ static v8::Handle<v8::Value> Open(const v8::Arguments& args);
+
+ GrooveFile *file;
+ private:
+ GNFile();
+ ~GNFile();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> GetDirty(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+ static v8::Handle<v8::Value> GetId(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+ static v8::Handle<v8::Value> GetFilename(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+
+ static v8::Handle<v8::Value> Close(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Duration(const v8::Arguments& args);
+ static v8::Handle<v8::Value> GetMetadata(const v8::Arguments& args);
+ static v8::Handle<v8::Value> SetMetadata(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Metadata(const v8::Arguments& args);
+ static v8::Handle<v8::Value> ShortNames(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Save(const v8::Arguments& args);
+};
+
+#endif
diff --git a/src/gn_fingerprinter.cc b/src/gn_fingerprinter.cc
new file mode 100644
index 0000000..12d64ad
--- /dev/null
+++ b/src/gn_fingerprinter.cc
@@ -0,0 +1,373 @@
+#include <node.h>
+#include "gn_fingerprinter.h"
+#include "gn_playlist_item.h"
+#include "gn_playlist.h"
+
+using namespace v8;
+
+GNFingerprinter::GNFingerprinter() {};
+GNFingerprinter::~GNFingerprinter() {
+ groove_fingerprinter_destroy(printer);
+};
+
+Persistent<Function> GNFingerprinter::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::NewSymbol(name), fn);
+}
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNFingerprinter::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GrooveFingerprinter"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(2);
+ // Methods
+ AddMethod(tpl, "attach", Attach);
+ AddMethod(tpl, "detach", Detach);
+ AddMethod(tpl, "getInfo", GetInfo);
+ AddMethod(tpl, "position", Position);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNFingerprinter::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNFingerprinter *obj = new GNFingerprinter();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNFingerprinter::NewInstance(GrooveFingerprinter *printer) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(instance);
+ gn_printer->printer = printer;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNFingerprinter::Create(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ GrooveFingerprinter *printer = groove_fingerprinter_create();
+ if (!printer) {
+ ThrowException(Exception::Error(String::New("unable to create fingerprinter")));
+ return scope.Close(Undefined());
+ }
+
+ // set properties on the instance with default values from
+ // GrooveFingerprinter struct
+ Local<Object> instance = GNFingerprinter::NewInstance(printer)->ToObject();
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(instance);
+ EventContext *context = new EventContext;
+ gn_printer->event_context = context;
+ context->event_cb = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ context->printer = printer;
+
+
+ instance->Set(String::NewSymbol("infoQueueSize"), Number::New(printer->info_queue_size));
+ instance->Set(String::NewSymbol("sinkBufferSize"), Number::New(printer->sink_buffer_size));
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNFingerprinter::Position(const Arguments& args) {
+ HandleScope scope;
+
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(args.This());
+ GrooveFingerprinter *printer = gn_printer->printer;
+
+ GroovePlaylistItem *item;
+ double pos;
+ groove_fingerprinter_position(printer, &item, &pos);
+
+ Local<Object> obj = Object::New();
+ obj->Set(String::NewSymbol("pos"), Number::New(pos));
+ if (item) {
+ obj->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(item));
+ } else {
+ obj->Set(String::NewSymbol("item"), Null());
+ }
+ return scope.Close(obj);
+}
+
+Handle<Value> GNFingerprinter::GetInfo(const Arguments& args) {
+ HandleScope scope;
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(args.This());
+ GrooveFingerprinter *printer = gn_printer->printer;
+
+ GrooveFingerprinterInfo info;
+ if (groove_fingerprinter_info_get(printer, &info, 0) == 1) {
+ Local<Object> object = Object::New();
+
+ if (info.fingerprint) {
+ Local<Array> int_list = Array::New();
+ for (int i = 0; i < info.fingerprint_size; i += 1) {
+ int_list->Set(Number::New(i), Number::New(info.fingerprint[i]));
+ }
+ object->Set(String::NewSymbol("fingerprint"), int_list);
+ } else {
+ object->Set(String::NewSymbol("fingerprint"), Null());
+ }
+ object->Set(String::NewSymbol("duration"), Number::New(info.duration));
+
+ if (info.item) {
+ object->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(info.item));
+ } else {
+ object->Set(String::NewSymbol("item"), Null());
+ }
+
+ groove_fingerprinter_free_info(&info);
+ return scope.Close(object);
+ } else {
+ return scope.Close(Null());
+ }
+}
+
+struct AttachReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GrooveFingerprinter *printer;
+ GroovePlaylist *playlist;
+ int errcode;
+ Persistent<Object> instance;
+ GNFingerprinter::EventContext *event_context;
+};
+
+static void EventAsyncCb(uv_async_t *handle, int status) {
+ HandleScope scope;
+
+ GNFingerprinter::EventContext *context = reinterpret_cast<GNFingerprinter::EventContext *>(handle->data);
+
+ // call callback signaling that there is info ready
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ argv[0] = Null();
+
+ TryCatch try_catch;
+ context->event_cb->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+
+ uv_mutex_lock(&context->mutex);
+ uv_cond_signal(&context->cond);
+ uv_mutex_unlock(&context->mutex);
+}
+
+static void EventThreadEntry(void *arg) {
+ GNFingerprinter::EventContext *context = reinterpret_cast<GNFingerprinter::EventContext *>(arg);
+ while (groove_fingerprinter_info_peek(context->printer, 1) > 0) {
+ uv_mutex_lock(&context->mutex);
+ uv_async_send(&context->event_async);
+ uv_cond_wait(&context->cond, &context->mutex);
+ uv_mutex_unlock(&context->mutex);
+ }
+}
+
+static void AttachAsync(uv_work_t *req) {
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ r->errcode = groove_fingerprinter_attach(r->printer, r->playlist);
+
+ GNFingerprinter::EventContext *context = r->event_context;
+
+ uv_cond_init(&context->cond);
+ uv_mutex_init(&context->mutex);
+
+ uv_async_init(uv_default_loop(), &context->event_async, EventAsyncCb);
+ context->event_async.data = context;
+
+ uv_thread_create(&context->event_thread, EventThreadEntry, context);
+}
+
+static void AttachAfter(uv_work_t *req) {
+ HandleScope scope;
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("fingerprinter attach failed"));
+ } else {
+ argv[0] = Null();
+ }
+
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNFingerprinter::Attach(const Arguments& args) {
+ HandleScope scope;
+
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected object arg[0]")));
+ return scope.Close(Undefined());
+ }
+ if (args.Length() < 2 || !args[1]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[1]")));
+ return scope.Close(Undefined());
+ }
+
+ Local<Object> instance = args.This();
+
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args[0]->ToObject());
+
+ AttachReq *request = new AttachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[1]));
+ request->instance = Persistent<Object>::New(args.This());
+ request->playlist = gn_playlist->playlist;
+ GrooveFingerprinter *printer = gn_printer->printer;
+ request->printer = printer;
+ request->event_context = gn_printer->event_context;
+
+ // copy the properties from our instance to the player
+ printer->info_queue_size = (int)instance->Get(String::NewSymbol("infoQueueSize"))->NumberValue();
+ printer->sink_buffer_size = (int)instance->Get(String::NewSymbol("sinkBufferSize"))->NumberValue();
+
+ uv_queue_work(uv_default_loop(), &request->req, AttachAsync,
+ (uv_after_work_cb)AttachAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct DetachReq {
+ uv_work_t req;
+ GrooveFingerprinter *printer;
+ Persistent<Function> callback;
+ int errcode;
+ GNFingerprinter::EventContext *event_context;
+};
+
+static void DetachAsyncFree(uv_handle_t *handle) {
+ GNFingerprinter::EventContext *context = reinterpret_cast<GNFingerprinter::EventContext *>(handle->data);
+ delete context;
+}
+
+static void DetachAsync(uv_work_t *req) {
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+ r->errcode = groove_fingerprinter_detach(r->printer);
+ uv_cond_signal(&r->event_context->cond);
+ uv_thread_join(&r->event_context->event_thread);
+ uv_cond_destroy(&r->event_context->cond);
+ uv_mutex_destroy(&r->event_context->mutex);
+ uv_close(reinterpret_cast<uv_handle_t*>(&r->event_context->event_async), DetachAsyncFree);
+}
+
+static void DetachAfter(uv_work_t *req) {
+ HandleScope scope;
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("fingerprinter detach failed"));
+ } else {
+ argv[0] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNFingerprinter::Detach(const Arguments& args) {
+ HandleScope scope;
+ GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap<GNFingerprinter>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ DetachReq *request = new DetachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->printer = gn_printer->printer;
+ request->event_context = gn_printer->event_context;
+
+ uv_queue_work(uv_default_loop(), &request->req, DetachAsync,
+ (uv_after_work_cb)DetachAfter);
+
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNFingerprinter::Encode(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsArray()) {
+ ThrowException(Exception::TypeError(String::New("Expected Array arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ Local<Array> int_list = Local<Array>::Cast(args[0]);
+ int len = int_list->Length();
+ int32_t *raw_fingerprint = new int32_t[len];
+ for (int i = 0; i < len; i += 1) {
+ double val = int_list->Get(Number::New(i))->NumberValue();
+ raw_fingerprint[i] = (int32_t)val;
+ }
+ char *fingerprint;
+ groove_fingerprinter_encode(raw_fingerprint, len, &fingerprint);
+ delete[] raw_fingerprint;
+ Local<String> js_fingerprint = String::New(fingerprint);
+ groove_fingerprinter_dealloc(fingerprint);
+ return scope.Close(js_fingerprint);
+}
+
+Handle<Value> GNFingerprinter::Decode(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsString()) {
+ ThrowException(Exception::TypeError(String::New("Expected String arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ String::Utf8Value utf8fingerprint(args[0]->ToString());
+ char *fingerprint = *utf8fingerprint;
+
+ int32_t *raw_fingerprint;
+ int raw_fingerprint_len;
+ groove_fingerprinter_decode(fingerprint, &raw_fingerprint, &raw_fingerprint_len);
+ Local<Array> int_list = Array::New();
+
+ for (int i = 0; i < raw_fingerprint_len; i += 1) {
+ int_list->Set(Number::New(i), Number::New(raw_fingerprint[i]));
+ }
+ groove_fingerprinter_dealloc(raw_fingerprint);
+
+ return scope.Close(int_list);
+}
diff --git a/src/gn_fingerprinter.h b/src/gn_fingerprinter.h
new file mode 100644
index 0000000..16a5c56
--- /dev/null
+++ b/src/gn_fingerprinter.h
@@ -0,0 +1,43 @@
+#ifndef GN_FINGERPRINTER_H
+#define GN_FINGERPRINTER_H
+
+#include <node.h>
+
+#include <groovefingerprinter/fingerprinter.h>
+
+class GNFingerprinter : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GrooveFingerprinter *printer);
+
+ static v8::Handle<v8::Value> Create(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> Encode(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Decode(const v8::Arguments& args);
+
+ struct EventContext {
+ uv_thread_t event_thread;
+ uv_async_t event_async;
+ uv_cond_t cond;
+ uv_mutex_t mutex;
+ GrooveFingerprinter *printer;
+ v8::Persistent<v8::Function> event_cb;
+ };
+
+ EventContext *event_context;
+ GrooveFingerprinter *printer;
+
+ private:
+ GNFingerprinter();
+ ~GNFingerprinter();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> Attach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Detach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> GetInfo(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Position(const v8::Arguments& args);
+};
+
+#endif
diff --git a/src/gn_loudness_detector.cc b/src/gn_loudness_detector.cc
new file mode 100644
index 0000000..4d5a218
--- /dev/null
+++ b/src/gn_loudness_detector.cc
@@ -0,0 +1,320 @@
+#include <node.h>
+#include "gn_loudness_detector.h"
+#include "gn_playlist_item.h"
+#include "gn_playlist.h"
+
+using namespace v8;
+
+GNLoudnessDetector::GNLoudnessDetector() {};
+GNLoudnessDetector::~GNLoudnessDetector() {
+ groove_loudness_detector_destroy(detector);
+};
+
+Persistent<Function> GNLoudnessDetector::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::NewSymbol(name), fn);
+}
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNLoudnessDetector::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GrooveLoudnessDetector"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(2);
+ // Methods
+ AddMethod(tpl, "attach", Attach);
+ AddMethod(tpl, "detach", Detach);
+ AddMethod(tpl, "getInfo", GetInfo);
+ AddMethod(tpl, "position", Position);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNLoudnessDetector::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNLoudnessDetector *obj = new GNLoudnessDetector();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNLoudnessDetector::NewInstance(GrooveLoudnessDetector *detector) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(instance);
+ gn_detector->detector = detector;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNLoudnessDetector::Create(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ GrooveLoudnessDetector *detector = groove_loudness_detector_create();
+ if (!detector) {
+ ThrowException(Exception::Error(String::New("unable to create loudness detector")));
+ return scope.Close(Undefined());
+ }
+
+ // set properties on the instance with default values from
+ // GrooveLoudnessDetector struct
+ Local<Object> instance = GNLoudnessDetector::NewInstance(detector)->ToObject();
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(instance);
+ EventContext *context = new EventContext;
+ gn_detector->event_context = context;
+ context->event_cb = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ context->detector = detector;
+
+
+ instance->Set(String::NewSymbol("infoQueueSize"), Number::New(detector->info_queue_size));
+ instance->Set(String::NewSymbol("sinkBufferSize"), Number::New(detector->sink_buffer_size));
+ instance->Set(String::NewSymbol("disableAlbum"), Boolean::New(detector->disable_album));
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNLoudnessDetector::Position(const Arguments& args) {
+ HandleScope scope;
+
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(args.This());
+ GrooveLoudnessDetector *detector = gn_detector->detector;
+
+ GroovePlaylistItem *item;
+ double pos;
+ groove_loudness_detector_position(detector, &item, &pos);
+
+ Local<Object> obj = Object::New();
+ obj->Set(String::NewSymbol("pos"), Number::New(pos));
+ if (item) {
+ obj->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(item));
+ } else {
+ obj->Set(String::NewSymbol("item"), Null());
+ }
+ return scope.Close(obj);
+}
+
+Handle<Value> GNLoudnessDetector::GetInfo(const Arguments& args) {
+ HandleScope scope;
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(args.This());
+ GrooveLoudnessDetector *detector = gn_detector->detector;
+
+ GrooveLoudnessDetectorInfo info;
+ if (groove_loudness_detector_info_get(detector, &info, 0) == 1) {
+ Local<Object> object = Object::New();
+
+ object->Set(String::NewSymbol("loudness"), Number::New(info.loudness));
+ object->Set(String::NewSymbol("peak"), Number::New(info.peak));
+ object->Set(String::NewSymbol("duration"), Number::New(info.duration));
+
+ if (info.item) {
+ object->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(info.item));
+ } else {
+ object->Set(String::NewSymbol("item"), Null());
+ }
+
+ return scope.Close(object);
+ } else {
+ return scope.Close(Null());
+ }
+}
+
+struct AttachReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GrooveLoudnessDetector *detector;
+ GroovePlaylist *playlist;
+ int errcode;
+ Persistent<Object> instance;
+ GNLoudnessDetector::EventContext *event_context;
+};
+
+static void EventAsyncCb(uv_async_t *handle, int status) {
+ HandleScope scope;
+
+ GNLoudnessDetector::EventContext *context = reinterpret_cast<GNLoudnessDetector::EventContext *>(handle->data);
+
+ // call callback signaling that there is info ready
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ argv[0] = Null();
+
+ TryCatch try_catch;
+ context->event_cb->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+
+ uv_mutex_lock(&context->mutex);
+ uv_cond_signal(&context->cond);
+ uv_mutex_unlock(&context->mutex);
+}
+
+static void EventThreadEntry(void *arg) {
+ GNLoudnessDetector::EventContext *context = reinterpret_cast<GNLoudnessDetector::EventContext *>(arg);
+ while (groove_loudness_detector_info_peek(context->detector, 1) > 0) {
+ uv_mutex_lock(&context->mutex);
+ uv_async_send(&context->event_async);
+ uv_cond_wait(&context->cond, &context->mutex);
+ uv_mutex_unlock(&context->mutex);
+ }
+}
+
+static void AttachAsync(uv_work_t *req) {
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ r->errcode = groove_loudness_detector_attach(r->detector, r->playlist);
+
+ GNLoudnessDetector::EventContext *context = r->event_context;
+
+ uv_cond_init(&context->cond);
+ uv_mutex_init(&context->mutex);
+
+ uv_async_init(uv_default_loop(), &context->event_async, EventAsyncCb);
+ context->event_async.data = context;
+
+ uv_thread_create(&context->event_thread, EventThreadEntry, context);
+}
+
+static void AttachAfter(uv_work_t *req) {
+ HandleScope scope;
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("loudness detector attach failed"));
+ } else {
+ argv[0] = Null();
+ }
+
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNLoudnessDetector::Attach(const Arguments& args) {
+ HandleScope scope;
+
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected object arg[0]")));
+ return scope.Close(Undefined());
+ }
+ if (args.Length() < 2 || !args[1]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[1]")));
+ return scope.Close(Undefined());
+ }
+
+ Local<Object> instance = args.This();
+
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args[0]->ToObject());
+
+ AttachReq *request = new AttachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[1]));
+ request->instance = Persistent<Object>::New(args.This());
+ request->playlist = gn_playlist->playlist;
+ GrooveLoudnessDetector *detector = gn_detector->detector;
+ request->detector = detector;
+ request->event_context = gn_detector->event_context;
+
+ // copy the properties from our instance to the player
+ detector->info_queue_size = (int)instance->Get(String::NewSymbol("infoQueueSize"))->NumberValue();
+ detector->sink_buffer_size = (int)instance->Get(String::NewSymbol("sinkBufferSize"))->NumberValue();
+ detector->disable_album = (int)instance->Get(String::NewSymbol("disableAlbum"))->BooleanValue();
+
+ uv_queue_work(uv_default_loop(), &request->req, AttachAsync,
+ (uv_after_work_cb)AttachAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct DetachReq {
+ uv_work_t req;
+ GrooveLoudnessDetector *detector;
+ Persistent<Function> callback;
+ int errcode;
+ GNLoudnessDetector::EventContext *event_context;
+};
+
+static void DetachAsyncFree(uv_handle_t *handle) {
+ GNLoudnessDetector::EventContext *context = reinterpret_cast<GNLoudnessDetector::EventContext *>(handle->data);
+ delete context;
+}
+
+static void DetachAsync(uv_work_t *req) {
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+ r->errcode = groove_loudness_detector_detach(r->detector);
+ uv_cond_signal(&r->event_context->cond);
+ uv_thread_join(&r->event_context->event_thread);
+ uv_cond_destroy(&r->event_context->cond);
+ uv_mutex_destroy(&r->event_context->mutex);
+ uv_close(reinterpret_cast<uv_handle_t*>(&r->event_context->event_async), DetachAsyncFree);
+}
+
+static void DetachAfter(uv_work_t *req) {
+ HandleScope scope;
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("loudness detector detach failed"));
+ } else {
+ argv[0] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNLoudnessDetector::Detach(const Arguments& args) {
+ HandleScope scope;
+ GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap<GNLoudnessDetector>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ DetachReq *request = new DetachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->detector = gn_detector->detector;
+ request->event_context = gn_detector->event_context;
+
+ uv_queue_work(uv_default_loop(), &request->req, DetachAsync,
+ (uv_after_work_cb)DetachAfter);
+
+ return scope.Close(Undefined());
+}
diff --git a/src/gn_loudness_detector.h b/src/gn_loudness_detector.h
new file mode 100644
index 0000000..4f89d50
--- /dev/null
+++ b/src/gn_loudness_detector.h
@@ -0,0 +1,41 @@
+#ifndef GN_LOUDNESS_DETECTOR_H
+#define GN_LOUDNESS_DETECTOR_H
+
+#include <node.h>
+
+#include <grooveloudness/loudness.h>
+
+class GNLoudnessDetector : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GrooveLoudnessDetector *detector);
+
+ static v8::Handle<v8::Value> Create(const v8::Arguments& args);
+
+
+ struct EventContext {
+ uv_thread_t event_thread;
+ uv_async_t event_async;
+ uv_cond_t cond;
+ uv_mutex_t mutex;
+ GrooveLoudnessDetector *detector;
+ v8::Persistent<v8::Function> event_cb;
+ };
+
+ EventContext *event_context;
+ GrooveLoudnessDetector *detector;
+
+ private:
+ GNLoudnessDetector();
+ ~GNLoudnessDetector();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> Attach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Detach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> GetInfo(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Position(const v8::Arguments& args);
+};
+
+#endif
diff --git a/src/gn_player.cc b/src/gn_player.cc
new file mode 100644
index 0000000..a991353
--- /dev/null
+++ b/src/gn_player.cc
@@ -0,0 +1,357 @@
+#include <node.h>
+#include "gn_player.h"
+#include "gn_playlist.h"
+#include "gn_playlist_item.h"
+
+using namespace v8;
+
+GNPlayer::GNPlayer() {};
+GNPlayer::~GNPlayer() {
+ groove_player_destroy(player);
+ delete event_context;
+};
+
+Persistent<Function> GNPlayer::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::NewSymbol(name), fn);
+}
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNPlayer::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GroovePlayer"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(2);
+ // Fields
+ AddGetter(tpl, "id", GetId);
+ AddGetter(tpl, "playlist", GetPlaylist);
+ // Methods
+ AddMethod(tpl, "attach", Attach);
+ AddMethod(tpl, "detach", Detach);
+ AddMethod(tpl, "position", Position);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNPlayer::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNPlayer *obj = new GNPlayer();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNPlayer::NewInstance(GroovePlayer *player) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(instance);
+ gn_player->player = player;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNPlayer::GetId(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(info.This());
+ char buf[64];
+ snprintf(buf, sizeof(buf), "%p", gn_player->player);
+ return scope.Close(String::New(buf));
+}
+
+Handle<Value> GNPlayer::GetPlaylist(Local<String> property,
+ const AccessorInfo &info)
+{
+ HandleScope scope;
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(info.This());
+ GroovePlaylist *playlist = gn_player->player->playlist;
+ if (playlist) {
+ return scope.Close(GNPlaylist::NewInstance(playlist));
+ } else {
+ return scope.Close(Null());
+ }
+}
+
+Handle<Value> GNPlayer::Position(const Arguments& args) {
+ HandleScope scope;
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(args.This());
+ GroovePlaylistItem *item;
+ double pos;
+ groove_player_position(gn_player->player, &item, &pos);
+ Local<Object> obj = Object::New();
+ obj->Set(String::NewSymbol("pos"), Number::New(pos));
+ if (item) {
+ obj->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(item));
+ } else {
+ obj->Set(String::NewSymbol("item"), Null());
+ }
+ return scope.Close(obj);
+}
+
+struct AttachReq {
+ uv_work_t req;
+ Persistent<Function> callback;
+ GroovePlayer *player;
+ GroovePlaylist *playlist;
+ int errcode;
+ Persistent<Object> instance;
+ int device_index;
+ GNPlayer::EventContext *event_context;
+};
+
+static void EventAsyncCb(uv_async_t *handle, int status) {
+ HandleScope scope;
+
+ GNPlayer::EventContext *context = reinterpret_cast<GNPlayer::EventContext *>(handle->data);
+
+ // flush events
+ GroovePlayerEvent event;
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ while (groove_player_event_get(context->player, &event, 0) > 0) {
+ argv[0] = Number::New(event.type);
+
+ TryCatch try_catch;
+ context->event_cb->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+ }
+
+ uv_mutex_lock(&context->mutex);
+ uv_cond_signal(&context->cond);
+ uv_mutex_unlock(&context->mutex);
+}
+
+static void EventThreadEntry(void *arg) {
+ GNPlayer::EventContext *context = reinterpret_cast<GNPlayer::EventContext *>(arg);
+ while (groove_player_event_peek(context->player, 1) > 0) {
+ uv_mutex_lock(&context->mutex);
+ uv_async_send(&context->event_async);
+ uv_cond_wait(&context->cond, &context->mutex);
+ uv_mutex_unlock(&context->mutex);
+ }
+}
+
+static void AttachAsync(uv_work_t *req) {
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ r->player->device_index = r->device_index;
+ r->errcode = groove_player_attach(r->player, r->playlist);
+
+ GNPlayer::EventContext *context = r->event_context;
+
+ uv_cond_init(&context->cond);
+ uv_mutex_init(&context->mutex);
+
+ uv_async_init(uv_default_loop(), &context->event_async, EventAsyncCb);
+ context->event_async.data = context;
+
+ uv_thread_create(&context->event_thread, EventThreadEntry, context);
+}
+
+static void AttachAfter(uv_work_t *req) {
+ HandleScope scope;
+ AttachReq *r = reinterpret_cast<AttachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("player attach failed"));
+ } else {
+ argv[0] = Null();
+
+ Local<Object> actualAudioFormat = Object::New();
+ actualAudioFormat->Set(String::NewSymbol("sampleRate"),
+ Number::New(r->player->actual_audio_format.sample_rate));
+ actualAudioFormat->Set(String::NewSymbol("channelLayout"),
+ Number::New(r->player->actual_audio_format.channel_layout));
+ actualAudioFormat->Set(String::NewSymbol("sampleFormat"),
+ Number::New(r->player->actual_audio_format.sample_fmt));
+
+ r->instance->Set(String::NewSymbol("actualAudioFormat"), actualAudioFormat);
+ }
+
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNPlayer::Create(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ GroovePlayer *player = groove_player_create();
+ Handle<Object> instance = NewInstance(player)->ToObject();
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(instance);
+ EventContext *context = new EventContext;
+ gn_player->event_context = context;
+ context->event_cb = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ context->player = player;
+
+ // set properties on the instance with default values from
+ // GroovePlayer struct
+ Local<Object> targetAudioFormat = Object::New();
+ targetAudioFormat->Set(String::NewSymbol("sampleRate"),
+ Number::New(player->target_audio_format.sample_rate));
+ targetAudioFormat->Set(String::NewSymbol("channelLayout"),
+ Number::New(player->target_audio_format.channel_layout));
+ targetAudioFormat->Set(String::NewSymbol("sampleFormat"),
+ Number::New(player->target_audio_format.sample_fmt));
+
+ instance->Set(String::NewSymbol("deviceIndex"), Null());
+ instance->Set(String::NewSymbol("actualAudioFormat"), Null());
+ instance->Set(String::NewSymbol("targetAudioFormat"), targetAudioFormat);
+ instance->Set(String::NewSymbol("deviceBufferSize"),
+ Number::New(player->device_buffer_size));
+ instance->Set(String::NewSymbol("sinkBufferSize"),
+ Number::New(player->sink_buffer_size));
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNPlayer::Attach(const Arguments& args) {
+ HandleScope scope;
+
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected object arg[0]")));
+ return scope.Close(Undefined());
+ }
+ if (args.Length() < 2 || !args[1]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[1]")));
+ return scope.Close(Undefined());
+ }
+
+ Local<Object> instance = args.This();
+ Local<Value> targetAudioFormatValue = instance->Get(String::NewSymbol("targetAudioFormat"));
+ if (!targetAudioFormatValue->IsObject()) {
+ ThrowException(Exception::TypeError(String::New("Expected targetAudioFormat to be an object")));
+ return scope.Close(Undefined());
+ }
+
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args[0]->ToObject());
+
+ AttachReq *request = new AttachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[1]));
+ request->instance = Persistent<Object>::New(args.This());
+ request->playlist = gn_playlist->playlist;
+ GroovePlayer *player = gn_player->player;
+ request->player = player;
+ request->event_context = gn_player->event_context;
+
+ // copy the properties from our instance to the player
+ Local<Value> deviceIndex = instance->Get(String::NewSymbol("deviceIndex"));
+
+ if (deviceIndex->IsNull() || deviceIndex->IsUndefined()) {
+ request->device_index = -1;
+ } else {
+ request->device_index = (int) deviceIndex->NumberValue();
+ }
+ Local<Object> targetAudioFormat = targetAudioFormatValue->ToObject();
+ Local<Value> sampleRate = targetAudioFormat->Get(String::NewSymbol("sampleRate"));
+ double sample_rate = sampleRate->NumberValue();
+ double channel_layout = targetAudioFormat->Get(String::NewSymbol("channelLayout"))->NumberValue();
+ double sample_fmt = targetAudioFormat->Get(String::NewSymbol("sampleFormat"))->NumberValue();
+ player->target_audio_format.sample_rate = (int)sample_rate;
+ player->target_audio_format.channel_layout = (int)channel_layout;
+ player->target_audio_format.sample_fmt = (enum GrooveSampleFormat)(int)sample_fmt;
+
+ double device_buffer_size = instance->Get(String::NewSymbol("deviceBufferSize"))->NumberValue();
+ player->device_buffer_size = (int)device_buffer_size;
+
+ double sink_buffer_size = instance->Get(String::NewSymbol("sinkBufferSize"))->NumberValue();
+ player->sink_buffer_size = (int)sink_buffer_size;
+
+ uv_queue_work(uv_default_loop(), &request->req, AttachAsync,
+ (uv_after_work_cb)AttachAfter);
+
+ return scope.Close(Undefined());
+}
+
+struct DetachReq {
+ uv_work_t req;
+ GroovePlayer *player;
+ Persistent<Function> callback;
+ int errcode;
+ GNPlayer::EventContext *event_context;
+};
+
+static void DetachAsyncFree(uv_handle_t *handle) {
+}
+
+static void DetachAsync(uv_work_t *req) {
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+ r->errcode = groove_player_detach(r->player);
+ uv_cond_signal(&r->event_context->cond);
+ uv_thread_join(&r->event_context->event_thread);
+ uv_cond_destroy(&r->event_context->cond);
+ uv_mutex_destroy(&r->event_context->mutex);
+ uv_close(reinterpret_cast<uv_handle_t*>(&r->event_context->event_async), DetachAsyncFree);
+}
+
+static void DetachAfter(uv_work_t *req) {
+ HandleScope scope;
+ DetachReq *r = reinterpret_cast<DetachReq *>(req->data);
+
+ const unsigned argc = 1;
+ Handle<Value> argv[argc];
+ if (r->errcode < 0) {
+ argv[0] = Exception::Error(String::New("player detach failed"));
+ } else {
+ argv[0] = Null();
+ }
+ TryCatch try_catch;
+ r->callback->Call(Context::GetCurrent()->Global(), argc, argv);
+
+ delete r;
+
+ if (try_catch.HasCaught()) {
+ node::FatalException(try_catch);
+ }
+}
+
+Handle<Value> GNPlayer::Detach(const Arguments& args) {
+ HandleScope scope;
+ GNPlayer *gn_player = node::ObjectWrap::Unwrap<GNPlayer>(args.This());
+
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
+ ThrowException(Exception::TypeError(String::New("Expected function arg[0]")));
+ return scope.Close(Undefined());
+ }
+
+ DetachReq *request = new DetachReq;
+
+ request->req.data = request;
+ request->callback = Persistent<Function>::New(Local<Function>::Cast(args[0]));
+ request->player = gn_player->player;
+ request->event_context = gn_player->event_context;
+
+ uv_queue_work(uv_default_loop(), &request->req, DetachAsync,
+ (uv_after_work_cb)DetachAfter);
+
+ return scope.Close(Undefined());
+}
diff --git a/src/gn_player.h b/src/gn_player.h
new file mode 100644
index 0000000..d5092de
--- /dev/null
+++ b/src/gn_player.h
@@ -0,0 +1,47 @@
+#ifndef GN_PLAYER_H
+#define GN_PLAYER_H
+
+#include <node.h>
+
+#include <grooveplayer/player.h>
+
+class GNPlayer : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GroovePlayer *player);
+
+ static v8::Handle<v8::Value> Create(const v8::Arguments& args);
+
+ struct EventContext {
+ uv_thread_t event_thread;
+ uv_async_t event_async;
+ uv_cond_t cond;
+ uv_mutex_t mutex;
+ GroovePlayer *player;
+ v8::Persistent<v8::Function> event_cb;
+ };
+
+
+ GroovePlayer *player;
+ EventContext *event_context;
+
+ private:
+ GNPlayer();
+ ~GNPlayer();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> GetId(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+
+ static v8::Handle<v8::Value> GetPlaylist(
+ v8::Local<v8::String> property, const v8::AccessorInfo &info);
+
+ static v8::Handle<v8::Value> Attach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Detach(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Position(const v8::Arguments& args);
+};
+
+#endif
+
diff --git a/src/gn_playlist.cc b/src/gn_playlist.cc
new file mode 100644
index 0000000..050d561
--- /dev/null
+++ b/src/gn_playlist.cc
@@ -0,0 +1,240 @@
+#include <node.h>
+#include "gn_playlist.h"
+#include "gn_playlist_item.h"
+#include "gn_file.h"
+
+using namespace v8;
+
+GNPlaylist::GNPlaylist() {
+};
+GNPlaylist::~GNPlaylist() {
+ // TODO move this somewhere else because we create multiple objects with
+ // the same playlist pointer in player.playlist or encoder.playlist
+ // for example
+ groove_playlist_destroy(playlist);
+};
+
+Persistent<Function> GNPlaylist::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::NewSymbol(name), fn);
+}
+
+template <typename target_t, typename func_t>
+static void AddMethod(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->Set(String::NewSymbol(name),
+ FunctionTemplate::New(fn)->GetFunction());
+}
+
+void GNPlaylist::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GroovePlaylist"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
+ // Fields
+ AddGetter(tpl, "id", GetId);
+ AddGetter(tpl, "gain", GetGain);
+ // Methods
+ AddMethod(tpl, "play", Play);
+ AddMethod(tpl, "items", Playlist);
+ AddMethod(tpl, "pause", Pause);
+ AddMethod(tpl, "seek", Seek);
+ AddMethod(tpl, "insert", Insert);
+ AddMethod(tpl, "remove", Remove);
+ AddMethod(tpl, "position", DecodePosition);
+ AddMethod(tpl, "playing", Playing);
+ AddMethod(tpl, "clear", Clear);
+ AddMethod(tpl, "count", Count);
+ AddMethod(tpl, "setItemGain", SetItemGain);
+ AddMethod(tpl, "setItemPeak", SetItemPeak);
+ AddMethod(tpl, "setGain", SetGain);
+ AddMethod(tpl, "setFillMode", SetFillMode);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNPlaylist::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNPlaylist *obj = new GNPlaylist();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNPlaylist::NewInstance(GroovePlaylist *playlist) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(instance);
+ gn_playlist->playlist = playlist;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNPlaylist::GetId(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(info.This());
+ char buf[64];
+ snprintf(buf, sizeof(buf), "%p", gn_playlist->playlist);
+ return scope.Close(String::New(buf));
+}
+
+Handle<Value> GNPlaylist::GetGain(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(info.This());
+ return scope.Close(Number::New(gn_playlist->playlist->gain));
+}
+
+Handle<Value> GNPlaylist::Play(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ groove_playlist_play(gn_playlist->playlist);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::Playlist(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+
+ Local<Array> playlist = Array::New();
+
+ GroovePlaylistItem *item = gn_playlist->playlist->head;
+ int i = 0;
+ while (item) {
+ playlist->Set(Number::New(i), GNPlaylistItem::NewInstance(item));
+ item = item->next;
+ i += 1;
+ }
+
+ return scope.Close(playlist);
+}
+
+Handle<Value> GNPlaylist::Pause(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ groove_playlist_pause(gn_playlist->playlist);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::Seek(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GNPlaylistItem *gn_playlist_item =
+ node::ObjectWrap::Unwrap<GNPlaylistItem>(args[0]->ToObject());
+
+ double pos = args[1]->NumberValue();
+ groove_playlist_seek(gn_playlist->playlist, gn_playlist_item->playlist_item, pos);
+
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::Insert(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GNFile *gn_file = node::ObjectWrap::Unwrap<GNFile>(args[0]->ToObject());
+ double gain = 1.0;
+ double peak = 1.0;
+ if (!args[1]->IsNull() && !args[1]->IsUndefined()) {
+ gain = args[1]->NumberValue();
+ }
+ if (!args[2]->IsNull() && !args[2]->IsUndefined()) {
+ peak = args[2]->NumberValue();
+ }
+ GroovePlaylistItem *item = NULL;
+ if (!args[3]->IsNull() && !args[3]->IsUndefined()) {
+ GNPlaylistItem *gn_pl_item =
+ node::ObjectWrap::Unwrap<GNPlaylistItem>(args[3]->ToObject());
+ item = gn_pl_item->playlist_item;
+ }
+ GroovePlaylistItem *result = groove_playlist_insert(gn_playlist->playlist,
+ gn_file->file, gain, peak, item);
+
+ return scope.Close(GNPlaylistItem::NewInstance(result));
+}
+
+Handle<Value> GNPlaylist::Remove(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(args[0]->ToObject());
+ groove_playlist_remove(gn_playlist->playlist, gn_pl_item->playlist_item);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::DecodePosition(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GroovePlaylistItem *item;
+ double pos = -1.0;
+ groove_playlist_position(gn_playlist->playlist, &item, &pos);
+ Local<Object> obj = Object::New();
+ obj->Set(String::NewSymbol("pos"), Number::New(pos));
+ if (item) {
+ obj->Set(String::NewSymbol("item"), GNPlaylistItem::NewInstance(item));
+ } else {
+ obj->Set(String::NewSymbol("item"), Null());
+ }
+ return scope.Close(obj);
+}
+
+Handle<Value> GNPlaylist::Playing(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ int playing = groove_playlist_playing(gn_playlist->playlist);
+ return scope.Close(Boolean::New(playing));
+}
+
+Handle<Value> GNPlaylist::Clear(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ groove_playlist_clear(gn_playlist->playlist);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::Count(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ int count = groove_playlist_count(gn_playlist->playlist);
+ return scope.Close(Number::New(count));
+}
+
+Handle<Value> GNPlaylist::SetItemGain(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(args[0]->ToObject());
+ double gain = args[1]->NumberValue();
+ groove_playlist_set_item_gain(gn_playlist->playlist, gn_pl_item->playlist_item, gain);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::SetItemPeak(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(args[0]->ToObject());
+ double peak = args[1]->NumberValue();
+ groove_playlist_set_item_peak(gn_playlist->playlist, gn_pl_item->playlist_item, peak);
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::SetGain(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ groove_playlist_set_gain(gn_playlist->playlist, args[0]->NumberValue());
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::SetFillMode(const Arguments& args) {
+ HandleScope scope;
+ GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap<GNPlaylist>(args.This());
+ groove_playlist_set_fill_mode(gn_playlist->playlist, args[0]->NumberValue());
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GNPlaylist::Create(const Arguments& args) {
+ HandleScope scope;
+
+ GroovePlaylist *playlist = groove_playlist_create();
+ return scope.Close(GNPlaylist::NewInstance(playlist));
+}
diff --git a/src/gn_playlist.h b/src/gn_playlist.h
new file mode 100644
index 0000000..2dddbd6
--- /dev/null
+++ b/src/gn_playlist.h
@@ -0,0 +1,47 @@
+#ifndef GN_PLAYLIST_H
+#define GN_PLAYLIST_H
+
+#include <node.h>
+
+#include <groove/groove.h>
+
+class GNPlaylist : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GroovePlaylist *playlist);
+
+ static v8::Handle<v8::Value> Create(const v8::Arguments& args);
+
+ GroovePlaylist *playlist;
+
+
+ private:
+ GNPlaylist();
+ ~GNPlaylist();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> GetId(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+ static v8::Handle<v8::Value> GetGain(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+
+ static v8::Handle<v8::Value> Playlist(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Play(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Pause(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Seek(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Insert(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Remove(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Position(const v8::Arguments& args);
+ static v8::Handle<v8::Value> DecodePosition(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Playing(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Clear(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Count(const v8::Arguments& args);
+ static v8::Handle<v8::Value> SetItemGain(const v8::Arguments& args);
+ static v8::Handle<v8::Value> SetItemPeak(const v8::Arguments& args);
+ static v8::Handle<v8::Value> SetGain(const v8::Arguments& args);
+ static v8::Handle<v8::Value> SetFillMode(const v8::Arguments& args);
+};
+
+#endif
diff --git a/src/gn_playlist_item.cc b/src/gn_playlist_item.cc
new file mode 100644
index 0000000..db2cb24
--- /dev/null
+++ b/src/gn_playlist_item.cc
@@ -0,0 +1,71 @@
+#include <node.h>
+#include "gn_playlist_item.h"
+#include "gn_file.h"
+
+using namespace v8;
+
+GNPlaylistItem::GNPlaylistItem() { };
+GNPlaylistItem::~GNPlaylistItem() { };
+
+Persistent<Function> GNPlaylistItem::constructor;
+
+template <typename target_t, typename func_t>
+static void AddGetter(target_t tpl, const char* name, func_t fn) {
+ tpl->PrototypeTemplate()->SetAccessor(String::NewSymbol(name), fn);
+}
+
+void GNPlaylistItem::Init() {
+ // Prepare constructor template
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
+ tpl->SetClassName(String::NewSymbol("GroovePlaylistItem"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
+ // Fields
+ AddGetter(tpl, "file", GetFile);
+ AddGetter(tpl, "id", GetId);
+ AddGetter(tpl, "gain", GetGain);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+}
+
+Handle<Value> GNPlaylistItem::New(const Arguments& args) {
+ HandleScope scope;
+
+ GNPlaylistItem *obj = new GNPlaylistItem();
+ obj->Wrap(args.This());
+
+ return scope.Close(args.This());
+}
+
+Handle<Value> GNPlaylistItem::NewInstance(GroovePlaylistItem *playlist_item) {
+ HandleScope scope;
+
+ Local<Object> instance = constructor->NewInstance();
+
+ GNPlaylistItem *gn_playlist_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(instance);
+ gn_playlist_item->playlist_item = playlist_item;
+
+ return scope.Close(instance);
+}
+
+Handle<Value> GNPlaylistItem::GetFile(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(info.This());
+ return scope.Close(GNFile::NewInstance(gn_pl_item->playlist_item->file));
+}
+
+Handle<Value> GNPlaylistItem::GetId(Local<String> property, const AccessorInfo &info) {
+ HandleScope scope;
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(info.This());
+ char buf[64];
+ snprintf(buf, sizeof(buf), "%p", gn_pl_item->playlist_item);
+ return scope.Close(String::New(buf));
+}
+
+Handle<Value> GNPlaylistItem::GetGain(Local<String> property,
+ const AccessorInfo &info)
+{
+ HandleScope scope;
+ GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap<GNPlaylistItem>(info.This());
+ double gain = gn_pl_item->playlist_item->gain;
+ return scope.Close(Number::New(gain));
+}
diff --git a/src/gn_playlist_item.h b/src/gn_playlist_item.h
new file mode 100644
index 0000000..15645f7
--- /dev/null
+++ b/src/gn_playlist_item.h
@@ -0,0 +1,32 @@
+#ifndef GN_PLAYLIST_ITEM_H
+#define GN_PLAYLIST_ITEM_H
+
+#include <node.h>
+
+#include <groove/groove.h>
+
+class GNPlaylistItem : public node::ObjectWrap {
+ public:
+ static void Init();
+ static v8::Handle<v8::Value> NewInstance(GroovePlaylistItem *playlist_item);
+
+ GroovePlaylistItem *playlist_item;
+ private:
+ GNPlaylistItem();
+ ~GNPlaylistItem();
+
+ static v8::Persistent<v8::Function> constructor;
+ static v8::Handle<v8::Value> New(const v8::Arguments& args);
+
+ static v8::Handle<v8::Value> GetFile(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+ static v8::Handle<v8::Value> GetId(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+ static v8::Handle<v8::Value> GetGain(v8::Local<v8::String> property,
+ const v8::AccessorInfo &info);
+
+};
+
+#endif
+
+
diff --git a/src/groove.cc b/src/groove.cc
new file mode 100644
index 0000000..f4979a8
--- /dev/null
+++ b/src/groove.cc
@@ -0,0 +1,93 @@
+#include <node.h>
+#include <cstdlib>
+#include "gn_file.h"
+#include "gn_player.h"
+#include "gn_playlist.h"
+#include "gn_playlist_item.h"
+#include "gn_loudness_detector.h"
+#include "gn_fingerprinter.h"
+#include "gn_encoder.h"
+
+using namespace v8;
+using namespace node;
+
+Handle<Value> SetLogging(const Arguments& args) {
+ HandleScope scope;
+
+ if (args.Length() < 1 || !args[0]->IsNumber()) {
+ ThrowException(Exception::TypeError(String::New("Expected 1 number argument")));
+ return scope.Close(Undefined());
+ }
+ groove_set_logging(args[0]->NumberValue());
+ return scope.Close(Undefined());
+}
+
+Handle<Value> GetDevices(const Arguments& args) {
+ HandleScope scope;
+
+ Local<Array> deviceList = Array::New();
+ int device_count = groove_device_count();
+ for (int i = 0; i < device_count; i += 1) {
+ const char *name = groove_device_name(i);
+ deviceList->Set(Number::New(i), String::New(name));
+ }
+ return scope.Close(deviceList);
+}
+
+Handle<Value> GetVersion(const Arguments& args) {
+ HandleScope scope;
+
+ Local<Object> version = Object::New();
+ version->Set(String::NewSymbol("major"), Number::New(groove_version_major()));
+ version->Set(String::NewSymbol("minor"), Number::New(groove_version_minor()));
+ version->Set(String::NewSymbol("patch"), Number::New(groove_version_patch()));
+ return scope.Close(version);
+}
+
+template <typename target_t>
+static void SetProperty(target_t obj, const char* name, double n) {
+ obj->Set(String::NewSymbol(name), Number::New(n));
+}
+
+void Initialize(Handle<Object> exports) {
+ groove_init();
+ atexit(groove_finish);
+
+ GNFile::Init();
+ GNPlayer::Init();
+ GNPlaylist::Init();
+ GNPlaylistItem::Init();
+ GNLoudnessDetector::Init();
+ GNEncoder::Init();
+ GNFingerprinter::Init();
+
+ SetProperty(exports, "LOG_QUIET", GROOVE_LOG_QUIET);
+ SetProperty(exports, "LOG_ERROR", GROOVE_LOG_ERROR);
+ SetProperty(exports, "LOG_WARNING", GROOVE_LOG_WARNING);
+ SetProperty(exports, "LOG_INFO", GROOVE_LOG_INFO);
+
+ SetProperty(exports, "TAG_MATCH_CASE", GROOVE_TAG_MATCH_CASE);
+ SetProperty(exports, "TAG_DONT_OVERWRITE", GROOVE_TAG_DONT_OVERWRITE);
+ SetProperty(exports, "TAG_APPEND", GROOVE_TAG_APPEND);
+
+ SetProperty(exports, "EVERY_SINK_FULL", GROOVE_EVERY_SINK_FULL);
+ SetProperty(exports, "ANY_SINK_FULL", GROOVE_ANY_SINK_FULL);
+
+ SetProperty(exports, "_EVENT_NOWPLAYING", GROOVE_EVENT_NOWPLAYING);
+ SetProperty(exports, "_EVENT_BUFFERUNDERRUN", GROOVE_EVENT_BUFFERUNDERRUN);
+
+ SetMethod(exports, "setLogging", SetLogging);
+ SetMethod(exports, "getDevices", GetDevices);
+ SetMethod(exports, "getVersion", GetVersion);
+ SetMethod(exports, "open", GNFile::Open);
+ SetMethod(exports, "createPlayer", GNPlayer::Create);
+ SetMethod(exports, "createPlaylist", GNPlaylist::Create);
+ SetMethod(exports, "createLoudnessDetector", GNLoudnessDetector::Create);
+ SetMethod(exports, "createEncoder", GNEncoder::Create);
+ SetMethod(exports, "createFingerprinter", GNFingerprinter::Create);
+
+ SetMethod(exports, "encodeFingerprint", GNFingerprinter::Encode);
+ SetMethod(exports, "decodeFingerprint", GNFingerprinter::Decode);
+}
+
+NODE_MODULE(groove, Initialize)
diff --git a/test/danse.ogg b/test/danse.ogg
new file mode 100755
index 0000000..072df06
Binary files /dev/null and b/test/danse.ogg differ
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..3d2187c
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,146 @@
+var groove = require('../');
+var assert = require('assert');
+var path = require('path');
+var fs = require('fs');
+var ncp = require('ncp').ncp;
+var test = require('tap').test;
+var testOgg = path.join(__dirname, "danse.ogg");
+var bogusFile = __filename;
+var rwTestOgg = path.join(__dirname, "danse-rw.ogg");
+
+test("version", function(t) {
+ var ver = groove.getVersion();
+ t.strictEqual(typeof ver.major, 'number');
+ t.strictEqual(typeof ver.minor, 'number');
+ t.strictEqual(typeof ver.patch, 'number');
+ t.end();
+});
+
+test("logging", function(t) {
+ t.strictEqual(groove.LOG_ERROR, 16);
+ groove.setLogging(groove.LOG_INFO);
+ t.end();
+});
+
+test("open fails for bogus file", function(t) {
+ t.plan(1);
+ groove.open(bogusFile, function(err, file) {
+ t.equal(err.message, "open file failed");
+ });
+});
+
+test("open file and read metadata", function(t) {
+ t.plan(10);
+ groove.open(testOgg, function(err, file) {
+ t.ok(!err);
+ t.ok(file.id);
+ t.equal(file.filename, testOgg);
+ t.equal(file.dirty, false);
+ t.equal(file.metadata().TITLE, 'Danse Macabre');
+ t.equal(file.metadata().ARTIST, 'Kevin MacLeod');
+ t.equal(file.shortNames(), 'ogg');
+ t.equal(file.getMetadata('initial key'), 'C');
+ t.equal(file.getMetadata('bogus nonexisting tag'), null);
+ file.close(function(err) {
+ t.ok(!err);
+ });
+ });
+});
+
+test("update metadata", function(t) {
+ t.plan(7);
+ ncp(testOgg, rwTestOgg, function(err) {
+ t.ok(!err);
+ groove.open(rwTestOgg, doUpdate);
+ });
+ function doUpdate(err, file) {
+ t.ok(!err);
+ file.setMetadata('foo new key', "libgroove rules!");
+ t.equal(file.getMetadata('foo new key'), 'libgroove rules!');
+ file.save(function(err) {
+ t.ok(!err);
+ file.close(checkUpdate);
+ });
+ }
+ function checkUpdate(err) {
+ t.ok(!err);
+ groove.open(rwTestOgg, function(err, file) {
+ t.ok(!err);
+ t.equal(file.getMetadata('foo new key'), 'libgroove rules!', "update worked");
+ fs.unlinkSync(rwTestOgg);
+ });
+ }
+});
+
+test("create empty playlist", function (t) {
+ t.plan(2);
+ var playlist = groove.createPlaylist();
+ t.ok(playlist.id);
+ t.equivalent(playlist.items(), [], "empty playlist");
+});
+
+test("create empty player", function (t) {
+ t.plan(2);
+ var player = groove.createPlayer();
+ t.ok(player.id);
+ t.equal(player.targetAudioFormat.sampleRate, 44100);
+});
+
+test("playlist item ids", function(t) {
+ t.plan(8);
+ var playlist = groove.createPlaylist();
+ t.ok(playlist);
+ playlist.pause();
+ t.equal(playlist.playing(), false);
+ groove.open(testOgg, function(err, file) {
+ t.ok(!err, "opening file");
+ t.ok(playlist.position);
+ t.equal(playlist.gain, 1.0);
+ playlist.setGain(1.0);
+ var returned1 = playlist.insert(file, null);
+ var returned2 = playlist.insert(file, null);
+ var items1 = playlist.items();
+ var items2 = playlist.items();
+ t.equal(items1[0].id, items2[0].id);
+ t.equal(items1[0].id, returned1.id);
+ t.equal(items2[1].id, returned2.id);
+ });
+});
+
+test("create, attach, detach player", function(t) {
+ t.plan(2);
+ var playlist = groove.createPlaylist();
+ var player = groove.createPlayer();
+ player.attach(playlist, function(err) {
+ t.ok(!err, "attach");
+ player.detach(function(err) {
+ t.ok(!err, "detach");
+ });
+ });
+});
+
+test("create, attach, detach loudness detector", function(t) {
+ t.plan(2);
+ var playlist = groove.createPlaylist();
+ var detector = groove.createLoudnessDetector();
+ detector.attach(playlist, function(err) {
+ t.ok(!err, "attach");
+ detector.detach(function(err) {
+ t.ok(!err, "detach");
+ });
+ });
+});
+
+test("create, attach, detach encoder", function(t) {
+ t.plan(2);
+ var playlist = groove.createPlaylist();
+ var encoder = groove.createEncoder();
+ encoder.formatShortName = "ogg";
+ encoder.codecShortName = "vorbis";
+ encoder.attach(playlist, function(err) {
+ t.ok(!err, "attach");
+ encoder.detach(function(err) {
+ t.ok(!err, "detach");
+ });
+ });
+});
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-groove.git
More information about the Pkg-javascript-commits
mailing list