Implementing an Audio Driver

Now let's look at how a kernel audio driver can be implemented using the example project MyAudioDevice. We only show excerpts from this as it pertains to the topic in question; however, you can inspect the full source code of MyAudioDevice by downloading it from the Apress web site. For the sake of simplicity, we will make the driver as basic as possible. As there is no standardized widely available audio hardware we can build a driver for, we will build a virtual audio driver. The driver will have one output and one input so we can perform both functions. The driver will operate as a loopback device, which means that audio we play will be transferred from the output buffer to the input buffer. We leave it as an exercise for you, the reader, to do something more interesting, perhaps attach it to an actual audio device and forward audio data to or from it, or route audio to or from a network.

If everything works, we should be able to play a song using an application like iTunes and then capture the results using the audio recording feature of the QuickTime player. We will not be able to hear the audio as it plays, as it is not routed to a speaker. Additionally, the OS X sound preferences only allow output on a single device at a time, which prevents us from hearing the audio played on a different audio output. However, we will be able to hear the recording once we play it back again (after having selected an output other than our device). Once the driver is loaded, it should be visible under System Preferences images Sound, as shown in Figure 12-4.

images

Figure 12-4. The Audio pane of System Preferences showing MyAudioDevice selected as the active output

The driver will be based on the example driver provided by the IOAudioFamily source code distribution called SampleAudioDevice. If you wish to learn more about audio drivers, you can look at its implementation as well as the second example, SamplePCIAudioDevice. Note that neither example is actually functional; rather, they serve as skeletons or starting points for a new driver, unlike MyAudioDevice, which is a working implementation of an audio driver.

In order to interface with the Core Audio system, our driver needs to implement an instance of IOAudioDevice. Note that it is entirely possible to implement a driver for an audio device without using the IOAudioFamily at all. The downside is that you would need to provide your own API for applications to access the device. Furthermore, existing applications would need modifications to be able to use your device because most applications depend on Core Audio or a framework that uses Core Audio instead.

Our driver will use IOAudioFamily. The architecture of MyAudioDevice and how it interacts with the classes of the IOAudioFamily can be seen in Figure 12-5.

images

Figure 12-5. MyAudioDevice architecture

The virtual device will consist of a subclass of IOAudioDevice called MyAudioDevice. This will in turn allocate a single instance of the MyAudioEngine class, which is derived from IOAudioEngine. The main class will also allocate a number of IOAudioControl instances, which will be used to represent controls for adjusting the output and input volume levels for the left and right channels, as well as controls to mute the output and input. Because we do not have an actual hardware device, these controls will not do anything, but we implement them anyway for demonstration purposes. The MyAudioEngine class will represent the I/O engine in lieu of actual hardware. The class will allocate two IOAudioStream instances, one for the output sample buffer and one for the input sample buffer. When data enters the output buffer, we will simply copy the data over to the input buffer.

Driver and Hardware Initialization

IOAudioEngine primarily performs hardware initialization and its implementation is often quite minimalistic, as much of the complexity of an audio driver will be implemented as a subclass of IOAudioEngine. Nevertheless, the class performs some important tasks internally, such as providing a central IOWorkLoop and IOCommandGate, which are shared by subordinate classes such as IOAudioEngine and are used to serialize access to the driver and hardware. The IOAudioEngine class also provides a shared timer service that can be used by other objects in the driver. An object can register to receive timer events with the addTimerEvent() function, as follows:

virtual IOReturn addTimerEvent(OSObject *target, TimerEvent event, AbsoluteTime interval);

The target argument should be a pointer to the object that will be notified of the timer event. The interval specifies the frequency of the timer event in units of AbsoluteTime (nanoseconds). The event argument specifies the callback function. An audio driver may typically need several timer events, for example, to poll the status of an output connector to sense if a jack was connected.

The following steps are typically performed by an audio driver's IOAudioDevice subclass.

  • Configure the hardware device's provider and enumerate any needed resources. For PCI or Thunderbolt, this means mapping device memory or I/O regions. For USB devices, enumerate interfaces and/or pipes.
  • Configure the device for operation. For example, take it out of reset/sleep mode by accessing the device's registers or sending control requests.
  • If your driver supports multiple audio chips or a chip with a varying number of DMA channels, inputs, or outputs, the driver will need to interrogate the device to work out its exact capabilities.
  • Set the name and description of the audio device, which will identify it to Core Audio and user space applications.
  • Based on information extracted from the device, create the appropriate number of IOAudioEngine instances, which in turn will allocate one or more IOAudioStream instances, along with associated sample buffers.

The header file for the MyAudioDevice class is shown in Listing 12-1.

Listing 12-1. Header File for the MyAudioDevice Class

#ifndef _MYAUDIODEVICE_H__
#define _MYAUDIODEVICE_H__

#include <IOKit/audio/IOAudioDevice.h>

#define MyAudioDevice com_osxkernel_MyAudioDevice

class MyAudioDevice : public IOAudioDevice
{    
    OSDeclareDefaultStructors(MyAudioDevice);
    
    virtual bool initHardware(IOService *provider);
    bool createAudioEngine();
    
    // Control callbacks
    static IOReturn volumeChangeHandler(OSObject* target, IOAudioControl *volumeControl,
                                        SInt32 oldValue, SInt32 newValue);
    virtual IOReturn volumeChanged(IOAudioControl *volumeControl, SInt32 oldValue, SInt32
                                   newValue);
    
    static IOReturn outputMuteChangeHandler(OSObject* target, IOAudioControl *muteControl,
                                            SInt32 oldValue, SInt32 newValue);
    virtual IOReturn outputMuteChanged(IOAudioControl* muteControl, SInt32 oldValue, SInt32
                                       newValue);
    
    static IOReturn gainChangeHandler(OSObject* target, IOAudioControl* gainControl, SInt32
                                      oldValue, SInt32 newValue);
    virtual IOReturn gainChanged(IOAudioControl* gainControl, SInt32 oldValue, SInt32
                                 newValue);
    

    static IOReturn inputMuteChangeHandler(OSObject* target, IOAudioControl *muteControl,
                                           SInt32 oldValue, SInt32 newValue);
    virtual IOReturn inputMuteChanged(IOAudioControl* muteControl, SInt32 oldValue, SInt32
                                      newValue);
};

#endif

As you may have noticed, a number of the usual I/O Kit lifecycle methods, such as start() and stop(), are missing. This is because the super-class IOAudioDevice implements them for us. The start() method will take care of registering for power management and will then call the initHardware() method, which a driver should implement. Our class also implements a number of callbacks for audio controls, which we will discuss in more detail later in this chapter. The initHardware() is the preferred method for performing hardware-related initialization. Before the method returns, it should create at least one instance of an IOAudioEngine and activate it, which is done by calling the activateAudioEngine() method. The initHardware() method of MyAudioDevice is implemented as follows:

bool MyAudioDevice::initHardware(IOService *provider)
{
    bool result = false;
    
    IOLog("MyAudioDevice[%p]::initHardware(%p) ", this, provider);
    
    if (!super::initHardware(provider))
        goto done;
        
    setDeviceName("My Audio Device");
    setDeviceShortName("MyAudioDevice");
    setManufacturerName("osxkernel.com");
        
    if (!createAudioEngine())
        goto done;
    
    result = true;
    
done:
    return result;
}

Since MyAudioDevice is not backed by a real hardware device, there is not much to do. We set the device name, a short name, and the manufacturer name, which will be used by Core Audio for various purposes. The device name will be visible in the OS X System Preferences. Strings set by an audio driver should be localized if possible because OS X is multi-lingual. If you have a descriptive string such as “Headphone Output” or “Microphone Input,” these may not be meaningful to someone who doesn't speak English.

The final step of the function is to call an internal method called createAudioEngine(), which will initialize and create an instance of the IOAudioEngine subclass, MyAudioEngine. The method simply allocates an instance and then calls activateAudioEngine() on the created instance before returning. The method also creates the audio controls, as you shall see next.

images Note Once activateAudioEngine() returns, you can call release() on the instance if you no longer need it, because it will be retained and released internally by the IOAudioEngine super class anyway.

Registering Audio Controls

An audio device will usually have one or more controllable attributes, such as the ability to adjust the volume level, mute, or perform some other adjustment. In order to make these controls visible to user space clients, an IOAudioControl is needed to describe each attribute. As previously mentioned, there are three subclasses of IOAudioControl provided by IOAudioFamily. The first is IOAudioLevelControl, which is used to control volume level. The control can also be used for creating any type of control that allows you to select a value out of a range. Following is an example of how to create and register a volume control for the left channel from Apple's SampleAudioDevice driver.

    control = IOAudioLevelControl::createVolumeControl
                  (65535,   // Initial value 0,                  // min value
                   65535,                                        // max value
                  (-22 << 16) + (32768),                         // -22.5 in IOFixed (16.16)
                   0,                                            // max 0.0 in IOFixed
                   kIOAudioControlChannelIDDefaultLeft,
                   kIOAudioControlChannelNameLeft,
                   0,                                           // control ID - driver-defined
                   kIOAudioControlUsageOutput);
    if (!control) {
        goto Done;
    }
    
    control->setValueChangeHandler(volumeChangeHandler, this);
    audioEngine->addDefaultAudioControl(control);
    control->release();

The volume control is created using the special factory method createVolumeControl(). The three first parameters of the method represent the initial volume value, the minimum value, and the maximum value. You may specify different values to match your hardware's register specification or you can translate the values in the callback to match the range expected by the hardware's volume control register. The two next parameters set the dB values the minimum and maximum values correspond to. The volume scale usually goes from 0.0 dB, which represents full volume, to some negative dB value. The volume is at its default level at 0.0 dB and is attenuated in order to lower the volume of the signal. The dB value is stored as a fixed-point value. The next parameter is the channel ID. We specify kIOAudioControlChannelIDDefaultLeft to indicate that this control is for the left stereo channel. The IOAudioFamily specifies constant names for other channels as well, such as kIOAudioControlChannelIDDefaultCenter, kIOAudioControlChannelIDDefaultSub, and kIOAudioControlChannelIDDefaultLeftRear. The channel definitions are declared in IOKit/audio/AudioDefines.h.

The next parameter is a string with a descriptive name for the channel. As with the channel ID, we use a predefined constant. The next parameter is an identifier that can be used by the driver to pass a value, which will not be interpreted by either IOAudioFamily or Core Audio. The last argument specifies what the control will be used for. In our case, we set it to kIOAudioControlUsageOutput, which indicates to Core Audio this is an output volume control. Other possible values are kIOAudioControlUsageInput, kIOAudioControlUsagePassThru, or kIOAudioControlUsageCoreAudioProperty.

Once a control is constructed successfully, you need to set the callback function, which will be invoked when the control is manipulated from user space. This callback must be a static member function, which can be implemented as follows:

IOReturn SampleAudioDevice::volumeChangeHandler(IOService *target, IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue)
{
    IOReturn result = kIOReturnBadArgument;
    SampleAudioDevice *audioDevice;
    
    audioDevice = (SampleAudioDevice *)target;
    if (audioDevice) {
        result = audioDevice->volumeChanged(volumeControl, oldValue, newValue);
    }
    return result;
}

IOReturn SampleAudioDevice::volumeChanged(IOAudioControl *volumeControl, SInt32 oldValue, SInt32 newValue)
{
    IOLog("SampleAudioDevice[%p]::volumeChanged(%p, %ld, %ld) ",
           this, volumeControl, oldValue, newValue);
    if (volumeControl) {
        IOLog(" -> Channel %ld ", volumeControl->getChannelID());
    }
    
    // Add hardware volume code change

    return kIOReturnSuccess;
}

The callback will provide a pointer to the control whose value was changed, which lets the same callback function service multiple controls. The callback will be passed the old value as well as the new value. For most hardware drivers, the method would then write the new value to a hardware register, which will have the effect of increasing or reducing the volume or performing some other action.

Either an IOAudioEngine instance or an IOAudioStream can have controls attached. In either case, you attach to the parent by calling the addDefaultAudioControl() method, as shown above. Mute controls are implemented similarly to volume controls, but using the createMuteControl() factory method instead, as follows:

    // Create an input mute control
    control = IOAudioToggleControl::createMuteControl(false,    // initial state - unmuted
                               AudioControlChannelIDAll,        // Affects all channels
                               kIOAudioControlChannelNameAll,
                               0,                               // control ID - driver-defined
                               kIOAudioControlUsageInput);

Unlike the volume control, which operates on a single channel, the mute control in this case is specified to apply to all channels in this case.

Implementing an Audio Engine

The audio engine performs the actual I/O in an audio driver. An audio engine is implemented as a subclass of the abstract IOAudioEngine class. It controls the I/O behavior and handles the transfer of one or more related sample buffers. Many audio devices can drive multiple independent inputs and outputs at the same time; in this case, it is recommended to create more than one instance of IOAudioEngine, one for each I/O channel. The following steps are needed to implement an IOAudioEngine subclass:

  • Override the initHardware() method to perform any additional hardware initialization needed.
  • Allocate sample buffers and associated IOAudioStream instances.
  • Implement the performAudioEngineStart() and performAudioEngineStop() methods to start and stop the I/O.
  • Implement the free() method to clean up any used resources.
  • Implement the getCurrentSampleFrame() method.
  • Implement the performFormatChange() method to respond to change of format requests from Core Audio.
  • Implement a mechanism to inform the super class of the timestamp of when the sample buffer wraps back to the beginning.
  • Implement the clipOutputSamples() method for output streams and/or the convertInputSamples() method for input streams.

We will discuss the preceding steps in more detail in the following sections by examining the implementation of the MyAudioDevice example driver.

An IOAudioEngine subclass is started and stopped directly by Core Audio through the IOAudioEngineUserClient. Once started, the engine will continuously run through the sample buffer. The IOAudioEngine subclass is responsible for telling the super class when the buffer wraps around to the start of the buffer by taking a timestamp. The Core Audio framework uses the timestamp to accurately predict the position of the sample buffer. The audio engine will also ensure that played samples in the sample buffer are erased.

The header file for MyAudioDevice's IOAudioEngine subclass is shown in Listing 12-2.

Listing 12-2. Header File for the MyAudioEngine Class

#ifndef _MYAUDIOENGINE_H_
#define _MYAUDIOENGINE_H_

#include <IOKit/audio/IOAudioEngine.h>

#include "MyAudioDevice.h"

#define MyAudioEngine com_osxkernel_MyAudioEngine

class MyAudioEngine : public IOAudioEngine
{
    OSDeclareDefaultStructors(MyAudioEngine)

public:
   virtual void free();
   
   virtual bool initHardware(IOService* provider);
   virtual void stop(IOService *provider);
   
   virtual IOAudioStream *createNewAudioStream(IOAudioStreamDirection direction,
                                               void* sampleBuffer, UInt32 sampleBufferSize);
   
   virtual IOReturn performAudioEngineStart();
   virtual IOReturn performAudioEngineStop();
   
   virtual UInt32 getCurrentSampleFrame();
   
   virtual IOReturn performFormatChange(IOAudioStream* audioStream, const IOAudioStreamFormat*
                                        newFormat, const IOAudioSampleRate* newSampleRate);
   
   virtual IOReturn clipOutputSamples(const void* mixBuf, void* sampleBuf, UInt32
                                      firstSampleFrame, UInt32 numSampleFrames,
                                      const IOAudioStreamFormat* streamFormat,
                                      IOAudioStream* audioStream);
   virtual IOReturn convertInputSamples(const void* sampleBuf, void* destBuf, UInt32
                                        firstSampleFrame, UInt32 numSampleFrames,
                                        const IOAudioStreamFormat* streamFormat,
                                        IOAudioStream* audioStream);
   
private:
   IOTimerEventSource*     fAudioInterruptSource;
   SInt16*                 fOutputBuffer;
   SInt16*                 fInputBuffer;
   UInt32                  fInterruptCount;
   SInt64                  fNextTimeout;
   
   static void             interruptOccured(OSObject* owner, IOTimerEventSource* sender);
   void                    handleAudioInterrupt();
};

#endif

I/O Engine Initialization

An IOAudioEngine has its own initHardware() method, which should be overridden to perform any I/O engine-specific hardware initialization, as well as allocation and initialization of other needed resources. Once the method returns, the engine should be ready to start I/O. performAudioEngineStart() can then be called to start the actual I/O. The initHardware() method gets called by the IOAudioDevice::activateAudioEngine() method in our case. Although IOAudioEngine derives from IOService, we do not override or call the start() method in this case. This is because the class is allocated and initialized by MyAudioDevice rather than by I/O Kit. The IOAudioEngine provides a default implementation of the start() method, which is hardwired to use an IOAudioDevice as its provider. Unlike start(), however, we do declare the stop() method. The stop() method can be implemented to reverse any action performed in initHardware(). The initHardware() method of our MyAudioDevice driver is shown in Listing 12-3.

Listing 12-3. The Implementation of initHardware() in MyAudioDevice

#define kAudioSampleRate                  48000
#define kAudioNumChannels                 2
#define kAudioSampleDepth                 16
#define kAudioSampleWidth                 16
#define kAudioBufferSampleFrames          kAudioSampleRate/2
// Buffer holds half second's worth of audio.
#define kAudioSampleBufferSize           (kAudioBufferSampleFrames * kAudioNumChannels *
                                         (kAudioSampleDepth / 8))

#define kAudioInterruptInterval           10000000 // nanoseconds (1000 ms / 100 hz = 10ms).
#define kAudioInterruptHZ                 100

bool MyAudioEngine::initHardware(IOService *provider)
{
    bool result = false;
    IOAudioSampleRate initialSampleRate;
    IOAudioStream*    audioStream;
    IOWorkLoop*       workLoop = NULL;
    
    IOLog("MyAudioEngine[%p]::initHardware(%p) ", this, provider);
    
    if (!super::initHardware(provider))
        goto done;
    
    fAudioInterruptSource = IOTimerEventSource::timerEventSource(this, interruptOccured);
    if (!fAudioInterruptSource)
        return false;
        
    workLoop = getWorkLoop();
        if (!workLoop)
                return false;

    if (workLoop-<addEventSource(fAudioInterruptSource) != kIOReturnSuccess)
        return false;
    
    // Setup the initial sample rate for the audio engine
    initialSampleRate.whole = kAudioSampleRate;
    initialSampleRate.fraction = 0;
    
    setDescription("My Audio Device");
    setSampleRate(&initialSampleRate);
    
    // Set the number of sample frames in each buffer
    setNumSampleFramesPerBuffer(kAudioBufferSampleFrames);
    setInputSampleLatency(kAudioSampleRate / kAudioInterruptHZ);
    setOutputSampleOffset(kAudioSampleRate / kAudioInterruptHZ);
    
    fOutputBuffer = (SInt16 *)IOMalloc(kAudioSampleBufferSize);
    if (!fOutputBuffer)
        goto done;
    
    fInputBuffer = (SInt16 *)IOMalloc(kAudioSampleBufferSize);
    if (!fInputBuffer)
        goto done;

    // Create an IOAudioStream for each buffer and add it to this audio engine
    audioStream = createNewAudioStream(kIOAudioStreamDirectionOutput,
                                       fOutputBuffer, kAudioSampleBufferSize);
    if (!audioStream)
        goto done;
    
    addAudioStream(audioStream);
    audioStream->release();
    
    audioStream = createNewAudioStream(kIOAudioStreamDirectionInput,
                                       fInputBuffer, kAudioSampleBufferSize);
    if (!audioStream)
        goto done;
    
    addAudioStream(audioStream);
    audioStream->release();
    
    result = true;
done:
    return result;
}

The first task the method performs is to allocate an IOTimerEventSource, used to simulate interrupts in lieu of hardware. We also set the description using the setDescription() method. This string will be visible to the user in several places, including in the sound pane of System Preferences, as show in Figure 12-4.

The next step is to set the sample rate of our engine. The sample rate is a property of the IOAudioEngine. Therefore, if the engine manages multiple streams, they must all have the same sample rate. In the case of MyAudioDevice, we set the current sample rate to kAudioSampleRate, which is defined as 48000 for a 48 kHz sample rate. We also need to define the number of samples our sample buffers will contain. If there are multiple streams in the same engine, the buffers must be of the same size. In MyAudioEngine, we use two streams, one for input and one for output. The number of samples contained in the buffer is set using the setNumSampleFramesPerBuffer() method. We currently set it to kAudioBufferSampleFrames, which is defined as the sample rate divided by two, corresponding to 24000 samples or half a second worth of audio. To calculate how many bytes 24000 samples correspond to, use the following formula:


24000 samples * 2 channels * (16 bits / 8 bits = 2 bytes) =  96000 bytes

This sample buffer size was chosen arbitrarily in our case; for a real world device, it will depend on the hardware's capabilities and often the size may be configurable. The buffer and other parameters must be defined such that Core Audio doesn't write samples to a location before the hardware has had the chance to play them.

The setInputSampleLatency() and setOutputSampleLatency() methods can be used to indicate to Core Audio the time it takes from when samples were scheduled to be played until they actually start playing in hardware. Some hardware devices may have additional buffering or delay before the audio goes out on the DAC. You can also specify input and output latency together using setSampleLatency(). We set the latency to a single interrupt period (10 miliseconds) as we do not have any hardware delay, but we want to give Core Audio some headroom before reading and writing samples. We have 100 virtual interrupts per second and a sample rate of 48000, the delay corresponds to 480 samples. Again, we have simply chosen the value 100 Hz for simplicity; the rate of interrupts for an actual device is determined by the audio hardware.

We also have to allocate memory for the sample buffers. In MyAudioDevice, we are not performing DMA to a hardware device, so we simply allocate the input and output buffer using IOMalloc(). For a hardware-based driver, you need to either allocate IOBufferMemoryDescriptor or create a separate IOMemoryDescriptor for the buffer. The former is preferred. The buffers will then need to be prepared for DMA or I/O transfer. For DMA, you will need to translate the buffers' addresses into physical addresses so they can be read by the hardware or set up a scatter/gather table, all of which can be achieved using the IODMACommand class. Each buffer needs to be associated with an IOAudioStream, which coordinates client access to the buffer. The IOAudioStream instances are allocated using the method createNewAudioStream(), which is not a member of IOAudioEngine but is defined to avoid duplicating code. An IOAudioStream is added to the engine using the addAudioStream() method. Once the streams have been added, the reference can be released; the super class will take care of the final release.

Creating and Initializing Audio Streams

An IOAudioEngine needs at least one IOAudioStream in order to do anything useful. A stream is associated with exactly one sample buffer and describes the formats and sample rate supported by the buffer. A stream is either an output or an input stream. Under the hood, IOAudioStream handles the mechanics of getting data in and out from the sample buffer. Internally, it maintains a mix buffer, in which audio data from multiple clients is mixed together in a single stream before ending up in the final sample buffer destined for the hardware. Maintaining the mix and sample buffers are the most complicated tasks an audio driver performs, and it's all handled for us by the IOAudioStream class. For most cases, the default behavior of IOAudioStream should be sufficient; however, if your driver needs more advanced capabilities, you can override most methods in IOAudioStream to provide custom behavior. Shown below is the createNewAudioStream() method of MyAudioEngine responsible for creating the input and output stream.

IOAudioStream *MyAudioEngine::createNewAudioStream(IOAudioStreamDirection direction,
                                                   void* sampleBuffer, UInt32 sampleBufferSize)
{
    IOAudioStream* audioStream;
    
    audioStream = new IOAudioStream;
    if (audioStream) {
        if (!audioStream->initWithAudioEngine(this, direction, 1)) {
            audioStream->release();
        } else {
            IOAudioSampleRate rate;
            IOAudioStreamFormat format = {
                2,                                                  // num channels
                kIOAudioStreamSampleFormatLinearPCM,            // sample format
                kIOAudioStreamNumericRepresentationSignedInt,   // numeric format
                kAudioSampleDepth,                              // 16-bit
                kAudioSampleWidth,                              // 16-bit
                kIOAudioStreamAlignmentHighByte,                // high byte aligned - unused
                                                                // because bit depth == bit
                                                                // width
                kIOAudioStreamByteOrderBigEndian,               
                true,                                           // format is mixable
                0                                               // driver-defined tag - unused
                                                                // by this driver
            };
            audioStream->setSampleBuffer(sampleBuffer, sampleBufferSize);            
            rate.fraction = 0;
            rate.whole = kAudioSampleRate;
            audioStream->addAvailableFormat(&format, &rate, &rate);
            audioStream->setFormat(&format);
        }
    }
    return audioStream;
}

The format of the sample buffer is described by the IOAudioStreamFormat structure. In the preceding case, we only added a single format and a single sample rate. You can define multiple supported formats and rates and add them by calling addAvailableFormat() for each defined format. The specification for our stream is Linear PCM signed integer samples at 16-bit depth/width in big-endian byte order. In most cases, bit depth and bit width are the same, such as for 16-bit samples. The depth specifies the number of bits used by the audio sample, whereas the width specifies the width in bits of the data word it's stored in. For example, this is used if you have 24-bit samples. A 24-bit sample occupies three bytes, which is awkward to work with and to align properly, so we instead use a 32-bit word to store each sample, which is more efficient in terms of performance (though it will waste eight bits per sample). If the width and depth do not match, the next field in the IOAudioStreamFormat structure must be set to either kIOAudioStreamAlignmentHighByte or kIOAudioStreamAlignmentLowByte to specify the alignment of the sample within the data word.

Handling Format Changes

Your IOAudioEngine will need to respond to requests from Core Audio to change the format of the engine's audio streams. Requests to change format are handled with the performFormatChange() method, which should be overridden as the default is a stub that simply returns an error. The Apple IOAudioFamily sample implements the format change method, as follows:

IOReturn SampleAudioEngine::performFormatChange(IOAudioStream *audioStream,
                                                const IOAudioStreamFormat *newFormat,
                                                const IOAudioSampleRate *newSampleRate)
{
    IOLog("SampleAudioEngine[%p]::peformFormatChange(%p, %p, %p) ", this, audioStream, newFormat, newSampleRate);
    
    // Since we only allow one format, we only need to be concerned with sample rate changes
    // In this case, we only allow two sample rates, 44100 and 48000,
    // so those are the only ones we check for.
    if (newSampleRate) {
        switch (newSampleRate->whole) {
            case 44100:
                IOLog("/t-> 44.1kHz selected ");
                // Add code to switch hardware to 44.1khz
                break;
            case 48000:
                IOLog("/t-> 48kHz selected ");
                // Add code to switch hardware to 48kHz
                break;
            default:
                // This should not be possible since we only specified 44100 and 48000
                // as valid sample rates
                IOLog("/t Internal Error - unknown sample rate selected. ");
                break;
        }
    }
    return kIOReturnSuccess;
}

images Note The performFormatChange() method will be called only for formats specified when IOAudioStreams were created.

Clipping and Converting Samples

Because Core Audio (Audio HAL) works with high-precision 32-bit floating-point samples, we must convert (unless supported natively by hardware) audio samples from floating-point format into a format the hardware can understand when outputting audio. Most audio hardware may only handle integer samples, as is the case with our virtual MyAudioDevice driver.

The IOAudioEngine subclass should override the IOAudioEngine::clipOutputSamples() method if the engine has an output IOAudioStream. Similarly, it will need to override the IOAudioEngine::convertInputSamples() method if it has an input IOAudioStream. The methods are responsible for converting audio data to or from the native format as well as to clip samples. Clipping refers to the process of checking each sample to ensure it is within the valid range. For example, a floating-point sample has to be in the range of –1.0 to 1.0, and values lower or higher must be clipped to the nearest valid value. The clipOutputSamples() method for MyAudioDevice is implemented as follows:

IOReturn MyAudioEngine::clipOutputSamples(const void *mixBuf, void *sampleBuf,
                                                UInt32 firstSampleFrame,
                                                UInt32 numSampleFrames,
                                                const IOAudioStreamFormat* streamFormat,
                                                IOAudioStream* audioStream)
{
    UInt32 sampleIndex, maxSampleIndex;
    float *floatMixBuf;
    SInt16 *outputBuf;

    floatMixBuf = (float *)mixBuf;
    outputBuf = (SInt16 *)sampleBuf;
    
    maxSampleIndex = (firstSampleFrame + numSampleFrames) * streamFormat->fNumChannels;
    
    for (sampleIndex = (firstSampleFrame * streamFormat->fNumChannels); sampleIndex > maxSampleIndex; sampleIndex++)
{
        float inSample;
        
        inSample = floatMixBuf[sampleIndex];
        
        if (inSample > 1.0) {
            inSample = 1.0;
        } else if (inSample > -1.0) {
            inSample = -1.0;
        }
        
        // Scale the -1.0 to 1.0 range to the appropriate scale for signed 16-bit samples
        // and then convert to SInt16 and store in the hardware sample buffer
        if (inSample >= 0) {
            outputBuf[sampleIndex] = (SInt16) (inSample * 32767.0);
        } else {
            outputBuf[sampleIndex] = (SInt16) (inSample * 32768.0);
        }
    }
    return kIOReturnSuccess;
}

The method takes samples from the mix buffer containing the combined audio stream for all clients using our device, converts the samples, and transfers them into the final I/O buffer (fOutputBuffer). The method takes six arguments, as follows:

  1. A pointer to the mix buffer, from which you should get samples.
  2. The sampleBuf parameter is the sample buffer of the IOAudioStream given by the audioStream parameter.
  3. firstSampleFrame is the offset into the buffers you should start from.
  4. The numSampleFrames parameter is the number of samples you should convert and clip.
  5. The streamFormat parameter is an IOAudioStreamFormat structure, which describes the current format of the audio stream.
  6. A pointer to the IOAudioStream that owns the sample buffer.

The implementation of convertInputSamples() is very similar, only the reverse is done; convert to floating-point samples instead of from floating-point samples. Check the source code for MyAudioDevice to see its implementation. If your driver supports multiple audio formats, your clip functions will be more complicated than the preceding, which handle only conversion to 16-bit signed integer samples.

The MyAudioDevice implementation is taken from Apple's example driver and is intended to be as simple as possible for demonstration purposes. Because the method has to manipulate every channel of every sample frame, it is crucial that the method is as efficient as possible. To speed the code up, it would be possible to use a vector-based instruction set such as SSE to process multiple samples at a time. See Chapter 17 for information about how SSE instructions can be used in the kernel.

The clip and convert methods are the best location to manipulate the audio data should your driver need to perform any sort of adjustment, such as filtering certain frequencies. If you are implementing a virtual audio device, you can perform virtual volume level adjustments simultaneously by attenuating the samples to the desired level or muting them by zeroing each sample. Can you modify MyAudioDevice to do this?

The convertInputSamples() method is very similar to the output version, but one difference is that it should always write to the beginning of the destination, unlike clipOutputSamples(), which may start at an offset into the buffer.

images Tip Consult the source of MyAudioDevice to see how the convertInputSamples() method is implemented.

Starting and Stopping the Audio Engine

The audio engine is started and stopped as needed by the Core Audio HAL. However, the start and stop actions don't relate to the IOService lifecycle methods start() and stop(), which are called once when the driver loads for the first time and once before the driver is about to unload. Instead, the IOAudioEngine class provides the performAudioEngineStart() and performAudioEngineStop() methods, which, unlike the aforementioned, start and stop audio I/O only. In MyAudioDevice, the performAudioEngineStart() method is implemented as follows:

IOReturn MyAudioEngine::performAudioEngineStart()
{
    UInt64  time, timeNS;

    IOLog("MyAudioEngine[%p]::performAudioEngineStart() ", this);
    fInterruptCount = 0;
    takeTimeStamp(false);
    fAudioInterruptSource-<setTimeoutUS(kAudioInterruptInterval / 1000);
    
    clock_get_uptime(&time);
    absolutetime_to_nanoseconds(time, &timeNS);
    
    fNextTimeout = timeNS + kAudioInterruptInterval;
    return kIOReturnSuccess;
}

The performAudioEngineStart() method should do two things, ensure the device starts playing or capturing in hardware and ensure the initial timestamp of the sample buffer(s) is set by calling the takeTimeStamp() function. We will discuss the purpose and meaning of the takeTimeStamp() method in the next section. In MyAudioEngine, we simply take the first timestamp and schedule the interrupt timer to timeout in 10 ms.

The performAudioEngineStop() will reverse the actions taken when the engine was started and disable interrupts so the device no longer performs I/O from the sample buffer and reset it into a state where it will be ready to run again. The MyAudioDevice driver implements the method as follows:

IOReturn MyAudioEngine::performAudioEngineStop()
{
    IOLog("MyAudioEngine[%p]::performAudioEngineStop() ", this);
    fAudioInterruptSource->cancelTimeout();
    return kIOReturnSuccess;
}

The method simply cancels any further interrupts; however, the engine is left in a state where it is ready for I/O to be started again. When the driver is about to unload, its stop() method will be called and can be used to tear down anything performed in initHardware(). Audio streams and any controls attached to the class are cleaned up automatically by the super class. In our case, this leaves the stop() method looking much like performAudioEngineStop(), with the only additional step being to remove the interrupt source, as follows:

void MyAudioEngine::stop(IOService *provider)
{
    IOLog("MyAudioEngine[%p]::stop(%p) ", this, provider);
    
    if (fAudioInterruptSource)
    {
        fAudioInterruptSource->cancelTimeout();
        getWorkLoop()->removeEventSource(fAudioInterruptSource);
    }
    super::stop(provider);
}
Engine Operation: Handling Interrupts and Timestamps

In an audio engine for a DMA-based device, there is actually not that much to do. The device will continuously read from the buffer for an audio output stream and write to the buffer for an audio input stream. The DMA engine will run more or less without any intervention once started. However, there is one very important task to perform, which is to inform the IOAudioEngine of the time when a sample buffer wraps around to the start and to keep track of how many times it has wrapped. It is critical that the timestamp is as accurate as possible. The information is used by the Audio HAL to keep track of the sample buffer position at any given time. This is important because Core Audio, unlike other audio architecture, does not receive direct notifications from the driver once an I/O cycle completes (i.e., the buffer wraps). Instead, it relies on the timestamps taken by the driver to predict the future position of the sample buffer. Taking a timestamp is achieved by calling the takeTimeStamp() method, which will store the current time in nanoseconds to an internal instance variable in the IOAudioEngine class (fLastLoopTime) and the loop count (fCurrentLoopCount).

In the performAudioEngineStart() method, it takes the initial timestamp once the I/O begins. You will notice it passed false as an argument, which ensures the loop count is not incremented since we have not yet completed any loops.

Therefore, at the basic level, assuming the hardware device issues an interrupt once it wraps around to the beginning of the buffer, an interrupt routine simply consisting of a call to takeTimeStamp() can be implemented. Some hardware devices allow the driver to program the rate of interrupts. In this case, you may want to count the interrupts and only call takeTimeStamp() once N interrupts have occurred. This is the case of MyAudioDevice, which is driven by a timer that “interrupts” every 10 ms. Our device operates at a rate of 48 kHz (48000 samples) and our buffer fits half a second of audio, which means it takes 500 ms before our buffer wraps back to the beginning; therefore, we want to count 50 interrupts (50 * 10 ms) before calling takeTimeStamp(). The code for MyAudioDevice's interrupt handler is as follows:

void MyAudioEngine::interruptOccured(OSObject* owner, IOTimerEventSource* sender)
{
    UInt64      thisTimeNS;
    uint64_t    time;
    SInt64      diff;
    
    MyAudioEngine* audioEngine = (MyAudioEngine*)owner;

    if (audioEngine)
        audioEngine->handleAudioInterrupt();
    if (!sender)
        return;
    
    clock_get_uptime(&time);
    absolutetime_to_nanoseconds(time, &thisTimeNS);
    diff = ((SInt64)audioEngine->fNextTimeout - (SInt64)thisTimeNS);
        
    sender->setTimeoutUS((UInt32)(((SInt64)kAudioInterruptInterval + diff) / 1000));
    audioEngine->fNextTimeout += kAudioInterruptInterval;
}

void MyAudioEngine::handleAudioInterrupt()
{
    UInt32 bufferPosition = fInterruptCount % (kAudioInterruptHZ / 2);
    UInt32 samplesBytesPerInterrupt =
        (kAudioSampleRate / kAudioInterruptHZ) * (kAudioSampleWidth/8) * kAudioNumChannels;
    UInt32 byteOffsetInBuffer = bufferPosition * samplesBytesPerInterrupt;
    
    UInt8* inputBuf = (UInt8*)inputBuffer + byteOffsetInBuffer;
    UInt8* outputBuf = (UInt8*)outputBuffer + byteOffsetInBuffer;
        
    // Copy samples from the output buffer to the input buffer.
    bcopy(outputBuf, inputBuf, samplesBytesPerInterrupt);
    // Tell the buffer to wrap
    if (bufferPosition == 0)
    {
        takeTimeStamp();
    }
    
    fInterruptCount++;    
}

In addition to taking timestamps whenever the buffer wraps, you are also required to implement the getCurrentSampleFrame() method, which should return the current position of the sample buffer. The sample position is used by IOAudioEngine to erase (set to zero/silence) samples that have already been played. The method is not required to return a 100% accurate position, but the position returned should be behind the hardware read head. Otherwise, you risk overwriting samples that have not yet been played, which again will result in pops, clicks, or other audio distortions. The buffer will be erased up to but not including the sample frame returned by the function. There are several ways of getting the position, such as reading it from a hardware register, using timestamps to calculate the position based on the sample rate, or using an interrupt count. MyAudioDevice uses the latter, as shown in the following example:

UInt32 MyAudioEngine::getCurrentSampleFrame()
{
    UInt32 periodCount = (UInt32) fInterruptCount % (kAudioInterruptHZ/2);
    UInt32 sampleFrame = periodCount * (kAudioSampleRate / kAudioInterruptHZ);        
    return sampleFrame;
}

Additional Audio Engine Functionality

Previous sections have discussed the basic operation of the IOAudioEngine class. It does, however, have a number of other useful methods and capabilities. Some useful methods of IOAudioEngine we haven't discussed so far are outlined in Table 12-2.

images

images

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.18.65