Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmos Audio Infrastructure (CAI) #2318

Merged
merged 25 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d5c416e
Remove old audio code
ascpixi Jul 2, 2022
0ed1c27
Create the Cosmos Audio Infrastructure
ascpixi Jul 2, 2022
23cdfac
Create the AC97 driver
ascpixi Jul 2, 2022
6199bf5
Optimize memory operations in AudioBuffer
ascpixi Jul 3, 2022
91cdf3a
Uniformly enable unsafe code in Cosmos.HAL2
ascpixi Jul 3, 2022
fe40baa
Update AC97 documentation: incorrect -> invalid
ascpixi Jul 3, 2022
30b0edc
Improve AC97 startup routine
ascpixi Jul 3, 2022
16621a2
Add buffer size changing to the AC97 driver
ascpixi Jul 3, 2022
36c6dd0
AudioMixer: remove obsolete constructor + syntax cleanup
ascpixi Jul 3, 2022
3c98306
Create reset poll limit for the AC97 driver
ascpixi Jul 3, 2022
5bbfc5f
Create Cosmos Audio Infrastructure documentation
ascpixi Jul 3, 2022
fc373a9
Update Cosmos Audio Infrastructure documentation
ascpixi Jul 3, 2022
8d0eea9
Finish CAI documentation (how to read .WAV files)
ascpixi Jul 4, 2022
7eb2c27
Minor CAI documentation improvements
ascpixi Jul 4, 2022
0e147de
Add CAI documentation to the TOC
ascpixi Jul 4, 2022
4fb028a
Minor AudioBuffer syntax changes
ascpixi Jul 4, 2022
2a83785
Unify PropertyGroups for the Cosmos.HAL2 csproj
ascpixi Jul 4, 2022
f74fc0c
Fix Format property on MemoryAudioStream
ascpixi Jul 4, 2022
abf25df
Ensure the mode is Convert by default in AudioBufferWriter
ascpixi Jul 5, 2022
9f3c8c0
Create CAI tests
ascpixi Jul 5, 2022
ebfec56
Remove unused Position property from AudioMixer
ascpixi Jul 6, 2022
a75b6bc
CAI: Naming convention changes + minor syntax changes
ascpixi Jul 6, 2022
47b8313
Update CAI tests to use new PascalCase names
ascpixi Jul 6, 2022
941309e
Ensure the temporary writer buffer is allocated
ascpixi Jul 7, 2022
79e2c71
Make the .WAV parser work for non-standard headers
ascpixi Jul 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 81 additions & 0 deletions Docs/articles/Kernel/Audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# The Cosmos Audio Infrastructure (CAI)
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
The Cosmos Audio Infrastructure allows for audio manipulation/conversion, audio I/O, and communication between audio devices. The CAI was designed with simplicity and versatility in mind.

A basic example of playing audio through an AC97-compatible audio card:
```cs
var mixer = new AudioMixer();
var audioStream = MemoryAudioStream.FromWave(sampleAudioBytes);
var driver = AC97.Initialize(bufferSize: 4096);
mixer.Streams.Add(audioStream);

var audioManager = new AudioManager()
{
Stream = mixer,
Output = driver
};
audioManager.Enable();
```

## Audio Streams
An `AudioStream` is an object that can provide sample data to audio buffers. By design, the base `AudioStream` class does not have any length or position properties, as audio streams may be infinite - for example, an output stream from a microphone, or an audio mixer. All seekable streams inherit from the class `SeekableAudioStream`, which provides functionality for accessing the position/length properties and allows methods to determine whether they accept infinite and finite streams, or only finite streams.

### Reading audio streams from memory
You can create seekable audio streams from byte arrays using the `MemoryAudioStream` class:
```cs
byte[] bytes = GetArrayOfAudioSamplesFromSomewhere();
var memAudioStream = new MemoryAudioStream(new SampleFormat(AudioBitDepth.Bits16, 2, true), 48000, bytes);
```

However, usually, you will have an audio file which contains a header containing information about the format of the audio samples it contains. The MemoryAudioStream class features support for the [Waveform Audio File Format (WAVE)](https://en.wikipedia.org/wiki/WAV), commonly used with the .WAV extension. To create an memory audio stream from a .WAV file, simply do:
```cs
byte[] bytes = GetWavFileFromSomewhere();
var wavAudioStream = MemoryAudioStream.FromWave(bytes);
```
The method will parse the file and return a `MemoryAudioStream`. The sample format will be determined by using the .WAV header. Please keep in mind that this method only accepts uncompressed LPCM samples, which is the most common encoding used in .WAV files.

## Audio Mixing
The CAI includes an `AudioMixer` class out of the box. This class is an infinite `AudioStream` that mixes given streams together. Please keep in mind that mixing several audio streams together can result in [signal clipping](https://en.wikipedia.org/wiki/Clipping_(signal_processing)). In order to prevent clipping, it's recommended to either decrease the volume of the processed streams by using the `GainPostProcessor`, or implementing your own [audio limiter](https://en.wikipedia.org/wiki/Limiter).

## Audio Buffers
Audio buffers are commonly used in both hardware and software handling - for this reason, the `AudioBuffer` class exists to operate over an array of raw audio sample data.

### Audio Buffer R/W
Audio buffers can be easily written to or read from with the help of the `AudioBufferWriter` or `AudioBufferReader` classes, respectively. These classes automatically perform all bit-depth, channel, and sign conversions. Please keep in mind that conversion operations may be taxing on the CPU. It is recommended to use standard signed 16-bit PCM samples, but, if a conversion operation is necessary, it's recommended to perform them offline (as in, before feeding the unconverted streams into an audio mixer). The reason behind this is because processing the samples within a continously running audio driver will introduce audio crackle if the CPU cannot keep up with the conversion task.

## Audio Post-Processing
Audio streams can be processed before they write to an audio buffer by using the `PostProcessors` property on an `AudioStream` instance. Post-processing effects are simple to implement:

```cs
public class SilencePostProcessor : AudioPostProcessor {
public override void Process(AudioBuffer buffer){
Array.Clear(buffer.RawData);
}
}
```

The above example implements an audio post-processor that turns any audio stream into silence. A more complex example can be seen in the `GainPostProcessor` class, included with the CAI.

## Interfacing with hardware
All hardware interfacing is abstracted behind the `AudioDriver` class. It's recommended to operate an audio driver using the `AudioManager` class. Implementations of the `AudioDriver` class usually do not have a public constructor, as they can handle only one instance of an audio card - if that is the case, they should feature a static `Initialize` method and a static `Instance` property.

For example, to initialize the AC97 driver:
```cs
var driver = AC97.Initialize(4096);
```

As you can see in the example above, the AC97 initialization method accepts an integer parameter - this is the buffer size the AC97 will use. A higher buffer size will result in a decreased amount of clicks and will usually decrease mixing overhead, however, it will increase latency. Some drivers, like the AC97 driver, include support for changing the buffer size while it is running - however, support for this is not guaranteed.

After initializing a driver, it's recommended to handle it using `AudioManager`:
```cs
var audioManager = new AudioManager()
{
Stream = mixer,
Output = driver
};

audioManager.Enable();
```
The audio manager accepts a `Stream` and an `Output` property - the `Stream` is the audio stream that the audio manager will read samples from, which will in turn be provided to the underlying `Output` audio driver. The audio manager abstracts all hardware handling - however, if you need more control over the devices, you can use the driver classes directly.

> **Note**<br>
- > When interfacing with audio devices, remember not to overload the system when supplying the audio samples. When mixing several streams of audio of different formats, for example, the system can get too overloaded, and this will result in audio crackle, or the system won't be able to respond to the audio device in time, resulting in the audio device stopping all output unexpectedly.
32 changes: 13 additions & 19 deletions source/Cosmos.HAL2/Audio/AudioBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,8 @@ public AudioBuffer(int size, SampleFormat format)
/// </summary>
public void Flush()
{
int calculatedSize = format.size * Size;

if(buffer != null && buffer.Length == calculatedSize) {
// The buffer already exists and the size has not changed -
// set all of the bytes of the buffer to 0.
MemoryOperations.Fill(buffer, 0);
} else {
// The buffer either does not exist or the sizes do not match -
// allocate a new one in memory and assign it to our variable.
buffer = new byte[calculatedSize];
}
// Set all of the bytes of the buffer to 0.
MemoryOperations.Fill(buffer, 0);
}

/// <summary>
Expand Down Expand Up @@ -97,12 +88,15 @@ public unsafe void ReadSample(int index, byte* dest)
if (index >= Size)
throw new ArgumentOutOfRangeException(nameof(index));


int bufferOffset = index * format.size;

fixed(byte* bufferPtr = buffer) {
for (int i = 0; i < format.size; i++) {
*(dest + i) = *(bufferPtr + i);
}
MemoryOperations.Copy(
dest,
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
bufferPtr + bufferOffset,
format.size
);
}
}

Expand Down Expand Up @@ -131,15 +125,15 @@ public unsafe void ReadSampleChannel(int index, int channel, byte[] dest, int de
/// <exception cref="ArgumentOutOfRangeException">Thrown when attempting to write to a non-existent channel or sample.</exception>
public unsafe void ReadSampleChannel(int index, int channel, byte* dest)
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
{
// Hot path
int channelByteSize = format.ChannelSize;
int bufferOffset = (index * format.size) + (channelByteSize * channel);

fixed (byte* bufferPtr = buffer) {
for (int i = 0; i < channelByteSize; i++)
{
*(dest + i) = *(bufferPtr + bufferOffset + i);
}
MemoryOperations.Copy(
dest,
bufferPtr + bufferOffset,
channelByteSize
);
}
}

Expand Down
6 changes: 3 additions & 3 deletions source/Cosmos.HAL2/Cosmos.HAL2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='TEST|AnyCPU'">
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='|AnyCPU'">
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
53 changes: 46 additions & 7 deletions source/Cosmos.HAL2/Drivers/PCI/Audio/AC97.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ private unsafe struct BufferDescriptorListEntry
readonly IOPort pGlobalControl; // Controls basic AC97 functions
readonly IOPort pResetRegister; // Writing any value to port will cause a register reset

const uint RESET_POLL_LIMIT = 500; // The maximum amount of polls for a reset the driver can perform.

const int TC_RUN_OR_PAUSE = (1 << 0);
const int TC_TRANSFER_RESET = (1 << 1);
const int TC_ENABLE_LAST_VALID_BUF_INTERRUPT = (1 << 2);
Expand Down Expand Up @@ -93,7 +95,7 @@ private static ushort CreateMixerVolumeValue(byte right, byte left, bool mute)
/// given buffer size.
/// </summary>
/// <param name="bufferSize">The buffer size in samples to use. This value cannot be an odd number, as per the AC97 specification.</param>
/// <exception cref="ArgumentException">Thrown when the given buffer size is incorrect.</exception>
/// <exception cref="ArgumentException">Thrown when the given buffer size is invalid.</exception>
/// <exception cref="InvalidOperationException">Thrown when no AC97-compatible sound card is present.</exception>
private AC97(ushort bufferSize)
{
Expand Down Expand Up @@ -135,12 +137,19 @@ private AC97(ushort bufferSize)
pResetRegister.DWord = 0xDEADBEEF; // any value will do here

// Reset PCM out
uint polls = 0; // The amount we polled the device for a reset

pTransferControl.Byte = (byte)(pTransferControl.Byte | TC_TRANSFER_RESET);
while ((pTransferControl.Byte & TC_TRANSFER_RESET) != 0)
while ((pTransferControl.Byte & TC_TRANSFER_RESET) != 0 && polls < RESET_POLL_LIMIT)
{
// Wait until the byte is cleared
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
polls++;
}

// The device hasn't responded to our reset request. Probably not a fully-compatible AC97 card.
if (polls >= RESET_POLL_LIMIT)
throw new InvalidOperationException("No AC97-compatible device could be found - the reset timeout has expired.");

// Volume
pMasterVolume.Word = CreateMixerVolumeValue(AC97_VOLUME_MAX, AC97_VOLUME_MAX, false);
pPCMOutVolume.Word = CreateMixerVolumeValue(AC97_VOLUME_MAX, AC97_VOLUME_MAX, false);
Expand All @@ -163,12 +172,17 @@ private AC97(ushort bufferSize)
/// and has a running instance.
/// </summary>
/// <param name="bufferSize">The buffer size in samples to use. This value cannot be an odd number, as per the AC97 specification.</param>
/// <exception cref="ArgumentException">Thrown when the given buffer size is incorrect.</exception>
/// <exception cref="ArgumentException">Thrown when the given buffer size is invalid.</exception>
/// <exception cref="InvalidOperationException">Thrown when no AC97-compatible sound card is present.</exception>
public static AC97 Initialize(ushort bufferSize)
{
if (Instance != null)
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
{
if (Instance.bufferSizeSamples != bufferSize)
Instance.ChangeBufferSize(bufferSize);

return Instance;
}

Instance = new AC97(bufferSize);
return Instance;
Expand All @@ -195,15 +209,39 @@ private unsafe void CreateBuffers(ushort bufferSize)
bufferDescriptorList[i].bufferSize = bufferSize;
bufferDescriptorList[i].configuration |= BD_FIRE_INTERRUPT_ON_CLEAR;
}
}

/// <summary>
/// Changes the size of the internal buffers. This will result
/// in a slight interruption in audio.
/// </summary>
/// <param name="newSize">The new buffer size, in samples. This value cannot be an odd number, as per the AC97 specification.</param>
/// <exception cref="ArgumentException">Thrown when the given buffer size is invalid.</exception>
public void ChangeBufferSize(ushort newSize)
{
if (newSize % 2 != 0)
throw new ArgumentException("The new buffer size must be an even number.", nameof(newSize));

if (newSize == bufferSizeSamples)
return; // No action needed

CreateBuffers(newSize);
ProvideBuffers();
}

/// <summary>
/// Provides the buffers to the sound card.
/// </summary>
private void ProvideBuffers()
{
// Tell BDL location
fixed (void* ptr = bufferDescriptorList)
{
pBufferDescriptors.DWord = (uint)ptr;
}

// Set last valid index
lastValidIdx = 2;
lastValidIdx = 2; // Start at the 3rd buffer. This will give us some headroom and will decrease clicks.
pLastValidEntry.Byte = lastValidIdx;
}

Expand Down Expand Up @@ -274,9 +312,10 @@ public override void SetSampleFormat(SampleFormat sampleFormat)

public override void Enable()
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
{
// Set last valid index
lastValidIdx = 2;
pLastValidEntry.Byte = lastValidIdx;
if (Enabled)
return; // Ignore calls to Enable() if the driver is already enabled

ProvideBuffers();

uint globalControl = pGlobalControl.DWord;
globalControl &= ~((0x3U) << 22); // 16-bit output
Expand Down
22 changes: 4 additions & 18 deletions source/Cosmos.System2/Audio/AudioMixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,6 @@ public class AudioMixer : AudioStream {
uint sampleRate;
AudioBuffer mixBuffer;

/// <summary>
/// Initializes a new instance of the <see cref="AudioMixer"/> class,
/// with the specified initial internal mixing buffer size and sample format.
/// </summary>
/// <remarks>
/// This constructor should be used when the sizes and formats of the
/// buffers that will be given to the mixer are known. If there is a
/// mis-match between the mixing format and/or the buffer size and the given buffer,
/// the internal mixing buffer will be re-initialized with the right values.
/// </remarks>
/// <param name="format">The resulting format of the mixing process.</param>
/// <param name="bufferSize">The size of the mixing buffer, in audio samples.</param>
public AudioMixer(SampleFormat format, int bufferSize)
{
mixBuffer = new AudioBuffer(bufferSize, format);
streamReader = new AudioBufferReader(mixBuffer);
}

/// <summary>
/// Initializes a new instance of the <see cref="AudioMixer"/> class.
/// </summary>
Expand Down Expand Up @@ -166,10 +148,14 @@ private static short SaturationAdd(short a, short b)
{
if(a > 0) {
if (b > short.MaxValue - a)
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
{
return short.MaxValue;
}
}
else if (b < short.MinValue - a)
ascpixi marked this conversation as resolved.
Show resolved Hide resolved
{
return short.MinValue;
}

return (short)(a + b);
}
Expand Down