-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from SineStriker/master
Add SDL2 Playback Engine
- Loading branch information
Showing
7 changed files
with
722 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using NAudio.Wave; | ||
using SDL2; | ||
|
||
namespace TuneLab.Audio.SDL2; | ||
|
||
internal class SDLAudioEngine : IAudioEngine | ||
{ | ||
public event Action? PlayStateChanged; | ||
public event Action? ProgressChanged; | ||
|
||
public bool IsPlaying => _d.state == SDLPlaybackData.PlaybackState.Playing; | ||
|
||
public int SamplingRate => 44100; | ||
|
||
public double CurrentTime => (double)(_position) / SamplingRate; | ||
|
||
public void Init(IAudioProcessor processor) | ||
{ | ||
var context = SynchronizationContext.Current; | ||
if (context == null) | ||
{ | ||
throw new Exception("Can't get SynchronizationContext"); | ||
} | ||
|
||
_audioProcessor = processor; | ||
|
||
// Begin initialize SDL | ||
{ | ||
// 初始化SDL引擎,有备无患 | ||
_ = SDLHost.InitHost(); | ||
|
||
// 创建私有结构 | ||
_d = new SDLPlaybackData(); | ||
|
||
// 转发事件 | ||
_d.devChanged = (newVal, oldVal) => | ||
{ | ||
context.Post(_ => | ||
{ | ||
// ... | ||
}, null); | ||
Console.WriteLine($"SDLPLayback: Audio device change to {newVal}."); | ||
}; | ||
_d.stateChanged = (newVal, oldVal) => | ||
{ | ||
context.Post(_ => | ||
{ | ||
PlayStateChanged?.Invoke(); // | ||
}, null); | ||
Console.WriteLine($"SDLPLayback: Play state change to {newVal}."); | ||
}; | ||
|
||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||
{ | ||
_d.setDriver("directsound"); | ||
} | ||
|
||
// 创建 sample provider | ||
var sampleProvider = new SampleProvider(this); | ||
|
||
// 创建音频结构体 | ||
_d.spec.freq = SamplingRate; | ||
_d.spec.format = SDLGlobal.PLAYBACK_FORMAT; | ||
_d.spec.channels = (byte)sampleProvider.WaveFormat.Channels; | ||
_d.spec.silence = 0; | ||
|
||
_d.sampleProvider = sampleProvider; | ||
_d.setDevId(0); | ||
|
||
// 打开默认音频设备延迟到播放操作时 | ||
} | ||
// End initialize SDL | ||
|
||
System.Timers.Timer timer = new(16); | ||
timer.Elapsed += (s, e) => | ||
{ | ||
context.Post(_ => | ||
{ | ||
if (IsPlaying) | ||
{ | ||
ProgressChanged?.Invoke(); | ||
} | ||
}, null); | ||
}; | ||
timer.Start(); | ||
} | ||
|
||
public void Destroy() | ||
{ | ||
// 先关闭音频 | ||
Pause(); | ||
|
||
_d.sampleProvider = null; | ||
_d.setDevId(0); | ||
|
||
_d.setDriver(""); | ||
} | ||
|
||
public void Play() | ||
{ | ||
Console.WriteLine("Play"); | ||
if (IsPlaying) | ||
{ | ||
return; | ||
} | ||
|
||
// 如果没有打开音频设备那么打开第一个音频设备 | ||
if (_d.curDevId == 0) | ||
{ | ||
SwitchDevice(0); | ||
} | ||
|
||
_d.start(); | ||
} | ||
|
||
public void Pause() | ||
{ | ||
if (!IsPlaying) | ||
{ | ||
return; | ||
} | ||
|
||
_d.stop(); | ||
} | ||
|
||
public void Seek(double time) | ||
{ | ||
_position = (int)(time * SamplingRate); | ||
} | ||
|
||
public void SwitchDevice(int deviceNumber) | ||
{ | ||
if (_d.state == SDLPlaybackData.PlaybackState.Playing) | ||
{ | ||
Console.WriteLine("SDLPlayback: Don't change audio device when playing."); | ||
return; | ||
} | ||
|
||
_d.devNum = deviceNumber; | ||
|
||
// 打开音频设备 | ||
uint id; | ||
if ((id = SDL.SDL_OpenAudioDevice( | ||
SDL.SDL_GetAudioDeviceName(_d.devNum, 0), | ||
0, | ||
ref _d.spec, | ||
out _, | ||
0)) == 0) | ||
{ | ||
throw new IOException($"SDLPlayback: Failed to open audio device: {SDL.SDL_GetError()}."); | ||
} | ||
|
||
Console.WriteLine($"SDLPlayback: {SDL.SDL_GetAudioDeviceName(_d.devNum, 0)}"); | ||
|
||
_d.setDevId(id); | ||
} | ||
|
||
public List<string> GetDrivers() | ||
{ | ||
var res = new List<string>(); | ||
int cnt = SDL.SDL_GetNumAudioDrivers(); | ||
for (int i = 0; i < cnt; i++) | ||
{ | ||
var dev = SDL.SDL_GetAudioDriver(i); | ||
if (dev == "dummy" || dev == "disk") | ||
{ | ||
continue; | ||
} | ||
|
||
res.Add(dev); | ||
} | ||
|
||
return res; | ||
} | ||
|
||
public void SwitchDriver(string driver) | ||
{ | ||
_d.setDriver(driver); | ||
} | ||
|
||
// 成员变量 | ||
IAudioProcessor? _audioProcessor; | ||
int _position = 0; | ||
|
||
// SDL 相关 | ||
SDLPlaybackData _d; | ||
|
||
class SampleProvider(SDLAudioEngine engine) : ISampleProvider | ||
{ | ||
public WaveFormat WaveFormat => WaveFormat.CreateIeeeFloatWaveFormat(engine.SamplingRate, 2); | ||
|
||
public int Read(float[] buffer, int offset, int count) | ||
{ | ||
int position = engine._position; | ||
int length = count / 2; | ||
|
||
engine._position += length; | ||
|
||
for (int i = offset; i < offset + count; i++) | ||
{ | ||
buffer[i] = 0; | ||
} | ||
|
||
engine._audioProcessor?.ProcessBlock(buffer, offset, position, length); | ||
return count; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
using System; | ||
using SDL2; | ||
|
||
namespace TuneLab.Audio.SDL2; | ||
|
||
internal static class SDLGlobal | ||
{ | ||
// 全局配置信息 | ||
public static readonly ushort PLAYBACK_FORMAT = SDL.AUDIO_F32SYS; // 32位浮点 | ||
|
||
public static readonly ushort PLAYBACK_BUFFER_SAMPLES = 1024; // 默认缓冲区长度 | ||
|
||
public static readonly uint PLAYBACK_POLL_INTERVAL = 1; // 轮循时间间隔(ms) | ||
|
||
// 用户事件 | ||
public enum UserEvent | ||
{ | ||
SDL_EVENT_BUFFER_END = (int)SDL.SDL_EventType.SDL_USEREVENT + 1, | ||
SDL_EVENT_MANUAL_STOP, | ||
} | ||
|
||
public delegate void ValueChangeEvent<T>(T newVal, T orgVal); | ||
|
||
public static void FloatsToBytes(float[] floats, byte[] bytes, int size) | ||
{ | ||
for (int i = 0; i < size; i++) | ||
{ | ||
if (i >= floats.Length || i * 4 + 3 >= bytes.Length) | ||
{ | ||
break; | ||
} | ||
|
||
var b = BitConverter.GetBytes(floats[i]); | ||
bytes[i * 4] = b[0]; | ||
bytes[i * 4 + 1] = b[1]; | ||
bytes[i * 4 + 2] = b[2]; | ||
bytes[i * 4 + 3] = b[3]; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
using System; | ||
using System.ComponentModel; | ||
using System.Runtime.InteropServices; | ||
using SDL2; | ||
|
||
namespace TuneLab.Audio.SDL2; | ||
|
||
internal class SDLHost | ||
{ | ||
private static SDLHost self = null; | ||
|
||
// 初始化 | ||
public static SDLHost InitHost() | ||
{ | ||
return Instance; | ||
} | ||
|
||
// 反初始化 | ||
public static void QuitHost() | ||
{ | ||
if (self == null) | ||
{ | ||
return; | ||
} | ||
|
||
self = null; | ||
GC.Collect(); // 强制删除 | ||
} | ||
|
||
// 构造函数 | ||
public SDLHost() | ||
{ | ||
// 初始化单例 | ||
if (self != null) | ||
{ | ||
throw new Exception("SDLHost: Duplicated SDL Host."); | ||
} | ||
|
||
self = this; | ||
|
||
// 设置输出级别 | ||
SDL.SDL_LogSetPriority( | ||
(int)SDL.SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION, | ||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_INFO | ||
); | ||
|
||
// 初始化 | ||
if (SDL.SDL_Init(SDL.SDL_INIT_AUDIO) < 0) | ||
{ | ||
throw new Exception($"SDLHost: Failed to initialize SDL: {SDL.SDL_GetError()}."); | ||
} | ||
} | ||
|
||
// 析构函数 | ||
~SDLHost() | ||
{ | ||
SDL.SDL_Quit(); | ||
} | ||
|
||
public static SDLHost Instance => self == null ? self = new SDLHost() : self; | ||
|
||
// 获取版本号 | ||
public int[] GetVersion() | ||
{ | ||
SDL.SDL_GetVersion(out var ver); | ||
return new int[] { ver.major, ver.minor, ver.patch }; | ||
} | ||
|
||
// 获取输出设备数 | ||
public int NumOutputDevices() | ||
{ | ||
return SDL.SDL_GetNumAudioDevices(0); | ||
} | ||
|
||
// 获取输入设备数 | ||
public int NumInputDevices() | ||
{ | ||
return SDL.SDL_GetNumAudioDevices(1); | ||
} | ||
|
||
// 获取输出设备 | ||
public int NumAudioDrivers() | ||
{ | ||
return SDL.SDL_GetNumAudioDrivers(); | ||
} | ||
} |
Oops, something went wrong.