Repository: andrewrk/node-groove Branch: master Commit: 905cc5c0b2c2 Files: 36 Total size: 128.0 KB Directory structure: gitextract_vdtlnlz5/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── binding.gyp ├── example/ │ ├── devices.js │ ├── fingerprint.js │ ├── metadata.js │ ├── playlist.js │ ├── replaygain.js │ ├── transcode.js │ └── waveform.js ├── lib/ │ └── index.js ├── package.json ├── src/ │ ├── device.cc │ ├── device.h │ ├── encoder.cc │ ├── encoder.h │ ├── file.cc │ ├── file.h │ ├── fingerprinter.cc │ ├── fingerprinter.h │ ├── groove.cc │ ├── groove.h │ ├── loudness_detector.cc │ ├── loudness_detector.h │ ├── player.cc │ ├── player.h │ ├── playlist.cc │ ├── playlist.h │ ├── playlist_item.cc │ ├── playlist_item.h │ ├── waveform_builder.cc │ └── waveform_builder.h └── test/ ├── danse.ogg └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /build /node_modules ================================================ FILE: CHANGELOG.md ================================================ # 3.0.0 (UNRELEASED) * `player.setItemGain` and `player.setItemPeak` are gone in favor of `player.setItemGainPeak` * `groove.getDevices()` returns an object instead of an array. * `ANY_SINK_FULL` is now default instead of `EVERY_SINK_FULL` * `player.targetAudioFormat`, `player.actualAudioFormat`, `encoder.targetAudioFormat`, `encoder.actualAudioFormat`: - `channelLayout` - instead of a number it is an array of channel ids * `sampleFormat` - sample format enum values are different * `player.deviceBufferSize` - removed. This functionality no longer exists. * `player.sinkBufferSize` - removed. This functionality no longer exists. * `detector.sinkBufferSize` - removed. This functionality no longer exists. * `printer.sinkBufferSize` - removed. This functionality no longer exists. * `encoder.sinkBufferSize` - removed. This functionality no longer exists. * `player.deviceIndex` - removed in favor of `player.device`. * `player.device` is mandatory, and you must get a device reference by calling `groove.getDevices()`. You must call `groove.connectSoundBackend()` before calling `groove.getDevices()`. * After calling `groove.createPlaylist` you must call `playlist.destroy` when finished with the playlist. * Add `GrooveWaveformBuilder` for creating waveform visualizations * player: 'nowplaying' event renamed to 'nowPlaying' * player: 'bufferunderrun' event renamed to 'bufferUnderrun' * player: add more events: 'deviceClosed', 'deviceOpened', 'deviceOpenError', 'endOfPlaylist', 'wakeup'. * player: `targetAudioFormat`, `actualAudioFormat`, and `useExactAudioFormat` no longer exist. Instead, the player always opens the device with exactly the correct audio parameters for each song, or if the device doesn't support that, the next best in terms of audio quality. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Andrew Kelley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # node-groove Node.js bindings to [libgroove](https://github.com/andrewrk/libgroove) - generic music player backend library. Live discussion in `#libgroove` on [freenode](https://freenode.net/). Here are the main interfaces. See API Documentation below for more details. * GrooveFile - represents an open audio file. * GroovePlaylist - put files in the playlist, and the playlist decodes the files and fills up the attached sinks. * GroovePlayer - attach this sink to a playlist to play the decoded audio over the system's speakers. * GrooveEncoder - attach this sink to a playlist to obtain encoded audio buffers, such as an mp3 stream. * GrooveLoudnessDetector - attach this sink to a playlist to compute how loud files sound to the human ear, along with the "true peak" value. You can use this to implement ReplayGain. * GrooveFingerprinter - attach this sink to a playlist to compute an acoustid fingerprint. This can be used to look up a file by its audio content and figure out which tags should be applied. * GrooveWaveformBuilder - attach this sink to a playlist to compute a JSON representation of an audio file. This can be used to display a visualization of the audio file. ## Usage 1. Install libgroove to your system. libgroove is a set of 4 libraries; node-groove depends on all of them. So for example on ubuntu, make sure to install libgroove-dev, libgrooveplayer-dev, libgrooveloudness-dev, and libgroovefingerprinter-dev. 2. `npm install --save groove` ### Versions * node-groove >=3.0.0 depends on libgroove >=5.0.0 * node-groove >=2.4.0 <3.0.0 depends on libgroove >=4.3.0 <5.0.0 * node-groove 2.3.4 depends on libgroove <4.3.0 See CHANGELOG.md for release notes and upgrade guide. ### 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 * example/devices.js - list the playback devices on the system * example/waveform.js - calculate a waveformjs compatible representation of a media file ## 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.overrideDuration(duration) If you know for sure the actual duration of the file, call this function to set the actual duration in seconds of the file. `GrooveWaveformBuilder` will use this value instead of `file.duration()`. This must only be called when no `GroovePlaylistItem` references to this file. #### 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()`. Note: you probably only want one playlist. In node-groove, a playlist is a low-level audio processing concept, not to be confused with user-facing playlists where users might add, remove, and re-order songs. #### playlist.destroy() When finished with your playlist you must destroy it. #### 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.setItemGainPeak(playlistItem, gain, peak) `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). See `item.peak` #### playlist.setFillMode(mode) `mode` can be: * `groove.EVERY_SINK_FULL` 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` This is the default behavior. 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()`. A `GroovePlaylistItem` is merely a pointer into a `GroovePlaylist`. If you remove a playlist item from a playlist, any playlist item references you have lying around become dangling pointers. #### item.file Read-only. #### 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` Read-only. Use `playlist.setItemGain` to modify. #### 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. Read-only. Use `playlist.setItemPeak` to modify. #### 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. Read-only. ### GroovePlayer #### groove.getDevices() Before you can call this function, you must call `groove.connectSoundBackend()`. Returns an object like this: ```js { list: [ { name: "User-Friendly Device Name", id: "unique device ID that persists across plugs and unplugs", isRaw: false, // true if this device would claim exclusive access probeError: 3, // non zero if scanning this device did not work }, //... ], defaultIndex: 0, } ``` #### groove.connectSoundBackend([backend]) `backend` is optional. If left blank the best backend is automatically selected. Otherwise it can be one of these: * `groove.BACKEND_JACK` * `groove.BACKEND_PULSEAUDIO` * `groove.BACKEND_ALSA` * `groove.BACKEND_COREAUDIO` * `groove.BACKEND_WASAPI` * `groove.BACKEND_DUMMY` #### groove.disconnectSoundBackend() #### groove.createPlayer() Creates a GroovePlayer instance which you can then configure by setting properties. #### player.device Before calling `attach()`, set this to one of the devices returned from `groove.getDevices()`. #### 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()` #### player.on('devicereopened', handler) Fires when you have set `useExactAudioFormat` to `true` and the audio device has been closed and re-opened to match incoming audio data. `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` - array of channel ids * `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` - array of channel ids * `sampleFormat` #### encoder.sinkBufferSize How big the sink buffer should be, in sample frames. `createEncoder` defaults this to 8192 #### encoder.encodedBufferSize How big the encoded audio buffer should be, in bytes. `createEncoder` defaults this to 16384 #### 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.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.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()`. ### GrooveWaveformBuilder #### groove.createWaveformBuilder() returns a GrooveWaveformBuilder #### waveform.widthInFrames How many frames wide the waveform data will be. Defaults to 1920. If you have a song with 100 frames and `widthInFrames` is 50, then each waveform data frame will correspond to 2 frames of the original song. #### printer.infoQueueSizeBytes Set this to determine how far ahead into the playlist to look. #### waveform.attach(playlist, callback) `callback(err)` #### waveform.detach(callback) `callback(err)` #### waveform.getInfo() Returns `null` if no info available, or an object with these properties: * `buffer` - A `Buffer` of the waveform data, one unsigned 8 bit integer per `widthInFrames`. * `expectedDuration` - This is the duration in seconds that was used to create the waveform data. If this is different than `actualDuration` then the data is invalid and must be re-calculated, this time using `file.overrideDuration()` * `actualDuration` - This is the correct duration in seconds for the track, known only after waveform calculation is complete. * `item` - the GroovePlaylistItem that this applies to, or `null` if this info signals the end of playlist. #### waveform.position() Returns `{item, pos}` where `item` is the playlist item currently being calculated and `pos` is how many seconds into the song the waveform head is. #### waveform.on('info', handler) `handler()` Emitted when there is info available to get. You still need to get the info with `getInfo()`. ================================================ FILE: binding.gyp ================================================ { "targets": [ { "target_name": "groove", "sources": [ "src/player.cc", "src/groove.cc", "src/file.cc", "src/playlist.cc", "src/playlist_item.cc", "src/waveform_builder.cc", "src/loudness_detector.cc", "src/fingerprinter.cc", "src/encoder.cc", "src/device.cc", ], "libraries": [ "-lgroove" ], "include_dirs": [ "= 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], " [--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(); }); } ================================================ FILE: example/playlist.js ================================================ /* play several files in a row and then exit */ var groove = require('../'); var Pend = require('pend'); if (process.argv.length < 3) usage(); var playlist = groove.createPlaylist(); var player = groove.createPlayer(); groove.connectSoundBackend(); var devices = groove.getDevices(); var defaultDevice = devices.list[devices.defaultIndex]; player.device = defaultDevice; 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 files = []; var pend = new Pend(); for (var i = 2; i < process.argv.length; i += 1) { var o = { filename: process.argv[i], file: null, }; files.push(o); pend.go(openFileFn(o)); } pend.wait(function(err) { if (err) throw err; files.forEach(function(o) { playlist.insert(o.file); }); player.attach(playlist, function(err) { if (err) throw err; }); }); function openFileFn(o, filename) { return function(cb) { groove.open(o.filename, function(err, file) { if (err) return cb(err); o.file = file; cb(); }); }; } function cleanup() { playlist.clear(); files.forEach(function(o) { pend.go(function(cb) { o.file.close(cb); }); }); pend.wait(function(err) { if (err) throw err; player.detach(function(err) { if (err) throw err; playlist.destroy(); }); }); } function usage() { console.error("Usage: playlist file1 file2 ..."); process.exit(1); } ================================================ FILE: example/replaygain.js ================================================ /* replaygain scanner */ var groove = require('../'); var Pend = require('pend'); 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(); } }); var files = []; var pend = new Pend(); detector.attach(playlist, function(err) { if (err) throw err; var pend = new Pend(); for (var i = 2; i < process.argv.length; i += 1) { var o = { filename: process.argv[i], file: null, }; files.push(o); pend.go(openFileFn(o)); } pend.wait(function(err) { if (err) throw err; files.forEach(function(o) { playlist.insert(o.file, null); }); }); }); function openFileFn(o) { return function(cb) { groove.open(o.filename, function(err, file) { if (err) return cb(err); o.file = file; cb(); }); }; } function cleanup() { playlist.clear(); files.forEach(function(o) { pend.go(function(cb) { o.file.close(cb); }); }); pend.wait(function(err) { if (err) throw err; detector.detach(function(err) { if (err) throw err; playlist.destroy(); }); }); } function usage() { console.error("Usage: node replaygain.js file1 file2 ..."); process.exit(1); } ================================================ FILE: example/transcode.js ================================================ /* transcode a file into ogg vorbis */ var groove = require('../'); 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) { if (err) throw err; groove.open(process.argv[2], function(err, file) { if (err) throw err; playlist.insert(file, null); }); }); function cleanup() { var file = playlist.items()[0].file; playlist.clear(); file.close(function(err) { if (err) throw err; encoder.detach(function(err) { if (err) throw err; playlist.destroy(); }); }); } function usage() { console.error("Usage: node transcode.js inputfile outputfile"); process.exit(1); } ================================================ FILE: example/waveform.js ================================================ /* calculate a waveformjs compatible representation of a media file */ var groove = require('../'); main(); function main() { var inputFilename = null; for (var i = 2; i < process.argv.length; i += 1) { var arg = process.argv[i]; var overrideDuration = null; if (arg[0] === "-" && arg[1] === "-") { if (++i < process.argv.length) { if (arg === "--override-duration") { overrideDuration = parseFloat(process.argv[i]); } else { usageAndExit(); } } else { usageAndExit(); } } else if (!inputFilename) { inputFilename = arg; } else { usageAndExit(); } } if (!inputFilename) usageAndExit(); var playlist = groove.createPlaylist(); var waveform = groove.createWaveformBuilder(); var file = null; waveform.on('info', function() { var info = waveform.getInfo(); if (!info.item) { cleanup(); return; } if (Math.abs(info.expectedDuration - info.actualDuration) > 0.1) { console.error("invalid duration. re-run with --override-duration " + info.actualDuration); process.exit(1); return; } var data = { duration: info.actualDuration, waveformjs: Array.prototype.slice.call(info.buffer, 0), }; console.log(JSON.stringify(data)); }); groove.open(inputFilename, function(err, openedFile) { if (err) throw err; file = openedFile; if (overrideDuration) { file.overrideDuration(overrideDuration); } playlist.insert(file, null); waveform.attach(playlist, function(err) { if (err) throw err; }); }); function cleanup() { playlist.clear(); waveform.detach(function(err) { if (err) throw err; playlist.destroy(); file.close(function(err) { if (err) throw err; }); }); } } function usageAndExit() { console.error("Usage: node waveform.js [--override-duration seconds] file"); process.exit(1); } ================================================ FILE: lib/index.js ================================================ 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; var bindingsCreateWaveformBuilder = bindings.createWaveformBuilder; bindings.createPlayer = jsCreatePlayer; bindings.createEncoder = jsCreateEncoder; bindings.createLoudnessDetector = jsCreateLoudnessDetector; bindings.createFingerprinter = jsCreateFingerprinter; bindings.createWaveformBuilder = jsCreateWaveformBuilder; bindings.loudnessToReplayGain = loudnessToReplayGain; bindings.dBToFloat = dBToFloat; 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; case bindings._EVENT_DEVICE_CLOSED: player.emit('deviceClosed'); break; case bindings._EVENT_DEVICE_OPENED: player.emit('deviceOpened'); break; case bindings._EVENT_DEVICE_OPEN_ERROR: player.emit('deviceOpenError'); break; case bindings._EVENT_END_OF_PLAYLIST: player.emit('endOfPlaylist'); break; case bindings._EVENT_WAKEUP: player.emit('wakeup'); 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 jsCreateWaveformBuilder() { var waveform = bindingsCreateWaveformBuilder(eventCb); postHocInherit(waveform, EventEmitter); EventEmitter.call(waveform); return waveform; function eventCb() { waveform.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); } ================================================ FILE: package.json ================================================ { "name": "groove", "version": "2.4.0", "description": "bindings to libgroove - generic music player library", "main": "lib/index.js", "author": "Andrew Kelley ", "repository": { "type": "git", "url": "https://github.com/andrewrk/node-groove" }, "scripts": { "test": "mocha --reporter spec", "install": "node-gyp rebuild" }, "license": "MIT", "devDependencies": { "mocha": "~2.4.5", "ncp": "~2.0.0", "pend": "~1.2.0" }, "dependencies": { "bindings": "~1.2.1", "nan": "~2.3.5" }, "gypfile": true, "bugs": { "url": "https://github.com/andrewrk/node-groove/issues" }, "homepage": "https://github.com/andrewrk/node-groove", "directories": { "example": "example", "test": "test" } } ================================================ FILE: src/device.cc ================================================ #include "device.h" #include "file.h" using namespace v8; GNDevice::GNDevice() { }; GNDevice::~GNDevice() { soundio_device_unref(device); }; static Nan::Persistent constructor; void GNDevice::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("SoundIoDevice").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("name").ToLocalChecked(), GetName); Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); Nan::SetAccessor(proto, Nan::New("softwareLatencyMin").ToLocalChecked(), GetSoftwareLatencyMin); Nan::SetAccessor(proto, Nan::New("softwareLatencyMax").ToLocalChecked(), GetSoftwareLatencyMax); Nan::SetAccessor(proto, Nan::New("softwareLatencyCurrent").ToLocalChecked(), GetSoftwareLatencyCurrent); Nan::SetAccessor(proto, Nan::New("isRaw").ToLocalChecked(), GetIsRaw); Nan::SetAccessor(proto, Nan::New("probeError").ToLocalChecked(), GetProbeError); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNDevice::New) { Nan::HandleScope scope; GNDevice *obj = new GNDevice(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNDevice::NewInstance(SoundIoDevice *device) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNDevice *gn_device = node::ObjectWrap::Unwrap(instance); gn_device->device = device; return scope.Escape(instance); } NAN_GETTER(GNDevice::GetName) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->name).ToLocalChecked()); } NAN_GETTER(GNDevice::GetId) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->id).ToLocalChecked()); } NAN_GETTER(GNDevice::GetSoftwareLatencyMin) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->software_latency_min)); } NAN_GETTER(GNDevice::GetSoftwareLatencyMax) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->software_latency_max)); } NAN_GETTER(GNDevice::GetSoftwareLatencyCurrent) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->software_latency_current)); } NAN_GETTER(GNDevice::GetIsRaw) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->is_raw)); } NAN_GETTER(GNDevice::GetProbeError) { GNDevice *gn_device = node::ObjectWrap::Unwrap(info.This()); SoundIoDevice *device = gn_device->device; info.GetReturnValue().Set(Nan::New(device->probe_error)); } ================================================ FILE: src/device.h ================================================ #ifndef GN_DEVICE_H #define GN_DEVICE_H #include #include #include class GNDevice : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(SoundIoDevice *device); SoundIoDevice *device; private: GNDevice(); ~GNDevice(); static NAN_METHOD(New); static NAN_GETTER(GetName); static NAN_GETTER(GetId); static NAN_GETTER(GetSoftwareLatencyMin); static NAN_GETTER(GetSoftwareLatencyMax); static NAN_GETTER(GetSoftwareLatencyCurrent); static NAN_GETTER(GetIsRaw); static NAN_GETTER(GetProbeError); }; #endif ================================================ FILE: src/encoder.cc ================================================ #include #include "encoder.h" #include "playlist.h" #include "playlist_item.h" #include "groove.h" using namespace v8; GNEncoder::GNEncoder() {}; GNEncoder::~GNEncoder() { groove_encoder_destroy(encoder); delete event_context->event_cb; delete event_context; }; static Nan::Persistent constructor; void GNEncoder::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GrooveEncoder").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(2); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("actualAudioFormat").ToLocalChecked(), GetActualAudioFormat); // Methods Nan::SetPrototypeMethod(tpl, "attach", Attach); Nan::SetPrototypeMethod(tpl, "detach", Detach); Nan::SetPrototypeMethod(tpl, "getBuffer", GetBuffer); Nan::SetPrototypeMethod(tpl, "position", Position); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNEncoder::New) { Nan::HandleScope scope; GNEncoder *obj = new GNEncoder(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } NAN_GETTER(GNEncoder::GetActualAudioFormat) { Nan::HandleScope scope; GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(info.This()); GrooveEncoder *encoder = gn_encoder->encoder; Local layout = Nan::New(); for (int ch = 0; ch < encoder->actual_audio_format.layout.channel_count; ch += 1) { Nan::Set(layout, Nan::New(ch), Nan::New(encoder->actual_audio_format.layout.channels[ch])); } Local actualAudioFormat = Nan::New(); Nan::Set(actualAudioFormat, Nan::New("sampleRate").ToLocalChecked(), Nan::New(encoder->actual_audio_format.sample_rate)); Nan::Set(actualAudioFormat, Nan::New("channelLayout").ToLocalChecked(), layout); Nan::Set(actualAudioFormat, Nan::New("sampleFormat").ToLocalChecked(), Nan::New(encoder->actual_audio_format.format)); info.GetReturnValue().Set(actualAudioFormat); } Local GNEncoder::NewInstance(GrooveEncoder *encoder) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(instance); gn_encoder->encoder = encoder; return scope.Escape(instance); } static void EncoderEventAsyncCb(uv_async_t *handle) { Nan::HandleScope scope; GNEncoder::EventContext *context = reinterpret_cast(handle->data); const unsigned argc = 1; Local argv[argc]; argv[0] = Nan::Undefined(); TryCatch try_catch; context->event_cb->Call(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 EncoderEventThreadEntry(void *arg) { GNEncoder::EventContext *context = reinterpret_cast(arg); while (groove_encoder_buffer_peek(context->encoder, 1) > 0) { uv_mutex_lock(&context->mutex); if (context->emit_buffer_ok) { context->emit_buffer_ok = false; uv_async_send(&context->event_async); } uv_cond_wait(&context->cond, &context->mutex); uv_mutex_unlock(&context->mutex); } } class EncoderAttachWorker : public Nan::AsyncWorker { public: EncoderAttachWorker(Nan::Callback *callback, GrooveEncoder *encoder, GroovePlaylist *playlist, GNEncoder::EventContext *event_context, String::Utf8Value *format_short_name, String::Utf8Value *codec_short_name, String::Utf8Value *filename, String::Utf8Value *mime_type) : Nan::AsyncWorker(callback) { this->encoder = encoder; this->playlist = playlist; this->event_context = event_context; this->format_short_name = format_short_name; this->codec_short_name = codec_short_name; this->filename = filename; this->mime_type = mime_type; } ~EncoderAttachWorker() { delete format_short_name; delete codec_short_name; delete filename; delete mime_type; } void Execute() { encoder->format_short_name = format_short_name ? **format_short_name : NULL; encoder->codec_short_name = codec_short_name ? **codec_short_name : NULL; encoder->filename = filename ? **filename : NULL; encoder->mime_type = mime_type ? **mime_type : NULL; int err; if ((err = groove_encoder_attach(encoder, playlist))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_init(&event_context->cond); uv_mutex_init(&event_context->mutex); event_context->event_async.data = event_context; uv_async_init(uv_default_loop(), &event_context->event_async, EncoderEventAsyncCb); uv_thread_create(&event_context->event_thread, EncoderEventThreadEntry, event_context); } GrooveEncoder *encoder; GroovePlaylist *playlist; GNEncoder::EventContext *event_context; String::Utf8Value *format_short_name; String::Utf8Value *codec_short_name; String::Utf8Value *filename; String::Utf8Value *mime_type; }; NAN_METHOD(GNEncoder::Create) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } GrooveEncoder *encoder = groove_encoder_create(get_groove()); Local instance = NewInstance(encoder)->ToObject(); GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(instance); EventContext *context = new EventContext; gn_encoder->event_context = context; context->emit_buffer_ok = true; context->event_cb = new Nan::Callback(info[0].As()); context->encoder = encoder; // set properties on the instance with default values from // GrooveEncoder struct Local layout = Nan::New(); for (int ch = 0; ch < encoder->target_audio_format.layout.channel_count; ch += 1) { Nan::Set(layout, Nan::New(ch), Nan::New(encoder->target_audio_format.layout.channels[ch])); } Local targetAudioFormat = Nan::New(); Nan::Set(targetAudioFormat, Nan::New("sampleRate").ToLocalChecked(), Nan::New(encoder->target_audio_format.sample_rate)); Nan::Set(targetAudioFormat, Nan::New("channelLayout").ToLocalChecked(), layout); Nan::Set(targetAudioFormat, Nan::New("sampleFormat").ToLocalChecked(), Nan::New(encoder->target_audio_format.format)); Nan::Set(instance, Nan::New("bitRate").ToLocalChecked(), Nan::New(encoder->bit_rate)); Nan::Set(instance, Nan::New("actualAudioFormat").ToLocalChecked(), Nan::Null()); Nan::Set(instance, Nan::New("targetAudioFormat").ToLocalChecked(), targetAudioFormat); Nan::Set(instance, Nan::New("formatShortName").ToLocalChecked(), Nan::Null()); Nan::Set(instance, Nan::New("codecShortName").ToLocalChecked(), Nan::Null()); Nan::Set(instance, Nan::New("filename").ToLocalChecked(), Nan::Null()); Nan::Set(instance, Nan::New("mimeType").ToLocalChecked(), Nan::Null()); Nan::Set(instance, Nan::New("encodedBufferSize").ToLocalChecked(), Nan::New(encoder->encoded_buffer_size)); info.GetReturnValue().Set(instance); } NAN_METHOD(GNEncoder::Attach) { Nan::HandleScope scope; GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsObject()) { Nan::ThrowTypeError("Expected object arg[0]"); return; } if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); Local instance = info.This(); Local targetAudioFormatValue = instance->Get(Nan::New("targetAudioFormat").ToLocalChecked()); if (!targetAudioFormatValue->IsObject()) { Nan::ThrowTypeError("Expected targetAudioFormat to be an object"); return; } GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info[0]->ToObject()); GrooveEncoder *encoder = gn_encoder->encoder; // copy the properties from our instance to the encoder Local formatShortName = instance->Get(Nan::New("formatShortName").ToLocalChecked()); String::Utf8Value *format_short_name; if (formatShortName->IsNull() || formatShortName->IsUndefined()) { format_short_name = NULL; } else { format_short_name = new String::Utf8Value(formatShortName->ToString()); } Local codecShortName = instance->Get(Nan::New("codecShortName").ToLocalChecked()); String::Utf8Value *codec_short_name; if (codecShortName->IsNull() || codecShortName->IsUndefined()) { codec_short_name = NULL; } else { codec_short_name = new String::Utf8Value(codecShortName->ToString()); } Local filenameStr = instance->Get(Nan::New("filename").ToLocalChecked()); String::Utf8Value *filename; if (filenameStr->IsNull() || filenameStr->IsUndefined()) { filename = NULL; } else { filename = new String::Utf8Value(filenameStr->ToString()); } Local mimeType = instance->Get(Nan::New("mimeType").ToLocalChecked()); String::Utf8Value *mime_type; if (mimeType->IsNull() || mimeType->IsUndefined()) { mime_type = NULL; } else { mime_type = new String::Utf8Value(mimeType->ToString()); } Local targetAudioFormat = targetAudioFormatValue->ToObject(); Local layout = Local::Cast( targetAudioFormat->Get(Nan::New("channelLayout").ToLocalChecked())); encoder->target_audio_format.layout.channel_count = layout->Length(); for (int ch = 0; ch < encoder->target_audio_format.layout.channel_count; ch += 1) { Local channelId = layout->Get(Nan::New(ch)); encoder->target_audio_format.layout.channels[ch] = (SoundIoChannelId)(int)channelId->NumberValue(); } double sample_fmt = targetAudioFormat->Get(Nan::New("sampleFormat").ToLocalChecked())->NumberValue(); encoder->target_audio_format.format = (SoundIoFormat)(int)sample_fmt; Local sampleRate = targetAudioFormat->Get(Nan::New("sampleRate").ToLocalChecked()); double sample_rate = sampleRate->NumberValue(); encoder->target_audio_format.sample_rate = (int)sample_rate; double bit_rate = instance->Get(Nan::New("bitRate").ToLocalChecked())->NumberValue(); encoder->bit_rate = (int)bit_rate; double encoded_buffer_size = instance->Get(Nan::New("encodedBufferSize").ToLocalChecked())->NumberValue(); encoder->encoded_buffer_size = (int)encoded_buffer_size; AsyncQueueWorker(new EncoderAttachWorker(callback, encoder, gn_playlist->playlist, gn_encoder->event_context, format_short_name, codec_short_name, filename, mime_type)); } class EncoderDetachWorker : public Nan::AsyncWorker { public: EncoderDetachWorker(Nan::Callback *callback, GrooveEncoder *encoder, GNEncoder::EventContext *event_context) : Nan::AsyncWorker(callback) { this->encoder = encoder; this->event_context = event_context; } ~EncoderDetachWorker() {} void Execute() { int err; if ((err = groove_encoder_detach(encoder))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_signal(&event_context->cond); uv_thread_join(&event_context->event_thread); uv_cond_destroy(&event_context->cond); uv_mutex_destroy(&event_context->mutex); uv_close(reinterpret_cast(&event_context->event_async), NULL); } GrooveEncoder *encoder; GNEncoder::EventContext *event_context; }; NAN_METHOD(GNEncoder::Detach) { Nan::HandleScope scope; GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); GrooveEncoder *encoder = gn_encoder->encoder; if (!encoder->playlist) { Nan::ThrowTypeError("detach: not attached"); return; } AsyncQueueWorker(new EncoderDetachWorker(callback, encoder, gn_encoder->event_context)); } static void encoder_buffer_free(char *data, void *hint) { GrooveBuffer *buffer = reinterpret_cast(hint); groove_buffer_unref(buffer); } NAN_METHOD(GNEncoder::GetBuffer) { Nan::HandleScope scope; GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(info.This()); GrooveEncoder *encoder = gn_encoder->encoder; GrooveBuffer *buffer; int buf_result = groove_encoder_buffer_get(encoder, &buffer, 0); uv_mutex_lock(&gn_encoder->event_context->mutex); gn_encoder->event_context->emit_buffer_ok = true; uv_cond_signal(&gn_encoder->event_context->cond); uv_mutex_unlock(&gn_encoder->event_context->mutex); switch (buf_result) { case GROOVE_BUFFER_YES: { Local object = Nan::New(); Nan::MaybeLocal bufferObject = Nan::NewBuffer( reinterpret_cast(buffer->data[0]), buffer->size, encoder_buffer_free, buffer); Nan::Set(object, Nan::New("buffer").ToLocalChecked(), bufferObject.ToLocalChecked()); if (buffer->item) { Nan::Set(object, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(buffer->item)); } else { Nan::Set(object, Nan::New("item").ToLocalChecked(), Nan::Null()); } Nan::Set(object, Nan::New("pos").ToLocalChecked(), Nan::New(buffer->pos)); Nan::Set(object, Nan::New("pts").ToLocalChecked(), Nan::New(buffer->pts)); info.GetReturnValue().Set(object); break; } case GROOVE_BUFFER_END: { Local object = Nan::New(); Nan::Set(object, Nan::New("buffer").ToLocalChecked(), Nan::Null()); Nan::Set(object, Nan::New("item").ToLocalChecked(), Nan::Null()); Nan::Set(object, Nan::New("pos").ToLocalChecked(), Nan::Null()); Nan::Set(object, Nan::New("pts").ToLocalChecked(), Nan::Null()); info.GetReturnValue().Set(object); break; } default: info.GetReturnValue().Set(Nan::Null()); } } NAN_METHOD(GNEncoder::Position) { Nan::HandleScope scope; GNEncoder *gn_encoder = node::ObjectWrap::Unwrap(info.This()); GrooveEncoder *encoder = gn_encoder->encoder; GroovePlaylistItem *item; double pos; groove_encoder_position(encoder, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Nan::Set(obj, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(item)); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } ================================================ FILE: src/encoder.h ================================================ #ifndef GN_ENCODER_H #define GN_ENCODER_H #include #include #include class GNEncoder : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GrooveEncoder *encoder); static NAN_METHOD(Create); struct EventContext { uv_thread_t event_thread; uv_async_t event_async; uv_cond_t cond; uv_mutex_t mutex; GrooveEncoder *encoder; Nan::Callback *event_cb; bool emit_buffer_ok; }; GrooveEncoder *encoder; EventContext *event_context; private: GNEncoder(); ~GNEncoder(); static NAN_METHOD(New); static NAN_GETTER(GetActualAudioFormat); static NAN_METHOD(Attach); static NAN_METHOD(Detach); static NAN_METHOD(GetBuffer); static NAN_METHOD(Position); }; #endif ================================================ FILE: src/file.cc ================================================ #include #include "file.h" #include "groove.h" using namespace v8; GNFile::GNFile() {}; GNFile::~GNFile() {}; static Nan::Persistent constructor; void GNFile::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GrooveFile").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("filename").ToLocalChecked(), GetFilename); Nan::SetAccessor(proto, Nan::New("dirty").ToLocalChecked(), GetDirty); Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); // Methods Nan::SetPrototypeMethod(tpl, "close", Close); Nan::SetPrototypeMethod(tpl, "getMetadata", GetMetadata); Nan::SetPrototypeMethod(tpl, "setMetadata", SetMetadata); Nan::SetPrototypeMethod(tpl, "metadata", Metadata); Nan::SetPrototypeMethod(tpl, "shortNames", ShortNames); Nan::SetPrototypeMethod(tpl, "save", Save); Nan::SetPrototypeMethod(tpl, "duration", Duration); Nan::SetPrototypeMethod(tpl, "overrideDuration", OverrideDuration); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNFile::New) { Nan::HandleScope scope; assert(info.IsConstructCall()); GNFile *obj = new GNFile(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNFile::NewInstance(GrooveFile *file) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNFile *gn_file = node::ObjectWrap::Unwrap(instance); gn_file->file = file; return scope.Escape(instance); } NAN_GETTER(GNFile::GetDirty) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(gn_file->file->dirty)); } NAN_GETTER(GNFile::GetId) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); char buf[64]; snprintf(buf, sizeof(buf), "%p", gn_file->file); info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); } NAN_GETTER(GNFile::GetFilename) { GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(gn_file->file->filename).ToLocalChecked()); } NAN_METHOD(GNFile::GetMetadata) { GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsString()) { Nan::ThrowTypeError("Expected string arg[0]"); return; } int flags = 0; if (info.Length() >= 2) { if (!info[1]->IsNumber()) { Nan::ThrowTypeError("Expected number arg[1]"); return; } flags = (int)info[1]->NumberValue(); } String::Utf8Value key_str(info[0]->ToString()); GrooveTag *tag = groove_file_metadata_get(gn_file->file, *key_str, NULL, flags); if (tag) { info.GetReturnValue().Set(Nan::New(groove_tag_value(tag)).ToLocalChecked()); return; } info.GetReturnValue().Set(Nan::Null()); } NAN_METHOD(GNFile::SetMetadata) { GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsString()) { Nan::ThrowTypeError("Expected string arg[0]"); return; } if (info.Length() < 2 || !info[0]->IsString()) { Nan::ThrowTypeError("Expected string arg[1]"); return; } int flags = 0; if (info.Length() >= 3) { if (!info[2]->IsNumber()) { Nan::ThrowTypeError("Expected number arg[2]"); return; } flags = (int)info[2]->NumberValue(); } String::Utf8Value key_str(info[0]->ToString()); String::Utf8Value val_str(info[1]->ToString()); int err = groove_file_metadata_set(gn_file->file, *key_str, *val_str, flags); if (err < 0) { Nan::ThrowTypeError("set metadata failed"); return; } return; } NAN_METHOD(GNFile::OverrideDuration) { GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsNumber()) { Nan::ThrowTypeError("Expected number arg[0]"); return; } double duration = info[0]->NumberValue(); gn_file->file->override_duration = duration; } NAN_METHOD(GNFile::Metadata) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); Local metadata = Nan::New(); GrooveTag *tag = NULL; while ((tag = groove_file_metadata_get(gn_file->file, "", tag, 0))) { Nan::Set(metadata, Nan::New(groove_tag_key(tag)).ToLocalChecked(), Nan::New(groove_tag_value(tag)).ToLocalChecked()); } info.GetReturnValue().Set(metadata); } NAN_METHOD(GNFile::ShortNames) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(groove_file_short_names(gn_file->file)).ToLocalChecked()); } NAN_METHOD(GNFile::Duration) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(groove_file_duration(gn_file->file))); } class CloseWorker : public Nan::AsyncWorker { public: CloseWorker(Nan::Callback *callback, GrooveFile *file) : Nan::AsyncWorker(callback) { this->file = file; } ~CloseWorker() {} void Execute() { if (file) { groove_file_destroy(file); } else { SetErrorMessage("file already closed"); } } GrooveFile *file; }; NAN_METHOD(GNFile::Close) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); AsyncQueueWorker(new CloseWorker(callback, gn_file->file)); gn_file->file = NULL; } class OpenWorker : public Nan::AsyncWorker { public: OpenWorker(Nan::Callback *callback, String::Utf8Value *filename) : Nan::AsyncWorker(callback) { this->filename = filename; } ~OpenWorker() { delete filename; } void Execute() { file = groove_file_create(get_groove()); if (!file) { SetErrorMessage(groove_strerror(GrooveErrorNoMem)); return; } int err; if ((err = groove_file_open(file, **filename, **filename))) { SetErrorMessage(groove_strerror(err)); return; } } void HandleOKCallback() { Nan::HandleScope scope; Local argv[] = {Nan::Null(), GNFile::NewInstance(file)}; callback->Call(2, argv); } GrooveFile *file; String::Utf8Value *filename; }; NAN_METHOD(GNFile::Open) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsString()) { Nan::ThrowTypeError("Expected string arg[0]"); return; } if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); String::Utf8Value *filename = new String::Utf8Value(info[0]->ToString()); AsyncQueueWorker(new OpenWorker(callback, filename)); } class SaveWorker : public Nan::AsyncWorker { public: SaveWorker(Nan::Callback *callback, GrooveFile *file) : Nan::AsyncWorker(callback) { this->file = file; } ~SaveWorker() { } void Execute() { int err; if ((err = groove_file_save(file))) { SetErrorMessage(groove_strerror(err)); return; } } GrooveFile *file; }; NAN_METHOD(GNFile::Save) { Nan::HandleScope scope; GNFile *gn_file = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); AsyncQueueWorker(new SaveWorker(callback, gn_file->file)); } ================================================ FILE: src/file.h ================================================ #ifndef GN_FILE_H #define GN_FILE_H #include #include #include class GNFile : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GrooveFile *file); static NAN_METHOD(Open); GrooveFile *file; private: GNFile(); ~GNFile(); static NAN_METHOD(New); static NAN_GETTER(GetDirty); static NAN_GETTER(GetId); static NAN_GETTER(GetFilename); static NAN_METHOD(Close); static NAN_METHOD(Duration); static NAN_METHOD(GetMetadata); static NAN_METHOD(SetMetadata); static NAN_METHOD(Metadata); static NAN_METHOD(ShortNames); static NAN_METHOD(Save); static NAN_METHOD(OverrideDuration); }; #endif ================================================ FILE: src/fingerprinter.cc ================================================ #include "fingerprinter.h" #include "playlist_item.h" #include "playlist.h" #include "groove.h" using namespace v8; GNFingerprinter::GNFingerprinter() {}; GNFingerprinter::~GNFingerprinter() { groove_fingerprinter_destroy(printer); delete event_context->event_cb; delete event_context; }; static Nan::Persistent constructor; void GNFingerprinter::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GrooveFingerprinter").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(2); // Methods Nan::SetPrototypeMethod(tpl, "attach", Attach); Nan::SetPrototypeMethod(tpl, "detach", Detach); Nan::SetPrototypeMethod(tpl, "getInfo", GetInfo); Nan::SetPrototypeMethod(tpl, "position", Position); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNFingerprinter::New) { Nan::HandleScope scope; GNFingerprinter *obj = new GNFingerprinter(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNFingerprinter::NewInstance(GrooveFingerprinter *printer) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(instance); gn_printer->printer = printer; return scope.Escape(instance); } NAN_METHOD(GNFingerprinter::Create) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } GrooveFingerprinter *printer = groove_fingerprinter_create(get_groove()); if (!printer) { Nan::ThrowTypeError("unable to create fingerprinter"); return; } // set properties on the instance with default values from // GrooveFingerprinter struct Local instance = GNFingerprinter::NewInstance(printer)->ToObject(); GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(instance); EventContext *context = new EventContext; gn_printer->event_context = context; context->event_cb = new Nan::Callback(info[0].As()); context->printer = printer; Nan::Set(instance, Nan::New("infoQueueSize").ToLocalChecked(), Nan::New(printer->info_queue_size)); info.GetReturnValue().Set(instance); } NAN_METHOD(GNFingerprinter::Position) { Nan::HandleScope scope; GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(info.This()); GrooveFingerprinter *printer = gn_printer->printer; GroovePlaylistItem *item; double pos; groove_fingerprinter_position(printer, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Nan::Set(obj, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(item)); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } NAN_METHOD(GNFingerprinter::GetInfo) { Nan::HandleScope scope; GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(info.This()); GrooveFingerprinter *printer = gn_printer->printer; GrooveFingerprinterInfo print_info; if (groove_fingerprinter_info_get(printer, &print_info, 0) == 1) { Local object = Nan::New(); if (print_info.fingerprint) { Local int_list = Nan::New(); for (int i = 0; i < print_info.fingerprint_size; i += 1) { Nan::Set(int_list, Nan::New(i), Nan::New(print_info.fingerprint[i])); } Nan::Set(object, Nan::New("fingerprint").ToLocalChecked(), int_list); } else { Nan::Set(object, Nan::New("fingerprint").ToLocalChecked(), Nan::Null()); } Nan::Set(object, Nan::New("duration").ToLocalChecked(), Nan::New(print_info.duration)); if (print_info.item) { Nan::Set(object, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(print_info.item)); } else { Nan::Set(object, Nan::New("item").ToLocalChecked(), Nan::Null()); } groove_fingerprinter_free_info(&print_info); info.GetReturnValue().Set(object); } else { info.GetReturnValue().Set(Nan::Null()); } } static void PrinterEventAsyncCb(uv_async_t *handle) { Nan::HandleScope scope; GNFingerprinter::EventContext *context = reinterpret_cast(handle->data); // call callback signaling that there is info ready const unsigned argc = 1; Local argv[argc]; argv[0] = Nan::Null(); TryCatch try_catch; context->event_cb->Call(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 PrinterEventThreadEntry(void *arg) { GNFingerprinter::EventContext *context = reinterpret_cast(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); } } class PrinterAttachWorker : public Nan::AsyncWorker { public: PrinterAttachWorker(Nan::Callback *callback, GrooveFingerprinter *printer, GroovePlaylist *playlist, GNFingerprinter::EventContext *event_context) : Nan::AsyncWorker(callback) { this->printer = printer; this->playlist = playlist; this->event_context = event_context; } ~PrinterAttachWorker() {} void Execute() { int err; if ((err = groove_fingerprinter_attach(printer, playlist))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_init(&event_context->cond); uv_mutex_init(&event_context->mutex); event_context->event_async.data = event_context; uv_async_init(uv_default_loop(), &event_context->event_async, PrinterEventAsyncCb); uv_thread_create(&event_context->event_thread, PrinterEventThreadEntry, event_context); } GrooveFingerprinter *printer; GroovePlaylist *playlist; GNFingerprinter::EventContext *event_context; }; NAN_METHOD(GNFingerprinter::Attach) { Nan::HandleScope scope; GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsObject()) { Nan::ThrowTypeError("Expected object arg[0]"); return; } GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info[0]->ToObject()); if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); Local instance = info.This(); GrooveFingerprinter *printer = gn_printer->printer; // copy the properties from our instance to the player printer->info_queue_size = (int)instance->Get(Nan::New("infoQueueSize").ToLocalChecked())->NumberValue(); AsyncQueueWorker(new PrinterAttachWorker(callback, printer, gn_playlist->playlist, gn_printer->event_context)); } class PrinterDetachWorker : public Nan::AsyncWorker { public: PrinterDetachWorker(Nan::Callback *callback, GrooveFingerprinter *printer, GNFingerprinter::EventContext *event_context) : Nan::AsyncWorker(callback) { this->printer = printer; this->event_context = event_context; } ~PrinterDetachWorker() {} void Execute() { int err; if ((err = groove_fingerprinter_detach(printer))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_signal(&event_context->cond); uv_thread_join(&event_context->event_thread); uv_cond_destroy(&event_context->cond); uv_mutex_destroy(&event_context->mutex); uv_close(reinterpret_cast(&event_context->event_async), NULL); } GrooveFingerprinter *printer; GNFingerprinter::EventContext *event_context; }; NAN_METHOD(GNFingerprinter::Detach) { Nan::HandleScope scope; GNFingerprinter *gn_printer = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); AsyncQueueWorker(new PrinterDetachWorker(callback, gn_printer->printer, gn_printer->event_context)); } NAN_METHOD(GNFingerprinter::Encode) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsArray()) { Nan::ThrowTypeError("Expected Array arg[0]"); return; } Local int_list = Local::Cast(info[0]); int len = int_list->Length(); uint32_t *raw_fingerprint = new uint32_t[len]; for (int i = 0; i < len; i += 1) { double val = int_list->Get(Nan::New(i))->NumberValue(); raw_fingerprint[i] = (uint32_t)val; } char *fingerprint; groove_fingerprinter_encode(raw_fingerprint, len, &fingerprint); delete[] raw_fingerprint; Local js_fingerprint = Nan::New(fingerprint).ToLocalChecked(); groove_fingerprinter_dealloc(fingerprint); info.GetReturnValue().Set(js_fingerprint); } NAN_METHOD(GNFingerprinter::Decode) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsString()) { Nan::ThrowTypeError("Expected String arg[0]"); return; } String::Utf8Value utf8fingerprint(info[0]->ToString()); char *fingerprint = *utf8fingerprint; uint32_t *raw_fingerprint; int raw_fingerprint_len; groove_fingerprinter_decode(fingerprint, &raw_fingerprint, &raw_fingerprint_len); Local int_list = Nan::New(); for (int i = 0; i < raw_fingerprint_len; i += 1) { Nan::Set(int_list, Nan::New(i), Nan::New(raw_fingerprint[i])); } groove_fingerprinter_dealloc(raw_fingerprint); info.GetReturnValue().Set(int_list); } ================================================ FILE: src/fingerprinter.h ================================================ #ifndef GN_FINGERPRINTER_H #define GN_FINGERPRINTER_H #include #include #include class GNFingerprinter : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GrooveFingerprinter *printer); static NAN_METHOD(Create); static NAN_METHOD(Encode); static NAN_METHOD(Decode); struct EventContext { uv_thread_t event_thread; uv_async_t event_async; uv_cond_t cond; uv_mutex_t mutex; GrooveFingerprinter *printer; Nan::Callback *event_cb; }; EventContext *event_context; GrooveFingerprinter *printer; private: GNFingerprinter(); ~GNFingerprinter(); static NAN_METHOD(New); static NAN_METHOD(Attach); static NAN_METHOD(Detach); static NAN_METHOD(GetInfo); static NAN_METHOD(Position); }; #endif ================================================ FILE: src/groove.cc ================================================ #include #include #include #include "groove.h" #include "file.h" #include "player.h" #include "playlist.h" #include "playlist_item.h" #include "loudness_detector.h" #include "fingerprinter.h" #include "waveform_builder.h" #include "encoder.h" #include "device.h" using namespace v8; static SoundIo *soundio = NULL; static Groove *groove = NULL; Groove *get_groove() { return groove; } NAN_METHOD(SetLogging) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsNumber()) { Nan::ThrowTypeError("Expected 1 number argument"); return; } groove_set_logging(info[0]->NumberValue()); } NAN_METHOD(ConnectSoundBackend) { SoundIoBackend backend = SoundIoBackendNone; if (info.Length() == 1) { if (!info[0]->IsNumber()) { Nan::ThrowTypeError("Expected 0 or 1 args"); return; } backend = (SoundIoBackend)(int)info[0]->NumberValue(); } else if (info.Length() > 1) { Nan::ThrowTypeError("Expected 0 or 1 args"); return; } if (soundio->current_backend != SoundIoBackendNone) soundio_disconnect(soundio); int err = (backend == SoundIoBackendNone) ? soundio_connect(soundio) : soundio_connect_backend(soundio, backend); if (err) { Nan::ThrowError(soundio_strerror(err)); return; } } NAN_METHOD(DisconnectSoundBackend) { if (soundio->current_backend != SoundIoBackendNone) soundio_disconnect(soundio); } NAN_METHOD(GetDevices) { Nan::HandleScope scope; if (soundio->current_backend == SoundIoBackendNone) { Nan::ThrowError("no backend connected"); return; } soundio_flush_events(soundio); int output_count = soundio_output_device_count(soundio); int default_output = soundio_default_output_device_index(soundio); Local deviceList = Nan::New(); for (int i = 0; i < output_count; i += 1) { struct SoundIoDevice *device = soundio_get_output_device(soundio, i); Local deviceObject = GNDevice::NewInstance(device); Nan::Set(deviceList, Nan::New(i), deviceObject); } Local ret_value = Nan::New(); Nan::Set(ret_value, Nan::New("list").ToLocalChecked(), deviceList); Nan::Set(ret_value, Nan::New("defaultIndex").ToLocalChecked(), Nan::New(default_output)); info.GetReturnValue().Set(ret_value); } NAN_METHOD(GetVersion) { Nan::HandleScope scope; Local version = Nan::New(); Nan::Set(version, Nan::New("major").ToLocalChecked(), Nan::New(groove_version_major())); Nan::Set(version, Nan::New("minor").ToLocalChecked(), Nan::New(groove_version_minor())); Nan::Set(version, Nan::New("patch").ToLocalChecked(), Nan::New(groove_version_patch())); info.GetReturnValue().Set(version); } template static void SetProperty(target_t obj, const char* name, double n) { Nan::Set(obj, Nan::New(name).ToLocalChecked(), Nan::New(n)); } template static void SetMethod(target_t obj, const char* name, FNPTR fn) { Nan::Set(obj, Nan::New(name).ToLocalChecked(), Nan::GetFunction(Nan::New(fn)).ToLocalChecked()); } static void cleanup(void) { groove_destroy(groove); soundio_destroy(soundio); } NAN_MODULE_INIT(Initialize) { int err; soundio = soundio_create(); if (!soundio) { fprintf(stderr, "unable to initialize libsoundio: out of memory\n"); abort(); } if ((err = groove_create(&groove))) { fprintf(stderr, "unable to initialize libgroove: %s\n", groove_strerror(err)); abort(); } atexit(cleanup); GNFile::Init(); GNPlayer::Init(); GNPlaylist::Init(); GNPlaylistItem::Init(); GNLoudnessDetector::Init(); GNEncoder::Init(); GNFingerprinter::Init(); GNDevice::Init(); GNWaveformBuilder::Init(); SetProperty(target, "LOG_QUIET", GROOVE_LOG_QUIET); SetProperty(target, "LOG_ERROR", GROOVE_LOG_ERROR); SetProperty(target, "LOG_WARNING", GROOVE_LOG_WARNING); SetProperty(target, "LOG_INFO", GROOVE_LOG_INFO); SetProperty(target, "TAG_MATCH_CASE", GROOVE_TAG_MATCH_CASE); SetProperty(target, "TAG_DONT_OVERWRITE", GROOVE_TAG_DONT_OVERWRITE); SetProperty(target, "TAG_APPEND", GROOVE_TAG_APPEND); SetProperty(target, "EVERY_SINK_FULL", GrooveFillModeEverySinkFull); SetProperty(target, "ANY_SINK_FULL", GrooveFillModeAnySinkFull); SetProperty(target, "_EVENT_NOWPLAYING", GROOVE_EVENT_NOWPLAYING); SetProperty(target, "_EVENT_BUFFERUNDERRUN", GROOVE_EVENT_BUFFERUNDERRUN); SetProperty(target, "_EVENT_DEVICE_CLOSED", GROOVE_EVENT_DEVICE_CLOSED); SetProperty(target, "_EVENT_DEVICE_OPENED", GROOVE_EVENT_DEVICE_OPENED); SetProperty(target, "_EVENT_DEVICE_OPEN_ERROR", GROOVE_EVENT_DEVICE_OPEN_ERROR); SetProperty(target, "_EVENT_END_OF_PLAYLIST", GROOVE_EVENT_END_OF_PLAYLIST); SetProperty(target, "_EVENT_WAKEUP", GROOVE_EVENT_WAKEUP); SetProperty(target, "BACKEND_JACK", SoundIoBackendJack); SetProperty(target, "BACKEND_PULSEAUDIO", SoundIoBackendPulseAudio); SetProperty(target, "BACKEND_ALSA", SoundIoBackendAlsa); SetProperty(target, "BACKEND_COREAUDIO", SoundIoBackendCoreAudio); SetProperty(target, "BACKEND_WASAPI", SoundIoBackendWasapi); SetProperty(target, "BACKEND_DUMMY", SoundIoBackendDummy); SetMethod(target, "setLogging", SetLogging); SetMethod(target, "getDevices", GetDevices); SetMethod(target, "connectSoundBackend", ConnectSoundBackend); SetMethod(target, "disconnectSoundBackend", DisconnectSoundBackend); SetMethod(target, "getVersion", GetVersion); SetMethod(target, "open", GNFile::Open); SetMethod(target, "createPlayer", GNPlayer::Create); SetMethod(target, "createPlaylist", GNPlaylist::Create); SetMethod(target, "createLoudnessDetector", GNLoudnessDetector::Create); SetMethod(target, "createEncoder", GNEncoder::Create); SetMethod(target, "createFingerprinter", GNFingerprinter::Create); SetMethod(target, "createWaveformBuilder", GNWaveformBuilder::Create); SetMethod(target, "encodeFingerprint", GNFingerprinter::Encode); SetMethod(target, "decodeFingerprint", GNFingerprinter::Decode); } NODE_MODULE(groove, Initialize) ================================================ FILE: src/groove.h ================================================ #ifndef GN_GROOVE_H #define GN_GROOVE_H #include Groove *get_groove(); #endif ================================================ FILE: src/loudness_detector.cc ================================================ #include "loudness_detector.h" #include "playlist_item.h" #include "playlist.h" #include "groove.h" using namespace v8; GNLoudnessDetector::GNLoudnessDetector() {}; GNLoudnessDetector::~GNLoudnessDetector() { groove_loudness_detector_destroy(detector); delete event_context->event_cb; delete event_context; }; static Nan::Persistent constructor; void GNLoudnessDetector::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GrooveLoudnessDetector").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); // Methods Nan::SetPrototypeMethod(tpl, "attach", Attach); Nan::SetPrototypeMethod(tpl, "detach", Detach); Nan::SetPrototypeMethod(tpl, "getInfo", GetInfo); Nan::SetPrototypeMethod(tpl, "position", Position); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNLoudnessDetector::New) { Nan::HandleScope scope; GNLoudnessDetector *obj = new GNLoudnessDetector(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNLoudnessDetector::NewInstance(GrooveLoudnessDetector *detector) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(instance); gn_detector->detector = detector; return scope.Escape(instance); } NAN_METHOD(GNLoudnessDetector::Create) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } GrooveLoudnessDetector *detector = groove_loudness_detector_create(get_groove()); if (!detector) { Nan::ThrowTypeError("unable to create loudness detector"); return; } // set properties on the instance with default values from // GrooveLoudnessDetector struct Local instance = GNLoudnessDetector::NewInstance(detector)->ToObject(); GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(instance); EventContext *context = new EventContext; gn_detector->event_context = context; context->event_cb = new Nan::Callback(info[0].As()); context->detector = detector; Nan::Set(instance, Nan::New("infoQueueSize").ToLocalChecked(), Nan::New(detector->info_queue_size)); Nan::Set(instance, Nan::New("disableAlbum").ToLocalChecked(), Nan::New(detector->disable_album)); info.GetReturnValue().Set(instance); } NAN_METHOD(GNLoudnessDetector::Position) { Nan::HandleScope scope; GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(info.This()); GrooveLoudnessDetector *detector = gn_detector->detector; GroovePlaylistItem *item; double pos; groove_loudness_detector_position(detector, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Nan::Set(obj, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(item)); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } NAN_METHOD(GNLoudnessDetector::GetInfo) { Nan::HandleScope scope; GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(info.This()); GrooveLoudnessDetector *detector = gn_detector->detector; GrooveLoudnessDetectorInfo loudness_info; if (groove_loudness_detector_info_get(detector, &loudness_info, 0) == 1) { Local object = Nan::New(); Nan::Set(object, Nan::New("loudness").ToLocalChecked(), Nan::New(loudness_info.loudness)); Nan::Set(object, Nan::New("peak").ToLocalChecked(), Nan::New(loudness_info.peak)); Nan::Set(object, Nan::New("duration").ToLocalChecked(), Nan::New(loudness_info.duration)); if (loudness_info.item) { Nan::Set(object, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(loudness_info.item)); } else { Nan::Set(object, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(object); } else { info.GetReturnValue().Set(Nan::Null()); } } static void EventAsyncCb(uv_async_t *handle) { Nan::HandleScope scope; GNLoudnessDetector::EventContext *context = reinterpret_cast(handle->data); // call callback signaling that there is info ready const unsigned argc = 1; Local argv[argc]; argv[0] = Nan::Null(); TryCatch try_catch; context->event_cb->Call(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(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); } } class DetectorAttachWorker : public Nan::AsyncWorker { public: DetectorAttachWorker(Nan::Callback *callback, GrooveLoudnessDetector *detector, GroovePlaylist *playlist, GNLoudnessDetector::EventContext *event_context) : Nan::AsyncWorker(callback) { this->detector = detector; this->playlist = playlist; this->event_context = event_context; } ~DetectorAttachWorker() {} void Execute() { int err; if ((err = groove_loudness_detector_attach(detector, playlist))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_init(&event_context->cond); uv_mutex_init(&event_context->mutex); event_context->event_async.data = event_context; uv_async_init(uv_default_loop(), &event_context->event_async, EventAsyncCb); uv_thread_create(&event_context->event_thread, EventThreadEntry, event_context); } GrooveLoudnessDetector *detector; GroovePlaylist *playlist; GNLoudnessDetector::EventContext *event_context; }; NAN_METHOD(GNLoudnessDetector::Attach) { Nan::HandleScope scope; GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsObject()) { Nan::ThrowTypeError("Expected object arg[0]"); return; } GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info[0]->ToObject()); if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); Local instance = info.This(); GrooveLoudnessDetector *detector = gn_detector->detector; // copy the properties from our instance to the player detector->info_queue_size = (int)instance->Get(Nan::New("infoQueueSize").ToLocalChecked())->NumberValue(); detector->disable_album = (int)instance->Get(Nan::New("disableAlbum").ToLocalChecked())->BooleanValue(); AsyncQueueWorker(new DetectorAttachWorker(callback, detector, gn_playlist->playlist, gn_detector->event_context)); } class DetectorDetachWorker : public Nan::AsyncWorker { public: DetectorDetachWorker(Nan::Callback *callback, GrooveLoudnessDetector *detector, GNLoudnessDetector::EventContext *event_context) : Nan::AsyncWorker(callback) { this->detector = detector; this->event_context = event_context; } ~DetectorDetachWorker() {} void Execute() { int err; if ((err = groove_loudness_detector_detach(detector))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_signal(&event_context->cond); uv_thread_join(&event_context->event_thread); uv_cond_destroy(&event_context->cond); uv_mutex_destroy(&event_context->mutex); uv_close(reinterpret_cast(&event_context->event_async), NULL); } GrooveLoudnessDetector *detector; GNLoudnessDetector::EventContext *event_context; }; NAN_METHOD(GNLoudnessDetector::Detach) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); GNLoudnessDetector *gn_detector = node::ObjectWrap::Unwrap(info.This()); GrooveLoudnessDetector *detector = gn_detector->detector; AsyncQueueWorker(new DetectorDetachWorker(callback, detector, gn_detector->event_context)); } ================================================ FILE: src/loudness_detector.h ================================================ #ifndef GN_LOUDNESS_DETECTOR_H #define GN_LOUDNESS_DETECTOR_H #include #include #include class GNLoudnessDetector : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GrooveLoudnessDetector *detector); static NAN_METHOD(Create); struct EventContext { uv_thread_t event_thread; uv_async_t event_async; uv_cond_t cond; uv_mutex_t mutex; GrooveLoudnessDetector *detector; Nan::Callback *event_cb; }; EventContext *event_context; GrooveLoudnessDetector *detector; private: GNLoudnessDetector(); ~GNLoudnessDetector(); static NAN_METHOD(New); static NAN_METHOD(Attach); static NAN_METHOD(Detach); static NAN_METHOD(GetInfo); static NAN_METHOD(Position); }; #endif ================================================ FILE: src/player.cc ================================================ #include "player.h" #include "playlist.h" #include "playlist_item.h" #include "device.h" #include "groove.h" using namespace v8; GNPlayer::GNPlayer() {}; GNPlayer::~GNPlayer() { groove_player_destroy(player); delete event_context->event_cb; delete event_context; }; static Nan::Persistent constructor; void GNPlayer::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GroovePlayer").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(2); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); Nan::SetAccessor(proto, Nan::New("playlist").ToLocalChecked(), GetPlaylist); // Methods Nan::SetPrototypeMethod(tpl, "attach", Attach); Nan::SetPrototypeMethod(tpl, "detach", Detach); Nan::SetPrototypeMethod(tpl, "position", Position); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNPlayer::New) { Nan::HandleScope scope; GNPlayer *obj = new GNPlayer(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNPlayer::NewInstance(GroovePlayer *player) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNPlayer *gn_player = node::ObjectWrap::Unwrap(instance); gn_player->player = player; return scope.Escape(instance); } NAN_GETTER(GNPlayer::GetId) { Nan::HandleScope scope; GNPlayer *gn_player = node::ObjectWrap::Unwrap(info.This()); char buf[64]; snprintf(buf, sizeof(buf), "%p", gn_player->player); info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); } NAN_GETTER(GNPlayer::GetPlaylist) { Nan::HandleScope scope; GNPlayer *gn_player = node::ObjectWrap::Unwrap(info.This()); GroovePlaylist *playlist = gn_player->player->playlist; if (playlist) { Local tmp = GNPlaylist::NewInstance(playlist); info.GetReturnValue().Set(tmp); } else { info.GetReturnValue().Set(Nan::Null()); } } NAN_METHOD(GNPlayer::Position) { Nan::HandleScope scope; GNPlayer *gn_player = node::ObjectWrap::Unwrap(info.This()); GroovePlaylistItem *item; double pos; groove_player_position(gn_player->player, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Local tmp = GNPlaylistItem::NewInstance(item); Nan::Set(obj, Nan::New("item").ToLocalChecked(), tmp); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } static void PlayerEventAsyncCb(uv_async_t *handle) { Nan::HandleScope scope; GNPlayer::EventContext *context = reinterpret_cast(handle->data); // flush events GroovePlayerEvent event; const unsigned argc = 1; Local argv[argc]; while (groove_player_event_get(context->player, &event, 0) > 0) { argv[0] = Nan::New(event.type); TryCatch try_catch; context->event_cb->Call(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 PlayerEventThreadEntry(void *arg) { GNPlayer::EventContext *context = reinterpret_cast(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); } } class PlayerAttachWorker : public Nan::AsyncWorker { public: PlayerAttachWorker(Nan::Callback *callback, GroovePlayer *player, GroovePlaylist *playlist, GNPlayer::EventContext *event_context) : Nan::AsyncWorker(callback) { this->player = player; this->playlist = playlist; this->event_context = event_context; } ~PlayerAttachWorker() {} void Execute() { int err; if ((err = groove_player_attach(player, playlist))) { SetErrorMessage(groove_strerror(err)); return; } GNPlayer::EventContext *context = event_context; uv_cond_init(&context->cond); uv_mutex_init(&context->mutex); context->event_async.data = context; uv_async_init(uv_default_loop(), &context->event_async, PlayerEventAsyncCb); uv_thread_create(&context->event_thread, PlayerEventThreadEntry, context); } GroovePlayer *player; GroovePlaylist *playlist; GNPlayer::EventContext *event_context; }; NAN_METHOD(GNPlayer::Create) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } GroovePlayer *player = groove_player_create(get_groove()); if (!player) { Nan::ThrowTypeError("unable to create player"); return; } Local instance = NewInstance(player)->ToObject(); GNPlayer *gn_player = node::ObjectWrap::Unwrap(instance); EventContext *context = new EventContext; gn_player->event_context = context; context->event_cb = new Nan::Callback(info[0].As()); context->player = player; Nan::Set(instance, Nan::New("device").ToLocalChecked(), Nan::Null()); info.GetReturnValue().Set(instance); } NAN_METHOD(GNPlayer::Attach) { Nan::HandleScope scope; GNPlayer *gn_player = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsObject()) { Nan::ThrowTypeError("Expected object arg[0]"); return; } if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); Local instance = info.This(); GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info[0]->ToObject()); GroovePlayer *player = gn_player->player; // copy the properties from our instance to the player Local deviceObject = instance->Get(Nan::New("device").ToLocalChecked()); if (!deviceObject->IsObject() || deviceObject->IsUndefined() || deviceObject->IsNull()) { Nan::ThrowTypeError("Expected player.device to be an object"); return; } GNDevice *gn_device = node::ObjectWrap::Unwrap(deviceObject->ToObject()); player->device = gn_device->device; AsyncQueueWorker(new PlayerAttachWorker(callback, player, gn_playlist->playlist, gn_player->event_context)); } class PlayerDetachWorker : public Nan::AsyncWorker { public: PlayerDetachWorker(Nan::Callback *callback, GroovePlayer *player, GNPlayer::EventContext *event_context) : Nan::AsyncWorker(callback) { this->player = player; this->event_context = event_context; } ~PlayerDetachWorker() {} void Execute() { int err; if ((err = groove_player_detach(player))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_signal(&event_context->cond); uv_thread_join(&event_context->event_thread); uv_cond_destroy(&event_context->cond); uv_mutex_destroy(&event_context->mutex); uv_close(reinterpret_cast(&event_context->event_async), NULL); } GroovePlayer *player; GNPlayer::EventContext *event_context; }; NAN_METHOD(GNPlayer::Detach) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); GNPlayer *gn_player = node::ObjectWrap::Unwrap(info.This()); GroovePlayer *player = gn_player->player; AsyncQueueWorker(new PlayerDetachWorker(callback, player, gn_player->event_context)); } ================================================ FILE: src/player.h ================================================ #ifndef GN_PLAYER_H #define GN_PLAYER_H #include #include #include class GNPlayer : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GroovePlayer *player); static NAN_METHOD(Create); struct EventContext { uv_thread_t event_thread; uv_async_t event_async; uv_cond_t cond; uv_mutex_t mutex; GroovePlayer *player; Nan::Callback *event_cb; }; GroovePlayer *player; EventContext *event_context; private: GNPlayer(); ~GNPlayer(); static NAN_METHOD(New); static NAN_GETTER(GetId); static NAN_GETTER(GetPlaylist); static NAN_METHOD(Attach); static NAN_METHOD(Detach); static NAN_METHOD(Position); }; #endif ================================================ FILE: src/playlist.cc ================================================ #include #include "playlist.h" #include "playlist_item.h" #include "file.h" #include "groove.h" using namespace v8; GNPlaylist::GNPlaylist() { }; GNPlaylist::~GNPlaylist() { }; static Nan::Persistent constructor; void GNPlaylist::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GroovePlaylist").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); Nan::SetAccessor(proto, Nan::New("gain").ToLocalChecked(), GetGain); // Methods Nan::SetPrototypeMethod(tpl, "destroy", Destroy); Nan::SetPrototypeMethod(tpl, "play", Play); Nan::SetPrototypeMethod(tpl, "items", Playlist); Nan::SetPrototypeMethod(tpl, "pause", Pause); Nan::SetPrototypeMethod(tpl, "seek", Seek); Nan::SetPrototypeMethod(tpl, "insert", Insert); Nan::SetPrototypeMethod(tpl, "remove", Remove); Nan::SetPrototypeMethod(tpl, "position", DecodePosition); Nan::SetPrototypeMethod(tpl, "playing", Playing); Nan::SetPrototypeMethod(tpl, "clear", Clear); Nan::SetPrototypeMethod(tpl, "count", Count); Nan::SetPrototypeMethod(tpl, "setItemGainPeak", SetItemGainPeak); Nan::SetPrototypeMethod(tpl, "setGain", SetGain); Nan::SetPrototypeMethod(tpl, "setFillMode", SetFillMode); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNPlaylist::New) { Nan::HandleScope scope; assert(info.IsConstructCall()); GNPlaylist *obj = new GNPlaylist(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNPlaylist::NewInstance(GroovePlaylist *playlist) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(instance); gn_playlist->playlist = playlist; return scope.Escape(instance); } NAN_METHOD(GNPlaylist::Destroy) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); groove_playlist_destroy(gn_playlist->playlist); gn_playlist->playlist = NULL; } NAN_GETTER(GNPlaylist::GetId) { GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); char buf[64]; snprintf(buf, sizeof(buf), "%p", gn_playlist->playlist); info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); } NAN_GETTER(GNPlaylist::GetGain) { GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(gn_playlist->playlist->gain)); } NAN_METHOD(GNPlaylist::Play) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); groove_playlist_play(gn_playlist->playlist); return; } NAN_METHOD(GNPlaylist::Playlist) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); Local playlist = Nan::New(); GroovePlaylistItem *item = gn_playlist->playlist->head; int i = 0; while (item) { Nan::Set(playlist, Nan::New(i), GNPlaylistItem::NewInstance(item)); item = item->next; i += 1; } info.GetReturnValue().Set(playlist); } NAN_METHOD(GNPlaylist::Pause) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); groove_playlist_pause(gn_playlist->playlist); } NAN_METHOD(GNPlaylist::Seek) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GNPlaylistItem *gn_playlist_item = node::ObjectWrap::Unwrap(info[0]->ToObject()); double pos = info[1]->NumberValue(); groove_playlist_seek(gn_playlist->playlist, gn_playlist_item->playlist_item, pos); } NAN_METHOD(GNPlaylist::Insert) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GNFile *gn_file = node::ObjectWrap::Unwrap(info[0]->ToObject()); double gain = 1.0; double peak = 1.0; if (!info[1]->IsNull() && !info[1]->IsUndefined()) { gain = info[1]->NumberValue(); } if (!info[2]->IsNull() && !info[2]->IsUndefined()) { peak = info[2]->NumberValue(); } GroovePlaylistItem *item = NULL; if (!info[3]->IsNull() && !info[3]->IsUndefined()) { GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info[3]->ToObject()); item = gn_pl_item->playlist_item; } GroovePlaylistItem *result = groove_playlist_insert(gn_playlist->playlist, gn_file->file, gain, peak, item); info.GetReturnValue().Set(GNPlaylistItem::NewInstance(result)); } NAN_METHOD(GNPlaylist::Remove) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info[0]->ToObject()); groove_playlist_remove(gn_playlist->playlist, gn_pl_item->playlist_item); } NAN_METHOD(GNPlaylist::DecodePosition) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GroovePlaylistItem *item; double pos = -1.0; groove_playlist_position(gn_playlist->playlist, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Nan::Set(obj, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(item)); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } NAN_METHOD(GNPlaylist::Playing) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); int playing = groove_playlist_playing(gn_playlist->playlist); info.GetReturnValue().Set(Nan::New(playing)); } NAN_METHOD(GNPlaylist::Clear) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); groove_playlist_clear(gn_playlist->playlist); } NAN_METHOD(GNPlaylist::Count) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); int count = groove_playlist_count(gn_playlist->playlist); info.GetReturnValue().Set(Nan::New(count)); } NAN_METHOD(GNPlaylist::SetItemGainPeak) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info[0]->ToObject()); double gain = info[1]->NumberValue(); double peak = info[2]->NumberValue(); groove_playlist_set_item_gain_peak(gn_playlist->playlist, gn_pl_item->playlist_item, gain, peak); } NAN_METHOD(GNPlaylist::SetGain) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); groove_playlist_set_gain(gn_playlist->playlist, info[0]->NumberValue()); } NAN_METHOD(GNPlaylist::SetFillMode) { Nan::HandleScope scope; GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info.This()); GrooveFillMode mode = (GrooveFillMode) info[0]->NumberValue(); groove_playlist_set_fill_mode(gn_playlist->playlist, mode); } NAN_METHOD(GNPlaylist::Create) { Nan::HandleScope scope; GroovePlaylist *playlist = groove_playlist_create(get_groove()); Local tmp = GNPlaylist::NewInstance(playlist); info.GetReturnValue().Set(tmp); } ================================================ FILE: src/playlist.h ================================================ #ifndef GN_PLAYLIST_H #define GN_PLAYLIST_H #include #include #include class GNPlaylist : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GroovePlaylist *playlist); static NAN_METHOD(Create); GroovePlaylist *playlist; private: GNPlaylist(); ~GNPlaylist(); static NAN_METHOD(New); static NAN_METHOD(Destroy); static NAN_GETTER(GetId); static NAN_GETTER(GetGain); static NAN_METHOD(Playlist); static NAN_METHOD(Play); static NAN_METHOD(Pause); static NAN_METHOD(Seek); static NAN_METHOD(Insert); static NAN_METHOD(Remove); static NAN_METHOD(Position); static NAN_METHOD(DecodePosition); static NAN_METHOD(Playing); static NAN_METHOD(Clear); static NAN_METHOD(Count); static NAN_METHOD(SetItemGainPeak); static NAN_METHOD(SetGain); static NAN_METHOD(SetFillMode); }; #endif ================================================ FILE: src/playlist_item.cc ================================================ #include "playlist_item.h" #include "file.h" using namespace v8; GNPlaylistItem::GNPlaylistItem() { }; GNPlaylistItem::~GNPlaylistItem() { }; static Nan::Persistent constructor; void GNPlaylistItem::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GroovePlaylistItem").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("file").ToLocalChecked(), GetFile); Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); Nan::SetAccessor(proto, Nan::New("gain").ToLocalChecked(), GetGain); Nan::SetAccessor(proto, Nan::New("peak").ToLocalChecked(), GetPeak); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNPlaylistItem::New) { Nan::HandleScope scope; GNPlaylistItem *obj = new GNPlaylistItem(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNPlaylistItem::NewInstance(GroovePlaylistItem *playlist_item) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNPlaylistItem *gn_playlist_item = node::ObjectWrap::Unwrap(instance); gn_playlist_item->playlist_item = playlist_item; return scope.Escape(instance); } NAN_GETTER(GNPlaylistItem::GetFile) { GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info.This()); Local tmp = GNFile::NewInstance(gn_pl_item->playlist_item->file); info.GetReturnValue().Set(tmp); } NAN_GETTER(GNPlaylistItem::GetId) { GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info.This()); char buf[64]; snprintf(buf, sizeof(buf), "%p", gn_pl_item->playlist_item); info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); } NAN_GETTER(GNPlaylistItem::GetGain) { GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info.This()); double gain = gn_pl_item->playlist_item->gain; info.GetReturnValue().Set(Nan::New(gain)); } NAN_GETTER(GNPlaylistItem::GetPeak) { GNPlaylistItem *gn_pl_item = node::ObjectWrap::Unwrap(info.This()); double peak = gn_pl_item->playlist_item->peak; info.GetReturnValue().Set(Nan::New(peak)); } ================================================ FILE: src/playlist_item.h ================================================ #ifndef GN_PLAYLIST_ITEM_H #define GN_PLAYLIST_ITEM_H #include #include #include class GNPlaylistItem : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GroovePlaylistItem *playlist_item); GroovePlaylistItem *playlist_item; private: GNPlaylistItem(); ~GNPlaylistItem(); static NAN_METHOD(New); static NAN_GETTER(GetFile); static NAN_GETTER(GetId); static NAN_GETTER(GetGain); static NAN_GETTER(GetPeak); }; #endif ================================================ FILE: src/waveform_builder.cc ================================================ #include "waveform_builder.h" #include "playlist.h" #include "playlist_item.h" #include "groove.h" using namespace v8; GNWaveformBuilder::GNWaveformBuilder() {}; GNWaveformBuilder::~GNWaveformBuilder() { groove_waveform_destroy(waveform); delete event_context->event_cb; delete event_context; }; static Nan::Persistent constructor; void GNWaveformBuilder::Init() { // Prepare constructor template Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("GrooveWaveformBuilder").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(2); Local proto = tpl->PrototypeTemplate(); // Fields Nan::SetAccessor(proto, Nan::New("id").ToLocalChecked(), GetId); Nan::SetAccessor(proto, Nan::New("playlist").ToLocalChecked(), GetPlaylist); // Methods Nan::SetPrototypeMethod(tpl, "attach", Attach); Nan::SetPrototypeMethod(tpl, "detach", Detach); Nan::SetPrototypeMethod(tpl, "getInfo", GetInfo); Nan::SetPrototypeMethod(tpl, "position", Position); constructor.Reset(tpl->GetFunction()); } NAN_METHOD(GNWaveformBuilder::New) { Nan::HandleScope scope; GNWaveformBuilder *obj = new GNWaveformBuilder(); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } Local GNWaveformBuilder::NewInstance(GrooveWaveform *waveform) { Nan::EscapableHandleScope scope; Local cons = Nan::New(constructor); Local instance = cons->NewInstance(); GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(instance); gn_waveform->waveform = waveform; return scope.Escape(instance); } NAN_GETTER(GNWaveformBuilder::GetId) { Nan::HandleScope scope; GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); char buf[64]; snprintf(buf, sizeof(buf), "%p", gn_waveform->waveform); info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); } NAN_GETTER(GNWaveformBuilder::GetPlaylist) { Nan::HandleScope scope; GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); GroovePlaylist *playlist = gn_waveform->waveform->playlist; if (playlist) { Local tmp = GNPlaylist::NewInstance(playlist); info.GetReturnValue().Set(tmp); } else { info.GetReturnValue().Set(Nan::Null()); } } NAN_METHOD(GNWaveformBuilder::Position) { Nan::HandleScope scope; GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); GrooveWaveform *waveform = gn_waveform->waveform; GroovePlaylistItem *item; double pos; groove_waveform_position(waveform, &item, &pos); Local obj = Nan::New(); Nan::Set(obj, Nan::New("pos").ToLocalChecked(), Nan::New(pos)); if (item) { Nan::Set(obj, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(item)); } else { Nan::Set(obj, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(obj); } NAN_METHOD(GNWaveformBuilder::Create) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } GrooveWaveform *waveform = groove_waveform_create(get_groove()); if (!waveform) { Nan::ThrowTypeError("unable to create waveform builder"); return; } // set properties on the instance with default values from // GrooveWaveform struct Local instance = GNWaveformBuilder::NewInstance(waveform)->ToObject(); GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(instance); EventContext *context = new EventContext; gn_waveform->event_context = context; context->event_cb = new Nan::Callback(info[0].As()); context->waveform = waveform; Nan::Set(instance, Nan::New("infoQueueSizeBytes").ToLocalChecked(), Nan::New(waveform->info_queue_size_bytes)); Nan::Set(instance, Nan::New("widthInFrames").ToLocalChecked(), Nan::New(waveform->width_in_frames)); info.GetReturnValue().Set(instance); } static void buffer_free(char *data, void *hint) { GrooveWaveformInfo *waveform_info = reinterpret_cast(hint); groove_waveform_info_unref(waveform_info); } NAN_METHOD(GNWaveformBuilder::GetInfo) { Nan::HandleScope scope; GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); GrooveWaveform *waveform = gn_waveform->waveform; GrooveWaveformInfo *waveform_info; if (groove_waveform_info_get(waveform, &waveform_info, 0) == 1) { Local object = Nan::New(); if (waveform_info->data_size) { Local bufferObject = Nan::NewBuffer( (char*)waveform_info->data, waveform_info->data_size, buffer_free, waveform_info).ToLocalChecked(); Nan::Set(object, Nan::New("buffer").ToLocalChecked(), bufferObject); } else { Nan::Set(object, Nan::New("buffer").ToLocalChecked(), Nan::Null()); } double expected_duration = waveform_info->expected_frame_count / (double)waveform_info->sample_rate; double actual_duration = waveform_info->actual_frame_count / (double)waveform_info->sample_rate; Nan::Set(object, Nan::New("expectedDuration").ToLocalChecked(), Nan::New(expected_duration)); Nan::Set(object, Nan::New("actualDuration").ToLocalChecked(), Nan::New(actual_duration)); if (waveform_info->item) { Nan::Set(object, Nan::New("item").ToLocalChecked(), GNPlaylistItem::NewInstance(waveform_info->item)); } else { Nan::Set(object, Nan::New("item").ToLocalChecked(), Nan::Null()); } info.GetReturnValue().Set(object); } else { info.GetReturnValue().Set(Nan::Null()); } } static void EventAsyncCb(uv_async_t *handle) { Nan::HandleScope scope; GNWaveformBuilder::EventContext *context = reinterpret_cast(handle->data); // call callback signaling that there is info ready const unsigned argc = 1; Local argv[argc]; argv[0] = Nan::Null(); TryCatch try_catch; context->event_cb->Call(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) { GNWaveformBuilder::EventContext *context = reinterpret_cast(arg); while (groove_waveform_info_peek(context->waveform, 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); } } class WaveformAttachWorker : public Nan::AsyncWorker { public: WaveformAttachWorker(Nan::Callback *callback, GrooveWaveform *waveform, GroovePlaylist *playlist, GNWaveformBuilder::EventContext *event_context) : Nan::AsyncWorker(callback) { this->waveform = waveform; this->playlist = playlist; this->event_context = event_context; } ~WaveformAttachWorker() {} void Execute() { int err; if ((err = groove_waveform_attach(waveform, playlist))) { SetErrorMessage(groove_strerror(err)); return; } GNWaveformBuilder::EventContext *context = event_context; uv_cond_init(&context->cond); uv_mutex_init(&context->mutex); context->event_async.data = context; uv_async_init(uv_default_loop(), &context->event_async, EventAsyncCb); uv_thread_create(&context->event_thread, EventThreadEntry, context); } GrooveWaveform *waveform; GroovePlaylist *playlist; GNWaveformBuilder::EventContext *event_context; }; NAN_METHOD(GNWaveformBuilder::Attach) { Nan::HandleScope scope; GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); if (info.Length() < 1 || !info[0]->IsObject()) { Nan::ThrowTypeError("Expected object arg[0]"); return; } if (info.Length() < 2 || !info[1]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[1]"); return; } Nan::Callback *callback = new Nan::Callback(info[1].As()); Local instance = info.This(); GNPlaylist *gn_playlist = node::ObjectWrap::Unwrap(info[0]->ToObject()); GrooveWaveform *waveform = gn_waveform->waveform; // copy the properties from our instance to the player waveform->info_queue_size_bytes = (int)instance->Get(Nan::New("infoQueueSizeBytes").ToLocalChecked())->NumberValue(); waveform->width_in_frames = (int)instance->Get(Nan::New("widthInFrames").ToLocalChecked())->NumberValue(); AsyncQueueWorker(new WaveformAttachWorker(callback, waveform, gn_playlist->playlist, gn_waveform->event_context)); } class WaveformDetachWorker : public Nan::AsyncWorker { public: WaveformDetachWorker(Nan::Callback *callback, GrooveWaveform *waveform, GNWaveformBuilder::EventContext *event_context) : Nan::AsyncWorker(callback) { this->waveform = waveform; this->event_context = event_context; } ~WaveformDetachWorker() {} void Execute() { int err; if ((err = groove_waveform_detach(waveform))) { SetErrorMessage(groove_strerror(err)); return; } uv_cond_signal(&event_context->cond); uv_thread_join(&event_context->event_thread); uv_cond_destroy(&event_context->cond); uv_mutex_destroy(&event_context->mutex); uv_close(reinterpret_cast(&event_context->event_async), NULL); } GrooveWaveform *waveform; GroovePlaylist *playlist; GNWaveformBuilder::EventContext *event_context; }; NAN_METHOD(GNWaveformBuilder::Detach) { Nan::HandleScope scope; if (info.Length() < 1 || !info[0]->IsFunction()) { Nan::ThrowTypeError("Expected function arg[0]"); return; } Nan::Callback *callback = new Nan::Callback(info[0].As()); GNWaveformBuilder *gn_waveform = node::ObjectWrap::Unwrap(info.This()); GrooveWaveform *waveform = gn_waveform->waveform; AsyncQueueWorker(new WaveformDetachWorker(callback, waveform, gn_waveform->event_context)); } ================================================ FILE: src/waveform_builder.h ================================================ #ifndef GN_WAVEFORM_BUILDER_H #define GN_WAVEFORM_BUILDER_H #include #include #include class GNWaveformBuilder : public node::ObjectWrap { public: static void Init(); static v8::Local NewInstance(GrooveWaveform *waveform); static NAN_METHOD(Create); struct EventContext { uv_thread_t event_thread; uv_async_t event_async; uv_cond_t cond; uv_mutex_t mutex; GrooveWaveform *waveform; Nan::Callback *event_cb; }; EventContext *event_context; GrooveWaveform *waveform; private: GNWaveformBuilder(); ~GNWaveformBuilder(); static NAN_METHOD(New); static NAN_GETTER(GetId); static NAN_GETTER(GetPlaylist); static NAN_METHOD(Attach); static NAN_METHOD(Detach); static NAN_METHOD(GetInfo); static NAN_METHOD(Position); }; #endif ================================================ FILE: test/test.js ================================================ var groove = require('../'); var assert = require('assert'); var path = require('path'); var fs = require('fs'); var ncp = require('ncp').ncp; var testOgg = path.join(__dirname, "danse.ogg"); var bogusFile = __filename; var rwTestOgg = path.join(__dirname, "danse-rw.ogg"); var it = global.it; it("version", function() { var ver = groove.getVersion(); assert.strictEqual(typeof ver.major, 'number'); assert.strictEqual(typeof ver.minor, 'number'); assert.strictEqual(typeof ver.patch, 'number'); }); it("logging", function() { assert.strictEqual(groove.LOG_ERROR, 16); groove.setLogging(groove.LOG_QUIET); }); it("open fails for bogus file", function(done) { groove.open(bogusFile, function(err, file) { assert.strictEqual(err.message, "unknown format"); done(); }); }); it("open file and read metadata", function(done) { groove.open(testOgg, function(err, file) { assert.ok(!err); assert.ok(file.id); assert.strictEqual(file.filename, testOgg); assert.strictEqual(file.dirty, false); assert.strictEqual(file.metadata().TITLE, 'Danse Macabre'); assert.strictEqual(file.metadata().ARTIST, 'Kevin MacLeod'); assert.strictEqual(file.shortNames(), 'ogg'); assert.strictEqual(file.getMetadata('initial key'), 'C'); assert.equal(file.getMetadata('bogus nonexisting tag'), null); file.close(function(err) { if (err) throw err; done(); }); }); }); it("update metadata", function(done) { ncp(testOgg, rwTestOgg, function(err) { assert.ok(!err); groove.open(rwTestOgg, doUpdate); }); function doUpdate(err, file) { if (err) throw err; file.setMetadata('foo new key', "libgroove rules!"); assert.strictEqual(file.getMetadata('foo new key'), 'libgroove rules!'); file.save(function(err) { if (err) throw err; file.close(checkUpdate); }); } function checkUpdate(err) { assert.ok(!err); groove.open(rwTestOgg, function(err, file) { assert.ok(!err); assert.strictEqual(file.getMetadata('foo new key'), 'libgroove rules!'); fs.unlinkSync(rwTestOgg); done(); }); } }); it("create empty playlist", function (done) { var playlist = groove.createPlaylist(); assert.ok(playlist.id); assert.deepEqual(playlist.items(), []); done(); }); it("create empty player", function (done) { var player = groove.createPlayer(); assert.ok(player.id); done(); }); it("playlist item ids", function(done) { var playlist = groove.createPlaylist(); assert.ok(playlist); playlist.pause(); assert.equal(playlist.playing(), false); groove.open(testOgg, function(err, file) { assert.ok(!err, "opening file"); assert.ok(playlist.position); assert.strictEqual(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(); assert.strictEqual(items1[0].id, items2[0].id); assert.strictEqual(items1[0].id, returned1.id); assert.strictEqual(items2[1].id, returned2.id); done(); }); }); it("create, attach, detach player", function(done) { var playlist = groove.createPlaylist(); var player = groove.createPlayer(); groove.connectSoundBackend(); var devices = groove.getDevices(); var defaultDevice = devices.list[devices.defaultIndex]; player.device = defaultDevice; player.attach(playlist, function(err) { assert.ok(!err); player.detach(function(err) { assert.ok(!err); done(); }); }); }); it("create, attach, detach loudness detector", function(done) { var playlist = groove.createPlaylist(); var detector = groove.createLoudnessDetector(); detector.attach(playlist, function(err) { assert.ok(!err); detector.detach(function(err) { assert.ok(!err); done(); }); }); }); it("create, attach, detach encoder", function(done) { var playlist = groove.createPlaylist(); var encoder = groove.createEncoder(); encoder.formatShortName = "ogg"; encoder.codecShortName = "vorbis"; encoder.attach(playlist, function(err) { assert.ok(!err); encoder.detach(function(err) { assert.ok(!err); done(); }); }); }); it("create, attach, detach fingerprinter", function(done) { var playlist = groove.createPlaylist(); var fingerprinter = groove.createFingerprinter(); fingerprinter.attach(playlist, function(err) { assert.ok(!err); fingerprinter.detach(function(err) { assert.ok(!err); done(); }); }); }); it("create, attach, detach waveform builder", function(done) { var playlist = groove.createPlaylist(); var waveform = groove.createWaveformBuilder(); waveform.attach(playlist, function(err) { assert.ok(!err); waveform.detach(function(err) { assert.ok(!err); done(); }); }); });