Skip to content

Commit

Permalink
Add Audio (#277)
Browse files Browse the repository at this point in the history
* start implementing node graph

* update Sekai

* remove previous rendering impl

* add coordinate providing interfaces

* add `Node`

* add `Effect`

* add material-related interfaces

* add `ShaderMaterial`

* add `UnlitMaterial`

* add `Behavior`

* add `Renderable`

* add `RenderGroup`s

* add `RenderObject`s

* add `RenderData`

* add `RenderQueue` and `RenderContext`

* add `RenderTarget`

* add `Renderer`

* add `SortedFilteredCollection<T>`

* add `Drawable`

* add projectors

* add `World`

* add `Window` node

* add executable

* ensure material resources are applied

* remove `Renderable`

* cleanup pass

* add `ServiceLocator`

* add `Node.Services`

* make `Window` override `Services`

* refactor `World`

* start implementing content manager

* add `ContentManager` and `IContentLoader`

* add shader and texture loaders

* include stbisharp

* load content manager

* add more constructors to `UnlitMaterial`

* start working on audio

* add object pool interface

* add `AudioStream`

* add loader for `.wav` files

* add audio controllers and managers

* add manager to game

---------

Co-authored-by: Ayase Minori <[email protected]>
  • Loading branch information
LeNitrous and sr229 authored Jul 16, 2023
1 parent fd4e1c8 commit 70eb270
Show file tree
Hide file tree
Showing 6 changed files with 470 additions and 0 deletions.
23 changes: 23 additions & 0 deletions source/Vignette/Allocation/IObjectPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cosyne
// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.

namespace Vignette.Allocation;

/// <summary>
/// Defines a mechanism for objects that can pool <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of object being pooled.</typeparam>
public interface IObjectPool<T>
{
/// <summary>
/// Gets one <typeparamref name="T"/> from the pool.
/// </summary>
T Get();

/// <summary>
/// Returns <typeparamref name="T"/> back to the pool.
/// </summary>
/// <param name="item">The <typeparamref name="T"/> to return.</param>
/// <returns><see langword="true"/> if the item has been returned. Otherwise, <see langword="false"/>.</returns>
bool Return(T item);
}
222 changes: 222 additions & 0 deletions source/Vignette/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright (c) Cosyne
// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Sekai.Audio;
using Vignette.Allocation;

namespace Vignette.Audio;

public sealed class AudioManager : IObjectPool<AudioBuffer>
{
private const int max_buffer_size = 8192;
private const int max_buffer_count = 500;
private readonly AudioDevice device;
private readonly ConcurrentBag<AudioBuffer> bufferPool = new();
private readonly List<StreamingAudioController> controllers = new();

internal AudioManager(AudioDevice device)
{
this.device = device;
}

/// <summary>
/// Creates a new <see cref="IAudioController"/> for a <see cref="AudioStream"/>.
/// </summary>
/// <param name="stream">The audio stream to attach to the controller.</param>
/// <returns>An audio controller.</returns>
public IAudioController GetController(AudioStream stream)
{
return new StreamingAudioController(device.CreateSource(), stream, this);
}

internal void Update()
{
for (int i = 0; i < controllers.Count; i++)
{
controllers[i].Update();
}
}

/// <summary>
/// Returns an <see cref="IAudioController"/> back to the <see cref="AudioManager"/>.
/// </summary>
/// <param name="controller">The controller to return.</param>
public void Return(IAudioController controller)
{
if (controller is not StreamingAudioController streaming)
{
return;
}

if (!controllers.Remove(streaming))
{
return;
}

streaming.Dispose();
}

AudioBuffer IObjectPool<AudioBuffer>.Get()
{
if (!bufferPool.TryTake(out var buffer))
{
buffer = device.CreateBuffer();
}

return buffer;
}

bool IObjectPool<AudioBuffer>.Return(AudioBuffer item)
{
if (bufferPool.Count >= max_buffer_count)
{
item.Dispose();
return false;
}

bufferPool.Add(item);
return true;
}

private sealed class StreamingAudioController : IAudioController, IDisposable
{
public bool Loop { get; set; }

public TimeSpan Position
{
get => getTimeFromByteCount((int)stream.Position, stream.Format, stream.SampleRate);
set => seek(getByteCountFromTime(value, stream.Format, stream.SampleRate));
}

public TimeSpan Duration => getTimeFromByteCount((int)stream.Length, stream.Format, stream.SampleRate);

public TimeSpan Buffered => getTimeFromByteCount(buffered, stream.Format, stream.SampleRate);

public AudioSourceState State => source.State;

private int buffered;
private bool isDisposed;
private const int max_buffer_stream = 4;
private readonly AudioSource source;
private readonly AudioStream stream;
private readonly IObjectPool<AudioBuffer> bufferPool;

public StreamingAudioController(AudioSource source, AudioStream stream, IObjectPool<AudioBuffer> bufferPool)
{
this.source = source;
this.stream = stream;
this.bufferPool = bufferPool;
}

public void Play()
{
if (State != AudioSourceState.Paused)
{
seek(0);

for (int i = 0; i < max_buffer_stream; i++)
{
var buffer = bufferPool.Get();

if (!allocate(buffer))
{
break;
}

source.Enqueue(buffer);
}
}

source.Play();
}

public void Stop()
{
seek(0);
}

public void Pause()
{
source.Pause();
}

public void Update()
{
while (source.TryDequeue(out var buffer))
{
if (!allocate(buffer))
{
source.Loop = Loop;
break;
}

source.Enqueue(buffer);
}
}

public void Dispose()
{
if (isDisposed)
{
return;
}

source.Stop();

while(source.TryDequeue(out var buffer))
{
bufferPool.Return(buffer);
}

source.Dispose();

isDisposed = false;
}

private void seek(int position)
{
source.Stop();
source.Clear();
stream.Position = buffered = position;
}

private bool allocate(AudioBuffer buffer)
{
Span<byte> data = stackalloc byte[max_buffer_size];
int read = stream.Read(data);

if (read <= 0)
{
return false;
}

buffer.SetData<byte>(data[..read], stream.Format, stream.SampleRate);
buffered += read;

return true;
}
}

private static int getChannelCount(AudioFormat format)
{
return format is AudioFormat.Stereo8 or AudioFormat.Stereo16 ? 2 : 1;
}

private static int getSamplesCount(AudioFormat format)
{
return format is AudioFormat.Stereo8 or AudioFormat.Mono8 ? 8 : 16;
}

private static int getByteCountFromTime(TimeSpan time, AudioFormat format, int sampleRate)
{
return (int)time.TotalSeconds * sampleRate * getChannelCount(format) * (getSamplesCount(format) / 8);
}

private static TimeSpan getTimeFromByteCount(int count, AudioFormat format, int sampleRate)
{
return TimeSpan.FromSeconds(count / (sampleRate * getChannelCount(format) * (getSamplesCount(format) / 8)));
}
}
113 changes: 113 additions & 0 deletions source/Vignette/Audio/AudioStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Cosyne
// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.

using System.IO;
using Sekai.Audio;

namespace Vignette.Audio;

/// <summary>
/// A <see cref="Stream"/> containing pulse code modulation (PCM) audio data.
/// </summary>
public class AudioStream : Stream
{
/// <summary>
/// The audio stream's sample rate
/// </summary>
public int SampleRate { get; }

/// <summary>
/// The audio stream's format.
/// </summary>
public AudioFormat Format { get; }

public override bool CanRead => stream.CanRead;

public override bool CanSeek => stream.CanSeek;

public override bool CanWrite => stream.CanWrite;

public override long Length => stream.Length;

public override long Position
{
get => stream.Position;
set => stream.Position = value;
}

private bool isDisposed;
private readonly MemoryStream stream;

public AudioStream(byte[] buffer, AudioFormat format, int sampleRate)
: this(buffer, true, format, sampleRate)
{
}

public AudioStream(byte[] buffer, bool writable, AudioFormat format, int sampleRate)
{
Format = format;
stream = new MemoryStream(buffer, writable);
SampleRate = sampleRate;
}

public AudioStream(byte[] buffer, int index, int count, AudioFormat format, int sampleRate)
: this(buffer, index, count, true, format, sampleRate)
{
}

public AudioStream(byte[] buffer, int index, int count, bool writable, AudioFormat format, int sampleRate)
{
Format = format;
stream = new MemoryStream(buffer, index, count, writable);
SampleRate = sampleRate;
}

public AudioStream(int capacity, AudioFormat format, int sampleRate)
{
Format = format;
stream = new MemoryStream(capacity);
SampleRate = sampleRate;
}

public AudioStream(AudioFormat format, int sampleRate)
: this(0, format, sampleRate)
{
}

public override void Flush()
{
stream.Flush();
}

public override int Read(byte[] buffer, int offset, int count)
{
return stream.Read(buffer, offset, count);
}

public override long Seek(long offset, SeekOrigin origin)
{
return stream.Seek(offset, origin);
}

public override void SetLength(long value)
{
stream.SetLength(value);
}

public override void Write(byte[] buffer, int offset, int count)
{
stream.Write(buffer, offset, count);
}

protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}

stream.Dispose();

isDisposed = true;
}
}
Loading

0 comments on commit 70eb270

Please sign in to comment.