特集 : 作ってみよう,MyガジェットPart3

対象製品: Armadillo-9
先頭 Part 2 Part 4

(株)アットマークテクノ
荘司靖
SHOJI Yasushi

この文書は技術評論社「Software Design」2005年12月号に掲載されたものです。

本章では,シリアル通信を利用して,バーコードリーダが認識した情報からCD1 枚分の情報を受け取り,それを元に音楽を再生する部分を作成してみます.

バーコードリーダと音楽ストリーム再生

作成するシステムについて

この章ではバーコードリーダで読みとった情報を元に音楽を鳴らすプログラムを作成します(図1).プログラムの仕様ですが,バーコードリーダが認識した情報からCDアルバム1枚分の情報を受けとり,1曲目から最後まで再生し終了する簡単なものにします.

バーコードとアルバムとの紐付けはサーバ側に一任し,端末側はシンプルな作りにします.今回この部分は簡単なCGI スクリプトで行いますが,大規模システムではPHP とRDBMS の組み合わせを使うほうが,後々メンテナンスが楽かもしれません.また,最近流行のRuby on Railなどで実装するのもおもしろいでしょう.

本特集では,組み込み機器側に焦点を当てるため,CGI の部分はごく簡単に説明するだけに留めたいと思います.

●図1 システム概要

使用する機器

今回使用するバーコードリーダは,エス・アールが販売している「SD-720」を使います(写真1).このバーコードリーダはRS232Cでシリアル通信ができ,とても扱いやすいものになっています.

●写真1 エス・アール「SD-720」

シリアル通信

まず最初にシリアル通信のおさらいから始めます.その後,今回使用するArmadillo-9のシリアルポートについて説明します.

シリアルポート(UART/RS232C)

外付けモデムが主流だったころまでは,PCのシリアルポートを設定する人も多かったのではないでしょうか.最近の新しいPCでは,シリアルポートが付いていない物のほうが多くなってきて,USB シリアルが手放せなくなってきました.組み込み機器の開発,それもカーネルの移植などを行っている人にとって,シリアルポートはコンパイラの次くらいに重要なものだと思っています.

シリアル通信とは,ひとかたまりのデータを1ビットずつ順番に転送していく方法です.最近ではシリアルポートの他にもUSB やFirewire,SerialATA のように,多くのシリアル通信方法が実用化されています.

PCが採用しているシリアル通信方法は調歩同期方式と呼ばれるもので,スタートビットと呼ばれる信号とストップビットと呼ばれる信号の間に固定数のビットを順番に送り出していきます(図2).この方式を自動で行ってくれるハードウェアをUART(Universal asynchronous receiver transmitter)と呼びます.

●図2 調歩同期の波形

UART自体は波形を作るためのハードウェアであって,実際にケーブル上に乗る電気信号を規定しているものではありません.PCで使われているシリアルポートの電気的な部分など,ケーブルとのインターフェースを決めている規格はRS-232C(注1)という規格です.もともとは大型コンピュータなどに継ぐためのテレタイプとモデムを継ぐために規格化されれたものでした.オリジナルの規格のときには25ピンだったのですが,PCには9ピンのコネクタがよく用いられています.

注1)すでにEIA/TIA-232 と規格の名前が変っていますが,一般的に用いられているRS-232C という名前を使います.

Armadillo-9 のシリアルポート

Armadillo-9は,2つのシリアルポート(COM1とCOM2)を持っています.CPU 自体にUART は組み込まれていますが,RS-232Cの規格に合せるためにドライバ/レシーバと呼ばれる部品を使っています(写真2).

●写真2 Armadillo-9 のシリアルポートとドライバ/レシーバ

COM1 は前章までシリアルコンソールとして使用してきました.それでは「今回はCOM2 にバーコードリーダを接続」と行きたいところですが,あいにく出荷状態のCOM2 には一般的なシリアルのコネクタとして使われているD-SUB9ピンコネクタや,COM1にあるような10ピンのコネクタが実装されていません.もちろん,半田ごてを片手にピンヘッダを実装しても良いのですが,今回は前章までシリアルコンソールとして使っていた COM1に接続することにします.

シリアルポートの準備

COM1 をシリアルコンソールとして使っていると,プログラムとバーコードリーダがシリアル通信を行っているときにカーネルからのメッセージが割り込んできたり,loginを待っているプログラムが必要なデータを奪ってしまいます.そこで,COM1 をシリアルコンソールから解放します.

シリアルコンソールが使えなくなっしましますので,かならずtelnetでログインできることを確認してから作業を行ってください.

出荷時の状態では多くのプログラムがCOM1 を使用しています.これらを順番に止めていきます.

・hermitブートローダ
・kernel
・getty

手順(1)

最初はgettyから行います.gettyとはterminalを監視しているプロセスで,login認証を行うloginやシェルの起動を任されているプロセスです.このプロセスはinitプロセスによって起動され,起動時にどこのポートを監視するか伝えられます.initの設定ファイルである,/etc/inittabにその記述を見付けることができます(リスト1).

●リスト1 Armadillo-9 のinittab

::sysinit:/etc/init.d/rc

::respawn:/sbin/getty -L 115200 ttyAM0 vt102
::respawn:/sbin/getty 38400 tty1

::shutdown:/etc/init.d/reboot
::ctrlaltdel:/sbin/reboot

実際に動いているArmadillo-9で試してみるとよくわかります.viで/etc/inittabを開いて,リスト2のように該当個所の行頭に「#」を入れます.

●リスト2 inittab を変更する

#::respawn:/sbin/getty -L 115200 ttyAM0 vt102

# で始まる行はコメントとして扱われるので,init はttyAM0 を監視するためのgetty をrespawn(再起動)しなくなります.作業をシリアルコンソールで行っている場合,一度logout してしまうとなにも反応がなくなっしまうのがわかると思います.もちろんデーモンなどは動いていますので,telnetでログインするができます.

前章で説明したとおり,書き換えただけではフラッシュメモリに反映されません.このため,再起動してしまうと変更が破棄され,もとどおりlogin promptが表示されます.

/etc/inittab は,Atmark Dist の中ではvendors/AtmarkTechno/Armadillo-9/etc/inittabに置かれてい ます.このファイルを直接変更したイメージファイルを作成し,フラッシュメモリに書き込むことで,再起動時にログインプロンプトが表示されなくなります.

設定と接続

準備が終わったので,さっそくバーコードリーダをつないでみましょう.シリアル通信を行う機器をつなぐ場合,以下の点に気を付ける必要があります.

・ボーレート
・データパリティ
・データビット
・ストップビット

今回使用するバーコードリーダのデフォルト設定では表1となっています.

●表1 SD-720 のシリアル通信デフォルト設定
ボーレート 9600bps
データパリティ なし
データビット 8ビット
ストップビット 1

しかし,Armadillo-9 のCOM1 の設定は表2 となっているため,どちらかの設定を変更して揃える必要があります.

●表2 Armadillo-9 のシリアル通信デフォルト設定
ボーレート 115200bps
データパリティ なし
データビット 8ビット
ストップビット 1

今回は練習もかねて,COM1 の設定を変更することにします.多くのLinuxディストリビューションではstty というツールを使ってシリアルポートの設定を変更することができます.

PC 上で動くLinux の場合,/dev/ttyS0 が一番最初のシリアルポートに割り当てられていることが多いのですが,Armadillo-9 の場合は,先ほど編集した/etc/inittab からもわかるように/dev/ttyAM0がCOM1 に割り当てられています.幸い出荷時の 状態でstty が使えますので,まずは現状の設定を確認してみます.図3のように入力してください.

●図3 Armadillo-9 のシリアル通信設定を確認する

# stty -F /dev/ttyAM0
speed 115200 baud;
-brkint ixoff -imaxbel

出力された文字列の意味は表3のようになっています.詳しい情報についてはstty のman ページを参考にしてください.

●表3 stty コマンド出力の意味
speed 115200 baud; ボーレート 115200bps
-brkint ブレイクによるシグナルを発生させない
ixoff スタート/ストップ文字を送る
-maxbel オーバーフロー時にベルを鳴らさない
バーコードリーダのボーレートは9600bps なの で,ttyAM0も9600bpsに合わせます.図4のように, sttyに9600のオプションを渡してください.
●図4 ボーレートを9600bps に変更する

# stty -F /dev/ttyAM0 9600
# stty -F /dev/ttyAM0
speed 9600 baud;
-brkint ixoff -imaxbel

speed の行が9600 に変更されているのがわかります.これでバーコードリーダを接続する準備が整いました.

PCのシリアルポートにつながっていたケーブルを抜いて,バーコードリーダをつなぎます.telnetの画面で/dev/ttyAM0をcatさせた状態で,バーコードリーダで図5のバーコードを読み込んでみてください.

●図5 サンプルバーコード

図6のように表示されたら成功です.

●図6 バーコード読み込みテスト

# cat /dev/ttyAM0
SoftwareDesign

SoftwareDesign

何度かバーコードを読み込んでみるとわかりますが,データの間に空行が入ります.この空行が何なのか調べてみましょう.図7 のようにod というコマンドを使ってみます.

●図7 odコマンドを使用する

# od -x /dev/ttyAM0
0000000 6f53 7466 6177 6572 6544 6973 6e67 0a0a

Little Endianで表示されているため,2文字ずつ入れ替わっていますが,SoftwareDesign という文字列のASCIIコードが確認できます.最後にLINE FEED(0x0a)が2 回続いているため,1 行空行が空いたように見えていたわけです.つまり,このバーコードリーダの初期設定時には,データとデータの間にLINE FEED文字を2つ挿入するようです.

Linux / UNIX のシリアルポート

UNIX tty レイヤ

Linux はUNIX の流れを踏襲していますので,UNIXの歴史的なTTY(teltypeの略)の機能も引き継いでいます.マイコンなど比較的シンプルなCPU でシリアル通信プログラムを作成したことがある人ならば,設定を行ったあとは「FIFOからデータを読みだすだけ」なので,シリアル通信なんて簡単じゃないか(図8)と言われると思いますが,TTYのしくみを持つOSでは少し話が異なります.

●図8 単純なUARTの概念図

もちろんLinuxでも,UARTのレジスタと通信しているデバイスドライバ層では,マイコンで制御していることと同じようにコントロールレジスタやステータスレジスタ,tx/rx fifoにread/writeを行っているだけです.オーバフローやハードウェアの不具合がない限り,送られてきたデータをそのまま読み込むことができます.

しかし,Linuxのシリアルサブシステムはデバイスドライバ層で受け取ったデータをttyレイヤとttyline deciplineレイヤに通してから初めて,ユーザランド側にデータを渡すしくみになっています.

これは,UNIX 時代にはシステム1 台にVT100(xterm のman page などで見たことはありませんか?)のようなダム端末と呼ばれるターミナルハードウェアを何台もつないで,複数人で1台のホストコンピュータを使っていたときの名残です.今でも,consoleやX上で動かしているターミナルソフトウェアは,これらのハードウェアをエミュレートしています.

ターミナルでは[CTRL]と文字の組み合わせを同時に押すことで,実行中のプログラムにシグナルを飛ばして終了させたり([CTRL]+c),スクロールを止めたり([CTRL]+s),入力を終了させたり([CTRL]+d)することができます.これらの機能はすべてtty の層が処理をしています.また,PPPやSLIPなどシリアル通信を元にしたプロトコルなどもtty line deciplineレイヤに影響を与えます.

ttyレイヤは言い換えると,「渡されるデータを逐次監視し,決められたデータが通るときに適切な処理を行っている」層なのです.しかし,実際に単純な通信を行う場合は,送ってきたデータをそのまま読み取る必要があります.そこで,入力されたデータにまったく手をつけずに生のまま渡す設定にもできるようになっています.これをraw(生)モードと呼びます.

sttyでもrawというオプションを与えることでシリアルポートをraw モードに変更することができます(図9).反対にraw モードではない場合はcooked(料理された)モードと呼ばれます.

●図9 シリアルポートをraw モードに変更

# stty -F /dev/ttyAM0 raw
# stty -F /dev/ttyAM0
speed 9600 baud;
min = 1; time = 0;
-brkint -icrnl -imaxbel
-opost
-isig -icanon

新しく表4の4つが表示されました.

●表4 raw モードに変更した後のシリアル通信設定
-icrnl CARRIAGE RETURN をLINE FEED に変換しない
-opost 出力の後工程を行わない
-isig シグナルを発生させない
-icanon erase やkill,werase,rprnt を無効にする

この状態でもう一度バーコードリーダを使って図5を読み込んでみましょう(図10).

●図10 raw モードでバーコードを読み込む

# od -x /dev/ttyAM0
0000000 6f53 7466 6177 6572 6544 6973 6e67 0a0d

最後のバイトが0x0a から0x0d に変わっています.これはCARRIAGE RETURN 文字をLINEFEED文字に変更するという処理が,rawモードにすることによって無効になったため,実際にバーコードリーダが送っているデータが直接読めるようになったからです.つまり,今回使用するバーコードリーダは,データの終了をCARRIAGE RETURN とLINE FEED 文字で表しているのでした.

Linux シリアル通信プログラミング

UNIX 系OS でのシリアル通信のプログラミングは基本的に同じです.ttyデバイスファイルを開き,termios を使ってline discipline の設定を行います.最終的にはioctl で制御しますが,C library にtermiosの制御用関数群が用意されているので,今回はこちらを使います.

line disciplineが影響を持つモードをcookedモードと呼び,影響を及ぼさないモードをraw モードと呼びます.データを生のまま渡すか,手を入れて料理するか.これらはstty を使って設定することも可能です.

termiosでは大きくわけて表5の設定が可能です.

●表5 termios による設定
入力モード(c_iflag) 入力された文字に対して行う処理を決める
出力モード(c_oflag) 出力する文字に対して行う処理を決める
制御モード(c_cflag) シリアル通信の制御全般の設定
ローカルモード(c_lflag) 通信先に影響のない設定
制御文字(c_cc) 特別な意味を持つ文字の定義

/usr/include/bits/termios.hの内容はリスト3 のとおりです(注2).

●リスト3 struct termios(/usr/include/bits/termios.h)

struct termios
    {
        tcflag_t c_iflag; /* input mode flags */
        tcflag_t c_oflag; /* output mode flags */
        tcflag_t c_cflag; /* control mode flags */
        tcflag_t c_lflag; /* local mode flags */
        cc_t c_line; /* line discipline */
        cc_t c_cc[NCCS]; /* control characters */
        speed_t c_ispeed; /* input speed */
        speed_t c_ospeed; /* output speed */
    };

POSIXではボーレートの値はtermiosに含まれるとしていますが,どこにどのような名前で含まれるのかまでは規定していません.その代わりに,cfgetispeed( )やcfsetispeed( )などを規定しています.リスト3はglibc 2.3.5のstruct termiosですが,c_ispeedやc_ospeedメンバ変数を直接変更すると移植性が下ることになります.

注2)/usr/include/bits 以下のファイルは,プログラムから直接インクルードすることが禁止されています.プログラムからは,必ず/usr/include/termios.h などをインクルードしてください.

プログラム解説

リスト4のサンプルコードは,バーコード情報を/dev/ttyAM0 から読み出し,標準出力に出力するソースコードです.

●リスト4 barcode.c

#include < stdio.h >
#include < unistd.h >
#include < stdlib.h >
#include < termios.h >
#include < fcntl.h >
#include < signal.h >
#include < errno.h >
#include < sys/types.h >
#include < sys/stat.h >

#define FALSE (0)
#define TRUE (!FALSE)

#define TARGET_SERIAL "/dev/ttyAM0"
#define MAX_BARCODE_LEN 1024

static struct termios original;
static int need_restore;
static int fd;

static void restore(void)
{
    if (need_restore)
        tcsetattr(fd, TCSANOW, &original);
}

static void die(char *message)
{
    fprintf(stderr, "%s\n", message);
    restore();
    exit(128);
}

static void signal_handler(int unused)
{
    die("caught a signal");
}

static void set_sighandlers(void)
{
    signal(SIGHUP, signal_handler);
    signal(SIGINT, signal_handler);
    signal(SIGILL, signal_handler);
}

int main(int argc, char *argv[])
{
    int ret;
    struct termios new;

    set_sighandlers();

    fd = open(TARGET_SERIAL, O_RDONLY);
    if (fd < 0)
        die("Can't open serial file");
    ret = tcgetattr(fd, &original);
    if (ret < 0)
        die("Can't get terminal info");

    new = original;

    cfmakeraw(&new);
    cfsetispeed(&new, B9600);
    cfsetospeed(&new, B9600);

    need_restore = TRUE;
    tcsetattr(fd, TCSAFLUSH, &new);

    for (;;) {
        static char barcode[MAX_BARCODE_LEN];
        static char *pos = &barcode[0];
        static char c, prev;
        ssize_t size;

        size = read(fd, &c, 1);
        if ((size < 0) && (errno != EINTR))
            die("read error");
            else if (!size)
            die("unexpeced end of file");

            switch (c) {
            default:
                /* ordinaly char; push it to the
                 * buffer if there is still room
                 * for it, otherwise
                 * dump it silently */
                if ((pos - barcode) < MAX_BARCODE_LEN)
                    *pos++ = c;
                break;
                case '\r':
                    prev = c;
                break;
            case '\n':
                if (prev != '\r')
                    die("unexpected char sequence");
                else {
                    /* good */
                    *pos = prev = '\0';
                    printf(barcode);
                    printf("\n");
                    fflush(stdout);
                    pos = barcode;
                }
            }
    }

        return 0;
}

● set_sighandlers( )

まず最初にシグナルハンドラの設定を行います.このサンプルではSIGHUP,SIGINT,ISGILL の3つのシグナルだけにしかハンドラを設定していませんが,環境によっては他のシグナルにもハンドラを指定する必要が出てくると思います.

set_sighandlers( )の中で設定しているハンドラ(signal_handler())は,die()を呼ぶだけの簡単なものになっています.

● die( )

die( )は名前のとおり,死ぬための関数です.このサンプルコードでは,全体をシンプルにするためにエラーが発生した場合にはメッセージと共にすぐ終了するようにしました.

実際に組み込み機器の開発を行っている場合は,エラーが発生した場合でもいろいろな方法を使ってエラーを通知したり復旧したりするように作り込みます.コンピュータの業界では20対80の法則というのがありますが,組み込みの20対80の法則は,正常系のコード対エラー処理系のコードかもしれません.

もう1つdie( )の重要な仕事は,restore( )を呼ぶことです.シグナルハンドラからも呼ばれる必要があるため,オリジナルのtermiosを保持する変数はグローバル変数としています.

● need_restore

ただし,オリジナルのtermios情報を取得する前にもdie( )が呼ばれてしまうと,すべての変数が0で初期化されているオリジナル変数を使うことになってしまい,逆に設定を変更してしまうことになります(注3).

このため,need_restore変数を使ってリストアするかどうか判断すことにしました(リスト4の23,24行目).need_restoreをTRUEに設定しているのは,tcsetattr()の直前です.

need_restoreは,必ずtcsetattr( )の前で行う必要があります.これは,UNIXのシグナルは非同期で割り込んでくるために,「tcsetattr( )→need_restore = TRUE」の順番では,この間にシグナルが入ってしまった場合,restore されずに終了してしまう可能性があるからです.

注3)初期化されていないグローバル変数は BSS セクションに領域が取られるために, main( )が始まった時点では0 で初期化されています.

● main( )

それではmain( )に戻りましょう.set_signalhandlers( )が終わった後にシリアルデバイスファイルを開いています.正常にファイルディスクリプタが取得できたら,tcgetattr( )を使ってglobal変数のorignalに現在の設定を読み込ませます.

正常に読み込めた後,orignal の情報をnew に入れます.new はまったく別の設定を行うため,自分で1から値を代入しても問題ありませんが,必要な場所だけ変更するようにした場合はこの方法が簡単です.

次にnewをrawモード用に変更します.rawモードと言ってもすべての設定を変更するわけではありません.manページによると,cfmakerawは与えられたstruct termiosに対してリスト5 の設定を行うマクロ的な関数でしかありせん.このため,元々の設定によってはcfmakeraw()した後のnewであっても設定値が異なることがありますので注意してください.

●リスト5 cfmakeraw( )

cfmakeraw sets the terminal attributes as follows:
        termios_p->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP
                |INLCR|IGNCR|ICRNL|IXON);
        termios_p->c_oflag &= ~OPOST;
        termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
        termios_p->c_cflag &= ~(CSIZE|PARENB);
        termios_p->c_cflag |= CS8;

cfmakeraw( )でraw モードの設定を行った後は,ボーレートの設定を行います.すでに説明したとおり,termios構造体を直接触らずアクセスメッソドであるcfsetispeed()とcfsetospeed()を使います.

これでこのサンプル的にはすべての設定ができましたので,tcsetattr( )を使って設定を変更することができます.必ずtcsetattr( )の前にneed_restoreをTRUEに変更してください.

tcsetattr( )には,設定をするシリアルデバイスのファイルディスクリプタとオプショナルアクションを指定するフラグ,そして設定値を持っているtermios構造体へのポインタを渡します.

第2 引数であるフラグには表6 の3 種類があり,設定を行うときのふるまいを指定することが可能です.状況に合せて使いわける必要があります.

●表6 tcsetattr のふるまい指定フラグ
TCSANOW 設定を即座に変更する
TCSADRAIN 指定されているシリアルポートに書きこまれたデータがすべて転送され終った後に設定の更新 を行う
TCSAFLUSH 指定されたシリアルポートに書き込まれてデータがすべて転送され,かつ,受け取ったデータ のうちユーザランド側でread されていないデータが捨てられた後に設定の更新を行う

設定が終わったところで,シリアルデバイスファイルを読み込みましょう.rawモードではバーコードリーダが転送してくるデータフォーマットが\r\nとなることを思いだしながらコードを読んでください.

readは1文字ずつ行います(注4).readシステムコールは基本的にデータが読めるようになるまでブロックします(注5).

read が戻ってきたらまずはエラーチェックから始めます.ブロック中にシグナルが発生してしまうと,-1を戻しつつerrnoにEINTRを設定して関数が戻ってきてしまいます.このため,エラーを表わす-1が戻ってきた場合,errnoがEINTR以外であればエラーとして終了させ,EINTERの場合にはもう一度read に戻るようになっています.今回のサ ンプルコードでは,シグナルが入ってきた場合にはdie( )を呼んでいるだけなので,あまり気にしなくても良いのですが,シグナルを多用している場合には注意が必要です(注6).

readが0を戻した場合にはEnd of File(EOF)です.EOF時にはエラーとして終了させています.

エラーチェックが終わり,read が正常だった場合は読み込んだデータcの処理を始めます.このサンプルでは‘\r’または‘\n’以外は正常な値として処理することにします.cの値が正常値であり,かつバッファにまだ余裕がある場合にのみ,cの値をバッファに書き込みます.

cの値が‘\r’だった場合は次に‘\n’がくることが予測できるので,とりあえずprev にc の値を保存してreadに戻ります.

cの値が‘\n’だった場合には,1つ前の文字が‘\r’だったと予測できます.もしprevの値が‘\r’の場合にはデータの終わりを表わすので,正常処理としてバッファに溜まっているデータを標準出力に出力します.画面上見やすくするために‘\n’を別途出力しています.

cの値が‘\n’にもかかわらず,prevの値が‘\r’ではなかった場合は,正常なシークエンスではないのでエラーとします.

ソースコードをout of tree でコンパイルして,Armadill-9上で試してみます(図11).

図5 のバーコードを読み込むたびに,“SoftwareDesign”と表示されたら成功です.

●図11 Armadillo 上で実行

$ barcode
SoftwareDesign
SoftwareDesign
SoftwareDesign
注4)システムコールのオーバヘッドが気になる方は,ぜひまとめ読みをするコードを書いてみてください.
注5)open 時にO_NONBLOCK が指定されているファイルディスクリプタが対象のときはブロックしません.
注6)実際には,使用するバーコードリーダが出力する文字以外はすべてエラーとすべきです.

音の再生

次は音楽再生のほうを作ってみましょう.

仕様としては,再生側に音楽データを保持する容量を持たせないためにも,音楽データを外部サーバからストリームとして受け付け,逐次再生できるようにしたいと思います.サーバは複数台の機器へ別々のデータをストリーム配信するわけですから,ストリームデータは圧縮する必要があります.端末側は圧縮されたデータをデコードしつつ再生する機能が必要になります.

使用する機器

Armadillo-9 にはオーディオインターフェースとして,AC97(Audio Codec '97)とI2S(Inter-ICSound)が準備されていますが,実際にデジタル音声をアナログに変換するAD/DAコンバータは搭載されていません.アナログ機器はとてもデリケートなうえに,用途によって選ぶ幅があまりにも広いため,Armadillo-9 では使用する方に選んでいただく形にしています.

最近ではUSBオーディオアダプタが安価に入手できるようになったので,手軽に使ってみたい場合などとても便利です.今回の試作では玄人志向製「AUDIOJACK-USB」を使用し検証しています(写真3).USB audio device classをサポートしているものであれば,どのUSB Audio 機器を使っても同じように動かすことができると思います.

●写真3 玄人志向「AUDIOJACK-USB」

使用するソフトウェア

音楽再生用のアプリケーションを自分で作るのもとても楽しいと思いますが,せっかくオープンソースな環境で開発をしているわけですから,今回はalsaplayerという音楽再生プログラムを使ってみたいと思います.alsaplayerはデスクトップでも使われているアプリケーションですが,モジュール化がとても進んでいるため,無用の機能を外すことで,組み込み用途としても十分使えるようになっています.

本稿執筆時点での最新版は0.99.76です.このバージョンでは表7の機能がモジュール化されています.

●表7 alsaplayer 0.99.76 でモジュール化された機能
カテゴリ input interface output reader scope
名前 audiofile
cdda
flac
mad
mikmod
sndfile
vorbis
wav
deamon
gtk
text
xosd
alsa-0.5.x
alsa-final
esound
jack
nas
null
oss
sgi
sparc
file
http
blurscope
levelmeter
logbarfft
monoscope
opengl_spectrum
spacescope
synaescope

仕様的にストリームデータは圧縮されていることが望ましいため,今回はinput の中からvorbis(Ogg Vorbis:GPLで公開されているデジタルオーディオフォーマット)を使います.interfaceとしてはバックグラウンドで動いてくれれば問題ないので,daemonを使います.しかし,テスト時にはなんらかの出力があると便利なので一応text インターフェースも有効にしましょう.

出力は,2.6 系で組み込まれているALSA を使いたいのでalsa-final,readerはHTTPでサーバから音楽データを取り出すためのhttp を使います.一応テスト用にfileも作っておきましょう.

scopeはビジュアルエフェクト用ですので,今回は必要ありません.表8のモジュールを選択すれば,仕様を満たすことができそうです.

●表8 今回選択したモジュール
interface daemon
input ogg vorbis
output alsa
reader http

Ogg Vorbisを使うにあたり,一般的にデスクトップで使われているlibogg とlibvorbis を使わず,Tremor と呼ばれるVorbis ライブラリを使用します.Toremor はフットプリントが小さく組み込み用に開発されたVorbis互換のライブラリで,ARM用にチューンされていることも特徴の1つです.幸いArmadillo-9はCPUにARMを採用しているので,Tremorを使ってみたいと思います.

カーネル設定の変更

また,ALSAを使用するためにカーネルの設定を変更する必要があります.デフォルトのカーネルでは互換性を維持するためにOSS が有効になっていますが,OSSが有効な場合ALSAが有効にならない場合あるので注意してください.

図12 の内容をカーネルのメニューから選択し,逆に図13を外してください.USBメニューの中の「USB Audio」はOSS用のデバイスドライバですので,必ず非選択にしてください.

●図12 カーネルメニューから選択する内容

Device Drivers --->
   Sound --->
      <*> Sound card support
         Advanced Linux Sound Architecture --->
         <*> Advanced Linux Sound Architecture
            USB devices --->
                <*> USB Audio/MIDI driver
●図13 カーネルメニューから外す内容

Device Drivers --->
    USB support --->
        < > USB Audio support

libasound の組み込み

ALSAデバイスドライバをユーザランド側で使うときには,OSS のように直接デバイスを扱うのではなく,libasoundと呼ばれるライブラリを使用するのが一般的なようです.こちらも用意します.

あいにく,現在のAtmark Dist にはこれらのライブラリやアプリケーションが入っていないため,メニューを選択するだけで使えるようにはなりません.そこで,Atmark Dist に組み込むようにしてみます.

Atmark Dist に外部のライブラリやプログラムを組み込むのはとても簡単です.Atmark Dist 自体がmakeベースにしたビルドシステムを使っているので,Makefile を追加してあげるだけで簡単にターゲットボードに合った形でコンパイル,インストールを行ってくれます.

パッチを当てた後(注7),ビルドしてイメージファイルを作ります.イメージファイルの作成方法やフラッシュメモリへの書き込み方法は前章を参考にしてください.

音楽の再生

再起動後,alsaplayer を起動します.最初はoggファイルをダウンロードしてから再生してみます.ダウンロードするにはwgetを使うと便利です(図14).これで音楽の再生ができることが確認できました.

●図14 ファイルをダウンロードして再生

$ wget http://internal-server/path/to/my-oggfile.ogg
$ alsaplayer -i text my-ogg-file.ogg

次にサーバ側に複数の音楽データを置いて,それらのリストを書いたプレイリストを用意します.alsaplayerはプレイリストが与えられるとその中の音楽データを逐次再生していきます(図15).

●図15 プレイリストを使って音楽データを再生

$ alsaplayer -i text http://internal-server/path/to/play-list.pls

バーコード情報からplaylist への変換

さて,バーコードデータの取得と音楽の再生ができたところで,概要図内でできあがっていない部分の作成に取りかかります.

バーコード情報から音楽の情報へ変換は,サーバサイドで行います.組み込み機器を使ったシステム全体を設計する場合,できるだけサーバ側で処理を行うように設計し,組み込み機器端末はシンプルにしておいたほうが,あとあと楽ができることが多いと思います.

今回はサーバーサイドプログラミングの特集ではないので,CGIはshのcase文を使った単純なものにしました(リスト6).

●リスト6 バーコード情報から音楽情報への変換スクリプト(b2p.cgi)

#!/bin/sh

DATA_LOCATION=/path/to/data/location

case $QUERY_STRING in
barcode1*)
PLAYLIST_PATH=my-album1.pls
;;
barcode2*)
PLAYLIST_PATH=my-album2.pls
;;
esac

PLAYLIST_SIZE=`stat -c%s $DATA_LOCATION/$PLAYLIST_PATH`

echo "Content-type: audio/x-scpls"
echo "Content-Length: $PLAYLIST_SIZE"
echo ""

cat $DATA_LOCATION/$PLAYLIST_PATH

サーバに対してバーコードを投げるだけで,プレイリストの内容がaudio/x-scpls として戻るだけの単純なスクリプトです.

実際にalsaplayer で動かしてみましょう.alsaplayer は引数として与えられた文字列の最後が.m3u または.pls のときにだけplaylist が与えられたと判断します.このため,barcodeの最後に.m3uまたは.plsを付けるか,is_playlist()の挙動を変更する必要があります.

幸い,先ほど作ったb2p.cgi はbarcode の後に不要な文字列が付いていても問題ないよう,case 文を前方一致で判別するように作ってありますので,今回はalsaplayerに渡す文字列の最後尾に.pls を付けることで対応します(リスト7,8).

●リスト7 alsaplayer/app/Playlist.cpp の変更内容

787行目付近

enum plist_result
Playlist::Load(std::string const &uri, unsigned position, bool force)
{
    int pls = 0;

    // Check extension
    if(!force) {
        if(!is_playlist(uri.c_str()))
            return E_PL_DUBIOUS;
}
●リスト8 utilities.c の変更内容

136行目付近

int is_playlist(const char *path)
{
    char *ext;

    if (!path)
        return 0;
    ext = strrchr(path, '.');
    if (!ext)
        return 0;
    ext++;
    if (strncasecmp(ext, "pls", 3) == 0 ||
        strncasecmp(ext, "m3u", 3) == 0) {
            return 1;
    }
    return 0;
}

図16のようにして音楽が再生されれば成功です.

●図16 変換したプレイリストを使って音楽を再生

$ alsaplayer 'http://localhost/cgi-bin/b2p.cgi?barcode1.pls'

全部つないでみる

図1の概要図で表わした3つのコンポネントが揃ったので,すべてをつないで動かしてみます.リスト4のbarcode.cをbarcode_player.cとして,リスト9の関数を追加します.

●リスト9 barcode.c に追加する関数

static void refresh_playlist(char *barcode)
{
    pid_t pid;
    int status;

    pid = fork();
    if (pid < 0)
        die("fork failed");
    if (!pid) {
        char buffer[PATH_MAX];
        snprintf(buffer, PATH_MAX, "%s%s.pls",
                REQUEST_URI, barcode);
        execlp("alsaplayer", "alsaplayer",
                "--quiet",
                "--session-name", SESSION,
                "-E", buffer, NULL);
    }
    while (waitpid(pid, &status, 0) < 0) {
        if (errno != EINTR)
            die("waitpid failed");
    }
}

static void init_daemon(void)
{
    pid_t pid;

    pid = fork();
    if (pid < 0)
        die("fork for daemon failed");
    if (!pid) {
        execlp("alsaplayer",
                "/usr/bin/alsaplayer", "--quiet",
                "--session-name", SESSION,
                "-i", "daemon", "-o",
                "esound", NULL);
    }
}

さらに,元のbarcode.c の95行目付近を,refresh_playlist( )を呼びだすように変更します(リスト10).

この変更で,バーコードリーダの読み込みから再生まで可能になります.

●リスト10 barcode.c の内容を変更

95行目付近変更前

/* good */
*pos = prev = '\0';
printf(barcode);
printf("\n");
fflush(stdout);
pos = barcode;

変更後
/* good */
*pos = prev = '\0';
pos = barcode;
refresh_playlist(barcode);
先頭 Part 2 Part 4

Creative Commons License
この作品は、クリエイティブ・コモンズ・ライセンスの下でライセンスされています。