これでファイルサーバのflacやmp3が流せれば便利だなあ、と思ってた。
家から帰ってきたときに、ボタン一つで無尽蔵にジャズやら好きな曲が流れてきたら最高じゃないっすか。
もうそれを想像したら居ても立ってもいられない。
今はいちいちwindowsとか起動してそこからサーバにアクセスして再生してたのね。
別にそんなことしなくても、サーバのM/Bから光同軸が出せるピンが出ているし、
ちょっとした電子工作をすればサーバが赤外受信することはできるから、上に書いたことは技術的には簡単。
で、かなり前にarduinoボードと赤外線リモコン受信モジュール *0 を秋葉原で意気揚々と購入した。
しかし、居ても立ってもいられなくなっても、意外と買い物が終わると何か成し遂げた気分になって、どうでもよくなったりすることってあるよね!賢者モード的な感じの。
ということで、しばらく放置していた。
Arduino + IR Receiver
これではアカン、と思い久々に電子工作っぽいことをすることにした。
といっても配線もコードも以下のサイトをコピペしたら瞬殺だった。
Arduinoで学習リモコン
http://d.hatena.ne.jp/NeoCat/20090419/1240158722
これが赤外受信モジュール。
ブレッドに挿して終了。えーと、これは電子工作とは言えないわ><
さて、赤外送信機能はとりあえず要らないので、代わりにインジケータとしてms単位の点滅ループパタンを受信してそれを表示するものに差し替えたものが以下。
割り込みでやらなきゃ駄目だよなー、とかツッコミどころはあるし、実際点滅パターン変更の反応速度がイマイチな気がするけど、一応動くからまずはこれで行く。
といっても配線もコードも以下のサイトをコピペしたら瞬殺だった。
Arduinoで学習リモコン
http:/
これが赤外受信モジュール。
ブレッドに挿して終了。えーと、これは電子工作とは言えないわ><
さて、赤外送信機能はとりあえず要らないので、代わりにインジケータとしてms単位の点滅ループパタンを受信してそれを表示するものに差し替えたものが以下。
これで赤外のon/offのパターンがシリアルで取れるようになったので、後はlinuxで音を同軸に垂れ流すコードを書き散らすことにする。
- 1int ir_in = 8;
- 2int led_out = 13;
- 3unsigned int data[512] = {1000,500, 0}; // ms単位で点灯消灯を繰り返し並べる。null終端。
- 4
- 5int last = 0;
- 6unsigned long us = micros();
- 7
- 8int pwm_data_pos = 0;
- 9long pwm_consumed_time = 0;
- 10unsigned long pwm_prev_ms = millis();
- 11
- 12// セットアップ
- 13void setup() {
- 14 Serial.begin(57600); // シリアル通信速度の設定
- 15 pinMode(ir_in, INPUT); // 入出力ピンの設定
- 16 pinMode(led_out, OUTPUT);
- 17}
- 18
- 19// シリアルからの受信をチェック
- 20void checkSerial() {
- 21 if (Serial.available() > 0) {
- 22 if (Serial.read() == ' ') { // スペース受信で入力開始
- 23 Serial.print(">");
- 24 unsigned long x = 0;
- 25 int i = 0;
- 26 int len = 0;
- 27 while (1) {
- 28 while (Serial.available() == 0) {} // 次のバイトが来るまで無限ループで待つ
- 29 int a = Serial.read();
- 30 if (a >= '0' && a <= '9') { // 0~9を受信なら
- 31 len++;
- 32 x *= 10;
- 33 x += a - '0';
- 34 } else if (len) { // 数値を受信済み、かつ数値以外が来たら
- 35 data[i++] = x; // 受信した数値をdataに記憶
- 36 Serial.print(i);
- 37 Serial.print(":");
- 38 Serial.print(x);
- 39 Serial.print(" ");
- 40 if (x == 0 || i >= 512) { // 0受信で赤外線送信開始
- 41 pwm_data_pos = 0;
- 42 pwm_consumed_time = 0;
- 43 Serial.print(" $\n");
- 44 break;
- 45 }
- 46 x = 0;
- 47 len = 0;
- 48 }
- 49 }
- 50 }
- 51 }
- 52}
- 53
- 54// LEDを指定された周期で点滅させる
- 55void blink_led() {
- 56 unsigned long ms = millis();
- 57 unsigned long dt = ms - pwm_prev_ms;
- 58
- 59 pwm_consumed_time += dt;
- 60 if ((pwm_consumed_time) > data[pwm_data_pos]) {
- 61 pwm_consumed_time = 0; // data[pwm_data_pos];
- 62 pwm_data_pos ++;
- 63 digitalWrite(led_out, pwm_data_pos & 1);
- 64 if (data[pwm_data_pos] == 0) {
- 65 pwm_data_pos = 0;
- 66 }
- 67 }
- 68 pwm_prev_ms = ms;
- 69}
- 70
- 71void loop() {
- 72 unsigned int val;
- 73 unsigned int cnt = 0;
- 74
- 75 // Wait for incoming IR signal
- 76 while ((val = digitalRead(ir_in)) == last) { // パルスが切り替わるまで待機
- 77 if (++cnt >= 30000) { // 30000回ループで信号が終了したとみなす
- 78 if (cnt == 30000)
- 79 Serial.print("\n");
- 80 else
- 81 checkSerial(); // シリアル通信が来ているかチェック
- 82 cnt = 30000;
- 83 }
- 84 blink_led();
- 85 }
- 86
- 87 unsigned long us2 = micros();
- 88 unsigned long dt = us2 - us;
- 89 Serial.print(dt/10, DEC); // 赤外線のON/OFFが続いた時間を10us単位で送信
- 90 Serial.print(" ");
- 91 last = val;
- 92 us = us2;
- 93}
割り込みでやらなきゃ駄目だよなー、とかツッコミどころはあるし、実際点滅パターン変更の反応速度がイマイチな気がするけど、一応動くからまずはこれで行く。
Detect remote-controller signal and play music on Linux
ここは面倒なのでrubyで書こう。
まずはシリアルを読むライブラリを入れて置く。
ちなみに音はaplayを使って
本当は外部から操作できるプレイヤを用意して、それを叩けばいいんだけど、手頃なのが分からないからffmpeg+aplayでやることにする。
なお、iec958はspdifのことで環境によっては違うかも。デバイス名は
赤外の入力判定は、1回サンプリングしたものと比較して各パルスの誤差が3割以下なら同じものとすることにした。データを見た感じ、5割でも2割でも動くと思うから3割って数字に意味はない。
しかしrubyのArray.zipって便利だねえ。
で、再生時にそのまま先程のコマンドを突っ込んで、ストップ時にプロセスをkillするようにしたら、上手くいかないことが判明。
killしたプロセスだけが死んで、子以下のプロセスツリーが丸々残っている。
なんやこりゃ。
腹立ったので無理やりプロセスツリーを殺すようにしてみたけど、それも殺す順を気をつけないと上手くいかない。リーフのaplayを殺しても親のshellは生きているので直ぐにまたffmpegとaplayのプロセスが生成されてしまう。その後にshellのプロセスが殺されてもそのffmpegとaplayのプロセスは生き続ける。
参考までに、プロセスツリーの殺し方はこんな感じ。
Recursive Kill: Kill a Process Tree
てことで、少し面倒だけどpipeをruby上で制御することにした。
これでひとまずRANDOM PLAYとSTOPだけをサポートしたものができた。
まず、無理やりプロセスを殺しているので、STOP時に変な感じになっている。sleepせずにすぐにまたプロセスを生成するとパイプ周りがおかしくなるし、かなり無理やり動かしている。これは環境依存するパターンだから、直さなくちゃいけない。
本当は赤外を受信するプロセスからの指示とffmpegの出力をselectして、再生側プロセスも永続化すべき。
ただ、ちゃんとやる場合は、ffmpegを2重に起動してクロスフェードするとか、ストップしたらブチッと切らずに音量+周波数フェードをかけたりだとか、遊び心を沢山入れたいのだけど *1 、そこまでrubyでやるのは正直キツイ。いや、これくらいなら動くと思うけど、より複雑なエフェクトを後々掛けたくなることを考えれば微妙。
てことで、rubyでこれ以上追求するのもアレなので、player部分は暇なときにCで書き直そう。ってCは流石に腰が重いねえ。。
まずはシリアルを読むライブラリを入れて置く。
$ sudo apt-get install libserialport-ruby
ちなみに音はaplayを使って
$ for i in *.(flac|mp3); do; ffmpeg -i $i -f s16le -|aplay -f cd -D iec958; doneで鳴るので、それを叩くコードにする。
本当は外部から操作できるプレイヤを用意して、それを叩けばいいんだけど、手頃なのが分からないからffmpeg+aplayでやることにする。
なお、iec958はspdifのことで環境によっては違うかも。デバイス名は
$ aplay -L iec958:CARD=Intel,DEV=0 HDA Intel, ALC882 Digital IEC958 (S/PDIF) Digital Audio Output等として確認する。
赤外の入力判定は、1回サンプリングしたものと比較して各パルスの誤差が3割以下なら同じものとすることにした。データを見た感じ、5割でも2割でも動くと思うから3割って数字に意味はない。
しかしrubyのArray.zipって便利だねえ。
で、再生時にそのまま先程のコマンドを突っ込んで、ストップ時にプロセスをkillするようにしたら、上手くいかないことが判明。
killしたプロセスだけが死んで、子以下のプロセスツリーが丸々残っている。
なんやこりゃ。
腹立ったので無理やりプロセスツリーを殺すようにしてみたけど、それも殺す順を気をつけないと上手くいかない。リーフのaplayを殺しても親のshellは生きているので直ぐにまたffmpegとaplayのプロセスが生成されてしまう。その後にshellのプロセスが殺されてもそのffmpegとaplayのプロセスは生き続ける。
参考までに、プロセスツリーの殺し方はこんな感じ。
Recursive Kill: Kill a Process Tree
てことで、少し面倒だけどpipeをruby上で制御することにした。
これでひとまずRANDOM PLAYとSTOPだけをサポートしたものができた。
これは見ただけで分かるけど、かなりの手抜きが残っている。
- 1#!/usr/bin/ruby
- 2
- 3# Music Player with IR detector for SONY RM-PG7J
- 4# SONY RM-PG7J is bundled with SONY TA-DA7000ES.
- 5# code by mtm@kmkz.jp
- 6
- 7require 'serialport'
- 8
- 9IR_PORT = '/dev/ttyUSB0' # connect to arduino. check device name by dmesg
- 10AUDIO_OUT_DEVICE = 'iec958'
- 11
- 12$pid = nil
- 13$sp = nil
- 14
- 15def stop
- 16 p $pid
- 17 if $pid != nil
- 18 Process.kill("KILL", $pid)
- 19 Process.wait
- 20 sleep 2
- 21 end
- 22 $pid = nil
- 23 $sp.puts " 10000 1 0"
- 24end
- 25
- 26def play_dir(path)
- 27 stop
- 28
- 29 $pid = fork {
- 30 IO.popen("aplay -f cd -D #{AUDIO_OUT_DEVICE}", 'r+') do |aplay|
- 31 # do not use ~ in the path.
- 32 Dir::glob(path).each do |f|
- 33 $sp.puts " 500 500 0"
- 34 ef = f.gsub /[ ()\!']/, '\\\\\0'
- 35 p ef
- 36 IO.popen("ffmpeg -i #{ef} -f s16le -", 'r') do |ffmpeg|
- 37 aplay.write ffmpeg.read
- 38 end
- 39 end
- 40 end
- 41 }
- 42end
- 43
- 44MUSIC_PATH = '/home/mtm/file/music/'
- 45CD_PATH =
- 46 [
- 47 "jazz/Casiopea/1996 Super Best of Casiopea Disk 1/*.flac",
- 48 "jazz/Casiopea/1996 Super Best of Casiopea Disk 2/*.flac",
- 49 "jazz/Berardi Jazz Connection/Do It!/flac/*.flac",
- 50 "jazz/Torsten Goods/Manhattan Walls/*.flac",
- 51 "jazz/The New Mastersounds/Re__Mixed/*.flac",
- 52 "jazz/The New Mastersounds/Live in San Francisco/*.flac",
- 53 "jazz/小曽根真 Feat. No Name Horses/Jungle/*.flac",
- 54 "jazz/European Jazz Trio/BEST OF STANDARDS DISK1/*.flac",
- 55 "jazz/European Jazz Trio/BEST OF STANDARDS DISK2/*.flac",
- 56 ...
- 57 ]
- 58
- 59def play_random
- 60 play_dir MUSIC_PATH + CD_PATH[rand CD_PATH.size]
- 61end
- 62
- 63IR_CODE = {
- 64 # TV I/O
- 65 [249, 64, 115, 64, 56, 50, 129, 64, 56, 50, 129, 64, 56, 50, 69, 50, 127, 64, 58, 50, 67, 52, 69, 50, 66, 2571,
- 66 248, 64, 116, 64, 58, 50, 126, 64, 58, 50, 126, 64, 58, 49, 70, 50, 126, 64, 58, 50, 69, 49, 69, 50, 68, 2568,
- 67 250, 64, 116, 64, 56, 50, 129, 64, 56, 50, 128, 64, 55, 50, 69, 50, 129, 64, 56, 50, 67, 52, 68, 50, 67] =>
- 68 lambda {
- 69 p "TV I/O"
- 70 },
- 71
- 72 # STOP
- 73 [251, 64, 54, 50, 69, 50, 69, 50, 126, 64, 116, 64, 117, 64, 57, 50, 126, 64, 57, 52, 68, 51, 68, 50, 126, 2510,
- 74 252, 64, 55, 50, 70, 49, 69, 50, 126, 64, 116, 64, 116, 64, 58, 50, 127, 64, 58, 50, 70, 50, 69, 50, 127, 2510,
- 75 251, 64, 52, 53, 69, 50, 69, 50, 126, 64, 115, 64, 116, 64, 58, 50, 127, 64, 58, 50, 69, 50, 67, 52, 127] =>
- 76 lambda {
- 77 p "STOP"
- 78 stop
- 79 },
- 80
- 81 # PLAY
- 82 [250, 64, 57, 50, 128, 64, 56, 50, 69, 50, 129, 64, 116, 64, 53, 52, 130, 64, 53, 53, 68, 50, 69, 50, 129, 2507,
- 83 252, 64, 56, 50, 129, 64, 56, 50, 70, 49, 129, 64, 116, 64, 54, 53, 128, 64, 53, 53, 70, 49, 69, 50, 129, 2507,
- 84 249, 64, 58, 51, 128, 63, 57, 50, 68, 50, 130, 63, 116, 64, 56, 50, 128, 64, 56, 50, 69, 50, 68, 50, 130] =>
- 85 lambda {
- 86 p "PLAY"
- 87 play_random
- 88 },
- 89
- 90 # PAUSE
- 91 [249, 64, 115, 64, 56, 50, 69, 50, 129, 64, 116, 64, 116, 64, 56, 50, 129, 64, 55, 50, 69, 50, 69, 49, 130, 2447,
- 92 250, 64, 116, 64, 56, 50, 67, 52, 130, 64, 114, 64, 113, 64, 59, 50, 126, 64, 59, 50, 67, 52, 70, 49, 127, 2449,
- 93 248, 64, 116, 64, 58, 50, 67, 52, 127, 64, 116, 64, 118, 64, 57, 50, 126, 64, 59, 50, 66, 53, 68, 50, 127] =>
- 94 lambda {
- 95 p "PAUSE"
- 96 },
- 97
- 98 # TV CH +
- 99 [252, 64, 54, 52, 68, 51, 68, 52, 66, 53, 126, 64, 56, 52, 67, 52, 127, 64, 57, 52, 67, 52, 67, 52, 66, 2691,
- 100 252, 64, 53, 52, 68, 52, 68, 52, 67, 52, 126, 64, 56, 52, 69, 50, 126, 64, 57, 52, 67, 52, 68, 53, 67] =>
- 101 lambda {
- 102 p "TV CH +"
- 103 },
- 104
- 105 # TV CH -
- 106 [251, 64, 116, 64, 52, 54, 65, 55, 67, 52, 128, 63, 52, 54, 68, 51, 128, 64, 54, 52, 67, 52, 68, 52, 67, 2568,
- 107 252, 64, 115, 64, 52, 54, 66, 53, 65, 54, 129, 63, 54, 52, 69, 50, 128, 63, 54, 52, 67, 52, 70, 49, 68] =>
- 108 lambda {
- 109 p "TV CH -"
- 110 },
- 111
- 112 # TV VOL +
- 113 [249, 64, 58, 51, 126, 64, 58, 50, 66, 52, 126, 64, 56, 53, 66, 54, 125, 64, 59, 51, 66, 53, 66, 53, 66] =>
- 114 lambda {
- 115 p "TV VOL +"
- 116 },
- 117
- 118 # TV VOL -
- 119 [250, 64, 115, 64, 115, 64, 54, 52, 68, 52, 128, 64, 54, 52, 66, 52, 128, 64, 54, 51, 68, 51, 68, 52, 64] =>
- 120 lambda {
- 121 p "TV VOL -"
- 122 },
- 123}
- 124
- 125def analyze_ir(ar)
- 126 # remove dummy
- 127 ar.shift
- 128
- 129 # detect diff from registered ir code.
- 130 IR_CODE.each do |ir, func|
- 131 next if ar.size < ir.size
- 132
- 133 # remove unneeded code. (ir code is often mixed of keydown and keyup(*1), but we need only keydown code.)
- 134 # *1:maybe this is long press code. but it is not important.
- 135 far = ar[0, ir.size]
- 136
- 137 # reject if error is over 30%.
- 138 err = false
- 139 far.zip(ir).each {|a, b| err |= ((a - b).abs > b * 0.3)}
- 140 return func unless err
- 141 end
- 142 lambda {print "not match.\n"}
- 143end
- 144
- 145$sp = SerialPort.new(IR_PORT, 57600, 8, 1, SerialPort::NONE)
- 146
- 147line = ''
- 148loop do
- 149 IO.select [$sp]
- 150 s = $sp.gets
- 151 if s != nil
- 152 line += s
- 153 if s =~ /\n$/
- 154 analyze_ir(line.split(/\s+/).map{|i| i.to_i}).call
- 155 line = ''
- 156 end
- 157 end
- 158end
- 159
- 160$sp.close
まず、無理やりプロセスを殺しているので、STOP時に変な感じになっている。sleepせずにすぐにまたプロセスを生成するとパイプ周りがおかしくなるし、かなり無理やり動かしている。これは環境依存するパターンだから、直さなくちゃいけない。
本当は赤外を受信するプロセスからの指示とffmpegの出力をselectして、再生側プロセスも永続化すべき。
ただ、ちゃんとやる場合は、ffmpegを2重に起動してクロスフェードするとか、ストップしたらブチッと切らずに音量+周波数フェードをかけたりだとか、遊び心を沢山入れたいのだけど *1 、そこまでrubyでやるのは正直キツイ。いや、これくらいなら動くと思うけど、より複雑なエフェクトを後々掛けたくなることを考えれば微妙。
てことで、rubyでこれ以上追求するのもアレなので、player部分は暇なときにCで書き直そう。ってCは流石に腰が重いねえ。。
最後に
リモコンじゃなくて、AndroidアプリでTCP/IP経由の制御がいいかもなー。
となると、アンプの電源をAndroidから入れなきゃいけないから、結局赤外送信要りますな。
とまあ、夢は広がるが、嫁が居るときには常にTVを付けていなければならないという不文律があるため、あまり活躍する時はなさそう(涙)。他人が介在する世界では、技術の前に政治が常に必要とされるのである。
となると、アンプの電源をAndroidから入れなきゃいけないから、結局赤外送信要りますな。
とまあ、夢は広がるが、嫁が居るときには常にTVを付けていなければならないという不文律があるため、あまり活躍する時はなさそう(涙)。他人が介在する世界では、技術の前に政治が常に必要とされるのである。
*0 : PL-IRM0101-3。秋月で100円也
*1 : modpではいろいろ遊んだなあ
*1 : modpではいろいろ遊んだなあ
つまり会社に帰ってきたら確実にTVがついてるから使う場面ないということに作ってから気づいた…