From 32d5299a3ff50d4d8663d5474e713d1a292af5f7 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Thu, 16 Feb 2023 20:14:39 +0100 Subject: [PATCH] Add multiple audio channels (WIP) --- Capy64/Core/Audio.cs | 77 +++++++++++++++++--- Capy64/Runtime/Libraries/Audio.cs | 113 ++++++++++++++++++++++++------ 2 files changed, 157 insertions(+), 33 deletions(-) diff --git a/Capy64/Core/Audio.cs b/Capy64/Core/Audio.cs index 436a75c..79d93da 100644 --- a/Capy64/Core/Audio.cs +++ b/Capy64/Core/Audio.cs @@ -30,24 +30,78 @@ public class Audio : IDisposable } public const int SampleRate = 48000; - public const AudioChannels Channels = AudioChannels.Mono; - public readonly DynamicSoundEffectInstance Sound; + public const AudioChannels AudioChannel = AudioChannels.Mono; + public const int ChannelsCount = 5; + public readonly DynamicSoundEffectInstance[] Channels = new DynamicSoundEffectInstance[ChannelsCount]; + private bool[] freeChannels = new bool[ChannelsCount]; private static readonly Random rng = new(); public Audio() { - Sound = new DynamicSoundEffectInstance(SampleRate, Channels); + for (int i = 0; i < ChannelsCount; i++) + { + Channels[i] = new DynamicSoundEffectInstance(SampleRate, AudioChannel); + freeChannels[i] = true; + Channels[i].BufferNeeded += Audio_BufferNeeded; + } } - public TimeSpan Submit(byte[] buffer) + private void Audio_BufferNeeded(object sender, EventArgs e) { - Sound.SubmitBuffer(buffer); - return Sound.GetSampleDuration(buffer.Length); + for (int i = 0; i < ChannelsCount; i++) + { + if (Channels[i] == sender) + { + freeChannels[i] = true; + } + } + } - public byte[] GenerateWave(Waveform form, double frequency, TimeSpan time, float volume = 1f) + public int GetChannelId(int inp) { - var size = Sound.GetSampleSizeInBytes(time); + if (inp >= 0) + return inp; + + if (inp == -1) + { + for (int i = 0; i < ChannelsCount; i++) + { + if (freeChannels[i]) + return i; + } + } + + // -2 is all? + + return -3; + } + + public bool TryGetChannel(int id, out DynamicSoundEffectInstance channel, out int resolvedId) + { + resolvedId = GetChannelId(id); + + if (resolvedId >= 0) + channel = Channels[resolvedId]; + else + channel = null; + + return channel != null; + } + + public TimeSpan Submit(int id, byte[] buffer) + { + if (!TryGetChannel(id, out var channel, out var rId)) + return TimeSpan.Zero; + + channel.SubmitBuffer(buffer); + freeChannels[rId] = false; + return channel.GetSampleDuration(buffer.Length); + } + + public byte[] GenerateWave(DynamicSoundEffectInstance channel, Waveform form, double frequency, TimeSpan time, float volume = 1f) + { + var size = channel.GetSampleSizeInBytes(time); var buffer = new byte[size]; var step = 1d / SampleRate; @@ -63,7 +117,7 @@ public class Audio : IDisposable Waveform.Noise => rng.NextDouble() * 2 - 1, _ => throw new NotImplementedException(), }; - + value = Math.Clamp(value, -1, 1); var sample = (short)((value >= 0.0f ? value * short.MaxValue : value * short.MinValue * -1) * volume); if (!BitConverter.IsLittleEndian) @@ -117,6 +171,9 @@ public class Audio : IDisposable public void Dispose() { GC.SuppressFinalize(this); - Sound.Dispose(); + for (int i = 0; i < ChannelsCount; i++) + { + Channels[i]?.Dispose(); + } } } diff --git a/Capy64/Runtime/Libraries/Audio.cs b/Capy64/Runtime/Libraries/Audio.cs index ea28ba0..58aeff9 100644 --- a/Capy64/Runtime/Libraries/Audio.cs +++ b/Capy64/Runtime/Libraries/Audio.cs @@ -90,7 +90,7 @@ public class Audio : IComponent return 1; } - private static async Task DelayEmit(TimeSpan time) + private static async Task DelayEmit(TimeSpan time, DynamicSoundEffectInstance channel) { var waitTime = time - TimeSpan.FromMilliseconds(1000 / 60); if (waitTime.TotalMilliseconds < 0) @@ -98,7 +98,7 @@ public class Audio : IComponent await Task.Delay(waitTime); _game.LuaRuntime.QueueEvent("audio_end", LK => { - LK.PushInteger(_game.Audio.Sound.PendingBufferCount); + LK.PushInteger(channel.PendingBufferCount); return 1; }); } @@ -127,7 +127,16 @@ public class Audio : IComponent } } - if (_game.Audio.Sound.PendingBufferCount > queueLimit) + var id = (int)L.OptInteger(2, -1); + + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushBoolean(false); + L.PushString("Channel not available"); + return 2; + } + + if (channel.PendingBufferCount > queueLimit) { L.PushBoolean(false); L.PushString("queue is full"); @@ -137,11 +146,11 @@ public class Audio : IComponent try { - var ts = _game.Audio.Submit(buffer); - DelayEmit(ts); + var ts = _game.Audio.Submit(rid, buffer); + DelayEmit(ts, channel); - if (_game.Audio.Sound.State != SoundState.Playing) - _game.Audio.Sound.Play(); + if (channel.State != SoundState.Playing) + channel.Play(); } catch (Exception ex) { @@ -157,11 +166,11 @@ public class Audio : IComponent var freq = L.OptNumber(1, 440); var time = L.OptNumber(2, 1); + var timespan = TimeSpan.FromSeconds(time); + var volume = (float)L.OptNumber(3, 1); volume = Math.Clamp(volume, 0, 1); - var timespan = TimeSpan.FromSeconds(time); - var form = L.CheckOption(4, "sine", new string[] { "sine", @@ -172,37 +181,71 @@ public class Audio : IComponent null, }); - var buffer = _game.Audio.GenerateWave((Waveform)form, freq, timespan, volume); + var id = (int)L.OptInteger(5, -1); + + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushBoolean(false); + return 1; + } + + var buffer = _game.Audio.GenerateWave(channel, (Waveform)form, freq, timespan, volume); try { - var ts = _game.Audio.Submit(buffer); - DelayEmit(ts); + var ts = _game.Audio.Submit(rid, buffer); + DelayEmit(ts, channel); - if (_game.Audio.Sound.State != SoundState.Playing) - _game.Audio.Sound.Play(); + if (channel.State != SoundState.Playing) + channel.Play(); + + L.PushBoolean(true); } catch (Exception ex) { L.Error(ex.Message); } - return 0; + return 1; } private static int L_Resume(IntPtr state) { - _game.Audio.Sound.Resume(); + var L = Lua.FromIntPtr(state); + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushBoolean(false); + return 1; + } + + channel.Resume(); return 0; } private static int L_Pause(IntPtr state) { - _game.Audio.Sound.Pause(); + var L = Lua.FromIntPtr(state); + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushBoolean(false); + return 1; + } + + channel.Pause(); return 0; } private static int L_Stop(IntPtr state) { - _game.Audio.Sound.Stop(); + var L = Lua.FromIntPtr(state); + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushBoolean(false); + return 1; + } + + channel.Stop(); return 0; } @@ -211,7 +254,14 @@ public class Audio : IComponent { var L = Lua.FromIntPtr(state); - L.PushNumber(_game.Audio.Sound.Volume); + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushNil(); + return 1; + } + + L.PushNumber(channel.Volume); return 1; } @@ -219,10 +269,17 @@ public class Audio : IComponent { var L = Lua.FromIntPtr(state); - var volume = (float)L.CheckNumber(1); + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushNil(); + return 1; + } + + var volume = (float)L.CheckNumber(2); volume = Math.Clamp(volume, 0, 1); - _game.Audio.Sound.Volume = volume; + channel.Volume = volume; return 0; } @@ -231,7 +288,14 @@ public class Audio : IComponent { var L = Lua.FromIntPtr(state); - var status = _game.Audio.Sound.State switch + var id = (int)L.CheckInteger(1); + if (!_game.Audio.TryGetChannel(id, out var channel, out var rid)) + { + L.PushNil(); + return 1; + } + + var status = channel.State switch { SoundState.Playing => "playing", SoundState.Paused => "paused", @@ -246,6 +310,9 @@ public class Audio : IComponent private void OnClose(object sender, EventArgs e) { - _game.Audio.Sound.Stop(true); + foreach (var channel in _game.Audio.Channels) + { + channel.Stop(); + } } }