diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ac8c238 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Ryuclaw.Mac.sln" +} \ No newline at end of file diff --git a/Ryuclaw.Audio.Desktop/AbstractAudioSource.cs b/Ryuclaw.Audio.Desktop/AbstractAudioSource.cs new file mode 100644 index 0000000..ef3534f --- /dev/null +++ b/Ryuclaw.Audio.Desktop/AbstractAudioSource.cs @@ -0,0 +1,30 @@ +using System; +using System.Numerics; +using Ryuclaw.Shared.Audio; + +namespace Ryuclaw.Audio.Desktop +{ + public abstract class AbstractAudioSource : IAudioSource + { + private Vector3 _position { get; set; } + + public AbstractAudioSource() + { + } + + public abstract void Play(); + + public void SetPosition(Vector3 position) + { + _position = position; + } + + public abstract void Stop(); + + public void Update(float dt) + { + throw new NotImplementedException(); + } + } +} + diff --git a/Ryuclaw.Audio.Desktop/AudioCache.cs b/Ryuclaw.Audio.Desktop/AudioCache.cs new file mode 100644 index 0000000..02840fa --- /dev/null +++ b/Ryuclaw.Audio.Desktop/AudioCache.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Ryuclaw.Shared.Audio; +using Ryuclaw.Audio.Shared; + +namespace Ryuclaw.Audio.Desktop +{ + public class AudioCache : IAudioCache + { + private Dictionary _cache; + public AudioCache() + { + _cache = new(); + } + + public WaveReader Get(string name) + { + if (_cache.TryGetValue(name, out var reader)) + { + return reader; + } else + { + var newReader = new WaveReader(); + newReader.Load(name); + _cache[name] = newReader; + return newReader; + } + } + + public void Set(string name, IAudioSource source) + { + throw new NotImplementedException(); + } + + IAudioSource IAudioCache.Get(string name) + { + throw new NotImplementedException(); + } + } +} + diff --git a/Ryuclaw.Audio.Desktop/AudioEngine.cs b/Ryuclaw.Audio.Desktop/AudioEngine.cs new file mode 100644 index 0000000..177b133 --- /dev/null +++ b/Ryuclaw.Audio.Desktop/AudioEngine.cs @@ -0,0 +1,54 @@ +using System; +using Ryuclaw.Shared.Audio; +using OpenAL; +using System.Numerics; + +namespace Ryuclaw.Audio.Desktop; + +public class AudioEngine : IAudioEngine +{ + private readonly object _lock = new object(); + private IntPtr _context; + private IntPtr _device; + private AudioCache _cache; + + public AudioEngine() + { + _device = ALC10.alcOpenDevice(null); + _context = ALC10.alcCreateContext(_device, null); + + _cache = new AudioCache(); + } + + public IAudioSource CreateSource(string name) + { + throw new NotImplementedException(); + } + + public void SetListenerOrientation(Vector3 forward, Vector3 up) + { + throw new NotImplementedException(); + } + + public void SetListenerPosition(Vector3 position) + { + throw new NotImplementedException(); + } + + public bool Start() + { + ALC10.alcMakeContextCurrent(_context); + return true; + } + + public void Stop() + { + throw new NotImplementedException(); + } + + public void Update(float dt) + { + throw new NotImplementedException(); + } +} + diff --git a/Ryuclaw.Audio.Desktop/Class1.cs b/Ryuclaw.Audio.Desktop/Class1.cs deleted file mode 100644 index 0409fc1..0000000 --- a/Ryuclaw.Audio.Desktop/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ryuclaw.Audio.Desktop; - -public class Class1 -{ - -} - diff --git a/Ryuclaw.Audio.Desktop/MemoryAudioSource.cs b/Ryuclaw.Audio.Desktop/MemoryAudioSource.cs new file mode 100644 index 0000000..54b0c5b --- /dev/null +++ b/Ryuclaw.Audio.Desktop/MemoryAudioSource.cs @@ -0,0 +1,28 @@ +using System; +using Ryuclaw.Audio.Shared; + +namespace Ryuclaw.Audio.Desktop +{ + public class MemoryAudioSource : AbstractAudioSource + { + private WaveReader _reader; + + public MemoryAudioSource(string name) + { + _reader = new WaveReader(); + _reader.Load(name); + Console.WriteLine(_reader); + } + + public override void Play() + { + throw new NotImplementedException(); + } + + public override void Stop() + { + throw new NotImplementedException(); + } + } +} + diff --git a/Ryuclaw.Audio.Desktop/Ryuclaw.Audio.Desktop.csproj b/Ryuclaw.Audio.Desktop/Ryuclaw.Audio.Desktop.csproj index 2a2a25d..84473ca 100644 --- a/Ryuclaw.Audio.Desktop/Ryuclaw.Audio.Desktop.csproj +++ b/Ryuclaw.Audio.Desktop/Ryuclaw.Audio.Desktop.csproj @@ -8,5 +8,11 @@ + + + + + ..\..\..\code\OpenAL-CS\bin\Debug\netstandard2.0\OpenAL-CS.dll + diff --git a/Ryuclaw.Audio.Desktop/app.config b/Ryuclaw.Audio.Desktop/app.config new file mode 100644 index 0000000..bf7e384 --- /dev/null +++ b/Ryuclaw.Audio.Desktop/app.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Ryuclaw.Audio.Shared/Class1.cs b/Ryuclaw.Audio.Shared/Class1.cs new file mode 100644 index 0000000..cfc29b6 --- /dev/null +++ b/Ryuclaw.Audio.Shared/Class1.cs @@ -0,0 +1,7 @@ +namespace Ryuclaw.Audio.Shared; + +public class Class1 +{ + +} + diff --git a/Ryuclaw.Audio.Shared/Ryuclaw.Audio.Shared.csproj b/Ryuclaw.Audio.Shared/Ryuclaw.Audio.Shared.csproj new file mode 100644 index 0000000..4658cbf --- /dev/null +++ b/Ryuclaw.Audio.Shared/Ryuclaw.Audio.Shared.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/Ryuclaw.Audio.Shared/WaveHeader.cs b/Ryuclaw.Audio.Shared/WaveHeader.cs new file mode 100644 index 0000000..5554114 --- /dev/null +++ b/Ryuclaw.Audio.Shared/WaveHeader.cs @@ -0,0 +1,24 @@ +using System; +namespace Ryuclaw.Audio.Shared +{ + public class WaveHeader + { + public int SampleRate { get; private set; } + public int ChunkSize { get; private set; } + public int ByteRate { get; private set; } + public short BitsPerSample { get; private set; } + public short NumChannels { get; private set; } + public short BlockAlign { get; private set; } + + public WaveHeader(int sampleRate = 48000, int chunkSize = 16, int byteRate = 16, short bitsPerSample = 16, short numChannels = 1, short blockAlign = 8) + { + SampleRate = sampleRate; + ChunkSize = chunkSize; + ByteRate = byteRate; + BitsPerSample = bitsPerSample; + NumChannels = numChannels; + BlockAlign = blockAlign; + } + } +} + diff --git a/Ryuclaw.Audio.Shared/WaveReader.cs b/Ryuclaw.Audio.Shared/WaveReader.cs new file mode 100644 index 0000000..da3e98e --- /dev/null +++ b/Ryuclaw.Audio.Shared/WaveReader.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace Ryuclaw.Audio.Shared +{ + public class WaveReader + { + private WaveHeader _header; + public byte[] AudioData { get; private set; } + + public WaveReader() + { + _header = new WaveHeader(); + AudioData = new byte[0]; + } + + public void Load(string filePath) + { + using (var stream = new FileStream(filePath, FileMode.Open)) + using (var reader = new BinaryReader(stream)) + { + // RIFF header + string chunkID = new string(reader.ReadChars(4)); + if (chunkID != "RIFF") throw new Exception("Not a valid WAV file."); + + var ChunkSize = reader.ReadInt32(); // Chunk size + string format = new string(reader.ReadChars(4)); + if (format != "WAVE") throw new Exception("Not a valid WAV file."); + + // fmt sub-chunk + string subchunk1ID = new string(reader.ReadChars(4)); + if (subchunk1ID != "fmt ") throw new Exception("Expected fmt chunk."); + + int subChunk1Size = reader.ReadInt32(); + short audioFormat = reader.ReadInt16(); + var NumChannels = reader.ReadInt16(); + var SampleRate = reader.ReadInt32(); + var ByteRate = reader.ReadInt32(); // Byte rate + var BlockAlign = reader.ReadInt16(); // Block align + var BitsPerSample = reader.ReadInt16(); + _header = new WaveHeader(SampleRate, ChunkSize, ByteRate, BitsPerSample, NumChannels, BlockAlign); + if (audioFormat != 1 || subChunk1Size != 16) + { + throw new Exception("Only PCM format WAV files are supported."); + } + + // data sub-chunk + string subchunk2ID = new string(reader.ReadChars(4)); + if (subchunk2ID != "data") throw new Exception("Expected data chunk."); + + int subChunk2Size = reader.ReadInt32(); + AudioData = reader.ReadBytes(subChunk2Size); + } + } + } +} + diff --git a/Ryuclaw.Audio.Shared/WaveStreamer.cs b/Ryuclaw.Audio.Shared/WaveStreamer.cs new file mode 100644 index 0000000..aee7a8d --- /dev/null +++ b/Ryuclaw.Audio.Shared/WaveStreamer.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; + +namespace Ryuclaw.Audio.Shared +{ + public class WaveStreamer : IDisposable + { + private readonly FileStream _fileStream; + private readonly BinaryReader _reader; + + private long _dataPosition; // This will store the position where audio data begins in the file. + private int _dataLength; // This will store the length of the audio data. + + private WaveHeader _header; + + public WaveStreamer(string filePath) + { + _header = new WaveHeader(); + _fileStream = new FileStream(filePath, FileMode.Open); + _reader = new BinaryReader(_fileStream); + LoadHeader(); + } + + private void LoadHeader() + { + // RIFF header + string chunkID = new string(_reader.ReadChars(4)); + if (chunkID != "RIFF") throw new Exception("Not a valid WAV file."); + + var ChunkSize = _reader.ReadInt32(); // Chunk size + string format = new string(_reader.ReadChars(4)); + if (format != "WAVE") throw new Exception("Not a valid WAV file."); + + // fmt sub-chunk + string subchunk1ID = new string(_reader.ReadChars(4)); + if (subchunk1ID != "fmt ") throw new Exception("Expected fmt chunk."); + + int subChunk1Size = _reader.ReadInt32(); + short audioFormat = _reader.ReadInt16(); + var NumChannels = _reader.ReadInt16(); + var SampleRate = _reader.ReadInt32(); + var ByteRate = _reader.ReadInt32(); // Byte rate + var BlockAlign = _reader.ReadInt16(); // Block align + var BitsPerSample = _reader.ReadInt16(); + _header = new WaveHeader(SampleRate, ChunkSize, ByteRate, BitsPerSample, NumChannels, BlockAlign); + if (audioFormat != 1 || subChunk1Size != 16) + { + throw new Exception("Only PCM format WAV files are supported."); + } + + // data sub-chunk + string subchunk2ID = new string(_reader.ReadChars(4)); + if (subchunk2ID != "data") throw new Exception("Expected data chunk."); + + _dataLength = _reader.ReadInt32(); + _dataPosition = _fileStream.Position; // Store the position where audio data begins. + } + + public byte[] ReadData(int byteCount) + { + _fileStream.Seek(_dataPosition, SeekOrigin.Begin); + byte[] buffer = _reader.ReadBytes(byteCount); // Read a chunk of audio data. + _dataPosition = _fileStream.Position; // Update the position to start from next time. + return buffer; + } + + public bool HasDataLeft() + { + return _fileStream.Position < _dataPosition + _dataLength; + } + + public void Dispose() + { + _reader.Dispose(); + _fileStream.Dispose(); + } + } +} + diff --git a/Ryuclaw.Client.Mac/Ryuclaw.Client.Mac.csproj b/Ryuclaw.Client.Mac/Ryuclaw.Client.Mac.csproj index 8598972..74f5d34 100644 --- a/Ryuclaw.Client.Mac/Ryuclaw.Client.Mac.csproj +++ b/Ryuclaw.Client.Mac/Ryuclaw.Client.Mac.csproj @@ -9,13 +9,26 @@ false false + 4 + None false false + None + + + + + PreserveNewest + PreserveNewest + + + + diff --git a/Ryuclaw.Client.Mac/ViewController.cs b/Ryuclaw.Client.Mac/ViewController.cs index 5a4b694..6e7ede4 100644 --- a/Ryuclaw.Client.Mac/ViewController.cs +++ b/Ryuclaw.Client.Mac/ViewController.cs @@ -1,10 +1,13 @@ using ObjCRuntime; using Ryuclaw.Speech.MacOS; +using Ryuclaw.Audio.Desktop; namespace Ryuclaw.Client.Mac; public partial class ViewController : NSViewController { - private TTS synth; + private TTS synth = new TTS(); + private AudioEngine engine = new AudioEngine(); + protected ViewController (NativeHandle handle) : base (handle) { // This constructor is required if the view controller is loaded from a xib or a storyboard. @@ -16,8 +19,10 @@ public partial class ViewController : NSViewController { base.ViewDidLoad (); // Do any additional setup after loading the view. - synth = new TTS(); - synth.Speak("Rawr, world!"); + synth.SetVoice("Alex"); + synth.Speak("Rawr, rawr!"); + + engine.Start(); } public override NSObject RepresentedObject { diff --git a/Ryuclaw.Client.Mac/dylibs/soft_oal.dll.dylib b/Ryuclaw.Client.Mac/dylibs/soft_oal.dll.dylib new file mode 100755 index 0000000..0240a73 Binary files /dev/null and b/Ryuclaw.Client.Mac/dylibs/soft_oal.dll.dylib differ diff --git a/Ryuclaw.Mac.sln b/Ryuclaw.Mac.sln index 5d16a62..8029264 100644 --- a/Ryuclaw.Mac.sln +++ b/Ryuclaw.Mac.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryuclaw.Audio.Desktop", "Ry EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryuclaw.Client.Mac", "Ryuclaw.Client.Mac\Ryuclaw.Client.Mac.csproj", "{0C6AE29F-C44B-4876-ABE9-D36C79158BBD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryuclaw.Audio.Shared", "Ryuclaw.Audio.Shared\Ryuclaw.Audio.Shared.csproj", "{B03FE841-5714-43BA-9084-7259FFC78837}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {0C6AE29F-C44B-4876-ABE9-D36C79158BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C6AE29F-C44B-4876-ABE9-D36C79158BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C6AE29F-C44B-4876-ABE9-D36C79158BBD}.Release|Any CPU.Build.0 = Release|Any CPU + {B03FE841-5714-43BA-9084-7259FFC78837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B03FE841-5714-43BA-9084-7259FFC78837}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B03FE841-5714-43BA-9084-7259FFC78837}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B03FE841-5714-43BA-9084-7259FFC78837}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE