1.手動による音量調整機能
普段はよくチューニングした HAT24を使うとして、状況によっては自動音量調整(AVC)をオフにして、手動で音量を絞ったり大きくしたいシーンもあるでしょう。これを実現するために以下のような仕組みを追加することにしました。
①必要な部品
右の写真は基板取付用の薄型ボリュームです。これを秋月電子通商から入手しました。スイッチ付きの薄型小型ボリューム(B型10kΩ)です。基板取り付け用なので既製品のユニバーサル基板には組み付けにくいのですが(裏面の足の位置が合わない)、そこは工作で何とかなるだろうと割り切って採用しました。
スイッチを ONにすることで通常モードから手動モードへの切り替えを感知し、ボリュームの状態を参照して音量に反映させることにします。
②スイッチの ON/OFF確認
右図のように、スイッチはオーディオ・アダプタボードの [3.3V]へ接続し、10kΩの抵抗器を経由して [GND]へ繋ぎます。またスイッチと抵抗器の接点から Teensyの 2番ピンに接続します。スイッチが OFF状態では pin2はゼロVで LOWに、ONなら 3.3Vが現れて HIGHの状態になります。このように pin2の LOW/HIGHをチェックすることでスイッチの ON/OFFが判定できます。
③音量の調整
ボリューム両端の固定端子はオーディオ・アダプタボードの [3.3V]と [GND]に接続します。可変端子はオーディオ・アダプタの [VOL]に接続します。アダプタボードの [VOL]端子は Teensyの 15ピン(アナログ入力用の A1ピン)に接続されているので、ボリュームをゼロ位置から目いっぱい回すと A1ピンの値は 0~1023に変化します。
A1の値を、SGTL5000の音量設定範囲 0~1.0で、フルスケール信号の歪みのない最大出力とされる 0.8の範囲で、つまり 0~0.8にマッピングすれば、物理的な外部ボリュームによる音量調整ができることになります。A1の値を 1278.0で除算すれば出力値に変換できますが、出力をもう少し大きく 0.85程度にしたいので 1203.35で除算しました。
以下に、スイッチとボリュームの動作テスト用のスケッチを示します。ワニ口などの小型クリップとジャンパーワイヤを利用してオーディオ・アダプタと配線しスケッチをアップロードすると、スイッチやボリュームの変化をシリアルモニターで確認することができます。
/*
manualVolume.ino
Test manaual volume control.
*/
#include <Audio.h>
#define VOLSW_PIN 2
// Sound volume control factors
#define VOLUME_INIT 0.64 // Sound volume initial value
// Create Audio objects and connections
AudioInputI2S audioInput;
AudioOutputI2S audioOutput;
AudioControlSGTL5000 audioShield;
AudioConnection patchCord1(audioInput, 0, audioOutput, 0);
AudioConnection patchCord2(audioInput, 1, audioOutput, 1);
// Global variables
float currentVolume = VOLUME_INIT; // Current volume
bool autoVolume = false; // Auto volume control ON/OFF
bool manualVolumeMode = false; // Manual volume control mode
float current_volume, prev_volume; // Manual volume indicator
void setup() {
Serial.begin(9600);
pinMode(VOLSW_PIN, INPUT);
AudioMemory(12);
// Enable the audio shield.
audioShield.enable();
audioShield.muteHeadphone();
audioShield.volume(currentVolume);
audioShield.audioPostProcessorEnable();
audioShield.unmuteHeadphone();
Serial.println("Manual mode: volume test sart!");
}
void loop() {
int swVal = digitalRead(VOLSW_PIN);
if (swVal == HIGH && manualVolumeMode == false) {
audioShield.autoVolumeDisable();
manualVolumeMode = true;
Serial.println("switch: ON!");
}
else if (swVal == LOW && manualVolumeMode == true) {
audioShield.volume(currentVolume);
if (autoVolume)
audioShield.autoVolumeEnable();
else
audioShield.autoVolumeDisable();
manualVolumeMode = false;
Serial.println("switch: OFF!");
}
if (manualVolumeMode) {
int new_volume = analogRead(A1);
float w1 = new_volume / 1203.35;
int w2 = w1 * 100.0;
current_volume = 0.85 - w2 / 100.00;
if (current_volume > prev_volume+0.01 || current_volume < prev_volume-0.01) {
Serial.print("value: "); Serial.println(current_volume);
audioShield.volume(current_volume);
prev_volume = current_volume;
}
}
}
2.グローバル変数について
グローバル変数にはマクロ定義の値を設定しているものが多いので、まずマクロ定義の部分を見ておきましょう。これらは以降のコードで使用されるので必要に応じて参照してください。
①マクロ定義
リバース行はグローバル変数の初期値に設定されている値です。
// Use these pins with the Audio Shield
#define SDCARD_CS_PIN 10
#define SDCARD_MOSI_PIN 7 // Teensy 4 ignores this, uses pin 11
#define SDCARD_SCK_PIN 14 // Teensy 4 ignores this, uses pin 13
#define VOLSW_PIN 2 // Pin No. : Volume switch
// Basic attributes
#define SAMPLE_RATE 44100
#define NUM_BAND (7)
#define Q_UNIT 524288
#define EQ_PARAM_FILE "EQPARAM.CTL"
// Status and mode control command
#define MUTE_ONOFF 'm' // [m]ute switch
#define DO_UP 'u' // Count [u]p current variable
#define DO_DOWN 'd' // Count [d]own current variable
#define MODE_NO_CONTROL ' ' // control dissabled
#define MODE_VOLUME_CONTROL 'v' // [v]olume control mode
#define MODE_AUTO_VOLUME 'a' // [a]uto volume control mode
#define LINE_INOUT 'l' // [l]ine in/out
#define LINE_IN 'i' // line [i]n
#define LINE_OUT 'o' // line [o]ut
#define MODE_TUNING_VOLUME 't' // [t]une up volume for each channel
#define CHANNEL_BOTH 'b' // [b]oth channels
#define CHANNEL_LEFT 'l' // [l]eft channnel
#define CHANNEL_RIGHT 'r' // [r]ight channel
#define CHANGE_FILTER 'F' // change the [F]ilter type of the target band
#define CHANGE_FREQUENCY 'f' // Change the [f]requency of the target band
#define CHANGE_GAIN 'g' // Change the [g]ain of the target band
#define CHANGE_Q 'q' // Change the [q] value of the target band
#define CHANGE_BAND_PARAM 'c' // [c]hange PEQ parameters for each band
#define OUT_OF_BAND (-1) // Specified band is unspecified
#define SET_INITIAL_STATE 's' // [s]et to initial state
#define WRITE_PRMFILE 'w' // [w]rite parameter fille
#define TELL_STATUS '?' // Display status
#define HELP_ME 'h' // Display help information
// Sound volume control factors
#define VOLUME_MIN 0.0 // Sound volume min. value
#define VOLUME_MAX 1.0 // Sound volume max. value
#define VOLUME_STEP 0.02 // Sound volume step difference
#define VOLUME_INIT 0.6 // Sound volume initial value
#define AMP_MIN 0.0 // Tune up volume min. value
#define AMP_MAX 3.0 // Tune up volume max. value
#define AMP_BASEGAIN 1.0 // Tune up volume initial value
#define AMP_STEP 0.05 // Tune up volume step difference
#define LINE_IN_LEVEL 5 // Line in default voltage level
#define LINE_OUT_LEVEL 29 // Line out default voltage level
// Sound limiter control factors
#define LIMITER_PEAK_GAIN 0.85 // Limiter sound peak gain
#define LIMITER_ESCAPE_GAIN 0.40 // Limiter control escape value
// Auto volume control factors
#define AVC_MAXGAIN 1 // 6.0dB
#define AVC_RESPONSE 3 // 100ms
#define AVC_HARDLIMIT 0 // unused
#define AVC_THRESHOLD -10 // (dBFS)
#define AVC_ATTACK 6 // (dB/s)
#define AVC_DECAY 8 // (dB/s)
②グローバル変数
ミュート制御(inMute)、自動音量制御(autoVolume)、手動音量調整(manualVolumeMode)はそれぞれが有効か無効かを保持する変数です。これらは初期状態では無効(false)に設定しています。
currentModeには実行中の処理モードのマクロである MODE_VOLUME_CONTROL, MODE_TUNING_VOLUMEが設定されて、 [UP][DOUN]キーが押下された場合にどのように音量を調整するかの指標(インディケーター)となります。音量調整が不要な状態と初期状態では MODE_NO_CONTROLが設定されます。
targetBandにはパラメトリック・イコライザーの対象バンドNo.がセットされます。パラメータの変更をする時に使用します。それ以外の場合と範囲外のバンドNo.を指定した場合は OUT_OF_BANDが設定されます。
targetChannelは左右別々に音量調整する場合に CHANNEL_LEFTまたは CHANNEL_RIGHTが設定されます。初期状態または両チャンネル同時調整の場合は CHANNEL_BOTHが設定されます。
currentVolume、ampGain_L、ampGain_R、line_in_level、line_out_levelには、それぞれ現在の音量値、アンプのゲイン、ライン入出力レベルの初期値が設定されています。
初期値が設定されてない current_volumeと prev_volumeは、手動音量調整をする際に直前と現時点のボリュームの値をメモするために使用します。
// Global variables
bool inMute = false; // Mute ON/OFF
bool autoVolume = false; // Auto volume control ON/OFF
bool manualVolumeMode = false; // Manual volume control mode
int currentMode = MODE_NO_CONTROL; // Current processing mode
int targetBand = OUT_OF_BAND; // Bands to be processed
float currentVolume = VOLUME_INIT; // Current volume
char targetChannel = CHANNEL_BOTH; // Channels to be update
float ampGain_L = AMP_BASEGAIN; // Left channel gain
float ampGain_R = AMP_BASEGAIN; // Right channel gain
int line_in_level = LINE_IN_LEVEL; // Line in level
int line_out_level = LINE_OUT_LEVEL; // Line out level
float current_volume, prev_volume; // for Manual volume control
これ以外に、前章で取り上げたイコライザー用の変数もグローバル変数ですが、ここでは省略しました。
3.ユーザー定義関数
ユーザー定義関数とは、HAT24の機能を実現するために作成した固有の処理関数です。前章で取り上げたイコライザーの実装のための関数 setupEqualizer()、setFilter()などはいずれもユーザー定義関数です。これらの関数は、モニタリング処理で入力コマンドの解析や実行のために使用します。
モニタリング処理の説明にあたって、すべてのユーザー定義関数の役割を見ておくことにしましょう。
1) initParamTable
書式: void initParamTable(PEQPARAM source[], PEQPARAM target[])
機能: パラメトリック・イコライザーの全バンドのすべてのパラメータに、既定の初期値を設定します。
注意: 初回起動時に1回だけ、または環境設定の初期化指示があった場合にだけ呼ばれます。
2) setupEqualizer
書式: void setupEqualizer(PEQPARAM ptable[])
機能: 指定されたパラメータでパラメトリック・イコライザーを設定します。
注意: これによってイコライザーが起動されます。
3) changeVolume
書式: void changeVolume(char updown, float *curvol)
機能: アップ/ダウンの指示に応じて音量を調整します。
注意: 調整は音量制御範囲(VOLUME_MIN, VOLUME_MAX)内に規制されます。
4) tuneupVolume
書式: void tuneupVolume(char updown, char channel, float *ampgain_L, float *ampgain_R)
機能: 指定された左右チャンネルに対して、アップ/ダウン指示に応じて音量を調整します。
注意: 調整は音量制御範囲(AMP_MIN, AMP_MAX)内に規制されます。
5) limiterProcess
書式: void limiterProcess(float peak, float escape)
機能: 衝撃音を回避します(リミッターです)。
注意: 引数の peak, escapeは、マクロ LIMITER_PEAK_GAIN, LIMITER_ESCAPE_GAINで定義された値です。
6) getFilter
書式: bool getFilter(int band, PEQPARAM *param)
機能: 指定したバンドのパラメトリック・イコライザー変数を取得します。
注意: 不正なバンドNo.が指定されると false、正しい場合は trueを返却します。
7) setFilter
書式:void setFilter(int band, PEQPARAM param)
機能: 指定したバンドのパラメトリック・イコライザー変数からフィルター係数を計算して設定します。
注意: フィルター係数の計算は前章4節を参照してください。この関数によってイコライザーが起動します。
8) updateFilterTable
書式: void updateFilterTable(int band, PEQPARAM param)
機能: 指定したバンドのパラメトリック・イコライザー変数を更新し、フィルター係数を計算して設定します。
注意: 上記の setFilterを呼んでフィルター係数を計算しています。
9) changeFrequencyParam
書式: void changeFrequencyParam(int band, float fc)
機能: 指定したバンドのパラメトリック・イコライザーのカットオフ周波数(中央周波数)を変更します。
注意: 変更と同時にイコライザーが再起動されます。
10) changeGainParam
書式: void changeGainParam(int band, float gain)
機能: 指定したバンドのパラメトリック・イコライザーのゲインを変更します。
注意: 変更と同時にイコライザーが再起動されます。
11) changeQParam
書式:void changeQParam(int band, float Q)
機能: 指定したバンドのパラメトリック・イコライザーの Q値を変更します。
注意: 変更と同時にイコライザーが再起動されます。
12) changeFilterType
書式: void changeFilterType(int band, String filtname)
機能: 指定したバンドのパラメトリック・イコライザーのフィルター種別を変更します。
注意: 変更と同時にイコライザーが再起動されます。
13) checkFilterType
書式: int checkFilterType(int band, String filtname)
機能: フィルター種別名を判別して対応するフィルター種別コード(整数値)を通知します。
注意: 一致するフィルター種別名がない場合は (-1)を返却します。
14) parseBandParameters
書式: bool parseBandParameters(PEQPARAM *param)
機能: 指定されたパラメトリック・イコライザーのパラメータを検証します。
注意: パラメータすべてが正しければ trueを、エラーがあれば falseを返却します。
15) changeBandParameters
書式: void changeBandParameters(int band, PEQPARAM param)
機能: 指定したバンドのパラメトリック・イコライザーのパラメータ値一式を変更します。
注意: -
16) readParameterFile
書式: bool readParameterFile(float &vol, float &gain_L, float &gain_R, PEQPARAM ptable[])
機能: パラメータファイルを読んで音量、左右のアンプ・ゲイン、全バンドのパラメトリック・イコライザー変数を
設定します。
注意: パラメータファイルが存在しない場合は falseを、正常に読み込むと trueを返却します。
17) writeParameterFile
書式: void writeParameterFile(float, float, float, PEQPARAM ptable[])
機能: 音量、左右のアンプ・ゲイン、全バンドのパラメトリック・イコライザー変数の値をパラメータファイルに
書き出します。
注意: 既存ファイルを破棄して新たに作成します。書き込みノイズ防止のためミュートをかけます。
18) displayStatus
書式: void displayStatus(float vol, float gain_L, float gain_R, PEQPARAM ptable[])
機能: シリアルモニターに現在の動作環境情報を表示します。
表示例:
19) getFilterName
書式: String getFilterName(int type)
機能: フィルター種別コードに対応した正規のフィルター名を返却します。
注意: 該当しないフィルター種別コードの場合は "FILTER_?????? "を返却します。
20) helpInfo
書式: void helpInfo(void)
機能: シリアルモニターにコマンドの種類・用法を一覧表示します。
注意: -
4.モニタリング処理の詳細
上記のマクロ定義やグローバル変数、ユーザー定義関数を使用して、シリアルモニターから入力されたコマンドと補助パラメータを解析し、適切な処理を実行します。以下ではスケッチのモニタリング処理部、すなわち loop()関数の部分をピックアップしています。ただし、操作時のシリアルモニターへの応答メッセージやエラー表示などは割愛し、各所に詳細なコメントを記述しています。
長いコードですが構成は簡単です。キー入力があれば、38行目以降でコマンドとオプションパラメータを取り出し、107行目からそれぞれのコマンドに対応した処理を実行しています。
void loop() { /*==== リミッターを通す ====*/ limiterProcess(LIMITER_PEAK_GAIN, LIMITER_ESCAPE_GAIN); /*==== ボリュームのスイッチをチェックする ====*/ int swVal = digitalRead(VOLSW_PIN); if (swVal == HIGH && manualVolumeMode == false) { // VOLSA_PINが HIGHなら audioShield.autoVolumeDisable(); // AVCを無効にして manualVolumeMode = true; // manualVolumeModeを有効にする } if (swVal == LOW && manualVolumeMode == true) { // VOLSA_PINが LOWなら audioShield.volume(currentVolume); // 音量を以前の状態に再設定し if (autoVolume) // autoVolumeが有効なら audioShield.autoVolumeEnable(); // AVCを有効にする else // そうでなければ audioShield.autoVolumeDisable(); // AVCを無効にする manualVolumeMode = false; // manualVolumeModeを無効にする return; // Skip subsequent processing ==> } // ボリュームの変化を反映させる if (manualVolumeMode) { // manualVolumeModeが有効なら int new_volume = analogRead(A1); // アナログポートの値を取得して float w1 = new_volume / 1203.35; // ほぼ、0~0.85にマッピングする int w2 = w1 * 100.0; current_volume = 0.85 - w2 / 100.00; if (current_volume > prev_volume+0.01 || current_volume < prev_volume-0.01) { audioShield.volume(current_volume); // SGTL5000の音量を変更する prev_volume = current_volume; } return; // Skip subsequent processing ==> } /* シリアルモニターのキー入力がなければ何もしない */ if (Serial.available() < 1) return; /*==== 最初の1文字をコマンドとして取り出す ====*/ char buf[2] = {' '}; Serial.readBytes(buf, 1); char cmd = buf[0]; // 現在と同じ MODE_VOLUME_CONTROLまたは MODE_TUNING_VOLUMEが入力されたら if ((cmd == MODE_VOLUME_CONTROL || cmd == MODE_TUNING_VOLUME) && cmd == currentMode) { currentMode = MODE_NO_CONTROL; // 処理モードをクリアして復帰する return; // Skip subsequent processing ==> } char channel = ' '; int band = OUT_OF_BAND; float value = 0.0; String strvalue = ""; struct PEQPARAM param; /*==== 次の文字列をチャネルまたはパラメータ値として1文字取り出す ====*/ // コマンドが MODE_TUNING_VOLUMEの場合はチャンネルを、 if (cmd == MODE_TUNING_VOLUME) { if (Serial.available() > 1) { Serial.readBytes(buf, 1); channel = buf[0]; } } // コマンドが CHANGE_FREQUENCYまたは CHANGE_GAINまたは CHANGE_Qの場合はバンドNo.と続く値を、 if (cmd == CHANGE_FREQUENCY || cmd == CHANGE_GAIN || cmd == CHANGE_Q) { String str = Serial.readString(); int pos = str.indexOf(','); band = str.substring(0, pos).toInt(); value = (str.substring(pos + 1)).toFloat(); } // コマンドが CHANGE_FILTERの場合はバンドNo.と続く文字列を、 if (cmd == CHANGE_FILTER) { String str = Serial.readString(); int pos = str.indexOf(','); band = str.substring(0, pos).toInt(); int pos2 = str.indexOf('\n', pos+1); if (pos2 > pos) strvalue = str.substring(pos+1, pos2); else strvalue = str.substring(pos + 1); } // コマンドが CHANGE_BAND_PARAMの場合はバンドNo.と後続のパラメータ1式を、 if (cmd == CHANGE_BAND_PARAM) { if (!parseBandParameters(¶m)) { band = OUT_OF_BAND; } else { changeBandParameters(param.band, param); } } // コマンドが LINE_INOUTの場合は IN/OUT種別とレベル値を、それぞれ取り出す。 if (cmd == LINE_INOUT) { Serial.readBytes(buf, 1); char kind = buf[0]; String str = Serial.readString(); int lvl = (str.substring(1)).toInt(); if (kind == LINE_OUT && (lvl >=13 && lvl <=31)) { audioShield.lineOutLevel(lvl); line_out_level = lvl; } else if (kind == LINE_IN && (lvl >=0 && lvl <=15)) { audioShield.lineInLevel(lvl); line_in_level = lvl; } } /*==== 各コマンドを評価して実行する ====*/ switch (cmd) { case MODE_VOLUME_CONTROL: // 実行モードを MODE_VOLUME_CONTROLに設定する currentMode = MODE_VOLUME_CONTROL; break; case MODE_TUNING_VOLUME: // 実行モードを MODE_TUNING_VOLUMEにして、対象チャンネルを設定する currentMode = MODE_TUNING_VOLUME; targetChannel = CHANNEL_BOTH; if (channel == CHANNEL_LEFT || channel == CHANNEL_RIGHT || channel == CHANNEL_BOTH) targetChannel = channel; break; case MUTE_ONOFF: // ミュート中でなければミュートを設定し、そうでなければミュートを解除する if (inMute == false) { inMute = true; audioShield.muteHeadphone(); } else { inMute = false; audioShield.unmuteHeadphone(); } break; case MODE_AUTO_VOLUME: // AVCが無効状態なら条件を設定して有効にし、有効状態なら無効にする if (autoVolume == false) { autoVolume = true; audioShield.autoVolumeControl(AVC_MAXGAIN, AVC_RESPONSE, AVC_HARDLIMIT, AVC_THRESHOLD, AVC_ATTACK, AVC_DECAY); audioShield.autoVolumeEnable(); } else { autoVolume = false; audioShield.autoVolumeDisable(); } break; case DO_UP: // 実行モードが MODE_VOLUME_CONTROLなら共通音量を、MODE_TUNING_VOLUMEなら左/の右アンプゲインをアップする。 switch (currentMode) { case MODE_VOLUME_CONTROL: changeVolume(DO_UP, ¤tVolume); break; case MODE_TUNING_VOLUME: tuneupVolume(DO_UP, targetChannel, &Gain_L, &Gain_R); break; default: break; } default: break; case DO_DOWN: // 実行モードが MODE_VOLUME_CONTROLなら共通音量を、MODE_TUNING_VOLUMEなら左/の右アンプゲインをダウンする。 switch (currentMode) { case MODE_VOLUME_CONTROL: changeVolume(DO_DOWN, ¤tVolume); break; case MODE_TUNING_VOLUME: tuneupVolume(DO_DOWN, targetChannel, &Gain_L, &Gain_R); break; default: break; } break; case CHANGE_FREQUENCY: // パラメトリック・イコライザーの対象バンドのカットオフ周波数を変更する if (band != OUT_OF_BAND) { changeFrequencyParam(band, value); } break; case CHANGE_GAIN: // パラメトリック・イコライザーの対象バンドのゲインを変更する if (band != OUT_OF_BAND) { changeGainParam(band, value); } break; case CHANGE_Q: // パラメトリック・イコライザーの対象バンドの Q値を変更する if (band != OUT_OF_BAND) { changeQParam(band, value); } break; case CHANGE_FILTER: // パラメトリック・イコライザーの対象バンドのフィルター・タイプを変更する if (band != OUT_OF_BAND) { changeFilterType(band, strvalue); } break; case CHANGE_BAND_PARAM: // パラメトリック・イコライザーの対象バンドのパラメータ1式を変更する if (band != OUT_OF_BAND) { changeBandParameters(band, param); } break; case SET_INITIAL_STATE: // 環境設定を初期状態に戻す initParamTable(preset, ptable); inMute = false; autoVolume = false; currentMode = MODE_NO_CONTROL; targetBand = OUT_OF_BAND; currentVolume = VOLUME_INIT; targetChannel = CHANNEL_BOTH; ampGain_L = ampGain_R = AMP_BASEGAIN; line_in_level = LINE_IN_LEVEL; line_out_level = LINE_OUT_LEVEL; break; case WRITE_PRMFILE: // 現在の動作環境情報を SDに書き込む writeParameterFile(currentVolume, ampGain_L, ampGain_R, ptable); break; case TELL_STATUS: // 現在の動作環境情報をシリアルモニターに表示する displayStatus(currentVolume, ampGain_L, ampGain_R, ptable); break; case HELP_ME: // コマンド・ヘルプをシリアルモニターに表示する helpInfo(); break; } // キー入力の後続部分をクリアする while (Serial.available() > 0) { char t = Serial.read(); } }
Arduimo IDEで動作させると、シリアルモニターにメッセージや動作状態が表示されます。これらについてはダウンロードしたスケッチの loop()をご覧下さい。
最終の次回では、ハードウェアの組立・仕上工程をまとめ、チューニングについて述べる予定です。お楽しみに!