見出し画像

ALSAの音量をロータリーエンコーダで制御してみる

自分のラズパイ5に取り付けているIQaudioのオーディオボードPi-DAC+にはロータリーエンコーダを取り付ける端子が出ています。これにロータリーエンコーダをつなぐことで、ALSAオーディオのボリュームを制御することができるということのようです。

Pi-DAC+の外観とインタフェース

ただ、何か回路がつながっているというわけではなく、単にGPIO23, GPIO24とGNDとが端子として出ているだけのもののようで、最近のRaspberry PiのDAC+ではこの端子は省略されているようです。GPIOの端子から直接とれば済みますもんね。

Pi-DAC+の説明書(ロータリエンコーダの章を抜粋)

IQaudioのGitHubにはこの端子につないだロータリーエンコーダで、ALSAの音量を制御するサンプルコード(IQ_rot.c)が置いてあります。しかし残念なことに、上記の説明書にも書かれているようにWiringPiを使っているためラズパイ5では動かすことができません。

libgpiodライブラリを使ってラズパイ5のGPIOを制御できるようになったので、このサンプルコードをWiringPiの代わりにlibgpiodを使うように修正してラズパイ5で動くようにしてみました。


ハードの準備

まずは、ロータリーエンコーダをPi-DAC+の”ROT. ENC"の端子につなげました。手持ちのロータリーエンコーダを脱着できるようにピンソケットでつなげました。

なお、今回のソフトはPi-DAC+に依存するものではないので、ロータリーエンコーダの真ん中をGND、両側をどこかのGPIOに直接つなげればPi-DAC+がなくても動きます。

Pi-DAC+につないだロータリーエンコーダ

修正したソースコード

ちょっと長いですが、IQ_rot.cを修正したコードは以下の通りです。

// Rotary Encoder Sample app - IQ_rot.c 
// Makes use of WIRINGPI Library
// USES Raspberry Pi GPIO 23 and 24.
// Adjusts ALSA volume (based on Left channel value) up or down to correspond with rotary encoder direction
// Assumes IQAUDIO.COM Pi-DAC volume range -103dB to 0dB
//
// G.Garrity Aug 30th 2015 IQaudIO.com
//
// Compile with gcc IQ_rot.c -oIQ_rot -lwiringPi -lasound
//
// Make sure you have the most upto date WiringPi installed on the Pi to be used.
//
//	Oct 9th 2024:  Modified by KAKAKKO to use the libgpiod library.
//

#include <stdio.h>
#include <alsa/asoundlib.h>
#include <alsa/mixer.h>
#include <stdbool.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>

#include <gpiod.h>

#include <errno.h>
#include <string.h>

/*
   Rotary encoder connections:
   Encoder A      - gpio 23   (IQAUDIO.COM PI-DAC 23)
   Encoder B      - gpio 24   (IQAUDIO.COM PI-DAC 24)
   Encoder Common - Pi ground (IQAUDIO.COM PI-DAC GRD)
*/

// Define DEBUG_PRINT TRUE for output
#define DEBUG_PRINT 1		// 1 debug messages, 0 none

// Define the Raspberry Pi IO Pins being used
#define ENCODER_A 23		// GPIO 23
#define ENCODER_B 24		// GPIO 24

#define TRUE	1
#define FALSE	0

static volatile int encoderPos;
static volatile int lastEncoded;
static volatile int encoded;
static volatile int inCriticalSection = FALSE;

static snd_mixer_t *handle;

static pthread_t gpio_th;
static volatile bool quit = false;

// Called whenever there is GPIO activity on the defined pins.
static int encoderPulse(int evtype, unsigned int offset, const timespec *ts, void *data)
{
   /*

             +---------+         +---------+      0
             |         |         |         |
   A         |         |         |         |
             |         |         |         |
   +---------+         +---------+         +----- 1

       +---------+         +---------+            0
       |         |         |         |
   B   |         |         |         |
       |         |         |         |
   ----+         +---------+         +---------+  1

   */

	static int currentA;
	static int currentB;

	if(quit == true){
		return -1;
    }

	if(	(evtype != GPIOD_CTXLESS_EVENT_CB_RISING_EDGE) &&
		(evtype != GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE)){
		return 0;
	}

	if(inCriticalSection==TRUE) return 0;

	inCriticalSection = TRUE;

	if(offset == ENCODER_A){
		if(evtype == GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE){
			currentA = 0;
		}
		else{
			currentA = 1;
		}
	}
	else if(offset == ENCODER_B){
		if(evtype == GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE){
			currentB = 0;
		}
		else{
			currentB = 1;
		}
	}

	int MSB = currentA;
	int LSB = currentB;

	int encoded = (MSB << 1) | LSB;
	int sum = (lastEncoded << 2) | encoded;

	if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011){
		encoderPos++;
	}
	else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000){
		encoderPos--;
	}

	lastEncoded = encoded;

	inCriticalSection = FALSE;

	return 0;
}

static void *task_gpio(void *arg)
{
	int event_type = GPIOD_CTXLESS_EVENT_BOTH_EDGES;
	unsigned int offset[2] = {ENCODER_A, ENCODER_B};
	unsigned int num_lines = 2;
	timespec ts = {1, 0};	// wait time {sec, nsec}
	bool active_low = 1;

	// pull-up internal resister 
	for(int i = 0; i < (int)num_lines; ++i){
		char pull_cmd[100];
		snprintf(pull_cmd, sizeof(pull_cmd), "pinctrl set %d pu", offset[i]);
		system(pull_cmd);
	}

	gpiod_ctxless_event_monitor_multiple(
	   "gpiochip4",
	   event_type,
	   offset,
	   num_lines,
	   active_low,
	   "IQ_rot_gpiod",
	   &ts,
	   NULL,
	   encoderPulse,
	   NULL
	);

	if(DEBUG_PRINT) printf("leave task_gpio\n");

	return 0;
}

static void sig_handler(int sig)
{
	quit = true;
	snd_mixer_close(handle);
	pthread_join(gpio_th, NULL);
	exit(1);
}

int main(int argc, char * argv[])
{
	int pos = 125;
	long min, max;
	long gpiodelay_value = 250;		// was 50
	int vol_step = 10;

	snd_mixer_selem_id_t *sid;

	const char *card = "hw:CARD=DAC";
	const char *selem_name = "Digital";
	int x;
	long currentVolume;

	printf("IQaudIO.com Pi-DAC Volume Control support Rotary Encoder) v1.5 Aug 30th 2015\n");
	printf("Modified version for libgpiod v1.5k Oct 9th 2024\n\n");

	signal(SIGINT, sig_handler);

	encoderPos = pos;

	// Setup ALSA access
	snd_mixer_open(&handle, 0);
	snd_mixer_attach(handle, card);
	snd_mixer_selem_register(handle, NULL, NULL);
	snd_mixer_load(handle);

	snd_mixer_selem_id_alloca(&sid);
	snd_mixer_selem_id_set_index(sid, 0);
	snd_mixer_selem_id_set_name(sid, selem_name);
	snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);

	snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
	if(DEBUG_PRINT) printf("Returned card VOLUME range - min: %ld, max: %ld\n", min, max);

	vol_step = (max - min) / 200;
	if(vol_step < 1){
		vol_step = 1;
	}
	if(DEBUG_PRINT) printf("vol_step = %d\n", vol_step);

	// Minimum given is mute, we need the first real value
	min++;

	// Get current volume
	x = snd_mixer_selem_get_playback_volume (elem, SND_MIXER_SCHN_FRONT_LEFT, &currentVolume);
	if(x < 0) printf("%d %s\n", x, snd_strerror(x));
	else if(DEBUG_PRINT) printf("Current ALSA volume LEFT: %ld\n", currentVolume);

	x = snd_mixer_selem_get_playback_volume (elem, SND_MIXER_SCHN_FRONT_RIGHT, &currentVolume);
	if(x < 0) printf("%d %s\n", x, snd_strerror(x));
	else if(DEBUG_PRINT) printf("Current ALSA volume RIGHT: %ld\n", currentVolume);

	// start GPIO monitor thread
	pthread_create(&gpio_th, NULL, task_gpio, NULL);

	// Now sit and spin waiting for GPIO pins to go active...
	while(1){
		if(encoderPos != pos){
			snd_mixer_handle_events(handle); //handle external events such that volume is correct
			// get current volume
			x = snd_mixer_selem_get_playback_volume (elem, SND_MIXER_SCHN_FRONT_LEFT, &currentVolume);
			if(x < 0) printf("%d %s\n", x, snd_strerror(x));
			else if(DEBUG_PRINT) printf(" Current ALSA volume: %ld\n", currentVolume);

			// Adjust for MUTE
			if(currentVolume < min){
				currentVolume = 0;
				if(DEBUG_PRINT) printf(" Current ALSA volume set to min: %ld\n", currentVolume); 
			}

			// What way did the encoder go?
			if(encoderPos > pos){
				pos = encoderPos;
				currentVolume = currentVolume + vol_step;

				// Adjust for MAX volume
				if (currentVolume > max) currentVolume = max;
				if (DEBUG_PRINT) printf("Volume UP %d - %ld", pos, currentVolume);
			}
			else if(encoderPos < pos){
				pos = encoderPos;
				currentVolume = currentVolume - vol_step;

				// Adjust for MUTE
				if (currentVolume < min) currentVolume = 0;
				if (DEBUG_PRINT) printf("Volume DOWN %d - %ld", pos, currentVolume);
			}

			x = snd_mixer_selem_set_playback_volume_all(elem, currentVolume);
			if(x < 0) printf(" ERROR %d %s\n", x, snd_strerror(x));
			else if(DEBUG_PRINT) printf(" Volume successfully set to %ld using ALSA!\n", currentVolume);
		}

		// Check x times per second, MAY NEED TO ADJUST THS FREQUENCY FOR SOME ENCODERS */
		usleep(gpiodelay_value * 1000);
	}

	// We never get here but should close the sockets etc. on exit.
	snd_mixer_close(handle);
}

アルゴリズムや全体的なプログラムの構成はオリジナルのIQ_rot.cをそのまま利用しています。

libgpiodのGPIOの入力イベント待ちのAPIの関数は、関数を呼び出すとイベントループで回り続けてしまい関数から戻ってこないため、今回は別スレッドとして動作させています。
コールバック関数(encoderPulse())の戻り値を負の値にすれば、gpiod_ctxless_event_monitor_multiple()関数はワンショットのイベント検出になるので、スレッドを使わなくても動作させることもできると思います。

あとはCtrl+Cで終了するためのシグナル処理を入れました。

また、1クリックで音量レベルをどのくらい±するか(vol_step)を、音量の最小値と最大値から決めるようにしました。最大値と最小値の間を200段階で変化させるようにさせています。

ALSAのデバイスカード名(card)は"hw:CARD=DAC"、制御対象ボリューム名(selem_name)は"Digital"としています。

お使いのオーディオカードに合わせて書き換える必要があるかもしれません。aplay -Lで表示されるデバイスカード名の中から適当なものを探してください。

コンパイル方法

上記をIQ_rot_gpiod.cppというファイル名で保存して、下記のようにコンパイルします。

$ g++ -o IQ_rot_gpiod IQ_rot_gpiod.cpp -lgpiod -lasound

実行方法

実行すると下のような感じで、ボリュームの値が変わります。
cardやselem_nameに指定した文字列が正しくない場合にはエラーになると思います。

$ ./IQ_rot_gpiod 
IQaudIO.com Pi-DAC Volume Control support Rotary Encoder) v1.5 Aug 30th 2015
Modified version for libgpiod v1.5k Oct 9th 2024

Returned card VOLUME range - min: 0, max: 207
vol_step = 1
Current ALSA volume LEFT: 192
Current ALSA volume RIGHT: 192
 Current ALSA volume: 192
Volume UP 129 - 193 Volume successfully set to 193 using ALSA!
 Current ALSA volume: 193
Volume UP 137 - 194 Volume successfully set to 194 using ALSA!
 Current ALSA volume: 194
Volume UP 143 - 195 Volume successfully set to 195 using ALSA!
 Current ALSA volume: 195
Volume UP 149 - 196 Volume successfully set to 196 using ALSA!
 Current ALSA volume: 196
....

ボリュームの値が変わっているかはalsamixerで確認できます。alsamixerを起動し、F6でサウンドカード「RPi DAC+」を選び、「Digital」の項目の値が、ロータリーエンコーダの回転に合わせて変化すればOKです。「RPi DAC+」のミキシング画面を見ても「Digital」の項目が見えない場合は、カーソルキー「→」で画面を移動すれば「Digital」の項目が出てくると思います。

card="hw:CARD=DAC", selm_name="Digital"の場合の制御ボリューム

card = "default", selm_name="Master"にした場合は、defaultのMasterボリュームが制御されるかと思います。

card="default", selm_name="Master"の場合の制御ボリューム

以上、デジタル出力の前の部分で音量を調整するのが良いのかはやや疑問が残るのですが、一応、GPIOにつないだロータリーエンコーダでALSAオーディオの音量調整ができることを確認できました。

いいなと思ったら応援しよう!