[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