coding, photo, plant and demo

*リモコン1つでPCとアンプを楽々操作

tech 20100531 004829
アンプのリモコンにPLAYやらPAUSEやらのボタンが付いてるけど、
これでファイルサーバのflacやmp3が流せれば便利だなあ、と思ってた。

家から帰ってきたときに、ボタン一つで無尽蔵にジャズやら好きな曲が流れてきたら最高じゃないっすか。
もうそれを想像したら居ても立ってもいられない。

今はいちいちwindowsとか起動してそこからサーバにアクセスして再生してたのね。
別にそんなことしなくても、サーバのM/Bから光同軸が出せるピンが出ているし、
ちょっとした電子工作をすればサーバが赤外受信することはできるから、上に書いたことは技術的には簡単。

で、かなり前にarduinoボードと赤外線リモコン受信モジュール *0 を秋葉原で意気揚々と購入した。

しかし、居ても立ってもいられなくなっても、意外と買い物が終わると何か成し遂げた気分になって、どうでもよくなったりすることってあるよね!賢者モード的な感じの。
ということで、しばらく放置していた。

Arduino + IR Receiver

これではアカン、と思い久々に電子工作っぽいことをすることにした。
といっても配線もコードも以下のサイトをコピペしたら瞬殺だった。

Arduinoで学習リモコン
http://d.hatena.ne.jp/NeoCat/20090419/1240158722


これが赤外受信モジュール。


ブレッドに挿して終了。えーと、これは電子工作とは言えないわ><

さて、赤外送信機能はとりあえず要らないので、代わりにインジケータとしてms単位の点滅ループパタンを受信してそれを表示するものに差し替えたものが以下。

  1. 1int ir_in = 8;
  2. 2int led_out = 13;
  3. 3unsigned int data[512] = {1000,500, 0}; // ms単位で点灯消灯を繰り返し並べる。null終端。
  4. 4
  5. 5int last = 0;
  6. 6unsigned long us = micros();
  7. 7
  8. 8int pwm_data_pos = 0;
  9. 9long pwm_consumed_time = 0;
  10. 10unsigned long pwm_prev_ms = millis();
  11. 11
  12. 12// セットアップ
  13. 13void setup() {
  14. 14 Serial.begin(57600); // シリアル通信速度の設定
  15. 15 pinMode(ir_in, INPUT); // 入出力ピンの設定
  16. 16 pinMode(led_out, OUTPUT);
  17. 17}
  18. 18
  19. 19// シリアルからの受信をチェック
  20. 20void checkSerial() {
  21. 21 if (Serial.available() > 0) {
  22. 22 if (Serial.read() == ' ') { // スペース受信で入力開始
  23. 23 Serial.print(">");
  24. 24 unsigned long x = 0;
  25. 25 int i = 0;
  26. 26 int len = 0;
  27. 27 while (1) {
  28. 28 while (Serial.available() == 0) {} // 次のバイトが来るまで無限ループで待つ
  29. 29 int a = Serial.read();
  30. 30 if (a >= '0' && a <= '9') { // 0~9を受信なら
  31. 31 len++;
  32. 32 x *= 10;
  33. 33 x += a - '0';
  34. 34 } else if (len) { // 数値を受信済み、かつ数値以外が来たら
  35. 35 data[i++] = x; // 受信した数値をdataに記憶
  36. 36 Serial.print(i);
  37. 37 Serial.print(":");
  38. 38 Serial.print(x);
  39. 39 Serial.print(" ");
  40. 40 if (x == 0 || i >= 512) { // 0受信で赤外線送信開始
  41. 41 pwm_data_pos = 0;
  42. 42 pwm_consumed_time = 0;
  43. 43 Serial.print(" $\n");
  44. 44 break;
  45. 45 }
  46. 46 x = 0;
  47. 47 len = 0;
  48. 48 }
  49. 49 }
  50. 50 }
  51. 51 }
  52. 52}
  53. 53
  54. 54// LEDを指定された周期で点滅させる
  55. 55void blink_led() {
  56. 56 unsigned long ms = millis();
  57. 57 unsigned long dt = ms - pwm_prev_ms;
  58. 58
  59. 59 pwm_consumed_time += dt;
  60. 60 if ((pwm_consumed_time) > data[pwm_data_pos]) {
  61. 61 pwm_consumed_time = 0; // data[pwm_data_pos];
  62. 62 pwm_data_pos ++;
  63. 63 digitalWrite(led_out, pwm_data_pos & 1);
  64. 64 if (data[pwm_data_pos] == 0) {
  65. 65 pwm_data_pos = 0;
  66. 66 }
  67. 67 }
  68. 68 pwm_prev_ms = ms;
  69. 69}
  70. 70
  71. 71void loop() {
  72. 72 unsigned int val;
  73. 73 unsigned int cnt = 0;
  74. 74
  75. 75 // Wait for incoming IR signal
  76. 76 while ((val = digitalRead(ir_in)) == last) { // パルスが切り替わるまで待機
  77. 77 if (++cnt >= 30000) { // 30000回ループで信号が終了したとみなす
  78. 78 if (cnt == 30000)
  79. 79 Serial.print("\n");
  80. 80 else
  81. 81 checkSerial(); // シリアル通信が来ているかチェック
  82. 82 cnt = 30000;
  83. 83 }
  84. 84 blink_led();
  85. 85 }
  86. 86
  87. 87 unsigned long us2 = micros();
  88. 88 unsigned long dt = us2 - us;
  89. 89 Serial.print(dt/10, DEC); // 赤外線のON/OFFが続いた時間を10us単位で送信
  90. 90 Serial.print(" ");
  91. 91 last = val;
  92. 92 us = us2;
  93. 93}
これで赤外のon/offのパターンがシリアルで取れるようになったので、後はlinuxで音を同軸に垂れ流すコードを書き散らすことにする。

割り込みでやらなきゃ駄目だよなー、とかツッコミどころはあるし、実際点滅パターン変更の反応速度がイマイチな気がするけど、一応動くからまずはこれで行く。

Detect remote-controller signal and play music on Linux

ここは面倒なのでrubyで書こう。
まずはシリアルを読むライブラリを入れて置く。
$ 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. 1#!/usr/bin/ruby
  2. 2
  3. 3# Music Player with IR detector for SONY RM-PG7J
  4. 4# SONY RM-PG7J is bundled with SONY TA-DA7000ES.
  5. 5# code by mtm@kmkz.jp
  6. 6
  7. 7require 'serialport'
  8. 8
  9. 9IR_PORT = '/dev/ttyUSB0' # connect to arduino. check device name by dmesg
  10. 10AUDIO_OUT_DEVICE = 'iec958'
  11. 11
  12. 12$pid = nil
  13. 13$sp = nil
  14. 14
  15. 15def stop
  16. 16 p $pid
  17. 17 if $pid != nil
  18. 18 Process.kill("KILL", $pid)
  19. 19 Process.wait
  20. 20 sleep 2
  21. 21 end
  22. 22 $pid = nil
  23. 23 $sp.puts " 10000 1 0"
  24. 24end
  25. 25
  26. 26def play_dir(path)
  27. 27 stop
  28. 28
  29. 29 $pid = fork {
  30. 30 IO.popen("aplay -f cd -D #{AUDIO_OUT_DEVICE}", 'r+') do |aplay|
  31. 31 # do not use ~ in the path.
  32. 32 Dir::glob(path).each do |f|
  33. 33 $sp.puts " 500 500 0"
  34. 34 ef = f.gsub /[ ()\!']/, '\\\\\0'
  35. 35 p ef
  36. 36 IO.popen("ffmpeg -i #{ef} -f s16le -", 'r') do |ffmpeg|
  37. 37 aplay.write ffmpeg.read
  38. 38 end
  39. 39 end
  40. 40 end
  41. 41 }
  42. 42end
  43. 43
  44. 44MUSIC_PATH = '/home/mtm/file/music/'
  45. 45CD_PATH =
  46. 46 [
  47. 47 "jazz/Casiopea/1996 Super Best of Casiopea Disk 1/*.flac",
  48. 48 "jazz/Casiopea/1996 Super Best of Casiopea Disk 2/*.flac",
  49. 49 "jazz/Berardi Jazz Connection/Do It!/flac/*.flac",
  50. 50 "jazz/Torsten Goods/Manhattan Walls/*.flac",
  51. 51 "jazz/The New Mastersounds/Re__Mixed/*.flac",
  52. 52 "jazz/The New Mastersounds/Live in San Francisco/*.flac",
  53. 53 "jazz/小曽根真 Feat. No Name Horses/Jungle/*.flac",
  54. 54 "jazz/European Jazz Trio/BEST OF STANDARDS DISK1/*.flac",
  55. 55 "jazz/European Jazz Trio/BEST OF STANDARDS DISK2/*.flac",
  56. 56 ...
  57. 57 ]
  58. 58
  59. 59def play_random
  60. 60 play_dir MUSIC_PATH + CD_PATH[rand CD_PATH.size]
  61. 61end
  62. 62
  63. 63IR_CODE = {
  64. 64 # TV I/O
  65. 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. 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. 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. 68 lambda {
  69. 69 p "TV I/O"
  70. 70 },
  71. 71
  72. 72 # STOP
  73. 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. 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. 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. 76 lambda {
  77. 77 p "STOP"
  78. 78 stop
  79. 79 },
  80. 80
  81. 81 # PLAY
  82. 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. 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. 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. 85 lambda {
  86. 86 p "PLAY"
  87. 87 play_random
  88. 88 },
  89. 89
  90. 90 # PAUSE
  91. 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. 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. 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. 94 lambda {
  95. 95 p "PAUSE"
  96. 96 },
  97. 97
  98. 98 # TV CH +
  99. 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. 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. 101 lambda {
  102. 102 p "TV CH +"
  103. 103 },
  104. 104
  105. 105 # TV CH -
  106. 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. 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. 108 lambda {
  109. 109 p "TV CH -"
  110. 110 },
  111. 111
  112. 112 # TV VOL +
  113. 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. 114 lambda {
  115. 115 p "TV VOL +"
  116. 116 },
  117. 117
  118. 118 # TV VOL -
  119. 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. 120 lambda {
  121. 121 p "TV VOL -"
  122. 122 },
  123. 123}
  124. 124
  125. 125def analyze_ir(ar)
  126. 126 # remove dummy
  127. 127 ar.shift
  128. 128
  129. 129 # detect diff from registered ir code.
  130. 130 IR_CODE.each do |ir, func|
  131. 131 next if ar.size < ir.size
  132. 132
  133. 133 # remove unneeded code. (ir code is often mixed of keydown and keyup(*1), but we need only keydown code.)
  134. 134 # *1:maybe this is long press code. but it is not important.
  135. 135 far = ar[0, ir.size]
  136. 136
  137. 137 # reject if error is over 30%.
  138. 138 err = false
  139. 139 far.zip(ir).each {|a, b| err |= ((a - b).abs > b * 0.3)}
  140. 140 return func unless err
  141. 141 end
  142. 142 lambda {print "not match.\n"}
  143. 143end
  144. 144
  145. 145$sp = SerialPort.new(IR_PORT, 57600, 8, 1, SerialPort::NONE)
  146. 146
  147. 147line = ''
  148. 148loop do
  149. 149 IO.select [$sp]
  150. 150 s = $sp.gets
  151. 151 if s != nil
  152. 152 line += s
  153. 153 if s =~ /\n$/
  154. 154 analyze_ir(line.split(/\s+/).map{|i| i.to_i}).call
  155. 155 line = ''
  156. 156 end
  157. 157 end
  158. 158end
  159. 159
  160. 160$sp.close
これは見ただけで分かるけど、かなりの手抜きが残っている。

まず、無理やりプロセスを殺しているので、STOP時に変な感じになっている。sleepせずにすぐにまたプロセスを生成するとパイプ周りがおかしくなるし、かなり無理やり動かしている。これは環境依存するパターンだから、直さなくちゃいけない。
本当は赤外を受信するプロセスからの指示とffmpegの出力をselectして、再生側プロセスも永続化すべき。

ただ、ちゃんとやる場合は、ffmpegを2重に起動してクロスフェードするとか、ストップしたらブチッと切らずに音量+周波数フェードをかけたりだとか、遊び心を沢山入れたいのだけど *1 、そこまでrubyでやるのは正直キツイ。いや、これくらいなら動くと思うけど、より複雑なエフェクトを後々掛けたくなることを考えれば微妙。
てことで、rubyでこれ以上追求するのもアレなので、player部分は暇なときにCで書き直そう。ってCは流石に腰が重いねえ。。

最後に

リモコンじゃなくて、AndroidアプリでTCP/IP経由の制御がいいかもなー。
となると、アンプの電源をAndroidから入れなきゃいけないから、結局赤外送信要りますな。

とまあ、夢は広がるが、嫁が居るときには常にTVを付けていなければならないという不文律があるため、あまり活躍する時はなさそう(涙)。他人が介在する世界では、技術の前に政治が常に必要とされるのである。
*0 : PL-IRM0101-3。秋月で100円也
*1 : modpではいろいろ遊んだなあ

10.05.31 20:58 miu
なんですかその不文律はw突っ込まずにはいられません。
10.06.05 09:31 mtm
TVっ子なのでうちにいるときは常にTVをつけてるんですな。
つまり会社に帰ってきたら確実にTVがついてるから使う場面ないということに作ってから気づいた…
コメントする