Skip to content

Commit

Permalink
add audio controllers and managers
Browse files Browse the repository at this point in the history
  • Loading branch information
LeNitrous committed Jul 16, 2023
1 parent 7531961 commit f390f8e
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
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)));
}
}
53 changes: 53 additions & 0 deletions source/Vignette/Audio/IAudioController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Cosyne
// Licensed under GPL 3.0 with SDK Exception. See LICENSE for details.

using System;
using Sekai.Audio;

namespace Vignette.Audio;

/// <summary>
/// Provides access to audio playback controls.
/// </summary>
public interface IAudioController
{
/// <summary>
/// Gets or sets whether audio playback should loop.
/// </summary>
bool Loop { get; set; }

/// <summary>
/// Gets or seeks the current playback position.
/// </summary>
TimeSpan Position { get; set; }

/// <summary>
/// Gets total playable duration.
/// </summary>
TimeSpan Duration { get; }

/// <summary>
/// Gets the duration of the buffered data.
/// </summary>
TimeSpan Buffered { get; }

/// <summary>
/// Gets the state of this audio controller.
/// </summary>
AudioSourceState State { get; }

/// <summary>
/// Starts audio playback.
/// </summary>
void Play();

/// <summary>
/// Stops audio playback.
/// </summary>
void Stop();

/// <summary>
/// Pauses audio playback.
/// </summary>
void Pause();
}

0 comments on commit f390f8e

Please sign in to comment.