diff --git a/OpenAI.sln b/OpenAI.sln index 968679d1..f9cd1a08 100755 --- a/OpenAI.sln +++ b/OpenAI.sln @@ -42,6 +42,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerateDocs", "src\helpers\GenerateDocs\GenerateDocs.csproj", "{ECC219F0-209A-412B-ADEC-6D97AB379E7C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Microphone", "src\libs\OpenAI.Microphone\OpenAI.Microphone.csproj", "{C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.CLI", "src\libs\OpenAI.CLI\OpenAI.CLI.csproj", "{AAD812B2-77D6-42E0-A3FA-432BD6A73791}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,14 @@ Global {ECC219F0-209A-412B-ADEC-6D97AB379E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECC219F0-209A-412B-ADEC-6D97AB379E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECC219F0-209A-412B-ADEC-6D97AB379E7C}.Release|Any CPU.Build.0 = Release|Any CPU + {C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5}.Release|Any CPU.Build.0 = Release|Any CPU + {AAD812B2-77D6-42E0-A3FA-432BD6A73791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAD812B2-77D6-42E0-A3FA-432BD6A73791}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAD812B2-77D6-42E0-A3FA-432BD6A73791}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAD812B2-77D6-42E0-A3FA-432BD6A73791}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -79,6 +91,8 @@ Global {A3F06E45-DFA8-4236-BFF5-425091762548} = {AAA11B78-2764-4520-A97E-46AA7089A588} {2D8B78DE-7269-417B-9D0B-8981FA513ACB} = {E793AF18-4371-4EBD-96FC-195EB1798855} {ECC219F0-209A-412B-ADEC-6D97AB379E7C} = {1A008ECD-2300-4BE4-A302-49DDF8BE0D54} + {C008F8CF-1A0E-4FE5-A5E1-5672D3B3A1E5} = {61E7E11E-4558-434C-ACE8-06316A3097B3} + {AAD812B2-77D6-42E0-A3FA-432BD6A73791} = {61E7E11E-4558-434C-ACE8-06316A3097B3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CED9A020-DBA5-4BE6-8096-75E528648EC1} diff --git a/src/libs/OpenAI.CLI/Commands/VoiceCommand.cs b/src/libs/OpenAI.CLI/Commands/VoiceCommand.cs new file mode 100644 index 00000000..45213dab --- /dev/null +++ b/src/libs/OpenAI.CLI/Commands/VoiceCommand.cs @@ -0,0 +1,28 @@ +using System.CommandLine; + +namespace OpenAI.CLI.Commands; + +/// +/// +/// +internal sealed class VoiceCommand : Command +{ + public VoiceCommand() : base(name: "voice", description: "Generates client sdk using a OpenAPI spec.") + { + var inputOption = new Argument( + name: "input", + getDefaultValue: () => string.Empty, + description: "Input file path"); + AddArgument(inputOption); + + this.SetHandler( + HandleAsync, + inputOption); + } + + private static async Task HandleAsync( + string input) + { + Console.WriteLine("Done."); + } +} \ No newline at end of file diff --git a/src/libs/OpenAI.CLI/OpenAI.CLI.csproj b/src/libs/OpenAI.CLI/OpenAI.CLI.csproj new file mode 100644 index 00000000..6fdb4d52 --- /dev/null +++ b/src/libs/OpenAI.CLI/OpenAI.CLI.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + $(NoWarn) + + + + tryAGI.OpenAI.CLI + true + openai + Advanced Voice from command line + openai;advanced-voice;ai;realtime;api;microphone + + + + + + + + + + + diff --git a/src/libs/OpenAI.CLI/Program.cs b/src/libs/OpenAI.CLI/Program.cs new file mode 100644 index 00000000..a43cd4db --- /dev/null +++ b/src/libs/OpenAI.CLI/Program.cs @@ -0,0 +1,8 @@ +using System.CommandLine; +using OpenAI.CLI.Commands; + +var rootCommand = new RootCommand( + description: "CLI tool to use AutoSDK"); +rootCommand.AddCommand(new VoiceCommand()); + +return await rootCommand.InvokeAsync(args).ConfigureAwait(false); \ No newline at end of file diff --git a/src/libs/OpenAI.CLI/Properties/launchSettings.json b/src/libs/OpenAI.CLI/Properties/launchSettings.json new file mode 100644 index 00000000..492768f8 --- /dev/null +++ b/src/libs/OpenAI.CLI/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FixOpenApiSpec": { + "commandName": "Project", + "commandLineArgs": "" + } + } +} \ No newline at end of file diff --git a/src/libs/OpenAI.Microphone/Microphone.cs b/src/libs/OpenAI.Microphone/Microphone.cs new file mode 100644 index 00000000..a8bc5f31 --- /dev/null +++ b/src/libs/OpenAI.Microphone/Microphone.cs @@ -0,0 +1,206 @@ +using System.Runtime.InteropServices; +using PortAudioSharp; + +namespace OpenAI; + +/// +/// Implements a Microphone using PortAudio +/// +public sealed class Microphone : IDisposable +{ + /// + /// + /// + static Microphone() + { + PortAudio.Initialize(); + } + + /// + /// + /// + public static void Terminate() + { + PortAudio.Terminate(); + } + + /// + /// + /// + public Action? PushCallback { get; set; } + + /// + /// Sample rate + /// + public int SampleRate { get; set; } = 48_000; + + /// + /// Chunk size + /// + public int ChunkSize { get; set; } = 32_000; + + /// + /// Number of channels + /// + public int Channels { get; set; } = 1; + + /// + /// Input device index + /// + public int DeviceIndex { get; set; } = PortAudio.NoDevice; + + /// + /// Sample Format + /// + public SampleFormat SampleFormat { get; set; } = SampleFormat.Int16; + + /// + /// + /// + public bool IsMuted { get; private set; } + + private PortAudioSharp.Stream? _stream; + private CancellationTokenSource? _exitToken; + + /// + /// Start begins the listening on the microphone + /// + /// + public bool Start() + { + if (_stream != null) + { + return false; + } + + // reset exit token + _exitToken?.Dispose(); + _exitToken = new CancellationTokenSource(); + + // Get the device info + if (DeviceIndex == PortAudio.NoDevice) + { + DeviceIndex = PortAudio.DefaultInputDevice; + if (DeviceIndex == PortAudio.NoDevice) + { + return false; + } + } + + DeviceInfo info = PortAudio.GetDeviceInfo(DeviceIndex); + + // Create the stream + _stream = new PortAudioSharp.Stream( + inParams: new StreamParameters + { + device = DeviceIndex, + channelCount = Channels, + sampleFormat = SampleFormat, + suggestedLatency = info.defaultLowInputLatency, + hostApiSpecificStreamInfo = IntPtr.Zero, + }, + outParams: null, + sampleRate: SampleRate, + framesPerBuffer: (uint)ChunkSize, + streamFlags: StreamFlags.ClipOff, + callback: Сallback, + userData: IntPtr.Zero + ); + + // Start the stream + _stream.Start(); + return true; + } + + /// + /// + /// + public void Stop() + { + // Check if we have a stream + if (_stream == null) + { + return; + } + + // signal stop + _exitToken?.Cancel(); + + // Stop the stream + _stream.Stop(); + + _stream.Dispose(); + _stream = null; + _exitToken?.Dispose(); + _exitToken = null; + } + + private StreamCallbackResult Сallback( + nint input, + nint output, + uint frameCount, + ref StreamCallbackTimeInfo timeInfo, + StreamCallbackFlags statusFlags, + nint userDataPtr) + { + // Check if the input is null + if (input == IntPtr.Zero) + { + return StreamCallbackResult.Continue; + } + + // Check if the exit token is set + if (_exitToken is { IsCancellationRequested: true }) + { + return StreamCallbackResult.Abort; + } + + // copy and send the data + byte[] buf = new byte[frameCount * sizeof(Int16)]; + + if (IsMuted) + { + buf = new byte[buf.Length]; + } + else + { + Marshal.Copy(input, buf, 0, buf.Length); + } + + PushCallback?.Invoke(buf, buf.Length); + + return StreamCallbackResult.Continue; + } + + /// + /// + /// + public void Mute() + { + if (_stream != null) + { + return; + } + + IsMuted = true; + } + + /// + /// + /// + public void Unmute() + { + if (_stream != null) + { + return; + } + + IsMuted = false; + } + + /// + public void Dispose() + { + Stop(); + } +} diff --git a/src/libs/OpenAI.Microphone/OpenAI.Microphone.csproj b/src/libs/OpenAI.Microphone/OpenAI.Microphone.csproj new file mode 100644 index 00000000..ef044d78 --- /dev/null +++ b/src/libs/OpenAI.Microphone/OpenAI.Microphone.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0;net8.0 + + + + + <_Parameter1>false + + + + + tryAGI.OpenAI.Microphone + Provides Microphone access + openai;advanced-voice;ai;realtime;api;microphone + false + + + + + + + \ No newline at end of file diff --git a/src/tests/OpenAI.IntegrationTests/OpenAI.IntegrationTests.csproj b/src/tests/OpenAI.IntegrationTests/OpenAI.IntegrationTests.csproj index 04109c6f..55163185 100644 --- a/src/tests/OpenAI.IntegrationTests/OpenAI.IntegrationTests.csproj +++ b/src/tests/OpenAI.IntegrationTests/OpenAI.IntegrationTests.csproj @@ -19,6 +19,7 @@ + @@ -30,6 +31,7 @@ + diff --git a/src/tests/OpenAI.IntegrationTests/Tests.Realtime.cs b/src/tests/OpenAI.IntegrationTests/Tests.Realtime.cs index 4090ae5f..28861554 100755 --- a/src/tests/OpenAI.IntegrationTests/Tests.Realtime.cs +++ b/src/tests/OpenAI.IntegrationTests/Tests.Realtime.cs @@ -2,6 +2,25 @@ namespace OpenAI.IntegrationTests; public partial class Tests { + [Test] + [Explicit] + public async Task Microphone() + { + using var microphone = new Microphone(); + microphone.PushCallback = (bytes, count) => + { + Console.WriteLine($"Pushed {count} bytes."); + }; + + microphone.Start(); + + await Task.Delay(TimeSpan.FromSeconds(15)); + + microphone.Stop(); + + OpenAI.Microphone.Terminate(); + } + [Test] [Explicit] public async Task Realtime()