Skip to content

Commit

Permalink
Merge pull request #26 from SineStriker/master
Browse files Browse the repository at this point in the history
Add SDL2 Playback Engine
  • Loading branch information
LiuYunPlayer authored Jun 7, 2024
2 parents 707291f + 247af59 commit 50a8930
Show file tree
Hide file tree
Showing 7 changed files with 722 additions and 1 deletion.
3 changes: 2 additions & 1 deletion TuneLab/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using TuneLab.GUI;
using TuneLab.Extensions.Voices;
using TuneLab.Audio.NAudio;
using TuneLab.Audio.SDL2;

namespace TuneLab;

Expand Down Expand Up @@ -48,7 +49,7 @@ public override void OnFrameworkInitializationCompleted()
};

AudioUtils.Init(new NAudioCodec());
AudioEngine.Init(new NAudioEngine());
AudioEngine.Init(new SDLAudioEngine());
ExtensionManager.LoadExtensions();
desktop.MainWindow = new MainWindow();
}
Expand Down
213 changes: 213 additions & 0 deletions TuneLab/Audio/SDL2/SDLAudioEngine.cs
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;
}
}
}
40 changes: 40 additions & 0 deletions TuneLab/Audio/SDL2/SDLGlobal.cs
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];
}
}
}
86 changes: 86 additions & 0 deletions TuneLab/Audio/SDL2/SDLHost.cs
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();
}
}
Loading

0 comments on commit 50a8930

Please sign in to comment.