ALSA:TLV

音量のデータ
ここまでで、要素の情報と、要素の値が取得できました。

音量やチャンネルマップ、ミュートの設定、各端子が接続されているかなどの情報があります。

要素の値を変更すると、これらの音量の値などを変更することができます。
マスター音量
一例として、再生時のマスター音量の情報と値を見てみます。

マスター音量の名前は、"Master Playback Volume" です。

# 要素の情報
-- [28] --
<name> Master Playback Volume
type: (2) INTEGER
readable: 1
writable: 1
volatile: 0
inactive: 0
locked: 0
tlv_readable: 1
tlv_writable: 0
tlv_commandable: 0
owner: 0
user: 0
count: 1
(int) min:0, max:64, step:0

# 要素の値
-- [28] --
<name> Master Playback Volume
type: (2) INTEGER
27,

マスター音量の要素は、INTEGER (数値) タイプで、読み書き可、TLV での読み込みが可となっています。
値の範囲は、0〜64 です。
値の個数は一つで、現在の値は 27 になっています。

つまり、音量は、0〜64 の範囲でしか指定できないということです。
dB
ALSA では、TLV 機能を使うことで、dB (デシベル) 単位で音量を指定することができます。
(正確には、dB 値を、生の音量値に変換する)
TLV
要素の値を操作する時、TLV 機能を使うことで、値に関する補足情報を読み書きしたり、要素に対してコマンドを実行することができます。

音量の場合は、要素の値とは別に、TLV で dB の情報が付加されているので、それを読み込むことで、指定できる dB 値の範囲を取得することができます。

なお、各要素で TLV 操作ができるかは、要素情報の is_tlv_readable, is_tlv_writable, is_tlv_commandable で判断できるので、そちらで確認してから使用してください。

マスター音量の場合は、TLV の読み込みのみが可能となっています。
TLV 読み込み
int snd_ctl_elem_tlv_read(snd_ctl_t *ctl, const snd_ctl_elem_id_t *id,
    unsigned int *tlv, unsigned int tlv_size);

要素の識別子 (snd_ctl_elem_id_t) を指定して、指定した要素の TLV 値を読み込みます。

tlv値が格納される配列
tlv_sizetlv 配列の全体のバイト数

tlv_size は、データ全体が読み込めるサイズである必要があります。
TLV データの実際のサイズは取得できないので、大きめに用意しておくといいでしょう。

dB 情報 (コンテナは除く) の場合は、最大で 8 * sizeof(int) です。

正しく読み込めなかった場合は、tlv[0] = -1, tlv[1] = 0 になります。
TLV データ
TLV データには、「タイプ・サイズ・値」の3つの要素があり、整数の配列に格納されます。

tlv[0]TLV タイプ
tlv[1]値全体のバイト数 (個数ではない)
tlv[2..]値。タイプによって、数や意味は異なる。

先頭にタイプ、2番目に値部分のサイズ、3番目以降に値が入ります。

音量の dB 関連値の場合は、直接この値を使うことはせずに、対応する関数を使います。
TLV タイプ
#define SND_CTL_TLVT_CONTAINER   0x0000 //コンテナ

#define SND_CTL_TLVT_DB_SCALE    0x0001 //dB スケール
#define SND_CTL_TLVT_DB_LINEAR   0x0002 //線形音量 (0.0〜1.0)
#define SND_CTL_TLVT_DB_RANGE    0x0003 //dB 範囲
#define SND_CTL_TLVT_DB_MINMAX   0x0004 //dB 最小値/最大値
#define SND_CTL_TLVT_DB_MINMAX_MUTE   0x0005 //dB 最小値/最大値(ミュート)

//チャンネルマップ
#define SND_CTL_TLVT_CHMAP_FIXED   0x00101
#define SND_CTL_TLVT_CHMAP_VAR     0x00102
#define SND_CTL_TLVT_CHMAP_PAIRED  0x00103

TLV タイプは、配列の先頭で指定される値です。
現在は、dB とチャンネルマップ関連のタイプしかありません。

コンテナタイプは、値の中に、複数の TLV データが含まれる形になります。

音量は、基本的に SND_CTL_TLVT_DB_SCALE が使われます。
dB 情報
読み込んだ TLV データから、dB の情報を取得したい場合は、まず、snd_tlv_parse_dB_info() で、TLV データ内から、dB 情報の位置を取得します。

その後、snd_tlv_get_dB_range() などを使って、dB の範囲を取得したり、値を変換したりします。
TLV から dB 情報を検索
int snd_tlv_parse_dB_info(unsigned int *tlv, unsigned int tlv_size, unsigned int **db_tlvp);

指定された TLV データから、dB 情報 (SND_CTL_TLVT_DB_*) を検索し、先頭位置のポインタを db_tlvp に返します。
コンテナタイプであれば、その中のデータを見つけます。

tlv_sizetlv 配列全体のバイト数
戻り値見つかった場合は、その TLV データの全体バイト数 (タイプやサイズのデータも含む)。
見つからない場合は -ENOENT。
エラーの場合は、負のエラーコード。

成功した場合は、0 より大きい値になるので、注意してください。
dB の範囲を取得
int snd_tlv_get_dB_range(unsigned int *tlv, long rangemin, long rangemax, long *min, long *max);

snd_tlv_parse_dB_info() で取得したポインタを渡して、音量に指定できる dB 値の、最小値と最大値を取得します。

rangemin と rangemax は、要素の本来の数値の、最小値と最大値を指定します。
要素情報の、snd_ctl_elem_info_get_min() と snd_ctl_elem_info_get_max() で取得できる値です。

min, max に、dB 値の最小値と最大値が返ります。
この場合、0.01 dB 単位の整数となります。
生の音量値を dB 値に変換
int snd_tlv_convert_to_dB(unsigned int *tlv, long rangemin, long rangemax, long volume, long *db_gain);

snd_tlv_parse_dB_info() で取得したポインタを渡します。
そこから dB 情報を取得し、生の音量値を、dB 値に変換します。

現在の音量値から、dB 単位の値を取得する時に使います。

volume は、変換する生の音量値です。
db_gain に、変換された db 値が返ります (0.01 dB 単位)。
dB 値を生の音量値に変換
int snd_tlv_convert_from_dB(unsigned int *tlv,
    long rangemin, long rangemax, long db_gain, long *value, int xdir);

snd_tlv_parse_dB_info() で取得したポインタを渡します。
そこから dB 情報を取得し、dB 値を、生の音量値に変換します。

dB 単位で音量を変更したい時に使います。

rangemin
rangemax
生の音量の最小値と最大値
db_gain変換する dB 値 (0.01 dB 単位)
value変換された、生の音量値が返る
xdir整数に変換する時の、切り上げの方向。
正の値で切り上げ、負の値で切り捨て。
0 で、最も近い方向に切り上げ。
プログラム
TLV で読み込み可能な要素の TLV データを出力します。
TLV データに dB 情報があれば、dB 範囲と、現在の音量の dB 値を出力します。

$ cc -o 05-tlv 05-tlv.c -lasound

#include <stdio.h>
#include <alsa/asoundlib.h>

int main(int argc,char **argv)
{
    snd_ctl_t *ctl;
    snd_ctl_elem_list_t *list;
    snd_ctl_elem_info_t *info;
    snd_ctl_elem_value_t *value;
    snd_ctl_elem_id_t *eid;
    uint32_t i,j,cnt,vcnt,vcnt2;
    unsigned int tlv[8],*ptlv;
    long min,max,val1,val2;

    if(snd_ctl_open(&ctl, (argc >= 2)? argv[1]: "default", 0))
        return 1;

    //リスト取得

    snd_ctl_elem_list_malloc(&list);

    snd_ctl_elem_list(ctl, list);

    cnt = snd_ctl_elem_list_get_count(list);

    snd_ctl_elem_list_alloc_space(list, cnt);
    snd_ctl_elem_list(ctl, list);

    //リスト

    snd_ctl_elem_info_malloc(&info);
    snd_ctl_elem_value_malloc(&value);
    snd_ctl_elem_id_malloc(&eid);

    for(i = 0; i < cnt; i++)
    {
        snd_ctl_elem_list_get_id(list, i, eid);

        snd_ctl_elem_info_set_id(info, eid);
        snd_ctl_elem_info(ctl, info);

        //TLV

        if(snd_ctl_elem_info_is_tlv_readable(info))
        {
            snd_ctl_elem_tlv_read(ctl, eid, tlv, 8 * sizeof(int));

            printf("-- [%d] --\n", i);
            printf("<name> %s\n", snd_ctl_elem_list_get_name(list, i));
            printf("type:%d, size:%d\n", tlv[0], tlv[1]);

            vcnt = tlv[1] / sizeof(int);

            if(tlv[0] == SND_CTL_TLVT_CONTAINER)
            {
                //コンテナ

                for(ptlv = tlv + 2; vcnt; )
                {
                    printf("{\n type:%d, size:%d\n [", ptlv[0], ptlv[1]);

                    vcnt2 = ptlv[1] / sizeof(int);
                    
                    for(j = 0; j < vcnt2; j++)
                        printf("%u, ", ptlv[2 + j]);

                    ptlv += 2 + vcnt2;
                    vcnt -= 2 + vcnt2;
                }

                printf("]\n}\n");
            }
            else
            {
                printf("[ ");
                
                for(j = 0; j < vcnt; j++)
                    printf("%u, ", tlv[2 + j]);

                printf("]\n");
            }

            //dB

            if(snd_tlv_parse_dB_info(tlv, 8 * sizeof(int), &ptlv) > 0)
            {
                min = snd_ctl_elem_info_get_min(info);
                max = snd_ctl_elem_info_get_max(info);

                //range
                
                if(snd_tlv_get_dB_range(ptlv, min, max, &val1, &val2) == 0)
                    printf("dB range: %.2f, %.2f\n", val1 * 0.01, val2 * 0.01);

                //raw -> dB

                snd_ctl_elem_value_set_id(value, eid);
                snd_ctl_elem_read(ctl, value);

                if(snd_tlv_convert_to_dB(ptlv, min, max,
                    snd_ctl_elem_value_get_integer(value, 0), &val1) == 0)
                {
                    printf("current dB: %.2f\n", val1 * 0.01);
                }
            }
        }
    }

    snd_ctl_elem_id_free(eid);
    snd_ctl_elem_value_free(value);
    snd_ctl_elem_info_free(info);

    //

    snd_ctl_elem_list_free_space(list);
    snd_ctl_elem_list_free(list);

    snd_ctl_close(ctl);

    return 0;
}
実行結果
-- [28] --
<name> Master Playback Volume
type:1, size:8
[ 4294960896, 100, ]
dB range: -64.00, 0.00
current dB: -37.00
-- [58] --
<name> Playback Channel Map
type:0, size:16
{
 type:258, size:8
 [3, 4, ]
}
-- [59] --
<name> PCM Playback Volume
type:1, size:8
[ 4294962196, 20, ]
dB range: -51.00, 0.00
current dB: 0.00
音量
音量は、SND_CTL_TLVT_DB_SCALE タイプで、値は2つになっています。

1つ目の値は、フラグなども含めた値なので、直接ユーザーが操作することはできません。
詳細は、ALSA ライブラリのソースコードを見ればわかります。

alsamixer コマンド (alsa-utils パッケージ) で、各音量をチェックしてみてください。
dB の最小値と最大値、現在の dB 値が一致しているのが確認できると思います。



Master の 17 という値は、音量を 0〜100 で表現した時の値です。

alsamixer のソースコードを見るとわかりますが、少し複雑な計算となっているので、PulseAudio などのアプリで見た時の音量と、値が一致しない場合があります。
音量の連動
再生のマスター音量は、PulseAudio の出力(シンク)の音量と連動しているので、ALSA のマスター音量を変更すると、PulseAudio の出力音量も変更されます。

逆も同じで、pavucontrol で PulseAudio の出力音量を変更すると、alsamixer 上の音量も変更されます。
ただし、この場合、マスター音量の他に、PCM の音量も微妙に変化するようです。
チャンネルマップ
チャンネルマップは、コンテナの情報になっています。

Playback Channel Map は、SND_CTL_TLVT_CHMAP_VAR タイプで、値が2つです。
3 と 4 は、チャンネル位置を意味し、SND_CHMAP_FL (左) と SND_CHMAP_FR (右) が指定されています。