Tuesday, July 21, 2015

Sidetone, first draft working

It turns out that yes, you can take input from a mic and put it into headphones, using Qt. The minimal process is this.

Acquire a list of available audio devices for input or output. For example,

self.input_info_list = QAudioDeviceInfo.availableDevices( QAudio.AudioInput )

The list items are QAudioDeviceInfo objects.

Populate a combobox (popup menu thing) with the names of the available devices, for example,

self.cb_inputs.addItems(
            [ audio_info.deviceName() for audio_info in self.input_info_list ]
            )

Present the two comboxes, one for input devices and one for output, and await a selection on either. Now it gets complicated, because on the currentIndexChanged signal from either combox, you maybe have not created any devices, or you've created one but not the other, blah blah. Anyway say you are creating an input device. You get new_index an index into that list of device info objects.

        audio_info = self.input_info_list[ new_index ]
        # Create a new QAudioInput based on that.
        preferred_format = audio_info.preferredFormat()
        self.input_device = QAudioInput( audio_info, preferred_format )
        self.input_device.setVolume( 1.0 )
        self.input_device.setBufferSize( 384 )

Now you have an input device. That last step, setting the buffer size, is import, as will be discussed in a minute.

Now the user selects an output device from that list.

        audio_info = self.otput_info_list[ new_index ]
        preferred_format = audio_info.preferredFormat()
        self.otput_device = QAudioOutput( audio_info, preferred_format )
        self.otput_device.setVolume( self.volume.value() / 100 )
        if self.input_device :
            self.input_device.start( self.otput_device.start() )

The very last line is what connects the input device to the output. The value of self.otput_device.start() is the QIODevice that the output device uses. Calling the input device's start() method and passing a QIODevice tells it, this is your target, the sink for your input data. The input device starts putting data into the QIODevice, and the output device takes it out and reproduces it.

In principle the exact reverse should also work, i.e. self.otput_device.start( self.input_device.start() ), but for some reason that leads to strange audio artifacts.

Anyway, the first time I ran this, without setting the buffersize, the sidetone was there but (as with the XCode demo program I wrote about) there was an echo, as if I were talking into a rather large barrel. It turns out the default buffer size is 4096 bytes. I changed the buffer size to 2048 and the echo became less. Then to 1024. Then to 512, with an improvement each time. At a buffer of 256, the audio stream developed a flutter or rapid "picket-fence" noise. Setting it back to 384 removed the noise. The sidetone still has a detectable echo, a ring, but it is tolerable.

Next I have to try it out on my laptop, which is where it will be used, simultaneous with the 3CX VOIP app that I am required to use. If Sidetone can co-exist with 3CX I'll be a happy camper.

If you'd like to play with Sidetone, it is right here on github.

No comments: