date: Nov 10, 2003
author: Pau Arumí

A Hidden AudioManager

What's Wrong?

It's mandatory explicit to the user

The user must create an AudioManager variable passing sample-rate and internal buffer size arguments. The user also have to Start the AudioManager (here the user may be tempted to think AudioManager is a Processing!)

The problem is that those details are mandatory explicit. In easy examples the user should not deal with them. He should be able to configure the proper AudioIn/Out object (maybe providing a sample-rate argument) and having the processing object talk with a well hidden AudioManager.

The problem is not only making AudioManager details as buffer-size and Starting explicit. The very presence of a non used and at the same time user-defined AudioManager variable is (and it should be) very unconfortable to the user. Notice that there no context-semantic (ala mutex-lock) applies.

A working code example (extracted from examples/FilePlayback_example.cxx):

	CLAM::AudioManager theAudioManager( playbackSampleRate, frameSize );
	// and start it to tell the OS setup things for our application
	theAudioManager.Start();

	// Now we will create two AudioOut objects, one for each channel in our card. Note that even
	// if the sound you are playing is mono, you must write something onto both channels.
	CLAM::AudioOut leftChannel;
	CLAM::AudioOut rightChannel;

	// from here: configuration of processings and data, Starts
	// finally the Dos loop and Stops
	

A kind-of singleton with some problems.

I'll begin with a snapshot of how it works (see the src/Tools/AudioManager source files for more details on this). AudioManager is a kind-of singleton because it have an static pointer to the instance. But the object itself is not an static object (thus not created in load time). It simply goes like that: the user creates an AudioManager instance and this becames the instance. Further instantiations will result on an assert (or throw?) to fail.

The problem here is that the AudioManager and AudioIn/Out processing (IO-processing from now on) are very sensible to creation and destruction order.
(a) an IO-processing needs the AudioManager to be (explicitly) created before its configuration and
(b) the AudioManager instance needs to outlive all the IO-processing objects.

For example, declaring the AudioManager as a temporary object will cause failure because the instance dies before the IO-processings. Or more dangerous (being more likely to happen): inside a Composite declaring an AudioManager attribute after an IO-processing attribute will cause failure.

Furthermore, the semantics of these errors messages are very distant from the real cause (inflicting phisical pain to the user). This, of course, can be easily worked out but in my opinion doesn't fix the real problem.

Proposed solution

Hide the audio manager to the user, except when the user is interested in latency settings. Make the audio manager a well behaved Meyer's Singleton. Thus in case of using the singleton directly from user code, use accessors methods not a constructor.

How to get rid of audio manager Start

The AudioManager Start just calls all the Starts of registered audio devices. Audio devices fullfils an abstract interface defined in concrete classes, one for each implementation of a device. The Start of a concrete device performs implementation specific tascs as alocating interal buffers.

Well, why not to do that at configuration time? not only solves our problem in hands, I think it's also more appropiate. The concrete IO-processing already knows (through its generic interface) its assigned audio device(s), let's rename the device Start method Configure and let the processing ConcreteConfigure call each assigned device Configure.

How to manage (different) sampling rates

CLAM currently configures all devices sampling-rate when the (singleton) audio manager gets created. My proposal is not doing it in a centralized way: user can configure (or not) IO-processings with sampling rate. If a second configuration causes discordance with the first the second processing configuration will fail (with the appropiate exception/message). Anyway, hipotetically two different devices could run at different sampling rate (i.e. having two hardwares) this way we could cope allowed discordances (and unalowed ones).

How to manage audio manager buffer size

Just the basic idea: make it hidden. Each concrete device should know its minimum buffer-size and each IO-processing object should be able to do readings/writtings of whatever framesize from/to the concrete device. If that's not currently possible, some modifications should be made in IO-processings.

Tunning latency settings (internal device buffer sizes) could be left to the AudioManager singleton (as suggested above) or be transferred to the IO-processings configuration. I don't know what is the best.

Actions

The refactoring should be achieved by:

Backward compatibility

It would couse some non-traumatic compatibility breaks with easy transition. Next point discusses the latest.

How to make the transition in client code?

To avoid user compilations problems we could left dummy constructor and Start methods to AudioManager. And depracate this interface.

Depending on the solution about lattency settings, client code should give value a new lattency attribute in IO-processings configs.