diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fbae34e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PIFM_PATH=/home/pi/PiFmRds/src/pi_fm_rds +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..4c5b5fd --- /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({ 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:", filepath); + + currentStream = broadcast(filepath); + }, logErrors); +} + + +function logErrors(err){ + console.error(err); +} 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/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 diff --git a/lib/encoder.js b/lib/encoder.js new file mode 100644 index 0000000..c85efaa --- /dev/null +++ b/lib/encoder.js @@ -0,0 +1,55 @@ +'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 resolvedFilepath = path.resolve(resolvedOptions.baseDir, filepath); + var stream = fs.createReadStream(resolvedFilepath); + + 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 diff --git a/package.json b/package.json index 9915723..0dc2403 100644 --- a/package.json +++ b/package.json @@ -2,10 +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" + "when": "^3.6.3", + "yargs": "^1.3.3" + }, + "bin": { + "mpd2fm": "./bin/cli.js" }, "devDependencies": {}, "scripts": { 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); -}