試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

PulseAudio上のオーディオをWAVE保存するツールをGTK+のGUIアプリケーションに

PulseAudio上の音声をWAVE保存するためのコードをVala言語で作成(仮)」と「PulseAudio上のオーディオをWAVE保存するツールの続き(詳細なオーディオ形式の指定とエラーメッセージの処理についてのメモ)」を踏まえた上でGTK+を用いて各種パラメータ指定をGUIから行えるようにしたValaコードを下に貼り付ける。

  1. メモ
    1. メインループについて
    2. ラムダ式(匿名関数)
  2. コード
  3. 使い方

メモ

メインループについて
以前の例ではGLibの「メインループ」を手動で作成して回していたが、GTK+では(GTK+アプリケーションに必須の)Gtk.main()を呼ぶことによりGStreamerも動作するようになる。
データを流し始めたい/止めたいところでパイプラインオブジェクトのメンバ関数set_state()を呼ぶ形で制御を行う。

ラムダ式(匿名関数)
C#言語のバージョン3.0以上において

([引数...]) =>
{
(処理...)
}

という書式で小さな名無しの関数への参照を含むオブジェクト(デリゲート)を表現することができるようになっているのだが、Vala言語でもこれが利用できる。何かのイベントに対するハンドラ関数の中で短い処理しかしないようなときに

obj.signal += ([引数...]) =>
{
(処理...)
};

のように記述する形などで役に立つ。Vala公式サイトのGTK+のサンプルなどでもよく使用されている。
この匿名関数(ラムダ)は「波括弧で(関数の中の)処理群を囲む」という形以外に単一の式で使用することもできる。上のような関数での処理が1行であれば「obj.signal += (arg) => (処理...);」とすることが可能。また、下のように、操作だけでなく、条件を真偽値として返すような使い方もある。

namespace LambdaAndDelegateTest
{
  class MainClass
  {
    /*
     * ここの宣言では右辺(戻り値)の型を「delegate」の右に書いて
     * その後ろに名前、引数の型を記述
     */
    delegate int  MyIntDelegate  (int x, int y);
    delegate bool MyBoolDelegate (int x, int y);
    public static void main (string[] args)
    {
      /*
       * 左辺は関数に名前を付ける感覚
       * 右辺では引数とそれに対する操作や条件の部分を指定する
       */
      MyIntDelegate  plus        = (x, y) => x + y;   // 和(int)
      MyIntDelegate  times       = (x, y) => x * y;   // 積(int)
      MyBoolDelegate y_is_bigger = (x, y) => x < y;   // yが大きいかどうか(bool)
      MyBoolDelegate y_equals_x  = (x, y) => x == y;  // xとyが等しいかどうか(bool)
      print ("plus(1, 2): %d\n", plus (1, 2));
      print ("times(4, 5): %d\n", times (4, 5));
      print ("y_is_bigger(3, 4): ");
      if (y_is_bigger (3, 4))
        print ("true\n");  // yが大きいのでtrue
      else
        print ("false\n");
      print ("y_equals_x(4, 1): ");
      if (y_equals_x (4, 1))
        print ("true\n");
      else
        print ("false\n");  // 等しくないのでfalse
    }
  }
}

上のコードの実行結果は

plus(1, 2): 3                      
times(4, 5): 20
y_is_bigger(3, 4): true
y_equals_x(4, 1): false

となる。

コード

コンパイルにはValaの他、GLib/GTK+/GStreamerの全ての開発パッケージが必要。
[任意]ファイル名: parecwavetest3.vala ライセンス: GPL-3

using GLib;
using Gst;
using Gtk;

/*
 * PaRecWave(Test3)
 * GStreamerを用いてPulseAudio上の音声をWAVEファイルに保存するGUIアプリケーション
 * (C) 2009 kakurasan
 * Licensed under GPL-3
 * valac --pkg gstreamer-0.10 --pkg gtk+-2.0 -o parecwavetest3 parecwavetest3.vala
 */

namespace PaRecWaveTest3
{
  /* 要素のリンクに失敗したときの例外(ユーザ定義例外) */
  errordomain LinkError
  {
    FAILED,  /* エラーコード */
  }
  /* オーディオの詳細なフォーマット */
  struct WavFormat
  {
    public int rate;
    public int channels;
    public int depth;
  }
  /* 要素へのパラメータをまとめたもの */
  struct ElemParams
  {
    public WavFormat fmt;
    public string device;
    public string location;
  }
  class PaRecWavePipeline : Gst.Pipeline
  {
    Gst.Caps caps;
    Gst.Element elem_src;
    Gst.Element elem_queue;
    Gst.Element elem_conv;
    Gst.Element elem_wavenc;
    Gst.Element elem_sink;
    weak MainWindow win;  // weak/unownedは参照カウンタを増やさない「弱い参照」
    public PaRecWavePipeline (string name, MainWindow win, ElemParams params)
    {
      this.win = win;
      /* 名前 */
      this.set_name (name);
      /* メッセージ通知用の通り道(バス)上のメッセージを取得 */
      this.bus.add_signal_watch ();
      this.bus.message += this.on_bus_message;
      /* 各要素(プラグイン) */
      this.elem_src    = Gst.ElementFactory.make ("pulsesrc", null);
      this.elem_queue  = Gst.ElementFactory.make ("queue", null);
      this.elem_conv   = Gst.ElementFactory.make ("audioconvert", null);
      this.elem_wavenc = Gst.ElementFactory.make ("wavenc", null);
      this.elem_sink   = Gst.ElementFactory.make ("filesink", null);
      /* 要素を並べる */
      this.add_many (this.elem_src, this.elem_queue, this.elem_conv, this.elem_wavenc, this.elem_sink);
      /* フィルタ */
      this.caps = new Gst.Caps.simple ("audio/x-raw-int",
                                       "rate",     typeof (int), params.fmt.rate,
                                       "channels", typeof (int), params.fmt.channels,
                                       "depth",    typeof (int), params.fmt.depth);
      /* 幾つかの要素を設定 */
      this.elem_src.set ("device",   params.device);     // キャプチャするPulseAudioのデバイス
      this.elem_sink.set ("location", params.location);  // 出力ファイルのパス
    }
    /*
     * 「throws LinkError」は
     * この関数の中で例外LinkErrorが発生する可能性があることを示す
     */
    void link_elements () throws LinkError
    {
      /* それぞれの接続試行で成功/失敗の結果が真偽値として返る */
      /* 「throw new Exceptionname.CODE ("message");」で例外を発生させる */
      if (!this.elem_src.link (this.elem_queue))
        throw new LinkError.FAILED ("pulsesrc->queue");
      if (!this.elem_queue.link (this.elem_conv))
        throw new LinkError.FAILED ("queue->audioconvert");
      if (!this.elem_conv.link_filtered (this.elem_wavenc, this.caps))  // フィルタ付き
        throw new LinkError.FAILED ("audioconvert->wavenc");
      if (!this.elem_wavenc.link (this.elem_sink))
        throw new LinkError.FAILED ("wavenc->filesink");
    }
    public void start_recording ()
    {
#if DEBUG
      GLib.debug ("start_recording()");
#endif
      try
      {
        /* 例外を起こすかもしれない処理 */
        this.link_elements ();
      }
      catch (LinkError e)
      {
        /* LinkErrorが発生したときの処理 */
        GLib.warning ("LinkError: %s", e.message);
        Gtk.MessageDialog errdlg = new MessageDialog (this.win, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, "Cannot link element: %s", e.message);
        errdlg.run ();
        errdlg.destroy ();
      }
      /* ボタンの状態を変更 */
      this.win.buttons_set_sensitive (false, true);
      /* データを流し始める */
      this.set_state (Gst.State.PLAYING);
    }
    public void stop_recording ()
    {
#if DEBUG
      GLib.debug ("stop_recording()");
#endif
      /* 流れを止める */
      this.set_state (Gst.State.NULL);
      this.win.buttons_set_sensitive (true, false);
    }
    /* パイプライン内のメッセージ・バス上にメッセージが流れたときの処理 */
    void on_bus_message (Gst.Message message)
    {
      if (message.type == Gst.MessageType.ERROR)
      {
        GLib.Error gerror;
        string debug;
        Gtk.MessageDialog errdlg;
        message.parse_error (out gerror, out debug);  // それぞれ中身がセットされる
        this.stop_recording ();
        GLib.warning (gerror.message, null);
        errdlg = new MessageDialog (this.win, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, "Error occurred: %s", gerror.message);
        errdlg.run ();
        errdlg.destroy ();
      }
    }
  }
  class MainWindow : Gtk.Window
  {
    Gtk.AccelGroup accelgroup;
    Gtk.ImageMenuItem item_quit;
    Gtk.Menu menu_file;
    Gtk.MenuItem item_file;
    Gtk.MenuBar menubar;
    Gtk.Label label_rate;
    Gtk.Label label_channels;
    Gtk.Label label_depth;
    Gtk.Label label_device;
    Gtk.Label label_outfile;
    Gtk.SpinButton spinbtn_rate;
    Gtk.SpinButton spinbtn_channels;
    Gtk.SpinButton spinbtn_depth;
    Gtk.Entry entry_device;
    Gtk.Entry entry_outfile;
    Gtk.Button btn_outfile;
    Gtk.Button btn_rec;
    Gtk.Button btn_stop;
    Gtk.HBox hbox_rate;
    Gtk.HBox hbox_channels;
    Gtk.HBox hbox_depth;
    Gtk.HBox hbox_device;
    Gtk.HBox hbox_outfile;
    Gtk.HBox hbox_btn;
    Gtk.VBox vbox;
    PaRecWavePipeline pipeline;
    public MainWindow ()
    {
      /* 起動時のGUI部品上の値 */
      int default_rate = 44100;
      int default_channels = 2;
      int default_depth = 16;
      string default_device = "alsa_output.default.monitor";
      /* ショートカットキー(アクセラレータ) */
      this.accelgroup = new Gtk.AccelGroup ();
      this.add_accel_group (accelgroup);
      /* メニュー項目 */
      this.item_quit = new Gtk.ImageMenuItem.from_stock (Gtk.STOCK_QUIT, accelgroup);
      this.menu_file = new Gtk.Menu ();
      this.menu_file.add (item_quit);
      this.item_file = new Gtk.MenuItem.with_mnemonic ("_File");
      this.item_file.set_submenu (menu_file);
      this.menubar = new Gtk.MenuBar ();
      this.menubar.append (item_file);
      /* ラベル */
      this.label_rate = new Gtk.Label.with_mnemonic ("S_ampling rate:");
      this.label_channels = new Gtk.Label.with_mnemonic ("_Channels:");
      this.label_depth = new Gtk.Label.with_mnemonic ("De_pth:");
      this.label_device = new Gtk.Label.with_mnemonic ("_Device:");
      this.label_outfile = new Gtk.Label.with_mnemonic ("_Output file:");
      /* スピンボタン */
      this.spinbtn_rate = new Gtk.SpinButton (new Gtk.Adjustment (default_rate, 1, 999999, 1, 1000, 0), 0, 0);
      this.spinbtn_channels = new Gtk.SpinButton (new Gtk.Adjustment (default_channels, 1, 99, 1, 2, 0), 0, 0);
      this.spinbtn_depth = new Gtk.SpinButton (new Gtk.Adjustment (default_depth, 1, 99, 1, 4, 0), 0, 0);
      /* テキストエントリ */
      this.entry_device = new Gtk.Entry ();
      this.entry_device.text = default_device;
      this.entry_outfile = new Gtk.Entry ();
      /* ラベルのショートカットキーからフォーカスする部品(ニーモニック・ウィジェット) */
      this.label_rate.mnemonic_widget = this.spinbtn_rate;
      this.label_channels.mnemonic_widget = this.spinbtn_channels;
      this.label_depth.mnemonic_widget = this.spinbtn_depth;
      this.label_device.mnemonic_widget = this.entry_device;
      this.label_outfile.mnemonic_widget = this.entry_outfile;
      /* ファイル選択(参照)ボタン */
      this.btn_outfile = new Gtk.Button.with_label ("...");
      /* ボタン */
      this.btn_rec = new Gtk.Button.from_stock (Gtk.STOCK_MEDIA_RECORD);
      this.btn_stop = new Gtk.Button.from_stock (Gtk.STOCK_MEDIA_STOP);
      /* レイアウト用コンテナ */
      this.hbox_rate = new Gtk.HBox (false, 0);
      this.hbox_rate.pack_start (this.label_rate, false, false, 0);
      this.hbox_rate.pack_end (this.spinbtn_rate, false, false, 0);
      this.hbox_channels = new Gtk.HBox (false, 0);
      this.hbox_channels.pack_start (this.label_channels, false, false, 0);
      this.hbox_channels.pack_end (this.spinbtn_channels, false, false, 0);
      this.hbox_depth = new Gtk.HBox (false, 0);
      this.hbox_depth.pack_start (this.label_depth, false, false, 0);
      this.hbox_depth.pack_end (this.spinbtn_depth, false, false, 0);
      this.hbox_device = new Gtk.HBox (false, 0);
      this.hbox_device.pack_start (this.label_device, false, false, 0);
      this.hbox_device.pack_end (this.entry_device, true, true, 0);
      this.hbox_outfile = new Gtk.HBox (false, 0);
      this.hbox_outfile.pack_start (this.label_outfile, false, false, 0);
      this.hbox_outfile.pack_start (this.entry_outfile, true, true, 0);
      this.hbox_outfile.pack_start (this.btn_outfile, false, false, 0);
      this.hbox_btn = new Gtk.HBox (false, 0);
      this.hbox_btn.pack_start (this.btn_rec, true, true, 0);
      this.hbox_btn.pack_end (this.btn_stop, true, true, 0);
      this.vbox = new Gtk.VBox (false, 0);
      this.vbox.pack_start (this.menubar, false, false, 0);
      this.vbox.pack_start (this.hbox_rate, false, false, 0);
      this.vbox.pack_start (this.hbox_channels, false, false, 0);
      this.vbox.pack_start (this.hbox_depth, false, false, 0);
      this.vbox.pack_start (this.hbox_device, false, false, 0);
      this.vbox.pack_start (this.hbox_outfile, false, false, 0);
      this.vbox.pack_start (this.hbox_btn, true, true, 0);
      /* シグナル */
      /* 「(source) => { ... }」は引数sourceを受け取る匿名関数 */
      this.btn_outfile.clicked += (source) =>
      {
        /* ファイル選択ダイアログから得たファイル名をテキストエントリに表示 */
        Gtk.FileFilter filter_wav = new FileFilter ();
        Gtk.FileChooserDialog savedlg = new FileChooserDialog ("Output file", this, Gtk.FileChooserAction.SAVE);
        filter_wav.add_mime_type ("audio/x-wav");  // WAVEだけを表示させる
        savedlg.local_only = true;
        savedlg.add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                             Gtk.STOCK_SAVE, Gtk.ResponseType.OK);
        savedlg.filter = filter_wav;
        if (savedlg.run () == Gtk.ResponseType.OK)
        {
          string filepath = savedlg.get_filename ();
          string filebase = GLib.Path.get_basename (filepath);
          string ext = null;
          if (filebase.length > 4)
          {
            ext = filebase.substring (-4, 4);  // 末尾4文字
#if DEBUG
            GLib.debug ("ext: %s", ext);
#endif
          }
          /* 拡張子.wavがなければ付ける */
          if (ext == null || (ext != ".wav" && ext != ".WAV"))
            filepath += ".wav";
          this.entry_outfile.text = filepath;
        }
        savedlg.destroy ();
      };  // 「this.btn_outfile.clicked += ...」の文の最後
      this.btn_rec.clicked += (source) =>
      {
        /* 上書き確認 */
        if (GLib.FileUtils.test (this.entry_outfile.text, GLib.FileTest.IS_REGULAR))
        {
          bool cancel;
          Gtk.MessageDialog cfdlg = new MessageDialog (this, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, "File \"%s\" exists, overwrite?", this.entry_outfile.text);  // 用意されていない組み合わせなのでボタン無しにして後から追加
          cfdlg.add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK);
          cancel = (cfdlg.run () != Gtk.ResponseType.OK);
          cfdlg.destroy ();
          if (cancel)
            return;
        }
        WavFormat fmt =
        {
          (int) this.spinbtn_rate.value,      // rate
          (int) this.spinbtn_channels.value,  // channels
          (int) this.spinbtn_depth.value      // depth
        };
        ElemParams params =
        {
          fmt,                                // format(rate/channels/depth)
          this.entry_device.text,             // PulseAudio device name
          this.entry_outfile.text             // output WAVE file
        };
#if DEBUG
        GLib.debug ("rate: %d channels: %d depth: %d device: %s outfile: %s",
                    params.fmt.rate, params.fmt.channels, params.fmt.depth,
                    this.entry_device.text, this.entry_outfile.text);
#endif
        /*
         * GStreamerのパイプライン
         * 前回使用したパイプラインがある場合はこの代入時に古いほうが捨てられる
         * (他に参照がないため、参照カウンタが0になる)
         */
        this.pipeline = new PaRecWavePipeline ("parecwave", this, params);
        this.pipeline.start_recording ();
      };
      this.btn_stop.clicked += (source) => this.pipeline.stop_recording ();
      this.item_quit.activate += Gtk.main_quit;
      this.destroy += Gtk.main_quit;
      /* ウィンドウ */
      this.add (vbox);
      this.title = "PaRecWave(Test3)";
      this.btn_stop.sensitive = false;  // 起動時は停止できないようにする
    }
    /* ボタンの操作可否状態を一括変更 */
    public void buttons_set_sensitive (bool rec, bool stop)
    {
      this.btn_rec.sensitive = rec;
      this.btn_stop.sensitive = stop;
    }
  }
  class MainClass
  {
    public static int main (string[] args)
    {
      Gst.init (ref args);
      Gtk.init (ref args);
      MainWindow win = new MainWindow ();
      win.show_all ();
      Gtk.main ();
      return 0;
    }
  }
}

まだ細かい部分で改善の余地はあるので、パッケージ化して公開するとしたらもう少し調整を行うことにする。
(2009/6/2)拡張子の判別の部分では文字列(string)オブジェクトのメンバ関数has_suffix()が使えることが分かったが、ここのコードは修正しない。

使い方

まず、動作には

が必要。

サンプルレート/チャンネル数/ビット深度/キャプチャしたいPulseAudioのデバイス名/出力ファイルをそれぞれ入力し、「録音」ボタンを押し、止めたいところで「停止」を押す。
バイス名については、ALSAへの出力がされている場合

  • アプリケーション内のオーディオ(出力): alsa_output.default.monitor
  • 外部入力(ライン入力やマイク): alsa_input.default

のように指定する。ALSA以外のsource/sinkモジュールを.paファイル*1で指定している場合はそれに合った名前を指定する。
録音が正常に開始されてもファイルサイズが0のままとなる場合、「PulseAudioデーモンがサウンドバイスを解放して休んでいる状態のときに他のJACKなどのサウンドデーモンやOSSアプリケーションなどがデバイスを使用していて、録音開始時にPulseAudioが動かない」ということが原因となっているかもしれない。

使用したバージョン:

  • Vala 0.7.3
  • GStreamer 0.10.22
  • PulseAudio 0.9.15
  • GStreamer PulseAudioプラグイン 0.10.14

*1:PulseAudioデーモン向けの設定ファイルの1つで、既定では${HOME}/.pulse/default.pa、無ければ/etc/pulse/default.paが使用される