Browse Source

Added audio support. Currently only supports 16-bit little-endian.

improved_timing
Ian Burgmyer 7 years ago
parent
commit
e57eb7b7c4
  1. 11
      DotSDL/Audio/AudioBuffer.cs
  2. 26
      DotSDL/Audio/AudioFormat.cs
  3. 47
      DotSDL/Audio/FormatConverter.cs
  4. 170
      DotSDL/Audio/Playback.cs
  5. 4
      DotSDL/Events/EventHandler.cs
  6. 221
      DotSDL/Sdl/Audio.cs
  7. 25
      Samples/Sample.Audio/Window.cs

11
DotSDL/Audio/AudioBuffer.cs

@ -0,0 +1,11 @@
namespace DotSDL.Audio {
/// <summary>
/// Represents an audio buffer.
/// </summary>
public struct AudioBuffer {
/// <summary>
/// The samples in the audio buffer.
/// </summary>
public double[] Samples;
}
}

26
DotSDL/Audio/AudioFormat.cs

@ -0,0 +1,26 @@
namespace DotSDL.Audio {
/// <summary>
/// The audio formats supported by DotSDL.
/// </summary>
public enum AudioFormat {
/// <summary>
/// An 8-bit integral audio format.
/// </summary>
Integer8,
/// <summary>
/// A 16-bit integral audio format.
/// </summary>
Integer16,
/// <summary>
/// A 32-bit integral audio format.
/// </summary>
Integer32,
/// <summary>
/// A 32-bit floating point audio format.
/// </summary>
Float32
}
}

47
DotSDL/Audio/FormatConverter.cs

@ -0,0 +1,47 @@
using System;
using SdlAudio = DotSDL.Sdl.Audio;
namespace DotSDL.Audio {
internal static class FormatConverter {
/// <summary>
/// Converts a DotSDL <see cref="AudioBuffer"/> to an SDL-compatible byte array.
/// </summary>
/// <param name="buffer">A DotSDL <see cref="AudioBuffer"/>.</param>
/// <param name="stream">The byte array to write the converted data to.</param>
/// <param name="format">The SDL <see cref="AudioFormat"/> to convert to.</param>
internal static void ConvertFormat(AudioBuffer buffer, ref byte[] stream, SdlAudio.AudioFormat format) {
switch(format) {
case SdlAudio.AudioFormat.SignedShortLittleEndian:
ToSint16Lsb(buffer, ref stream);
break;
default:
throw new NotImplementedException();
}
}
/// <summary>
/// Converts an audio buffer to a byte array with little-endian signed shorts.
/// </summary>
/// <param name="buffer">A DotSDL <see cref="AudioBuffer"/>.</param>
/// <param name="stream">The byte array to write the converted data to.</param>
private static void ToSint16Lsb(AudioBuffer buffer, ref byte[] stream) {
if(BitConverter.IsLittleEndian) {
// Little-endian architecture.
for(var i = 0; i < buffer.Samples.Length; i++) {
var sample = buffer.Samples[i];
var newSample = (short)(sample * short.MaxValue);
stream[i * 2 + 1] = (byte)(newSample >> 8);
stream[i * 2] = (byte)newSample;
}
} else {
// Big-endian architecture.
for(var i = 0; i < buffer.Samples.Length; i++) {
var sample = buffer.Samples[i];
var newSample = (short)(sample * short.MaxValue);
stream[i * 2] = (byte)newSample;
stream[i * 2 + 1] = (byte)(newSample >> 8);
}
}
}
}
}

170
DotSDL/Audio/Playback.cs

@ -0,0 +1,170 @@
using DotSDL.Sdl;
using SdlAudio = DotSDL.Sdl.Audio;
using System;
using System.Runtime.InteropServices;
namespace DotSDL.Audio {
/// <summary>
/// Represents a streaming audio playback engine.
/// </summary>
public class Playback {
private const ushort DefaultBufferSize = 4096;
private readonly SdlInit _sdlInit = SdlInit.Instance;
private readonly uint _deviceId;
/// <summary>
/// The SDL <see cref="SdlAudio.AudioSpec"/> that is currently active.
/// </summary>
private SdlAudio.AudioSpec _sdlAudioSpec;
/// <summary>
/// The frequency for the open audio device, in hertz.
/// </summary>
public int Frequency { get; }
/// <summary>
/// The bit size for the open audio device.
/// </summary>
public byte BitSize { get; }
/// <summary>
/// The number of sound channels for the open audio device.
/// </summary>
public byte Channels { get; }
/// <summary>
/// The buffer size for the open audio device, in bytes.
/// </summary>
public uint BufferSizeBytes { get; }
/// <summary>
/// The buffer size for the open audio device, in samples.
/// </summary>
public ushort BufferSizeSamples { get; }
/// <summary>
/// Fired when the audio device is requesting more data.
/// </summary>
public event EventHandler<AudioBuffer> BufferEmpty;
/// <summary>
/// Initializes a new instance of the audio engine.
/// </summary>
/// <param name="freqency">The desired frequency, in hertz.</param>
/// <param name="format">The desired audio format.</param>
/// <param name="channels">The desired number of channels.</param>
public Playback(int freqency, AudioFormat format, byte channels)
: this(freqency, format, channels, DefaultBufferSize) { }
/// <summary>
/// Initializes a new instance of the audio engine.
/// </summary>
/// <param name="freqency">The desired frequency, in hertz.</param>
/// <param name="format">The desired audio format.</param>
/// <param name="channels">The desired number of channels.</param>
/// <param name="buffer">The desired buffer size, in samples.</param>
public Playback(int freqency, AudioFormat format, byte channels, ushort buffer) {
_sdlInit.InitSubsystem(Init.SubsystemFlags.Audio);
SdlAudio.AudioSpec actual;
var desired = new SdlAudio.AudioSpec {
Freq = freqency,
Format = GetBestAudioFormat(format),
Channels = channels,
Silence = 0,
Samples = buffer,
Padding = 0,
Size = 0,
Callback = Callback,
Userdata = IntPtr.Zero
};
_deviceId = SdlAudio.OpenAudioDevice(IntPtr.Zero, 0, ref desired, out actual, SdlAudio.AllowedChanges.AllowAnyChange);
// Populate the obtained values in the object properties.
Frequency = actual.Freq;
Channels = actual.Channels;
BufferSizeSamples = actual.Samples;
BufferSizeBytes = actual.Size;
BitSize = SdlAudio.BitSize((ushort)actual.Format);
_sdlAudioSpec = actual;
}
/// <summary>
/// Called when the object is about to be destroyed.
/// </summary>
~Playback() {
Close();
}
/// <summary>
/// The SDL audio callback function.
/// </summary>
/// <param name="userdata">The user data passed in from the audio spec.</param>
/// <param name="stream">The stream to be filled in.</param>
/// <param name="len">The length of the stream array, in bytes.</param>
private void Callback(IntPtr userdata, IntPtr stream, int len) {
if(BufferEmpty == null) return;
var buffer = new AudioBuffer { Samples = new double[BufferSizeSamples] };
BufferEmpty(this, buffer);
var newStream = new byte[len];
FormatConverter.ConvertFormat(buffer, ref newStream, _sdlAudioSpec.Format);
Marshal.Copy(newStream, 0, stream, len);
}
/// <summary>
/// Closes the audio engine.
/// </summary>
public void Close() {
Pause();
SdlAudio.CloseAudioDevice(_deviceId);
}
/// <summary>
/// Stops processing audio.
/// </summary>
public void Pause() {
SdlAudio.PauseAudioDevice(_deviceId, SdlAudio.AudioState.Paused);
}
/// <summary>
/// Starts processing audio. This must be called after the device is
/// initialized for callback processing to start occurring.
/// </summary>
public void Play() {
SdlAudio.PauseAudioDevice(_deviceId, SdlAudio.AudioState.Unpaused);
}
/// <summary>
/// Retrieves the best SDL audio format to suit the desired DotSDL format.
/// </summary>
/// <param name="format">The DotSDL <see cref="AudioFormat"/> to use.</param>
/// <returns>An SDL <see cref="SdlAudio.AudioFormat"/> that best fits the value
/// passed in <paramref name="format"/>.</returns>
private static SdlAudio.AudioFormat GetBestAudioFormat(AudioFormat format) {
var littleEndian = BitConverter.IsLittleEndian;
switch(format) {
case AudioFormat.Integer8:
return SdlAudio.AudioFormat.SignedByte;
case AudioFormat.Integer32:
return littleEndian
? SdlAudio.AudioFormat.SignedIntLittleEndian
: SdlAudio.AudioFormat.SignedIntBigEndian;
case AudioFormat.Float32:
return littleEndian
? SdlAudio.AudioFormat.FloatLittleEndian
: SdlAudio.AudioFormat.FloatBigEndian;
default:
return littleEndian
? SdlAudio.AudioFormat.SignedShortLittleEndian
: SdlAudio.AudioFormat.SignedShortBigEndian;
}
}
}
}

4
DotSDL/Events/EventHandler.cs

@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using SdlEvents = DotSDL.Sdl.Events;
@ -36,9 +35,6 @@ namespace DotSDL.Events {
case SdlEvents.EventType.WindowEvent:
var wnd = CastEvent<SdlEvents.SdlWindowEvent>(sdlEvent);
return EventConversion.Convert(wnd);
default:
Debug.WriteLine(sdlEvent.Type);
break;
}
return null;
}

221
DotSDL/Sdl/Audio.cs

@ -0,0 +1,221 @@
using System;
using System.Runtime.InteropServices;
namespace DotSDL.Sdl {
/// <summary>
/// Contains the necessary constants and function imports from SDL_audio.h.
/// </summary>
internal static class Audio {
internal const ushort FormatMaskBitSize = 0xFF;
internal const ushort FormatMaskDataType = 1 << 8;
internal const ushort FormatMaskEndian = 1 << 12;
internal const ushort FormatMaskSigned = 1 << 15;
/// <summary>
/// Returns the bit size of the audio format.
/// </summary>
/// <param name="format">The SDL audio format.</param>
/// <returns>The bit size of the audio format.</returns>
internal static byte BitSize(ushort format) {
return (byte)(format & FormatMaskBitSize);
}
/// <summary>
/// Determines whether the specified format is big-endian.
/// </summary>
/// <param name="format">The SDL audio format.</param>
/// <returns><c>true</c> if the audio format is big-endian, otherwise <c>false</c>.</returns>
internal static bool IsBigEndian(ushort format) {
return (format & FormatMaskEndian) != 0;
}
/// <summary>
/// Determines whether the specified format is a floating-point format.
/// </summary>
/// <param name="format">The SDL audio format.</param>
/// <returns><c>true</c> if the audio format is represented by floats,
/// otherwise <c>false</c>.</returns>
internal static bool IsFloat(ushort format) {
return (format & FormatMaskDataType) != 0;
}
/// <summary>
/// Determines whether the specified format is a signed integral format.
/// </summary>
/// <param name="format">The SDL audio format.</param>
/// <returns><c>true</c> if the audio format is a signed integral format,
/// otherwise <c>false</c>.</returns>
internal static bool IsSigned(ushort format) {
return (format & FormatMaskSigned) != 0;
}
/// <summary>
/// A structure that contains the audio output format. It also contains a
/// callback that is called when the audio device needs more data.
/// </summary>
internal struct AudioSpec {
/// <summary>The DSP frequency, in hertz.</summary>
internal int Freq;
/// <summary>The audio data format.</summary>
internal AudioFormat Format;
/// <summary>The number of sound channels.</summary>
internal byte Channels;
/// <summary>Audio buffer silence value (calculated).</summary>
internal byte Silence;
/// <summary>Audio buffer size in samples (power of 2).</summary>
internal ushort Samples;
/// <summary>Unused.</summary>
internal ushort Padding;
/// <summary>Audio buffer size in bytes (calculated).</summary>
internal uint Size;
/// <summary>The function to call when the audio device needs more data.</summary>
[MarshalAs(UnmanagedType.FunctionPtr)]
internal AudioCallback Callback;
/// <summary>A pointer that is passed to the <see cref="Callback"/> function.</summary>
internal IntPtr Userdata;
}
/// <summary>
/// A function pointer representing the audio callback function.
/// </summary>
/// <param name="userdata">Userdata passed to the function.</param>
/// <param name="stream">The stream to be filled in by the application.</param>
/// <param name="len">The length of the array in <paramref name="stream"/>.</param>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void AudioCallback(IntPtr userdata, IntPtr stream, int len);
/// <summary>
/// Specifies how SDL should behave when a device cannot offer a specific feature. If
/// the application requests a feature that the hardware doesn't offer, SDL will always
/// try to get the closest equivalent.
/// </summary>
[Flags]
internal enum AllowedChanges : int {
/// <summary>Do not allow any audio spec changes.</summary>
DisallowChange = 0x00,
/// <summary>Allow frequency changes.</summary>
AllowFrequencyChange = 0x01,
/// <summary>Allow the audio format to change.</summary>
AllowFormatChange = 0x02,
/// <summary>Allow the number of channels to change.</summary>
AllowChannelsChange = 0x04,
/// <summary>Allow any detail in the audio spec to change.</summary>
AllowAnyChange = AllowFrequencyChange | AllowFormatChange | AllowChannelsChange
}
/// <summary>
/// A list of audio formats supported by SDL.
/// </summary>
internal enum AudioFormat : ushort {
/// <summary>Unsigned byte. Equivalent to AUDIO_U8.</summary>
UnsignedByte = 0x0008,
/// <summary>Signed byte. Equivalent to AUDIO_S8.</summary>
SignedByte = 0x8008,
/// <summary>Little-endian unsigned short. Equivalent to AUDIO_U16LSB.</summary>
UnsignedShortLittleEndian = 0x0010,
/// <summary>Little-endian signed short. Equivalent to AUDIO_S16LSB.</summary>
SignedShortLittleEndian = 0x8010,
/// <summary>Big-endian unsigned short. Equivalent to AUDIO_U16MSB.</summary>
UnsignedShortBigEndian = 0x1010,
/// <summary>Big-endian signed short. Equivalent to AUDIO_S16MSB.</summary>
SignedShortBigEndian = 0x9010,
/// <summary>Little-endian signed int. Equivalent to AUDIO_S32LSB.</summary>
SignedIntLittleEndian = 0x8020,
/// <summary>Big-endian signed int. Equivalent to AUDIO_S32MSB.</summary>
SignedIntBigEndian = 0x9020,
/// <summary>Little-endian floats. Equivalent to AUDIO_F32LSB.</summary>
FloatLittleEndian = 0x8120,
/// <summary>Big-endian floats. Equivalent to AUDIO_F32MSB.</summary>
FloatBigEndian = 0x9120
}
/// <summary>
/// The audio playback state..
/// </summary>
internal enum AudioState : int {
/// <summary>Audio playback is paused.</summary>
Unpaused = 0,
/// <summary>Audio playback is unpaused.</summary>
Paused = 1
}
/// <summary>
/// Shuts down audio processing and closes the audio device.
/// </summary>
/// <param name="dev">An audio device previously opened by <see cref="OpenAudioDevice"/>.</param>
[DllImport(Meta.DllName, EntryPoint = "SDL_CloseAudioDevice", CallingConvention = CallingConvention.Cdecl)]
internal static extern void CloseAudioDevice(uint dev);
/// <summary>
/// Open a specific audio device. Passing in a device name of NULL
/// requests the most reasonable default (and is equivalent to calling
/// SDL_OpenAudio).
/// </summary>
/// <param name="device">The name of the device to open, as reported
/// SDL_GetAudioDriverName(). Some drivers allow arbitrary and
/// driver-specific strings, such as a hostname/IP address for a remote
/// audio server, or a filename in the diskaudio driver.</param>
/// <param name="isCapture">Non-zero to specify a device should be opened
/// for recording, not playback.</param>
/// <param name="desired">An <see cref="AudioSpec"/> structure representing
/// the desired output format.</param>
/// <param name="obtained">An <see cref="AudioSpec"/> structure filled in
/// with the actual output format.</param>
/// <param name="allowedChanges"></param>
/// <returns>A non-zero value on success or zero on failure. This will never
/// return 1, since SDL reserves that ID for the SDL_OpenAudio() function.</returns>
[DllImport(Meta.DllName, EntryPoint = "SDL_OpenAudioDevice", CallingConvention = CallingConvention.Cdecl)]
internal static extern uint OpenAudioDevice(IntPtr device, int isCapture, ref AudioSpec desired, out AudioSpec obtained, AllowedChanges allowedChanges);
/// <summary>
/// Open a specific audio device. Passing in a device name of NULL
/// requests the most reasonable default (and is equivalent to calling
/// SDL_OpenAudio).
/// </summary>
/// <param name="device">The name of the device to open, as reported
/// SDL_GetAudioDriverName(). Some drivers allow arbitrary and
/// driver-specific strings, such as a hostname/IP address for a remote
/// audio server, or a filename in the diskaudio driver.</param>
/// <param name="isCapture">Non-zero to specify a device should be opened
/// for recording, not playback.</param>
/// <param name="desired">An <see cref="AudioSpec"/> structure representing
/// the desired output format.</param>
/// <param name="obtained">An <see cref="AudioSpec"/> structure filled in
/// with the actual output format.</param>
/// <param name="allowedChanges"></param>
/// <returns>A non-zero value on success or zero on failure. This will never
/// return 1, since SDL reserves that ID for the SDL_OpenAudio() function.</returns>
[DllImport(Meta.DllName, EntryPoint = "SDL_OpenAudioDevice", CallingConvention = CallingConvention.Cdecl)]
internal static extern uint OpenAudioDevice(string device, int isCapture, ref AudioSpec desired, out AudioSpec obtained, AllowedChanges allowedChanges);
/// <summary>
/// Pause and unpause audio callback processing.
/// </summary>
/// <param name="dev">The audio device to change.</param>
/// <param name="pauseOn">The <see cref="AudioState"/> to switch to.</param>
[DllImport(Meta.DllName, EntryPoint = "SDL_PauseAudioDevice", CallingConvention = CallingConvention.Cdecl)]
internal static extern void PauseAudioDevice(uint dev, AudioState pauseOn);
}
}

25
Samples/Sample.Audio/Window.cs

@ -1,5 +1,7 @@
using DotSDL.Graphics;
using DotSDL.Audio;
using DotSDL.Graphics;
using DotSDL.Input.Keyboard;
using System;
namespace Sample.Audio {
internal class Window : SdlWindow {
@ -17,7 +19,11 @@ namespace Sample.Audio {
private const byte OnDelta = 24;
private const byte OffColor = 72;
private Color TextColor = new Color { A = 255, R = 159, G = 159, B = 159 };
private readonly Color TextColor = new Color { A = 255, R = 159, G = 159, B = 159 };
private Playback _audio;
private ulong _time;
private int _audioFreq;
public Window(int width, int height) : base("DotSDL Audio Example",
new Point{ X = WindowPosUndefined, Y = WindowPosUndefined },
@ -25,6 +31,21 @@ namespace Sample.Audio {
36, 9) {
KeyPressed += Window_KeyPressed;
KeyReleased += Window_KeyReleased;
_audio = new Playback(44100, AudioFormat.Integer16, 1);
_audioFreq = _audio.Frequency;
Console.WriteLine($"Audio opened: {_audioFreq}hz, {_audio.BitSize}-bit, {_audio.Channels} channels, {_audio.BufferSizeSamples} sample buffer, {_audio.BufferSizeBytes} byte buffer");
_audio.BufferEmpty += Audio_BufferEmpty;
_maxFreq = _audioFreq / 2;
_audio.Play();
}
private void Audio_BufferEmpty(object sender, AudioBuffer e) {
var t = (Math.PI * 2.0 * _freq) / _audioFreq;
for(var i = 0; i < e.Samples.Length; i++)
e.Samples[i] = Math.Sin(_time++ * t);
}
private void DrawGlyph(ref Canvas canvas, char ch, int xPos, Color c) {

Loading…
Cancel
Save