はじめに
Zoomやその他の一部ビデオ通話アプリにはミュート中に発話するとアラートが出る機能があります。
がんばって喋っていてもミュートに気づいてなければもう一度同じ内容を話す事になります。
Agora SDKでは2022年7月現在、このようなミュート中の発話検知機能が提供していません。
この記事では、Agora SDKを利用したビデオ通話アプリにミュート中の発話検知機能をつける方法について解説します。
前提
発話検知はiOS/Androidそれぞれの標準のマイク入力APIを活用します。
ミュート解除後は通常どおりAgora SDKのAPIでマイク入力を取得してパブリッシュします。
Androidの実装方法
ベースとなるパッケージはAgora社公式のGithubに公開されているAgora-Android-Tutorial-1to1を利用します。
VideoChatViewActivity.javaにコードを追加していきます。
AudioRecordの実装についてはこちらを参考にさせて頂きました。
//中略
public class VideoChatViewActivity extends AppCompatActivity {
//中略
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
@Override
public void onLocalAudioStateChanged(int state, int error){
if(state == 0){
startAudioRecord();//*4
}
}
}
//中略
private void joinChannel() {
if (TextUtils.isEmpty(token) || TextUtils.equals(token, "#YOUR ACCESS TOKEN#")) {
token = null; // default, no token
}
mRtcEngine.joinChannel(token, "demoChannel1", "Extra Optional Data", 0);
initAudioRecord();//*1
}
//中略
private AudioRecord audioRecord;
private int SAMPLING_RATE = 44100;
private int bufSize;
private short[] shortData;
private void initAudioRecord() {
bufSize = android.media.AudioRecord.getMinBufferSize(SAMPLING_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
SAMPLING_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufSize);
shortData = new short[bufSize / 2];
audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
@Override
public void onPeriodicNotification(AudioRecord recorder) {
audioRecord.read(shortData, 0, bufSize / 2);
for(short s : shortData){
if(s>1000){
//閾値を超えたらアラート表示等の実装 *2
}
}
}
@Override
public void onMarkerReached(AudioRecord recorder) {
}
});
audioRecord.setPositionNotificationPeriod(bufSize / 2);
}
//中略
private void startAudioRecord(){
audioRecord.startRecording();
}
private void stopAudioRecord(){
audioRecord.stop();
}
public void onLocalAudioMuteClicked(View view) {
mMuted = !mMuted;
mRtcEngine.muteLocalAudioStream(mMuted);
int res = mMuted ? R.drawable.btn_mute : R.drawable.btn_unmute;
mMuteBtn.setImageResource(res);
if(mMuted){
mRtcEngine.enableLocalAudio(false);//*3
} else {
mRtcEngine.enableLocalAudio(true);//*5
stopAudioRecord();
}
}
}
*1でAndroidのマイク入力に関する初期設定を実行します。
*2でshortDataでボリュームを取得し、閾値を超えた場合に警告を出す等の実装をするイメージになります。
閾値は最適な値を独自で検討していただけたらと思います。
*3ではユーザーがミュート状態にした際、Agora SDKでのマイクキャプチャを停止させています。停止が完了した際、*4のコールバックが発生し、Androidの標準的なAPIでマイク入力を取得開始します。
*5ではミュート解除後に再度Agora SDKのAPIを利用してマイクキャプチャとパブリッシュを再開させます。
iOSの実装方法
ベースとなるパッケージはAgora社公式のGithubに公開されているAgora-iOS-Tutorial-Swift-1to1を利用します。
VideoChatViewController.swiftにコードを追加していきます。
マイク入力検知についてはこちらを参考にさせて頂きました。
//中略
import AudioToolbox
//中略
class VideoChatViewController: UIViewController{
//中略
var queue: AudioQueueRef!
var timer: Timer!
func startUpdatingVolume() {
// Set data format
var dataFormat = AudioStreamBasicDescription(
mSampleRate: 44100.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: AudioFormatFlags(kLinearPCMFormatFlagIsBigEndian | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked),
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0)
var audioQueue: AudioQueueRef? = nil
var error = noErr
error = AudioQueueNewInput(
&dataFormat,
AudioQueueInputCallback,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
.none,
.none,
0,
&audioQueue)
if error == noErr {
self.queue = audioQueue
}
AudioQueueStart(self.queue, nil)
var enabledLevelMeter: UInt32 = 1
AudioQueueSetProperty(self.queue, kAudioQueueProperty_EnableLevelMetering, &enabledLevelMeter, UInt32(MemoryLayout<UInt32>.size))
self.timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(VideoChatViewController.detectVolume(_:)),
userInfo: nil,
repeats: true)
self.timer?.fire()
}
func stopUpdatingVolume()
{
self.timer.invalidate()
self.timer = nil
AudioQueueFlush(self.queue)
AudioQueueStop(self.queue, false)
AudioQueueDispose(self.queue, true)
}
@objc func detectVolume(_ timer: Timer)
{
var levelMeter = AudioQueueLevelMeterState()
var propertySize = UInt32(MemoryLayout<AudioQueueLevelMeterState>.size)
AudioQueueGetProperty(
self.queue,
kAudioQueueProperty_CurrentLevelMeterDB,
&levelMeter,
&propertySize)
if levelMeter.mPeakPower >= -20{
//閾値を超えたらアラート表示等の実装 *1
}
}
//中略
@IBAction func didClickMuteButton(_ sender: UIButton) {
sender.isSelected.toggle()
agoraKit.muteLocalAudioStream(sender.isSelected)
if sender.isSelected == true{
self.startUpdatingVolume() //*2
}else{
self.stopUpdatingVolume() //*3
}
}
}
*1でボリュームを取得し、閾値を超えた場合に警告を出す等の実装をするイメージになります。
閾値は最適な値を独自で検討していただけたらと思います。
*2でiOSの標準的なAPIでマイク入力を取得開始します。
*3ではミュート解除後に再度Agora SDKのAPIを利用してマイクキャプチャとパブリッシュを再開させます。
補足
各OSで他にもマイクの入力検知方法があるかと思います。この記事で紹介している方法はあくまで一つの案であり、他にも良い方法があるかもしれません。
また、閾値についてもアプリ毎に最適な数値があると考えられます。このあたりは適宜調整していただけらと思います。