1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ミュート中の発話検知

Posted at

はじめに

Zoomやその他の一部ビデオ通話アプリにはミュート中に発話するとアラートが出る機能があります。
がんばって喋っていてもミュートに気づいてなければもう一度同じ内容を話す事になります。
Agora SDKでは2022年7月現在、このようなミュート中の発話検知機能が提供していません。
この記事では、Agora SDKを利用したビデオ通話アプリにミュート中の発話検知機能をつける方法について解説します。

前提

発話検知はiOS/Androidそれぞれの標準のマイク入力APIを活用します。
ミュート解除後は通常どおりAgora SDKのAPIでマイク入力を取得してパブリッシュします。

Androidの実装方法

ベースとなるパッケージはAgora社公式のGithubに公開されているAgora-Android-Tutorial-1to1を利用します。
VideoChatViewActivity.javaにコードを追加していきます。
AudioRecordの実装についてはこちらを参考にさせて頂きました。

VideoChatViewActivity.java
//中略
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にコードを追加していきます。
マイク入力検知についてはこちらを参考にさせて頂きました。

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で他にもマイクの入力検知方法があるかと思います。この記事で紹介している方法はあくまで一つの案であり、他にも良い方法があるかもしれません。
また、閾値についてもアプリ毎に最適な数値があると考えられます。このあたりは適宜調整していただけらと思います。

最後に

agora.ioに関するお問い合わせはこちらから
Agoraの詳細はこちら

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?