From a182f9692d5298be7324effb057d84fa8bbaba83 Mon Sep 17 00:00:00 2001 From: oncletom Date: Sat, 6 Dec 2014 13:02:41 +0000 Subject: [PATCH 1/5] Streamable encoder. - audio file content is streamed to child process instead of hardcoding the filepath into the spawn process, for reusability purpose - the streaming source is hot swaped if another file is asked to be read --- .env.example | 2 ++ bin/test.js | 17 ++++++++++++++++ lib/encoder.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 .env.example create mode 100644 bin/test.js create mode 100644 lib/encoder.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f12ee9b --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PIFM_PATH=/home/pi/PiFmRds/src/pi_fm_rds +NODE_ENV=production \ No newline at end of file diff --git a/bin/test.js b/bin/test.js new file mode 100644 index 0000000..d907e38 --- /dev/null +++ b/bin/test.js @@ -0,0 +1,17 @@ +'use strict'; + +/* + This test case demonstrates our ability to swap a stream with another one. + + $ node bin/test.js audioFile1.wav audioFile2.wav + + It will play an audio file and swap with a second audio file a second later. + */ + +var broadcast = require('../lib/encoder')({ env: 'test' }); + +broadcast(process.argv[2]); + +setTimeout(function(){ + broadcast(process.argv[3]) +}, 1000); \ No newline at end of file diff --git a/lib/encoder.js b/lib/encoder.js new file mode 100644 index 0000000..94c53af --- /dev/null +++ b/lib/encoder.js @@ -0,0 +1,54 @@ +'use strict'; + +var child_process = require('child_process'); +var path = require('path'); +var fs = require('fs'); + +// path is expected to be something like /home/pi/PiFmRds/src/pi_fm_rds (on a tinyfm vanilla install) +var pifmPath = process.env.PIFM_PATH || path.join('/', 'home', 'pi', 'PiFmRds', 'src', 'pi_fm_rds'); +var activeStream; + +module.exports = function(options){ + var resolvedOptions = options || {}; + var pipeline = resolveStreamer(resolvedOptions.env || process.env.NODE_ENV || null); + + return function streamFrom(filepath){ + clearStream(function(){ + var stream = fs.createReadStream(path.resolve(filepath)); + + stream.pipe(pipeline.stdin); + + return stream; + }); + }; +}; + +function clearStream(fn){ + if (activeStream){ + activeStream.unpipe(); + + activeStream.close(function(){ + activeStream = fn(); + }); + } + else { + process.nextTick(function(){ + activeStream = fn(); + }); + } +} + +function resolveStreamer(env){ + var args = []; + + if (env !== 'production'){ + args = ['play', ['-']]; + } + else { + var pifmPath = require.resolve(pifmPath); + + args = [pifmPath, ["-audio", "-"]]; + } + + return child_process.spawn.apply(child_process, args); +} \ No newline at end of file From ef004a2a2534679b453ee4dca0824e68733a9cf8 Mon Sep 17 00:00:00 2001 From: oncletom Date: Sat, 6 Dec 2014 13:48:41 +0000 Subject: [PATCH 2/5] Daemonify the script to enable a painless global install. --- bin/daemon.js | 25 +++++++++++++++++++ package.json | 5 +++- script.js | 69 --------------------------------------------------- 3 files changed, 29 insertions(+), 70 deletions(-) create mode 100755 bin/daemon.js delete mode 100644 script.js diff --git a/bin/daemon.js b/bin/daemon.js new file mode 100755 index 0000000..5dc90de --- /dev/null +++ b/bin/daemon.js @@ -0,0 +1,25 @@ +#!/usb/bin/env node + +'use strict'; + +var Mopidy = require('mopidy'); +var mopidy = new Mopidy({ + callingConvention: 'by-position-or-by-name', + webSocketUrl: 'ws://' + (process.env.HOST || 'localhost') + ':6680/mopidy/ws/' +}); + +var broadcast = require('../lib/encoder')({ env: 'test' }); +var currentStream; + +mopidy.on("event:trackPlaybackStarted", function (event) { + // catch the uri + var filepath = event.tl_track.track.uri.split(":").pop(); + console.log("-----------------------------------"); + console.log("New song:", uri); + + currentStream = broadcast(filepath); +}, logErrors); + +function logErrors(err){ + console.error(err); +} diff --git a/package.json b/package.json index 9915723..48d485a 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,14 @@ "name": "mpd2fm", "version": "1.0.0", "description": "tinyfm mdp to FM transmitter bridge.", - "main": "index.js", + "main": "./lib/encoder.js", "dependencies": { "mopidy": "^0.4.1", "when": "^3.6.3" }, + "bin": { + "mpd2fm": "./bin/daemon.js" + }, "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/script.js b/script.js deleted file mode 100644 index fe8bf55..0000000 --- a/script.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -var childProcess = require('child_process'); -var Mopidy = require('mopidy'); -var path = require('path'); - -var mopidy = new Mopidy({ - callingConvention: 'by-position-or-by-name', - webSocketUrl: 'ws://' + (process.env.HOST || 'localhost') + ':6680/mopidy/ws/' -}); - -var pifm = childProcess.spawn('ls', ['-l']); - -// path is expected to be something like /home/pi/PiFmRds/src/pi_fm_rds -var pifmPath = require.resolve(path.join('/', 'home', 'pi', 'PiFmRds', 'src', 'pi_fm_rds')); - -mopidy.on("event:trackPlaybackStarted", function (event) { - // catch the uri - var uri = event.tl_track.track.uri.split(":").pop(); - console.log("-----------------------------------"); - console.log("New song:", uri); - - // kill - pifm.kill(); - // sox.kill(); - - // respawn - // sox = childProcess.spawn("sox",["-t", "mp3", "/usr/share/tinyfm/media/" + uri, "-t", "wav", "-r", "22050", "-"]); - pifm = childProcess.spawn(pifmPath, ["-audio", "/usr/share/tinyfm/media/" + uri]); - - // sox.stdout.on('data', function (data) { - // pifm.stdin.write(data); - // }); - - // sox.stderr.on('data', function (data) { - // console.log('sox stderr: ' + data); - // }); - - // sox.on('close', function (code) { - // if (code !== 0) { - // console.log('sox process exited with code ' + code); - // } - // pifm.stdin.end(); - // }); - - pifm.stdout.on('data', function (data) { - console.log('stdout: ' + data); - }); - - pifm.stderr.on('data', function (data) { - console.log('stderr: ' + data); - }); - - pifm.on('close', function (code) { - console.log('child process exited with code ' + code); - }); - - // process.on('exit', function () { - // sox.kill(); - // }); - process.on('exit', function () { - pifm.kill(); - }); - -}, logErrors); - -function logErrors(err){ - console.error(err); -} From 10bd263f10e91b748d1f2be6243ecb8ca651be6e Mon Sep 17 00:00:00 2001 From: oncletom Date: Sat, 6 Dec 2014 14:19:25 +0000 Subject: [PATCH 3/5] CLI help and initd script output. --- .env.example | 3 ++- README.md | 30 ++++++++++++++++++++++++++- bin/cli.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ bin/daemon.js | 25 ---------------------- package.json | 5 +++-- 5 files changed, 91 insertions(+), 29 deletions(-) create mode 100755 bin/cli.js delete mode 100755 bin/daemon.js diff --git a/.env.example b/.env.example index f12ee9b..fbae34e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ PIFM_PATH=/home/pi/PiFmRds/src/pi_fm_rds -NODE_ENV=production \ No newline at end of file +NODE_ENV=production +HOST=localhost \ No newline at end of file diff --git a/README.md b/README.md index 8c32796..401bea2 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,32 @@ mpd2fm tracks *tinyfm* tracks and turn them into FM modulations. # Requirements - a [Raspberry Pi with an antenna/metalic wire](http://makezine.com/projects/make-38-cameras-and-av/raspberry-pirate-radio/#steppers); -- a [tinyfm sandbox](https://github.com/tinyfm/sandbox)-like Pi configuration; \ No newline at end of file +- a [tinyfm sandbox](https://github.com/tinyfm/sandbox)-like Pi configuration; + +# Install + +```bash +npm install -g git+https://github.com/tinyfm/mpd2fm.git +``` + +# Usage + +Use `mpd2fm --help` to display help about available commands. + +## init.d config + +Prints out the Debian `initd` config. + +```bash +mpd2fm --initd-config +``` + +Pretty much handy to daemonize the command-line tool on a Raspberry Pi. + +## Playback events to FM + +Listens to tinyfm audio playback events (backed by [mopidy](https://www.mopidy.com/)) and broadcasts them as an FM signal. + +``` +mpd2fm --base-dir /usr/share/tinyfm --start +``` \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..4c2781a --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var Mopidy = require('mopidy'); +var encoder = require('../lib/encoder'); + +var argv = require('yargs') + .options('initd-config', { + alias: 'i', + boolean: true, + default: false, + description: 'Displays out the related Debian initd script.' + }) + .options('base-dir', { + alias: 'd', + default: '/usr/share/tinyfm', + description: 'Base directory to resolve event media filepath from.' + }) + .options('start', { + boolean: true, + default: false, + description: 'Starts to media playback to FM broadcasting server.' + }) + .strict() + .argv; + +if (argv.initdConfig) { + return fs.createReadStream(path.join(__dirname, '..', 'dist', 'init.d', 'mpd2fm')) + .pipe(process.stdout); +} + +if (argv.start) { + var mopidy = new Mopidy({ + callingConvention: 'by-position-or-by-name', + webSocketUrl: 'ws://' + (process.env.HOST || 'localhost') + ':6680/mopidy/ws/' + }); + + var broadcast = encoder({ env: 'test' }); + var currentStream; + + mopidy.on("event:trackPlaybackStarted", function (event) { + // catch the uri + var filepath = event.tl_track.track.uri.split(":").pop(); + console.log("-----------------------------------"); + console.log("New song:", uri); + + currentStream = broadcast(filepath); + }, logErrors); +} + + +function logErrors(err){ + console.error(err); +} diff --git a/bin/daemon.js b/bin/daemon.js deleted file mode 100755 index 5dc90de..0000000 --- a/bin/daemon.js +++ /dev/null @@ -1,25 +0,0 @@ -#!/usb/bin/env node - -'use strict'; - -var Mopidy = require('mopidy'); -var mopidy = new Mopidy({ - callingConvention: 'by-position-or-by-name', - webSocketUrl: 'ws://' + (process.env.HOST || 'localhost') + ':6680/mopidy/ws/' -}); - -var broadcast = require('../lib/encoder')({ env: 'test' }); -var currentStream; - -mopidy.on("event:trackPlaybackStarted", function (event) { - // catch the uri - var filepath = event.tl_track.track.uri.split(":").pop(); - console.log("-----------------------------------"); - console.log("New song:", uri); - - currentStream = broadcast(filepath); -}, logErrors); - -function logErrors(err){ - console.error(err); -} diff --git a/package.json b/package.json index 48d485a..0dc2403 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "main": "./lib/encoder.js", "dependencies": { "mopidy": "^0.4.1", - "when": "^3.6.3" + "when": "^3.6.3", + "yargs": "^1.3.3" }, "bin": { - "mpd2fm": "./bin/daemon.js" + "mpd2fm": "./bin/cli.js" }, "devDependencies": {}, "scripts": { From 7835fbefa8518834f17b14121cf855775271f639 Mon Sep 17 00:00:00 2001 From: oncletom Date: Sat, 6 Dec 2014 16:43:02 +0000 Subject: [PATCH 4/5] Update init.d mpd2fm script. --- dist/init.d/mpd2fm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/init.d/mpd2fm b/dist/init.d/mpd2fm index 3772974..fe30128 100644 --- a/dist/init.d/mpd2fm +++ b/dist/init.d/mpd2fm @@ -6,17 +6,17 @@ # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: tinyfm audio playback to FM transmission. -# Description: +# Description: tinyfm audio playback to FM transmission. ### END INIT INFO # Author: tinyfm # PATH should only include /usr/* if it runs after the mountnfs.sh script -PATH=/sbin:/usr/sbin:/bin:/usr/bin -DESC="Description of the service" +PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin +DESC=tinyfm audio playback to FM transmission." NAME=mpd2fm -DAEMON=/usr/local/bin/node -DAEMON_ARGS="/home/pi/mpd2fm/script.js" +DAEMON=$(which mpd2fm) +DAEMON_ARGS="--start" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME From c707bcbd9b80324a9c13b303935b2569f7b8dc4c Mon Sep 17 00:00:00 2001 From: oncletom Date: Sat, 6 Dec 2014 16:43:47 +0000 Subject: [PATCH 5/5] Resolve audio filepath. PROBLEM: the child process ends when the audio piping ends. --- bin/cli.js | 4 ++-- lib/encoder.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 4c2781a..4c5b5fd 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -38,14 +38,14 @@ if (argv.start) { webSocketUrl: 'ws://' + (process.env.HOST || 'localhost') + ':6680/mopidy/ws/' }); - var broadcast = encoder({ env: 'test' }); + var broadcast = encoder({ baseDir: argv.baseDir, env: 'test' }); var currentStream; mopidy.on("event:trackPlaybackStarted", function (event) { // catch the uri var filepath = event.tl_track.track.uri.split(":").pop(); console.log("-----------------------------------"); - console.log("New song:", uri); + console.log("New song:", filepath); currentStream = broadcast(filepath); }, logErrors); diff --git a/lib/encoder.js b/lib/encoder.js index 94c53af..c85efaa 100644 --- a/lib/encoder.js +++ b/lib/encoder.js @@ -14,7 +14,8 @@ module.exports = function(options){ return function streamFrom(filepath){ clearStream(function(){ - var stream = fs.createReadStream(path.resolve(filepath)); + var resolvedFilepath = path.resolve(resolvedOptions.baseDir, filepath); + var stream = fs.createReadStream(resolvedFilepath); stream.pipe(pipeline.stdin);