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 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()

View file

@ -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);

View file

@ -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();
}
}

View file

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

View file

@ -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;
}
}