mirror of
https://github.com/Ale32bit/Capy64.git
synced 2025-01-18 18:46:43 +00:00
Finish audio library
This commit is contained in:
parent
bf6424c0d1
commit
3e3693bc2e
5 changed files with 195 additions and 24 deletions
|
@ -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()
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue