Finish audio library

This commit is contained in:
Alessandro Proto 2023-02-04 11:41:12 +01:00
parent bf6424c0d1
commit 3e3693bc2e
5 changed files with 195 additions and 24 deletions

View file

@ -3,6 +3,7 @@ local timer = require("timer")
local gpu = require("gpu") local gpu = require("gpu")
local fs = require("fs") local fs = require("fs")
local machine = require("machine") local machine = require("machine")
local audio = require("audio")
local bootSleep = 2000 local bootSleep = 2000
local bg = 0x0 local bg = 0x0
@ -145,6 +146,8 @@ local function bootScreen()
end end
end end
audio.beep(1000, 0.4, 0.2)
bootScreen() bootScreen()
term.clear() term.clear()

View file

@ -18,7 +18,7 @@ namespace Capy64;
public class Capy64 : Game, IGame 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( public static string AppDataPath = Path.Combine(
Environment.GetFolderPath( Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData, Environment.SpecialFolder.ApplicationData,
@ -32,6 +32,7 @@ public class Capy64 : Game, IGame
public int Height { get; set; } = 300; public int Height { get; set; } = 300;
public float Scale { get; set; } = 2f; public float Scale { get; set; } = 2f;
public Drawing Drawing { get; private set; } public Drawing Drawing { get; private set; }
public Audio Audio { get; private set; }
public LuaState LuaRuntime { get; set; } public LuaState LuaRuntime { get; set; }
public Eventing.EventEmitter EventEmitter { get; private set; } public Eventing.EventEmitter EventEmitter { get; private set; }
public DiscordIntegration Discord { get; set; } public DiscordIntegration Discord { get; set; }
@ -131,6 +132,8 @@ public class Capy64 : Game, IGame
Window.AllowUserResizing = true; Window.AllowUserResizing = true;
Window.ClientSizeChanged += OnWindowSizeChange; Window.ClientSizeChanged += OnWindowSizeChange;
Audio = new Audio();
NativePlugins = GetNativePlugins(); NativePlugins = GetNativePlugins();
Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider); Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider);

View file

@ -1,4 +1,5 @@
using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -8,15 +9,59 @@ using System.Threading.Tasks;
namespace Capy64.Core; namespace Capy64.Core;
public class Audio public class Audio : IDisposable
{ {
public const int SampleRate = 48000; 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();
} }
} }

View file

@ -16,6 +16,7 @@ public interface IGame
IList<IPlugin> Plugins { get; } IList<IPlugin> Plugins { get; }
GameWindow Window { get; } GameWindow Window { get; }
Drawing Drawing { get; } Drawing Drawing { get; }
Audio Audio { get; }
LuaState LuaRuntime { get; set; } LuaState LuaRuntime { get; set; }
Eventing.EventEmitter EventEmitter { get; } Eventing.EventEmitter EventEmitter { get; }
void ConfigureServices(IServiceProvider serviceProvider); void ConfigureServices(IServiceProvider serviceProvider);

View file

@ -1,5 +1,7 @@
using Capy64.API; using Capy64.API;
using Capy64.Core;
using KeraLua; using KeraLua;
using Microsoft.Xna.Framework.Audio;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -29,6 +31,41 @@ public class Audio : IPlugin
name = "play", name = "play",
function = L_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(), new(),
}; };
@ -48,13 +85,19 @@ public class Audio : IPlugin
{ {
try try
{ {
var time = Core.Audio.Play(buffer, out var soundEffect); var time = _game.Audio.Submit(buffer);
await Task.Delay(time - TimeSpan.FromMilliseconds(1000 / 60)); 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 => _game.LuaRuntime.QueueEvent("audio_end", LK =>
{ {
soundEffect.Dispose(); LK.PushInteger(_game.Audio.Sound.PendingBufferCount);
return 1;
return 0;
}); });
} }
@ -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) private static int L_Play(IntPtr state)
{ {
var L = Lua.FromIntPtr(state); var L = Lua.FromIntPtr(state);
var buffer = L.CheckBuffer(1); var buffer = L.CheckBuffer(1);
if (queue.Count > queueLimit) if (_game.Audio.Sound.PendingBufferCount > queueLimit)
{ {
L.PushBoolean(false); L.PushBoolean(false);
L.PushString("queue is full"); L.PushString("queue is full");
@ -80,19 +139,79 @@ public class Audio : IPlugin
queue.Enqueue(buffer); queue.Enqueue(buffer);
if (!isProcessing) ProcessAudio();
{
isProcessing = true;
Task.Run(async () =>
{
while (queue.TryDequeue(out var buffer))
{
await PlayAudio(buffer);
}
isProcessing = false;
});
}
return 0; 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;
}
} }