Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hardware pass-through of audio from input to speakers - not done in software

A few years back I wrote an custom application for my company that was only going to run a one specific model computer. The application had to be able to pass-through the audio coming in on the microphone jack to the speakers. Instead of handling the bytes coming into the jack and passing them to the speakers in software, I utlized the fact that I knew the specific hardware to write a function which enabled the sound card's built-in ability to loop audio from an input to the speakers. Here is that function (it was written in C using nothing more than mmsystem.dll):

int setMasterLevelsFromMicrophone (int volume, int mute)
{
    MMRESULT error;

    // Open the mixer
    HMIXER mixerHandle;
    if (error = mixerOpen (&mixerHandle, 0, 0, 0, 0))
        return 1;

    // Get the microphone source information
    MIXERLINE mixerline;
    mixerline.cbStruct = sizeof(MIXERLINE);
    mixerline.dwDestination = 0;
    if ((error = mixerGetLineInfo((HMIXEROBJ)mixerHandle, &mixerline, MIXER_GETLINEINFOF_DESTINATION)))
        return 2;

    // Get the microhone source controls
    MIXERCONTROL mixerControlArray[2];
    MIXERLINECONTROLS mixerLineControls;
    mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS);
    mixerLineControls.cControls = 2;
    mixerLineControls.dwLineID = mixerline.dwLineID;
    mixerLineControls.pamxctrl = &mixerControlArray[0];
    mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL);

    if ((error = mixerGetLineControls((HMIXEROBJ)mixerHandle, &mixerLineControls, MIXER_GETLINECONTROLSF_ALL)))
        return 3;

    // Set the microphone source volume
    MIXERCONTROLDETAILS_UNSIGNED value;
    MIXERCONTROLDETAILS mixerControlDetails;
    mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
    mixerControlDetails.dwControlID = mixerControlArray[0].dwControlID;
    mixerControlDetails.cChannels = 1;
    mixerControlDetails.cMultipleItems = 0;
    mixerControlDetails.paDetails = &value;
    mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
    value.dwValue = volume;
    if ((error = mixerSetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_SETCONTROLDETAILSF_VALUE)))
        return 4;

    // Set the microphone source mute
    mixerControlDetails.dwControlID = mixerControlArray[1].dwControlID;
    value.dwValue = mute;
    if ((error = mixerSetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_SETCONTROLDETAILSF_VALUE)))
        return 5;

    return 0;
}

As you can see, this method is highly specific to the hardware I was using at the time as I have hard coded many of the array indices for getting access to the specific properties of the mixer.

Now for the question.

Now it has been a few years and I need to modify an application I am currently writing in C# winforms to exibit the same behavior. That is to say, I need the audio received from the microphone or lini-in jack to pass-through directly to the speakers. The trick here is that the hardware is no longer closed. And the application needs to run on any machine running WinXP or higher.

I began working with the NAudio library for doing this passthrough in software mode (not utilizing the built-in sound card pass-through). here is the small little toolbox I created in C#:

using System;
using System.ComponentModel;
using NAudio.Wave;

namespace Media
{
    public partial class AudioToolbox : Component
    {
        private WaveIn waveIn = null;
        private WaveOutEvent waveOut = null;
        public int SampleRate { get; set; }
        public int BitsPerSample { get; set; }
        public int Channels { get; set; }

        public AudioToolbox()
        {
            InitializeComponent();

            SampleRate = 22050;
            BitsPerSample = 16;
            Channels = 1;
        }

        public void BeginReading(int deviceNumber)
        {
            if (waveIn == null)
            {
                waveIn = new WaveIn();
                waveIn.DeviceNumber = deviceNumber;
                waveIn.WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitsPerSample, Channels);
                waveIn.StartRecording();
            }
        }

        public void BeginLoopback()
        {
            if (waveIn != null && waveOut == null)
            {
                WaveInProvider waveInProvider = new WaveInProvider(waveIn);
                waveOut = new WaveOutEvent();
                waveOut.DeviceNumber = -1;  // Default output device
                waveOut.DesiredLatency = 300;
                waveOut.Init(waveInProvider);
                waveOut.Play();
            }
        }

        public void EndReading()
        {
            if (waveIn != null)
            {
                waveIn.StopRecording();
                waveIn.Dispose();
                waveIn = null;
            }
        }

        public void EndLoopback()
        {
            if (waveOut != null)
            {
                waveOut.Stop();
                waveOut.Dispose();
                waveOut = null;
            }
        }
    }
}

The problem I am having with this is (I assume) resources. This code does allow me to loop the audio out to the speakers but doing tasks on the system introduce popping and skipping in the audio. For example, if I open an application or quickly minimize and maximize a folder, the playback pops and skips.

Is there a way to somehow thread the NAudio library to avoid this popping and skipping? Or is it better for me to find a generic way to passthrough audio via hardware, as I did years ago with my C application?

EDIT:

My application that tests this audio toolbox is very simple. It is simply a default winforms application created by Visual Studio 2010. I added a single button to the form and the following event occuring on the click event:

private void button1_Click(object sender, EventArgs e)
{
    AudioToolbox atr = new AudioToolbox();
    atr.BeginReading(0);
    atr.BeginLoopback();
}

I also set the project to run in the .NET Framework 4, because that is the framework of the application that I need this toolbox to eventually integrate with. When I compile the application and click the button, I can hear audio being passed through from my microphone jack to the speakers. Then I open up windows file explorer and continuously minimize/maximize it. This action causes the audio to skip. Fail.

I just posted this question on the NAudio forums as well. In case anyone stumbles upon this page in the future, here is that link: Question as posted on NAudio forums

like image 379
Michael Mankus Avatar asked Aug 29 '12 12:08

Michael Mankus


People also ask

How do I turn audio input into output?

Right-click the device and select the 'default device option' to ensure it is the default speaker or output device. Select the 'reporting' tab, which is next to your 'playback' tab. Right-click the device and select the 'default option' to ensure that the mic you wish to use is the default mic.

Why is my sound not working?

You might have the sound muted or turned down low in the app. Check the media volume. If you still don't hear anything, verify that the media volume isn't turned down or off: Navigate to Settings.


2 Answers

I think you just need to fire off your processing into a seperate thread. You're doing all your work on the UI thread which is why whenever you do anything it suspends your processing. I'm assuming the audio is coming in chunks from an event. Events are handled on the thread that dispatched them which in this case is your ui thread.

Try wrapping your code in something like this

AudioToolbox atr = new AudioToolbox();
var audioThread = new Thread(()=> {
    atr.BeginReading(0);
    atr.BeginLoopback();
}).Start();

I don't see any reason why doing external tasks would cause an interruption. I have done realtime live audio and video processing in a single thread with no problems on many different machines. Maybe whats happening is that since its all in the ui thread that as it redraws the screen your audio processing is paused. If that's the case then the dedicated thread will solve this problem.

like image 154
devshorts Avatar answered Oct 27 '22 04:10

devshorts


This is the best I have been able to achieve so far for minimizing skipping. I am going to accept it as the answer so that any others who stumble upon this page will see what I did, but if anyone comes up with a better solution I will gladly select their answer.

First thing I had to do was abandon NAudio 1.5 which is the last official NAudio release. Instead, I grabbed the latest hot build which is a beta of NAudio 1.6. I did this because the beta for 1.6 includes a new WaveInProvider called WaveInEvent. WaveInEvent is benficial as it prevents calls to the GUI thread while reading in from the microphone jack.

Second thing I did was switch from WaveOutEvent to DirectSoundOut. I did this because in my testing, I found that when playing audio from a file, that WaveOutEvent would skip depending on my usage of the CPU, but that DirectSoundOut would not. So I have assumed that the same behavior occurs when playing audio from a microphone port. Therefore I am using DirectSoundOut for playing the audio from the microphone.

Here is my new AudioInputToolbox:

using System; 
using System.ComponentModel; 
using NAudio.Wave; 

namespace Media 
{ 
    public partial class AudioInputToolbox : Component 
    {
        private WaveInEvent waveIn = null;
        private DirectSoundOut waveOut = null;
        public int SampleRate { get; set; } 
        public int BitsPerSample { get; set; } 
        public int Channels { get; set; }

        public AudioInputToolbox() 
        { 
            InitializeComponent(); 

            SampleRate = 22050; 
            BitsPerSample = 16; 
            Channels = 1; 
        } 

        public void BeginReading(int deviceNumber) 
        {
            if (waveIn == null) 
            {
                waveIn = new WaveInEvent(); 
                waveIn.DeviceNumber = deviceNumber; 
                waveIn.WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitsPerSample, Channels);
                waveIn.StartRecording(); 
            } 
        }

        public void BeginLoopback() 
        {
            if (waveIn != null && waveOut == null)
            {
                waveOut = new DirectSoundOut(DirectSoundOut.DSDEVID_DefaultPlayback, 300);
                waveOut.Init(new WaveInProvider(waveIn));
                waveOut.Play();
            }
        }

        public void EndReading() 
        {
            if (waveIn != null) 
            { 
                waveIn.StopRecording(); 
                waveIn.Dispose(); 
                waveIn = null; 
            } 
        } 

        public void EndLoopback() 
        {
            if (waveOut != null)
            {
                waveOut.Stop();
                waveOut.Dispose();
                waveOut = null;
            }
        } 
    } 
} 

And here is the code for my new test application. It is simply a form with two buttons on it. Each button has a callback. One is a start button. The other is a stop button.

using System;
using System.Threading;
using System.Windows.Forms;
using Media;

public partial class AITL : Form
{
    AudioInputToolbox atr = new AudioInputToolbox();

    public AITL()
    {
        InitializeComponent();
    }

    private void startButton_Click(object sender, EventArgs e)
    {
        new Thread(() =>
        {
            atr.BeginReading(0);
            atr.BeginLoopback();
        }).Start();              
    }

    private void stopButton_Click(object sender, EventArgs e)
    {
        atr.EndReading();
        atr.EndLoopback();
    }
}

This approach does not solve my problem. It only serves to make the problem occur slightly less often and slightly less severely.

Again, I will gladly accept a different answer from anyone who can fully solve the issue of skipping. To re-iterate, I encounter skipping after I have pressed my start button and I repeatedly minimize and maximize a window. Any window. I have been doing it to windows explorer. (In my full featured application that this audio component needs to fit into, there is alot of GUI intensive mapping going on so this minimization/maximization of a window is a good simulation of that action).

like image 27
Michael Mankus Avatar answered Oct 27 '22 05:10

Michael Mankus