From 3e3693bc2ecf32e9b6817c4ad37ec32e698bf57e Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sat, 4 Feb 2023 11:41:12 +0100 Subject: [PATCH] Finish audio library --- Capy64/Assets/bios.lua | 3 + Capy64/Capy64.cs | 5 +- Capy64/Core/Audio.cs | 55 ++++++++++- Capy64/IGame.cs | 1 + Capy64/Runtime/Libraries/Audio.cs | 155 ++++++++++++++++++++++++++---- 5 files changed, 195 insertions(+), 24 deletions(-) diff --git a/Capy64/Assets/bios.lua b/Capy64/Assets/bios.lua index 30f097d..52696ba 100644 --- a/Capy64/Assets/bios.lua +++ b/Capy64/Assets/bios.lua @@ -3,6 +3,7 @@ local timer = require("timer") local gpu = require("gpu") local fs = require("fs") local machine = require("machine") +local audio = require("audio") local bootSleep = 2000 local bg = 0x0 @@ -145,6 +146,8 @@ local function bootScreen() end end +audio.beep(1000, 0.4, 0.2) + bootScreen() term.clear() \ No newline at end of file diff --git a/Capy64/Capy64.cs b/Capy64/Capy64.cs index 1c922cc..eade08e 100644 --- a/Capy64/Capy64.cs +++ b/Capy64/Capy64.cs @@ -18,7 +18,7 @@ namespace Capy64; public class Capy64 : Game, IGame { - public const string Version = "0.0.6-alpha"; + public const string Version = "0.0.7-alpha"; public static string AppDataPath = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData, @@ -32,6 +32,7 @@ public class Capy64 : Game, IGame public int Height { get; set; } = 300; public float Scale { get; set; } = 2f; public Drawing Drawing { get; private set; } + public Audio Audio { get; private set; } public LuaState LuaRuntime { get; set; } public Eventing.EventEmitter EventEmitter { get; private set; } public DiscordIntegration Discord { get; set; } @@ -131,6 +132,8 @@ public class Capy64 : Game, IGame Window.AllowUserResizing = true; Window.ClientSizeChanged += OnWindowSizeChange; + Audio = new Audio(); + NativePlugins = GetNativePlugins(); Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider); diff --git a/Capy64/Core/Audio.cs b/Capy64/Core/Audio.cs index 311cb00..a097d62 100644 --- a/Capy64/Core/Audio.cs +++ b/Capy64/Core/Audio.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,15 +9,59 @@ using System.Threading.Tasks; namespace Capy64.Core; -public class Audio +public class Audio : IDisposable { public const int SampleRate = 48000; - public static TimeSpan Play(byte[] buffer, out SoundEffect soundEffect) + public const AudioChannels Channels = AudioChannels.Mono; + public readonly DynamicSoundEffectInstance Sound; + public Audio() { - soundEffect = new SoundEffect(buffer, SampleRate, AudioChannels.Stereo); + Sound = new DynamicSoundEffectInstance(SampleRate, Channels); + } - soundEffect.Play(1, 0, 0); + public TimeSpan Submit(byte[] buffer) + { + Sound.SubmitBuffer(buffer); + return Sound.GetSampleDuration(buffer.Length); + } - return soundEffect.Duration; + public static byte[] GenerateSineWave(double frequency, double time, double volume = 1d) + { + var amplitude = 128 * volume; + var timeStep = 1d / SampleRate; + + var buffer = new byte[(int)(SampleRate * time)]; + var ctime = 0d; + for (int i = 0; i < buffer.Length; i++) + { + double angle = (Math.PI * frequency) * ctime; + double factor = 0.5 * (Math.Sin(angle) + 1.0); + buffer[i] = (byte)(amplitude * factor); + ctime += timeStep; + } + return buffer; + } + + public static byte[] GenerateSquareWave(double frequency, double time, double volume = 1d) + { + var amplitude = 128 * volume; + var timeStep = 1d / SampleRate; + + var buffer = new byte[(int)(SampleRate * time)]; + var ctime = 0d; + for (int i = 0; i < buffer.Length; i++) + { + double angle = (Math.PI * frequency) * ctime; + double factor = Math.Sin(angle); + buffer[i] = (byte)(factor >= 0 ? amplitude : 0); + ctime += timeStep; + } + return buffer; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + Sound.Dispose(); } } diff --git a/Capy64/IGame.cs b/Capy64/IGame.cs index fedda4a..9d30c67 100644 --- a/Capy64/IGame.cs +++ b/Capy64/IGame.cs @@ -16,6 +16,7 @@ public interface IGame IList Plugins { get; } GameWindow Window { get; } Drawing Drawing { get; } + Audio Audio { get; } LuaState LuaRuntime { get; set; } Eventing.EventEmitter EventEmitter { get; } void ConfigureServices(IServiceProvider serviceProvider); diff --git a/Capy64/Runtime/Libraries/Audio.cs b/Capy64/Runtime/Libraries/Audio.cs index 48e2995..15f677d 100644 --- a/Capy64/Runtime/Libraries/Audio.cs +++ b/Capy64/Runtime/Libraries/Audio.cs @@ -1,5 +1,7 @@ using Capy64.API; +using Capy64.Core; using KeraLua; +using Microsoft.Xna.Framework.Audio; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -29,6 +31,41 @@ public class Audio : IPlugin name = "play", function = L_Play, }, + new() + { + name = "beep", + function = L_Beep, + }, + new() + { + name = "resume", + function = L_Resume, + }, + new() + { + name = "pause", + function = L_Pause, + }, + new() + { + name = "stop", + function = L_Stop, + }, + new() + { + name = "getVolume", + function = L_GetVolume, + }, + new() + { + name = "setVolume", + function = L_SetVolume, + }, + new() + { + name = "status", + function = L_Status, + }, new(), }; @@ -48,13 +85,19 @@ public class Audio : IPlugin { try { - var time = Core.Audio.Play(buffer, out var soundEffect); - await Task.Delay(time - TimeSpan.FromMilliseconds(1000 / 60)); + var time = _game.Audio.Submit(buffer); + if (_game.Audio.Sound.State != Microsoft.Xna.Framework.Audio.SoundState.Playing) + { + _game.Audio.Sound.Play(); + } + var waitTime = time - TimeSpan.FromMilliseconds(1000 / 60); + if (waitTime.TotalMilliseconds < 0) + waitTime = time; + await Task.Delay(waitTime); _game.LuaRuntime.QueueEvent("audio_end", LK => { - soundEffect.Dispose(); - - return 0; + LK.PushInteger(_game.Audio.Sound.PendingBufferCount); + return 1; }); } @@ -64,13 +107,29 @@ public class Audio : IPlugin } } + private static void ProcessAudio() + { + if (isProcessing) + return; + + isProcessing = true; + Task.Run(async () => + { + while (queue.TryDequeue(out var buffer)) + { + await PlayAudio(buffer); + } + isProcessing = false; + }); + } + private static int L_Play(IntPtr state) { var L = Lua.FromIntPtr(state); var buffer = L.CheckBuffer(1); - if (queue.Count > queueLimit) + if (_game.Audio.Sound.PendingBufferCount > queueLimit) { L.PushBoolean(false); L.PushString("queue is full"); @@ -80,19 +139,79 @@ public class Audio : IPlugin queue.Enqueue(buffer); - if (!isProcessing) - { - isProcessing = true; - Task.Run(async () => - { - while (queue.TryDequeue(out var buffer)) - { - await PlayAudio(buffer); - } - isProcessing = false; - }); - } + ProcessAudio(); return 0; } + + private static int L_Beep(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var freq = L.CheckNumber(1); + var time = L.OptNumber(2, 1); + var volume = L.OptNumber(3, 1); + Math.Clamp(volume, 0, 1); + + var sine = Core.Audio.GenerateSquareWave(freq, time, volume); + + queue.Enqueue(sine); + + ProcessAudio(); + + return 0; + } + + private static int L_Resume(IntPtr state) + { + _game.Audio.Sound.Resume(); + return 0; + } + private static int L_Pause(IntPtr state) + { + _game.Audio.Sound.Pause(); + return 0; + } + private static int L_Stop(IntPtr state) + { + _game.Audio.Sound.Stop(); + return 0; + } + + private static int L_GetVolume(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + L.PushNumber(_game.Audio.Sound.Volume); + + return 1; + } + private static int L_SetVolume(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var volume = (float)L.CheckNumber(1); + volume = Math.Clamp(volume, 0, 1); + + _game.Audio.Sound.Volume = volume; + + return 0; + } + + private static int L_Status(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var status = _game.Audio.Sound.State switch + { + SoundState.Playing => "playing", + SoundState.Paused => "paused", + SoundState.Stopped => "stopped", + _ => "unknown", + }; + + L.PushString(status); + + return 1; + } }