|
| 1 | +namespace KokoroSharp.Internal; |
| 2 | + |
| 3 | +using NAudio.Wave; |
| 4 | + |
| 5 | +using OpenTK.Audio.OpenAL; |
| 6 | + |
| 7 | +using System.Diagnostics; |
| 8 | + |
| 9 | +/// <summary> Base class for cross platform playback, with API compatible with NAudio's API. </summary> |
| 10 | +/// <remarks> Each platform (Windows/Linux/MacOS) derives from this to expose a nice interface back to KokoroSharp. </remarks> |
| 11 | +public abstract class KokoroWaveOutEvent { |
| 12 | + public RawSourceWaveStream stream; |
| 13 | + |
| 14 | + /// <summary> Playback state. </summary> |
| 15 | + public abstract PlaybackState PlaybackState { get; } |
| 16 | + |
| 17 | + /// <summary> Initializes the buffer with an audio stream. </summary> |
| 18 | + public void Init(RawSourceWaveStream stream) => this.stream = stream; |
| 19 | + |
| 20 | + /// <summary> Plays back the audio stream that was initialized with. </summary> |
| 21 | + public abstract void Play(); |
| 22 | + |
| 23 | + /// <summary> Immediately stops the playback. Does not delete the 'stream' though. </summary> |
| 24 | + public abstract void Stop(); |
| 25 | + |
| 26 | + /// <summary> Adjust the volume of the playback. [0.0, to 1.0] </summary> |
| 27 | + public abstract void SetVolume(float volume); |
| 28 | + |
| 29 | + /// <summary> Disposes the instance. </summary> |
| 30 | + public abstract void Dispose(); |
| 31 | + |
| 32 | + /// <summary> Gets the percentage of how much was played </summary> |
| 33 | + public virtual float CurrentPercentage => stream.Position / (float) stream.Length; |
| 34 | + |
| 35 | + /// <summary> Pause not supported for simplicity. </summary> |
| 36 | + public void Pause() => throw new NotImplementedException("We're not gonna support this."); |
| 37 | +} |
| 38 | + |
| 39 | +// A wrapper for NAudio's WaveOutEvent. |
| 40 | +public class WindowsAudioPlayer : KokoroWaveOutEvent { |
| 41 | + readonly WaveOutEvent waveOut = new(); |
| 42 | + public override PlaybackState PlaybackState => waveOut.PlaybackState; |
| 43 | + public override void Dispose() => waveOut.Dispose(); |
| 44 | + public override void Play() { waveOut.Init(stream); waveOut.Play(); } |
| 45 | + public override void SetVolume(float volume) => waveOut.Volume = volume; |
| 46 | + public override void Stop() => waveOut.Stop(); |
| 47 | +} |
| 48 | + |
| 49 | +public class MacOSAudioPlayer : LinuxAudioPlayer { } |
| 50 | + |
| 51 | +// Warning: Terrible, TERRIBLE code.. |
| 52 | +public class LinuxAudioPlayer : KokoroWaveOutEvent { |
| 53 | + public static int BufferSize = 4096 * 64; // Yes it's long. Could use help to optimize. |
| 54 | + public static int BufferCount = 256; // 64 MB. Devs can shorten it if needed. |
| 55 | + |
| 56 | + int source; |
| 57 | + int[] buffers; |
| 58 | + Thread streamThread; |
| 59 | + bool stopRequested; |
| 60 | + PlaybackState state = PlaybackState.Stopped; |
| 61 | + |
| 62 | + public override PlaybackState PlaybackState => state; |
| 63 | + |
| 64 | + // ATM it's joining and creating new thread each time. Not the best idea. |
| 65 | + public override void Play() { |
| 66 | + if (streamThread != null) { Stop(); } |
| 67 | + var device = ALC.OpenDevice(null); |
| 68 | + var context = ALC.CreateContext(device, (int[]) null); |
| 69 | + ALC.MakeContextCurrent(context); |
| 70 | + source = AL.GenSource(); |
| 71 | + buffers = AL.GenBuffers(BufferCount); |
| 72 | + stopRequested = false; |
| 73 | + |
| 74 | + // Initialize the buffer |
| 75 | + for (int i = 0; i < BufferCount; i++) { |
| 76 | + if (GetBufferFromStream() is not byte[] data) { break; } |
| 77 | + FillALBuffer(buffers[i], data); |
| 78 | + } |
| 79 | + AL.SourceQueueBuffers(source, buffers); |
| 80 | + AL.SourcePlay(source); |
| 81 | + state = PlaybackState.Playing; |
| 82 | + |
| 83 | + streamThread = new Thread(() => { |
| 84 | + AL.GetSource(source, ALGetSourcei.BuffersProcessed, out int processed); |
| 85 | + |
| 86 | + var sw = Stopwatch.StartNew(); |
| 87 | + while (processed-- > 0 && !stopRequested) { |
| 88 | + int buf = AL.SourceUnqueueBuffer(source); |
| 89 | + if (GetBufferFromStream() is not byte[] data) { break; } |
| 90 | + FillALBuffer(buf, data); |
| 91 | + AL.SourceQueueBuffer(source, buf); |
| 92 | + Thread.Sleep(10); |
| 93 | + } |
| 94 | + |
| 95 | + while (!stopRequested && AL.GetSource(source, ALGetSourcei.SourceState) == (int) ALSourceState.Playing) { |
| 96 | + stream.Position = (int) ((sw.ElapsedMilliseconds / 1000f) * stream.WaveFormat.AverageBytesPerSecond); |
| 97 | + Thread.Sleep(10); |
| 98 | + } |
| 99 | + if (!stopRequested) { stream.Position = stream.Length; } |
| 100 | + else { stream.Position = (int) ((sw.ElapsedMilliseconds / 1000f) * stream.WaveFormat.AverageBytesPerSecond); } |
| 101 | + |
| 102 | + state = PlaybackState.Stopped; |
| 103 | + }); |
| 104 | + streamThread.Start(); |
| 105 | + |
| 106 | + unsafe void FillALBuffer(int buffer, byte[] data) { fixed (byte* ptr = data) { AL.BufferData(buffer, ALFormat.Mono16, (IntPtr) ptr, data.Length, stream.WaveFormat.SampleRate); } } |
| 107 | + byte[] GetBufferFromStream() { |
| 108 | + var buffer = new byte[BufferSize]; |
| 109 | + int bytesRead = stream.Read(buffer, 0, BufferSize); |
| 110 | + if (bytesRead < BufferSize) { Array.Resize(ref buffer, bytesRead); } |
| 111 | + return bytesRead > 0 ? buffer : null; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + public override void Stop() => Dispose(); |
| 116 | + public override void SetVolume(float volume) => AL.Source(source, ALSourcef.Gain, Math.Clamp(volume, 0, 1f)); // Technically supports > 1 volume but not sure if it's a good idea. |
| 117 | + public override void Dispose() { |
| 118 | + AL.SourceStop(source); |
| 119 | + state = PlaybackState.Stopped; |
| 120 | + stopRequested = true; |
| 121 | + streamThread?.Join(); |
| 122 | + streamThread = null; |
| 123 | + AL.DeleteSource(source); |
| 124 | + AL.DeleteBuffers(buffers); |
| 125 | + var context = ALC.GetCurrentContext(); |
| 126 | + var device = ALC.GetContextsDevice(context); |
| 127 | + ALC.DestroyContext(context); |
| 128 | + ALC.CloseDevice(device); |
| 129 | + } |
| 130 | +} |
0 commit comments