본문 바로가기
C#

[C#] 윈도우 마이크 컨트롤 (AudioSwitcher, NAudio)

by Jcoder 2021. 4. 6.

NuGet Package
실행1 - 평상시
실행2 - 음소거

 

서로 이벤트 발생

 

1. 최소 사양을 맞추기 위해 .NET Framework 4.5.2 버전을 사용

2. Winform, UserControl 사용 가능

3. 비주얼 스튜디오 16.9.3

 

디자인
실행3

1. 음소거 버튼 - 패널로 사용 (PictureBox로 대체가능)

2. ContextMenuStrip에 마이크와 믹서로만 사용하게 함 (별도로 마이크 리스트 전부를 가져와도 됨)

3. NAudio에는 마이크 기본 장치 변경이 없어 AudioSwitcher를 사용

4. NAudio는 이벤트, AudioSwitcher는 IObservable

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using AudioSwitcher.AudioApi;
using AudioSwitcher.AudioApi.CoreAudio;
using AudioSwitcher.AudioApi.Observables;

using Microsoft.Win32;

using NAudio.CoreAudioApi;
using NAudio.Wave;

namespace AudioControl
{
    public partial class Form1 : Form
    {
        private readonly string[] arrMicroPhoneList = new string[] {"마이크", "Microphone", "Mic", "MIC", "MicroPhone", "microphone", "mic" };
        private readonly string[] arrMicMixList = new string[] { "스테레오 믹스", "스테레오", "믹스", "들리는내용", "들리는 내용", "Stereo Mix", "Stereo", "Mix", "Stereo MIxer", "What U Hear", "Mixed Output", "Post-Mix", "Loop Back", "SUM", "virtual" };

        List<CoreAudioDevice> AudioDevices = null; // MicroPhone Device 리스트
        CoreAudioDevice defaultMicDeivce = null; // 현재 MicroPhone Device
        CoreAudioController coreAudioController = null; // MicroPhone Device 컨트롤러
        
        List<IDisposable> disposables = null; // 옵저버 패턴
        Action<DeviceVolumeChangedArgs> actionVolumeChange = null; // 볼륨 변경
        Action<DeviceMuteChangedArgs> actionMuteChange = null; // 음소거 변경
        Action<DefaultDeviceChangedArgs> actionDefalutDeviceChange = null; // 기본 장치 변경 
        Action<DeviceStateChangedArgs> actionStateDevice = null; // 상태 변경

        // 마이크 스펙트럼
        MMDeviceEnumerator mMDeviceEnumerator = null;
        WaveInEvent waveIn = null;
        bool gbIsStopWave = false;
        private static double audioValueMax = 0;
        private static double audioValueLast = 0;
        private static int RATE = 44100;
        private static int BUFFER_SAMPLES = 1024;

        public Form1()
        {
            InitializeComponent();

            // 필요한 이벤트 연결
            this.Load += Form1_Load;
            this.FormClosed += Form1_FormClosed;

            cms_MicrophoneList.ItemClicked += Cms_MicrophoneList_ItemClicked; // 메뉴 버튼 아이템 클릭 이벤트 추가
            trackBar1.Scroll += trackBar1_Scroll;
            pn_DeviceState.Click += pn_DeviceState_Click;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            actionVolumeChange = OnVolumeChanged; // 볼륨 변경
            actionMuteChange = OnMuteChanged; // 음소거 변경
            actionDefalutDeviceChange = OnDefaultChanged; // 기본 장치 변경 
            actionStateDevice = OnStateChanged; // 상태 변경

            disposables = new List<IDisposable>();
            mMDeviceEnumerator = new MMDeviceEnumerator(); // 마이크 에뮬레이터
            coreAudioController = new CoreAudioController(); // 마이크 스위처            

            trackBar1.Maximum = 100; // 기본 트랙바 max는 10
            pictureBox2.Width = 0; // 마이크 사운드 이미지 길이 0으로 설정
            GetDevices();
            SetUI();            
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (defaultMicDeivce != null)
                defaultMicDeivce.Dispose();
            if (coreAudioController != null)
                coreAudioController.Dispose();
            if (mMDeviceEnumerator != null)
                mMDeviceEnumerator.Dispose();
            if (waveIn != null)
                waveIn.Dispose();
        }

        private int? GetNAudioDeviceNumver()
        {
            MMDevice defalutMicDevice = mMDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Capture, NAudio.CoreAudioApi.Role.Multimedia);
            int? deviceIndex = 0;
            if (defalutMicDevice != null)
            {
                for (int i = 0; i < WaveIn.DeviceCount; i++)
                {
                    if (defalutMicDevice.FriendlyName.Equals(WaveIn.GetCapabilities(i).ProductName))
                    {
                        deviceIndex = i;
                        break;
                    }
                }
            }
            else
                return null;

            return deviceIndex;
        }

        private void GetDevices()
        {
            if (AudioDevices == null)
                AudioDevices = new List<CoreAudioDevice>();
            else            
                AudioDevices.Clear();                

            // UnKnown 장비 제외하고 리스트에 추가            
            foreach (CoreAudioDevice coreAudioDevice in coreAudioController.GetCaptureDevices().Where(coreAudioDevice => !coreAudioDevice.FullName.Contains("Unknown")))
                AudioDevices.Add(coreAudioDevice);

            // 1개라도 있을 때만
            if (AudioDevices.Count > 0)
            {
                ObservableClear();
                // 현재 선택된 녹음 장치 가져오기
                defaultMicDeivce = coreAudioController.DefaultCaptureDevice;                
                for (int i = 0; i < AudioDevices.Count; i++)
                {
                    disposables.Add(ObservableExtensions.Subscribe(AudioDevices[i].VolumeChanged, actionVolumeChange));
                    disposables.Add(ObservableExtensions.Subscribe(AudioDevices[i].MuteChanged, actionMuteChange));
                    disposables.Add(ObservableExtensions.Subscribe(AudioDevices[i].DefaultChanged, actionDefalutDeviceChange));
                    disposables.Add(ObservableExtensions.Subscribe(AudioDevices[i].StateChanged, actionStateDevice));
                }
            }
        }

        private void SetUI()
        {
            try
            {
                lock (cms_MicrophoneList)
                {
                    this.InvokeOnUiThreadIfRequired(() => cms_MicrophoneList.Items.Clear());
                    //기본 선택된 녹음 장치가 있으면 트랙바 볼륨 설정.
                    if (AudioDevices != null && AudioDevices.Count > 0 && defaultMicDeivce != null)
                    {
                        bool IsCheckMic = false; // 마이크 계열
                        bool IsCheckMix = false; // 믹스 계열

                        cms_MicrophoneList.Items.Add("마이크");
                        cms_MicrophoneList.Items.Add("믹스");

                        foreach (var mic in arrMicroPhoneList)
                        {
                            if (defaultMicDeivce.FullName.Contains(mic))
                            {
                                IsCheckMic = true;
                                cms_MicrophoneList.Items[0].Text = defaultMicDeivce.FullName;
                                ((ToolStripMenuItem)cms_MicrophoneList.Items[0]).Checked = true;
                                break;
                            }
                        }

                        foreach (var mix in arrMicMixList)
                        {
                            if (defaultMicDeivce.FullName.Contains(mix))
                            {
                                IsCheckMix = true;
                                cms_MicrophoneList.Items[1].Text = defaultMicDeivce.FullName;
                                ((ToolStripMenuItem)cms_MicrophoneList.Items[1]).Checked = true;
                                break;
                            }
                        }

                        cms_MicrophoneList.Items[0].Enabled = IsCheckMic;
                        cms_MicrophoneList.Items[1].Enabled = IsCheckMix;

                        if (waveIn == null)
                        {
                            int? devicenum = GetNAudioDeviceNumver();
                            if (devicenum != null)
                            {
                                waveIn = new WaveInEvent();
                                waveIn.DeviceNumber = (int)devicenum;
                                waveIn.WaveFormat = new WaveFormat(44100, 16, 1);
                                waveIn.BufferMilliseconds = (int)((double)BUFFER_SAMPLES / (double)RATE * 1000.0);
                                waveIn.DataAvailable += WaveIn_DataAvailable;
                                waveIn.RecordingStopped += (sender, e) => gbIsStopWave = false; pictureBox2.Width = 0;
                            }
                        }

                        if ((int)defaultMicDeivce.Volume > -1)
                            this.InvokeOnUiThreadIfRequired(() => trackBar1.Value = (int)defaultMicDeivce.Volume);

                        if (defaultMicDeivce.IsMuted)
                        {
                            this.InvokeOnUiThreadIfRequired(() => pn_DeviceState.BackColor = Color.Red); // 실제 적용시 이미지 변경                        
                            if (waveIn != null)
                                waveIn.StopRecording();
                        }
                        else
                        {
                            this.InvokeOnUiThreadIfRequired(() => pn_DeviceState.BackColor = Color.Green); // 실제 적용시 이미지 변경                        
                            if (waveIn != null && !gbIsStopWave)
                            {
                                waveIn.StartRecording();
                                gbIsStopWave = true;
                            }
                        }
                    }
                    else
                    {
                        cms_MicrophoneList.Items.Add("마이크");
                        cms_MicrophoneList.Items.Add("믹스");
                        cms_MicrophoneList.Items[0].Enabled = false;
                        cms_MicrophoneList.Items[1].Enabled = false;
                    }
                }
            }
            catch (Exception e)
            {
                //textBox1.AppendText(e.ToString() + Environment.NewLine);
                //textBox1.AppendText(e.Message + Environment.NewLine);
                //textBox1.AppendText(e.StackTrace + Environment.NewLine);
            }
            
        }

        private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
        {
            float max = 0;
            // interpret as 16 bit audio
            for (int index = 0; index < e.BytesRecorded; index += 2)
            {
                short sample = (short)((e.Buffer[index + 1] << 8) | e.Buffer[index + 0]);
                // to floating point
                var sample32 = sample / 32768f;
                // absolute value 
                if (sample32 < 0) sample32 = -sample32;
                // is this the max value?
                if (sample32 > max) max = sample32;
            }
            // calculate what fraction this peak is of previous peaks
            if (max > audioValueMax)
                audioValueMax = (double)max;

            audioValueLast = max;

            double frac = audioValueLast / audioValueMax;
            this.InvokeOnUiThreadIfRequired(() => pictureBox2.Width = (int)(frac * pictureBox1.Width));            
        }        

        // 메뉴 아이템 클릭시 기본 장치 변경
        private void Cms_MicrophoneList_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            if (!defaultMicDeivce.FullName.Equals(e.ClickedItem.Text))
            {
                CoreAudioDevice coreAudioDevice = AudioDevices.Find(x => x.FullName.Equals(e.ClickedItem.Text));

                if (coreAudioDevice != null)
                    coreAudioController.SetDefaultDevice(coreAudioDevice);
            }
        }

        // 볼륨 조절 옵저버
        private void OnVolumeChanged(DeviceVolumeChangedArgs deviceVolumeChangedArgs)
        {
            if (deviceVolumeChangedArgs.Device.FullName.Equals(defaultMicDeivce.FullName))
                this.InvokeOnUiThreadIfRequired(() => trackBar1.Value = (int)deviceVolumeChangedArgs.Volume);
        }

        // 음소거 옵저버
        private void OnMuteChanged(DeviceMuteChangedArgs deviceMuteChangedArgs)
        {
            if (deviceMuteChangedArgs.Device.FullName.Equals(defaultMicDeivce.FullName))
            {
                // TODO : 이미지로 변경
                if (deviceMuteChangedArgs.IsMuted)
                    this.InvokeOnUiThreadIfRequired(() => pn_DeviceState.BackColor = Color.Red); // 실제 적용시 이미지 변경
                else
                    this.InvokeOnUiThreadIfRequired(() => pn_DeviceState.BackColor = Color.Green); // 실제 적용시 이미지 변경
            }
        }

        // 기본 장치 변경 옵저버
        private void OnDefaultChanged(DefaultDeviceChangedArgs defaultDeviceChangedArgs)
        {
            if (((CoreAudioDevice)defaultDeviceChangedArgs.Device).IsDefaultDevice)
            {
                if (waveIn != null)
                {
                    waveIn.StopRecording();
                    waveIn.DataAvailable -= WaveIn_DataAvailable;
                    waveIn = null;
                }
                GetDevices();
                SetUI();
            }
        }

        // 상태 변경 옵저버 ex) 사용 -> 사용 안 함
        private void OnStateChanged(DeviceStateChangedArgs deviceStateChangedArgs)
        {
            // TODO : 메뉴 리스트 초기화
            // 1. Device State에 따라 메뉴 리스트에서 enable과 active시 추가
            this.InvokeOnUiThreadIfRequired(() => pictureBox2.Width = 0);
            if (((CoreAudioDevice)deviceStateChangedArgs.Device).IsDefaultDevice)
            {                
                if (waveIn != null)
                {
                    waveIn.StopRecording();
                    waveIn.DataAvailable -= WaveIn_DataAvailable;
                    waveIn = null;
                }
            }
            GetDevices();
            SetUI();
        }

        // 트랙바로 마이크 사운드 조절
        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            if (defaultMicDeivce != null)
                defaultMicDeivce.Volume = trackBar1.Value;
        }

        // 패널 클릭시 음소거, 음소거 해제
        private void pn_DeviceState_Click(object sender, EventArgs e)
        {
            if (defaultMicDeivce != null)
                defaultMicDeivce.Mute(!defaultMicDeivce.IsMuted);
        }

        private void ObservableClear()
        {
            if (disposables != null)
            {
                if (disposables.Count > 0)
                    foreach (var disposable in disposables)
                        disposable.Dispose();

                disposables.Clear();
            }
        }
    }

    public static class ControlExtensions
    {
        /// <summary>
        /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
        /// </summary>
        /// <param name="control">the control for which the update is required</param>
        /// <param name="action">action to be performed on the control</param>
        public static void InvokeOnUiThreadIfRequired(this Control control, Action action)
        {
            //If you are planning on using a similar function in your own code then please be sure to
            //have a quick read over https://stackoverflow.com/questions/1874728/avoid-calling-invoke-when-the-control-is-disposed
            //No action
            if (control.Disposing || control.IsDisposed || !control.IsHandleCreated)
            {
                return;
            }

            if (control.InvokeRequired)
            {
                control.BeginInvoke(action);
            }
            else
            {
                action.Invoke();
            }
        }
    }

}