├── .gitignore ├── .gitmodules ├── LICENSE.md ├── LoudPizza.Mp3 ├── LoudPizza.Mp3.csproj ├── Mp3Stream.cs └── Mp3StreamInstance.cs ├── LoudPizza.TestApp ├── LoudPizza.TestApp.csproj ├── Program.cs ├── Sdl2AudioBackend.cs ├── SdlAudioUtil.cs └── WaveWriter.cs ├── LoudPizza.Vorbis ├── LoudPizza.Vorbis.csproj └── VorbisAudioStream.cs ├── LoudPizza.sln └── LoudPizza ├── AudioSeekFlags.cs ├── Core ├── AlignedFloatBuffer.cs ├── Buffer256.cs ├── ChannelBuffer.cs ├── FFT.cs ├── Fader.cs ├── Handle.cs ├── SoLoud.3d.cs ├── SoLoud.BasicOps.cs ├── SoLoud.FaderOps.cs ├── SoLoud.FilterOps.cs ├── SoLoud.Getters.cs ├── SoLoud.Setters.cs ├── SoLoud.VoiceGroup.cs ├── SoLoud.VoiceOps.cs ├── SoLoud.cs └── TinyAlignedFloatBuffer.cs ├── Handles ├── SoLoudHandle.3D.cs ├── SoLoudHandle.BasicOps.cs ├── SoLoudHandle.FaderOps.cs ├── SoLoudHandle.FilterOps.cs ├── SoLoudHandle.Getters.cs ├── SoLoudHandle.Setters.cs ├── SoLoudHandle.VoiceGroup.cs ├── SoLoudHandle.cs ├── VoiceHandle.3D.cs ├── VoiceHandle.FaderOps.cs ├── VoiceHandle.FilterOps.cs ├── VoiceHandle.Getters.cs ├── VoiceHandle.Setters.cs ├── VoiceHandle.VoiceGroup.cs └── VoiceHandle.cs ├── LoudPizza.csproj ├── Mat3.cs ├── Modifiers ├── AudioAttenuator.cs ├── AudioCollider.cs ├── AudioFilter.cs ├── AudioFilterInstance.cs ├── AudioResampler.cs ├── CatmullRomAudioResampler.cs ├── ExponentialDistanceAudioAttenuator.cs ├── InverseDistanceAudioAttenuator.cs ├── LinearAudioResampler.cs ├── LinearDistanceAudioAttenuator.cs └── PointAudioResampler.cs ├── SoLoudStatus.cs ├── Sources ├── AudioBuffer.cs ├── AudioBufferInstance.cs ├── AudioBus.cs ├── AudioBusInstance.cs ├── AudioQueue.cs ├── AudioQueueInstance.cs ├── AudioSource.cs ├── AudioSourceInstance.cs ├── AudioSourceInstance3dData.cs ├── AudioStream.cs ├── AudioStreamInstance.cs ├── IAudioBus.cs ├── IAudioStream.cs └── Streaming │ ├── AudioStreamer.AudioBuffer.cs │ ├── AudioStreamer.ReadWorker.cs │ ├── AudioStreamer.SeekToken.cs │ ├── AudioStreamer.SeekWorker.cs │ ├── AudioStreamer.StreamHolder.cs │ ├── AudioStreamer.Worker.cs │ ├── AudioStreamer.cs │ ├── IRelativePlaybackRateChangeListener.cs │ └── StreamedAudioStream.cs ├── Time.cs └── Vector3Extensions.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "NLayer"] 2 | path = NLayer 3 | url = https://github.com/TechnologicalPizza/NLayer 4 | [submodule "NVorbis"] 5 | path = NVorbis 6 | url = https://github.com/TechnologicalPizza/NVorbis 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | LoudPizza audio engine 4 | Port of SoLoud 5 | 6 | Copyright (c) 2021 TechPizza 7 | 8 | This software is provided 'as-is', without any express or implied 9 | warranty. In no event will the authors be held liable for any damages 10 | arising from the use of this software. 11 | 12 | Permission is granted to anyone to use this software for any purpose, 13 | including commercial applications, and to alter it and redistribute it 14 | freely, subject to the following restrictions: 15 | 16 | 1. The origin of this software must not be misrepresented; you must not 17 | claim that you wrote the original software. If you use this software 18 | in a product, an acknowledgment in the product documentation would be 19 | appreciated but is not required. 20 | 21 | 2. Altered source versions must be plainly marked as such, and must not be 22 | misrepresented as being the original software. 23 | 24 | 3. This notice may not be removed or altered from any source 25 | distribution. 26 | 27 | # 28 | 29 | SoLoud contains various third party libraries which vary in licenses, 30 | but are all extremely liberal; no attribution in binary form is required. 31 | For more information, see SoLoud manual or http://soloud-audio.com/legal.html 32 | 33 | SoLoud proper is licensed under the zlib/libpng license: 34 | 35 | SoLoud audio engine 36 | Copyright (c) 2013-2018 Jari Komppa 37 | 38 | This software is provided 'as-is', without any express or implied 39 | warranty. In no event will the authors be held liable for any damages 40 | arising from the use of this software. 41 | 42 | Permission is granted to anyone to use this software for any purpose, 43 | including commercial applications, and to alter it and redistribute it 44 | freely, subject to the following restrictions: 45 | 46 | 1. The origin of this software must not be misrepresented; you must not 47 | claim that you wrote the original software. If you use this software 48 | in a product, an acknowledgment in the product documentation would be 49 | appreciated but is not required. 50 | 51 | 2. Altered source versions must be plainly marked as such, and must not be 52 | misrepresented as being the original software. 53 | 54 | 3. This notice may not be removed or altered from any source 55 | distribution. 56 | -------------------------------------------------------------------------------- /LoudPizza.Mp3/LoudPizza.Mp3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LoudPizza.Mp3/Mp3Stream.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NLayer; 3 | 4 | namespace LoudPizza 5 | { 6 | public class Mp3Stream : AudioSource 7 | { 8 | private Mp3StreamInstance mp3Instance; 9 | 10 | public MpegFile mpegFile; 11 | 12 | public Mp3Stream(Stream stream, bool leaveOpen) 13 | { 14 | mpegFile = new MpegFile(stream, leaveOpen); 15 | 16 | mChannels = (uint)mpegFile.Channels; 17 | mBaseSamplerate = mpegFile.SampleRate; 18 | 19 | // TODO: allow multiple streams from the intial stream by buffering 20 | mp3Instance = new Mp3StreamInstance(this, mpegFile); 21 | } 22 | 23 | public override Mp3StreamInstance createInstance() 24 | { 25 | return mp3Instance; 26 | //return new Mp3StreamInstance(this, mpegFile); 27 | } 28 | 29 | protected override void Dispose(bool disposing) 30 | { 31 | if (disposing) 32 | { 33 | mp3Instance.Dispose(); 34 | mpegFile.Dispose(); 35 | } 36 | base.Dispose(disposing); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LoudPizza.Mp3/Mp3StreamInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using NLayer; 4 | 5 | namespace LoudPizza 6 | { 7 | public unsafe class Mp3StreamInstance : AudioSourceInstance 8 | { 9 | protected Mp3Stream mParent; 10 | private MpegFile _mpegFile; 11 | 12 | public Mp3StreamInstance(Mp3Stream parent, MpegFile mpegFile) 13 | { 14 | mParent = parent ?? throw new ArgumentNullException(nameof(parent)); 15 | _mpegFile = mpegFile ?? throw new ArgumentNullException(nameof(mpegFile)); 16 | } 17 | 18 | [SkipLocalsInit] 19 | public override uint getAudio(float* aBuffer, uint aSamplesToRead, uint aBufferSize) 20 | { 21 | float* localBuffer = stackalloc float[1024]; 22 | Span localSpan = new Span(localBuffer, 1024); 23 | 24 | uint channels = mChannels; 25 | uint readTarget = aSamplesToRead * channels; 26 | if ((uint)localSpan.Length > readTarget) 27 | localSpan = localSpan.Slice(0, (int)readTarget); 28 | 29 | uint samplesRead = (uint)_mpegFile.ReadSamples(localSpan); 30 | if (samplesRead == 0) 31 | return 0; 32 | 33 | uint elements = samplesRead / channels; 34 | 35 | for (uint i = 0; i < channels; i++) 36 | { 37 | for (uint j = 0; j < elements; j++) 38 | { 39 | aBuffer[j + i * aBufferSize] = localBuffer[i + j * channels]; 40 | } 41 | } 42 | 43 | return elements; 44 | } 45 | 46 | public override SOLOUD_ERRORS seek(ulong aSamplePosition, float* mScratch, uint mScratchSize) 47 | { 48 | Console.WriteLine("SEEK: " + aSamplePosition); 49 | 50 | long offset = (long)(aSamplePosition - mStreamPosition); 51 | if (offset <= 0) 52 | { 53 | if (!_mpegFile.CanSeek) 54 | return SOLOUD_ERRORS.NOT_IMPLEMENTED; 55 | 56 | _mpegFile.Position = 0; 57 | mStreamPosition = 0; 58 | offset = (long)aSamplePosition; 59 | } 60 | 61 | ulong samples_to_discard = (ulong)offset; 62 | mStreamPosition += samples_to_discard; 63 | 64 | if (_mpegFile.CanSeek) 65 | { 66 | _mpegFile.Position = ((long)(mStreamPosition * mChannels)); 67 | mStreamPosition = (ulong)_mpegFile.Position / mChannels; 68 | } 69 | else 70 | { 71 | while (samples_to_discard != 0) 72 | { 73 | uint samples = mScratchSize / mChannels; 74 | if (samples > samples_to_discard) 75 | samples = (uint)samples_to_discard; 76 | 77 | uint read = getAudio(mScratch, samples, samples); 78 | if (read == 0) 79 | break; 80 | samples_to_discard -= read; 81 | } 82 | } 83 | 84 | return SOLOUD_ERRORS.SO_NO_ERROR; 85 | } 86 | 87 | public override bool hasEnded() 88 | { 89 | return (mFlags & FLAGS.LOOPING) == 0 && _mpegFile.EndOfFile; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LoudPizza.TestApp/LoudPizza.TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | true 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LoudPizza.TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Net.Http; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using LoudPizza.Core; 12 | using LoudPizza.Modifiers; 13 | using LoudPizza.Sources; 14 | using LoudPizza.Sources.Streaming; 15 | using LoudPizza.Vorbis; 16 | using SharpInterop.SDL2; 17 | 18 | namespace LoudPizza.TestApp 19 | { 20 | internal unsafe class Program 21 | { 22 | public static void Main(string[] args) 23 | { 24 | SDL.SDL_Init(SDL.SDL_INIT_AUDIO); 25 | 26 | SdlAudioUtil? audioUtil = new(isCapture: false); 27 | 28 | SoLoud soLoud = new(); 29 | 30 | AudioStreamer streamer = new(); 31 | streamer.Start(); 32 | 33 | int sampleRate = 48000; 34 | int channels = 2; 35 | int bufferSize = 1024; 36 | bool writeToFile = false; 37 | 38 | Sdl2AudioBackend? backend = null; 39 | if (!writeToFile) 40 | { 41 | backend = new Sdl2AudioBackend(soLoud); 42 | backend.Initialize((uint)sampleRate, (uint)bufferSize); 43 | } 44 | else 45 | { 46 | soLoud.postinit_internal((uint)sampleRate, (uint)bufferSize, (uint)channels); 47 | } 48 | 49 | SoLoudHandle so = new(soLoud); 50 | so.SetMaxActiveVoiceCount(16); 51 | 52 | NVorbis.VorbisReader reader = new("test1.ogg"); 53 | 54 | // HttpClient http = new(); 55 | // var request = new HttpRequestMessage(HttpMethod.Get, ""); 56 | // Console.WriteLine("Sending request"); 57 | // var response = http.Send(request, HttpCompletionOption.ResponseHeadersRead); 58 | // Console.WriteLine("Opening stream"); 59 | // var responseStream = response.Content.ReadAsStream(); 60 | // NVorbis.VorbisReader reader = new(responseStream, false); 61 | // Console.WriteLine("Initializing stream"); 62 | reader.Initialize(); 63 | // Console.WriteLine("Stream initialized"); 64 | 65 | VorbisAudioStream vorbisStream = new(reader); 66 | StreamedAudioStream streamedAudio = new(streamer, vorbisStream); 67 | streamer.RegisterStream(streamedAudio); 68 | 69 | AudioStream audioStream = new(soLoud, streamedAudio); 70 | 71 | byte[] file1 = File.ReadAllBytes("test1.raw"); 72 | AudioBuffer buf = new(soLoud); 73 | float[] floats = MemoryMarshal.Cast(file1).ToArray(); 74 | uint fileChannels = 1; 75 | if (fileChannels == 2) 76 | { 77 | float[] newFloats = new float[floats.Length]; 78 | int center = floats.Length / 2; 79 | for (int i = 0; i < center; i++) 80 | { 81 | newFloats[i + center * 0] = floats[i * 2 + 0]; 82 | newFloats[i + center * 1] = floats[i * 2 + 1]; 83 | } 84 | floats = newFloats; 85 | } 86 | buf.LoadRawWave(floats, 44100, fileChannels); 87 | 88 | for (int i = 0; i < 0; i++) 89 | { 90 | VoiceHandle h = so.Play(buf, paused: false); 91 | h.IsLooping = false; 92 | } 93 | 94 | VoiceHandle asHandle = default; 95 | if (!writeToFile) 96 | { 97 | asHandle = so.Play(audioStream); 98 | asHandle.IsProtected = true; 99 | asHandle.IsLooping = true; 100 | asHandle.RelativePlaySpeed = 1.0f; 101 | asHandle.Volume = 0.5f; 102 | //asHandle.StreamSamplePosition = 1657800; 103 | //asHandle.StreamSamplePosition = 4053600; 104 | } 105 | 106 | if (writeToFile) 107 | { 108 | using WaveWriter writer = new(new FileStream("output.wav", FileMode.Create), false, sampleRate, channels); 109 | 110 | float[] buffer = new float[bufferSize * channels]; 111 | short[] buffer16 = new short[bufferSize * channels]; 112 | 113 | Stopwatch w = new Stopwatch(); 114 | w.Start(); 115 | 116 | VoiceHandle group = so.CreateVoiceGroup(); 117 | 118 | int loops = (int)Math.Ceiling((sampleRate / (float)bufferSize) * 10); 119 | for (int i = 0; i < loops; i++) 120 | { 121 | //if (i == 5) 122 | //{ 123 | // h.IsPaused = false; 124 | //} 125 | // 126 | //if (i == 10) 127 | //{ 128 | // h.IsPaused = true; 129 | //} 130 | // 131 | //if (i == 15) 132 | //{ 133 | // h.IsPaused = false; 134 | // h.SchedulePause(2); 135 | //} 136 | 137 | //if (i % 2 == 0) 138 | //{ 139 | // VoiceHandle h = so.Play(buf, paused: false); 140 | // h.IsLooping = false; 141 | // h.Volume = 0.2f; 142 | // group.AddVoiceToGroup(h); 143 | //} 144 | 145 | fixed (float* bufferPtr = buffer) 146 | { 147 | soLoud.mix(bufferPtr, (uint)bufferSize); 148 | } 149 | 150 | //soLoud.mixSigned16(buffer16, (uint)bufferSize); 151 | 152 | //for (int j = 0; j < bufferSize * channels; j++) 153 | //{ 154 | // buffer[j] = buffer16[j] / (float)0x7fff; 155 | //} 156 | 157 | writer.WriteSamples(new ReadOnlySpan(buffer, 0, bufferSize * channels)); 158 | } 159 | 160 | w.Stop(); 161 | 162 | Console.WriteLine($"Mixing finished in {w.Elapsed.TotalMilliseconds:0.0}ms"); 163 | } 164 | 165 | AudioResampler[] resamplers = new AudioResampler[] 166 | { 167 | LinearAudioResampler.Instance, 168 | PointAudioResampler.Instance, 169 | CatmullRomAudioResampler.Instance, 170 | }; 171 | int resamplerIndex = 0; 172 | 173 | Console.WriteLine("space = skip 1s"); 174 | Console.WriteLine("left arrow = pitch down"); 175 | Console.WriteLine("right arrow = pitch up"); 176 | Console.WriteLine("up arrow = volume up"); 177 | Console.WriteLine("down arrow = volume down"); 178 | Console.WriteLine("R = cycle resampler"); 179 | Console.WriteLine(); 180 | 181 | while (true) 182 | { 183 | var key = Console.ReadKey().Key; 184 | if (key == ConsoleKey.Spacebar) 185 | { 186 | asHandle.StreamSamplePosition += 48000; 187 | Console.Write($"Skipped 1s (to {asHandle.StreamSamplePosition})"); 188 | } 189 | else if (key == ConsoleKey.RightArrow) 190 | { 191 | asHandle.RelativePlaySpeed = asHandle.RelativePlaySpeed + 0.025f; 192 | Console.Write($"Increased pitch to {asHandle.RelativePlaySpeed:0.00}"); 193 | } 194 | else if (key == ConsoleKey.LeftArrow) 195 | { 196 | asHandle.RelativePlaySpeed = Math.Max(asHandle.RelativePlaySpeed - 0.025f, 0.025f); 197 | Console.Write($"Decreased pitch to {asHandle.RelativePlaySpeed:0.00}"); 198 | } 199 | else if (key == ConsoleKey.UpArrow) 200 | { 201 | asHandle.Volume = Math.Min(asHandle.Volume + 0.01f, 1); 202 | Console.Write($"Increased volume to {asHandle.Volume:0.00}"); 203 | } 204 | else if (key == ConsoleKey.DownArrow) 205 | { 206 | asHandle.Volume = Math.Max(asHandle.Volume - 0.01f, 0f); 207 | Console.Write($"Decreased volume to {asHandle.Volume:0.00}"); 208 | } 209 | else if (key == ConsoleKey.R) 210 | { 211 | so.SetResampler(resamplers[resamplerIndex]); 212 | Console.Write($"esampler set to {so.GetResampler().GetType().Name}"); 213 | resamplerIndex = (resamplerIndex + 1) % resamplers.Length; 214 | } 215 | else if (key == ConsoleKey.P) 216 | { 217 | VoiceHandle h = so.Play(buf, paused: false); 218 | h.IsLooping = false; 219 | h.Volume = 0.2f; 220 | } 221 | Console.WriteLine(); 222 | } 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /LoudPizza.TestApp/Sdl2AudioBackend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using LoudPizza.Core; 4 | using static SharpInterop.SDL2.SDL; 5 | 6 | namespace LoudPizza.TestApp 7 | { 8 | public unsafe class Sdl2AudioBackend 9 | { 10 | private Stopwatch watch = new(); 11 | private int loops; 12 | 13 | public SDL_AudioSpec gActiveAudioSpec; 14 | public uint gAudioDeviceID; 15 | 16 | private SDL_AudioCallback audioCallback; 17 | 18 | public SoLoud SoLoud { get; } 19 | 20 | public Sdl2AudioBackend(SoLoud soloud) 21 | { 22 | SoLoud = soloud ?? throw new ArgumentNullException(nameof(soloud)); 23 | } 24 | 25 | public SoLoudStatus Initialize(uint sampleRate = 48000, uint bufferSize = 512, uint channels = 0) 26 | { 27 | //if (!SDL_WasInit(SDL_INIT_AUDIO)) 28 | //{ 29 | // if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) 30 | // { 31 | // return SOLOUD_ERRORS.UNKNOWN_ERROR; 32 | // } 33 | //} 34 | 35 | audioCallback = soloud_sdl2static_audiomixer; 36 | 37 | SDL_AudioSpec spec; 38 | spec.silence = default; 39 | spec.userdata = default; 40 | spec.size = default; 41 | spec.callback = audioCallback; 42 | 43 | spec.freq = (int)sampleRate; 44 | spec.format = AUDIO_F32; 45 | spec.channels = (byte)channels; 46 | spec.samples = (ushort)bufferSize; 47 | 48 | int flags = (int)(SDL_AUDIO_ALLOW_ANY_CHANGE & (~SDL_AUDIO_ALLOW_FORMAT_CHANGE)); 49 | 50 | gAudioDeviceID = SDL_OpenAudioDevice(IntPtr.Zero, 0, ref spec, out SDL_AudioSpec activeSpec, flags); 51 | if (gAudioDeviceID == 0) 52 | { 53 | spec.format = AUDIO_S16; 54 | 55 | gAudioDeviceID = SDL_OpenAudioDevice(IntPtr.Zero, 0, ref spec, out activeSpec, flags); 56 | } 57 | 58 | if (gAudioDeviceID == 0) 59 | { 60 | return SoLoudStatus.UnknownError; 61 | } 62 | 63 | SoLoud.postinit_internal((uint)activeSpec.freq, activeSpec.samples, activeSpec.channels); 64 | gActiveAudioSpec = activeSpec; 65 | 66 | SoLoud.mBackendCleanupFunc = soloud_sdl2_deinit; 67 | SoLoud.mBackendString = "SDL2"; 68 | 69 | SDL_PauseAudioDevice(gAudioDeviceID, 0); // start playback 70 | 71 | return SoLoudStatus.Ok; 72 | } 73 | 74 | private void soloud_sdl2static_audiomixer(IntPtr userdata, IntPtr stream, int length) 75 | { 76 | watch.Start(); 77 | if (gActiveAudioSpec.format == AUDIO_F32) 78 | { 79 | int samples = length / (gActiveAudioSpec.channels * sizeof(float)); 80 | SoLoud.mix((float*)stream, (uint)samples); 81 | } 82 | else 83 | { 84 | int samples = length / (gActiveAudioSpec.channels * sizeof(short)); 85 | SoLoud.mixSigned16((short*)stream, (uint)samples); 86 | } 87 | watch.Stop(); 88 | 89 | loops++; 90 | if (loops >= 48000 / 512) 91 | { 92 | Console.WriteLine("Mixing time: " + watch.Elapsed.TotalMilliseconds + "ms"); 93 | watch.Reset(); 94 | loops = 0; 95 | } 96 | } 97 | 98 | private void soloud_sdl2_deinit(SoLoud aSoloud) 99 | { 100 | SDL_CloseAudioDevice(gAudioDeviceID); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /LoudPizza.TestApp/SdlAudioUtil.cs: -------------------------------------------------------------------------------- 1 | using static SharpInterop.SDL2.SDL; 2 | 3 | namespace LoudPizza.TestApp 4 | { 5 | public unsafe class SdlAudioUtil 6 | { 7 | private string?[] _deviceNames; 8 | private SDL_AudioSpec[] _deviceSpecs; 9 | 10 | public SdlAudioUtil(bool isCapture) 11 | { 12 | int is_capture = isCapture ? 1 : 0; 13 | int count = SDL_GetNumAudioDevices(is_capture); 14 | 15 | string?[] deviceNames = new string[count]; 16 | SDL_AudioSpec[] deviceSpecs = new SDL_AudioSpec[count]; 17 | 18 | for (int i = 0; i < count; i++) 19 | { 20 | deviceNames[i] = SDL_GetAudioDeviceName(i, is_capture); 21 | 22 | int code = SDL_GetAudioDeviceSpec(i, is_capture, out SDL_AudioSpec spec); 23 | if (code == 0) 24 | { 25 | deviceSpecs[i] = spec; 26 | } 27 | } 28 | 29 | _deviceNames = deviceNames; 30 | _deviceSpecs = deviceSpecs; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LoudPizza.TestApp/WaveWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace LoudPizza.TestApp 7 | { 8 | public sealed class WaveWriter : IDisposable 9 | { 10 | private const string BLANK_HEADER = "RIFF\0\0\0\0WAVEfmt "; 11 | private const string BLANK_DATA_HEADER = "data\0\0\0\0"; 12 | 13 | private BinaryWriter _writer; 14 | 15 | public WaveWriter(Stream stream, bool leaveOpen, int sampleRate, int channels) 16 | { 17 | if (stream == null) 18 | throw new ArgumentNullException(nameof(stream)); 19 | 20 | _writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen); 21 | 22 | // basic header 23 | _writer.Write(Encoding.UTF8.GetBytes(BLANK_HEADER)); 24 | // fmt header size 25 | _writer.Write(18); 26 | // encoding (IeeeFloat) 27 | _writer.Write((short)3); 28 | // channels 29 | _writer.Write((short)channels); 30 | // samplerate 31 | _writer.Write(sampleRate); 32 | // averagebytespersecond 33 | int blockAlign = channels * sizeof(float); 34 | _writer.Write(blockAlign * sampleRate); 35 | // blockalign 36 | _writer.Write((short)blockAlign); 37 | // bitspersample (32) 38 | _writer.Write((short)32); 39 | // extrasize 40 | _writer.Write((short)0); 41 | // "data\0\0\0\0" 42 | _writer.Write(Encoding.UTF8.GetBytes(BLANK_DATA_HEADER)); 43 | } 44 | 45 | public void WriteSamples(ReadOnlySpan buf) 46 | { 47 | Span tmp = stackalloc byte[2048]; 48 | 49 | while (buf.Length > 0) 50 | { 51 | int toRead = Math.Min(tmp.Length / sizeof(float), buf.Length); 52 | 53 | ReadOnlySpan src = buf.Slice(0, toRead); 54 | Span dst = tmp.Slice(0, toRead * sizeof(float)); 55 | 56 | for (int i = 0; i < src.Length; i++) 57 | { 58 | BinaryPrimitives.WriteSingleLittleEndian(dst.Slice(i * sizeof(float), sizeof(float)), src[i]); 59 | } 60 | 61 | _writer.Write(dst); 62 | buf = buf.Slice(toRead); 63 | } 64 | } 65 | 66 | public void Dispose() 67 | { 68 | // RIFF chunk size 69 | _writer.Seek(4, SeekOrigin.Begin); 70 | _writer.Write((uint)(_writer.BaseStream.Length - 8)); 71 | 72 | // data chunk size 73 | _writer.Seek(44, SeekOrigin.Begin); 74 | _writer.Write((uint)(_writer.BaseStream.Length - 48)); 75 | 76 | _writer?.Dispose(); 77 | _writer = null!; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /LoudPizza.Vorbis/LoudPizza.Vorbis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LoudPizza.Vorbis/VorbisAudioStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Sources; 3 | using NVorbis; 4 | 5 | namespace LoudPizza.Vorbis 6 | { 7 | public class VorbisAudioStream : IAudioStream 8 | { 9 | public bool IsDisposed { get; private set; } 10 | public VorbisReader Reader { get; private set; } 11 | 12 | public uint Channels => (uint)Reader.Channels; 13 | 14 | public float SampleRate => Reader.SampleRate; 15 | 16 | public float RelativePlaybackSpeed => 1; 17 | 18 | public VorbisAudioStream(VorbisReader vorbisReader) 19 | { 20 | Reader = vorbisReader ?? throw new ArgumentNullException(nameof(vorbisReader)); 21 | } 22 | 23 | /// 24 | public uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 25 | { 26 | int sampleCount = Reader.ReadSamples(buffer, (int)samplesToRead, (int)channelStride); 27 | return (uint)sampleCount; 28 | } 29 | 30 | /// 31 | public bool HasEnded() 32 | { 33 | return Reader.IsEndOfStream; 34 | } 35 | 36 | /// 37 | public bool CanSeek() 38 | { 39 | return Reader.CanSeek; 40 | } 41 | 42 | /// 43 | public SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition) 44 | { 45 | long signedSamplePosition = (long)samplePosition; 46 | if (signedSamplePosition < 0) 47 | { 48 | resultPosition = (ulong)Reader.TotalSamples; 49 | return SoLoudStatus.EndOfStream; 50 | } 51 | 52 | // TODO: bubble up exceptions? 53 | try 54 | { 55 | Reader.SeekTo(signedSamplePosition); 56 | resultPosition = (ulong)Reader.SamplePosition; 57 | return SoLoudStatus.Ok; 58 | } 59 | catch (PreRollPacketException) 60 | { 61 | resultPosition = (ulong)Reader.SamplePosition; 62 | return SoLoudStatus.FileLoadFailed; 63 | } 64 | catch (SeekOutOfRangeException) 65 | { 66 | resultPosition = (ulong)Reader.TotalSamples; 67 | return SoLoudStatus.EndOfStream; 68 | } 69 | catch (Exception) 70 | { 71 | resultPosition = 0; 72 | return SoLoudStatus.UnknownError; 73 | } 74 | } 75 | 76 | protected virtual void Dispose(bool disposing) 77 | { 78 | if (!IsDisposed) 79 | { 80 | if (disposing) 81 | { 82 | Reader.Dispose(); 83 | Reader = null!; 84 | } 85 | IsDisposed = true; 86 | } 87 | } 88 | 89 | public void Dispose() 90 | { 91 | Dispose(disposing: true); 92 | GC.SuppressFinalize(this); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /LoudPizza.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32602.215 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoudPizza", "LoudPizza\LoudPizza.csproj", "{9A77CC06-8F59-4C6F-A4D0-DB996ED6D702}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoudPizza.Vorbis", "LoudPizza.Vorbis\LoudPizza.Vorbis.csproj", "{B38584DF-11DB-4B38-A65A-EAD46ED4C589}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoudPizza.Mp3", "LoudPizza.Mp3\LoudPizza.Mp3.csproj", "{BDA224D5-3DD3-45FE-87CE-FB2B0918BE6D}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {9A77CC06-8F59-4C6F-A4D0-DB996ED6D702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {9A77CC06-8F59-4C6F-A4D0-DB996ED6D702}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {9A77CC06-8F59-4C6F-A4D0-DB996ED6D702}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {9A77CC06-8F59-4C6F-A4D0-DB996ED6D702}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {B38584DF-11DB-4B38-A65A-EAD46ED4C589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B38584DF-11DB-4B38-A65A-EAD46ED4C589}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B38584DF-11DB-4B38-A65A-EAD46ED4C589}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B38584DF-11DB-4B38-A65A-EAD46ED4C589}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {BDA224D5-3DD3-45FE-87CE-FB2B0918BE6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BDA224D5-3DD3-45FE-87CE-FB2B0918BE6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BDA224D5-3DD3-45FE-87CE-FB2B0918BE6D}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BDA224D5-3DD3-45FE-87CE-FB2B0918BE6D}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {1BE42BFE-090C-4DE8-82DC-297907A6050E} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /LoudPizza/AudioSeekFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza 4 | { 5 | [Flags] 6 | public enum AudioSeekFlags 7 | { 8 | None = 0, 9 | 10 | /// 11 | /// The seek operation can complete later. 12 | /// 13 | NonBlocking = 1 << 0, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LoudPizza/Core/AlignedFloatBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Threading; 4 | 5 | namespace LoudPizza.Core 6 | { 7 | /// 8 | /// Handles aligned allocations to support vectorized operations. 9 | /// 10 | public unsafe struct AlignedFloatBuffer 11 | { 12 | /// 13 | /// Aligned pointer. 14 | /// 15 | public float* mData; 16 | 17 | /// 18 | /// Raw allocated pointer (for delete). 19 | /// 20 | public IntPtr mBasePtr; 21 | 22 | /// 23 | /// Size of buffer (w/out padding). 24 | /// 25 | public uint mFloats; 26 | 27 | /// 28 | /// Allocate and align buffer. 29 | /// 30 | public SoLoudStatus init(uint aFloats, uint alignment) 31 | { 32 | destroy(); 33 | 34 | mData = null; 35 | mFloats = aFloats; 36 | #if !NET6_0_OR_GREATER 37 | mBasePtr = Marshal.AllocHGlobal((int)(aFloats * sizeof(float) + alignment)); 38 | if (mBasePtr == IntPtr.Zero) 39 | return SoLoudStatus.OutOfMemory; 40 | mData = (float*)(((long)mBasePtr + (alignment - 1)) & ~(alignment - 1)); 41 | #else 42 | mBasePtr = (IntPtr)NativeMemory.AlignedAlloc(aFloats * sizeof(float), alignment); 43 | if (mBasePtr == IntPtr.Zero) 44 | return SoLoudStatus.OutOfMemory; 45 | mData = (float*)mBasePtr; 46 | #endif 47 | return SoLoudStatus.Ok; 48 | } 49 | 50 | public Span AsSpan() 51 | { 52 | if (mData == null) 53 | { 54 | throw new InvalidOperationException(); 55 | } 56 | return new Span(mData, (int)mFloats); 57 | } 58 | 59 | public Span AsSpan(int start) 60 | { 61 | return AsSpan().Slice(start); 62 | } 63 | 64 | public Span AsSpan(int start, int length) 65 | { 66 | return AsSpan().Slice(start, length); 67 | } 68 | 69 | public void destroy() 70 | { 71 | IntPtr ptr = Interlocked.Exchange(ref mBasePtr, IntPtr.Zero); 72 | mData = null; 73 | 74 | if (ptr != IntPtr.Zero) 75 | { 76 | #if NET6_0_OR_GREATER 77 | NativeMemory.AlignedFree((void*)ptr); 78 | #else 79 | Marshal.FreeHGlobal(ptr); 80 | #endif 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /LoudPizza/Core/Buffer256.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LoudPizza.Core 4 | { 5 | public unsafe struct Buffer256 6 | { 7 | public const int Length = 256; 8 | 9 | public fixed float Data[Length]; 10 | 11 | public float this[nint index] 12 | { 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | get => Data[index]; 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | set => Data[index] = value; 18 | } 19 | 20 | public float this[nuint index] 21 | { 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | get => Data[index]; 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | set => Data[index] = value; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LoudPizza/Core/ChannelBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LoudPizza.Core 4 | { 5 | public unsafe struct ChannelBuffer 6 | { 7 | public fixed float Data[SoLoud.MaxChannels]; 8 | 9 | public float this[nint index] 10 | { 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | get => Data[index]; 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | set => Data[index] = value; 16 | } 17 | 18 | public float this[nuint index] 19 | { 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | get => Data[index]; 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | set => Data[index] = value; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LoudPizza/Core/Fader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Core 4 | { 5 | public struct Fader 6 | { 7 | /// 8 | /// Value to fade from. 9 | /// 10 | public float mFrom; 11 | 12 | /// 13 | /// Value to fade to. 14 | /// 15 | public float mTo; 16 | 17 | /// 18 | /// Delta between from and to. 19 | /// 20 | public float mDelta; 21 | 22 | /// 23 | /// Total time to fade. 24 | /// 25 | public Time mTime; 26 | 27 | /// 28 | /// Time fading started. 29 | /// 30 | public Time mStartTime; 31 | 32 | /// 33 | /// Time fading will end. 34 | /// 35 | public Time mEndTime; 36 | 37 | /// 38 | /// Current value. Used in case time rolls over. 39 | /// 40 | public float mCurrent; 41 | 42 | /// 43 | /// Active flag. 44 | /// 45 | public State mActive; 46 | 47 | /// 48 | /// Set up LFO. 49 | /// 50 | public void setLFO(float aFrom, float aTo, Time aTime, Time aStartTime) 51 | { 52 | mActive = State.LFO; 53 | mCurrent = 0; 54 | mFrom = aFrom; 55 | mTo = aTo; 56 | mTime = aTime; 57 | mDelta = (aTo - aFrom) / 2; 58 | if (mDelta < 0) 59 | mDelta = -mDelta; 60 | mStartTime = aStartTime; 61 | mEndTime = MathF.PI * 2 / mTime; 62 | } 63 | 64 | /// 65 | /// Set up fader. 66 | /// 67 | public void set(float aFrom, float aTo, Time aTime, Time aStartTime) 68 | { 69 | mCurrent = mFrom; 70 | mFrom = aFrom; 71 | mTo = aTo; 72 | mTime = aTime; 73 | mStartTime = aStartTime; 74 | mDelta = aTo - aFrom; 75 | mEndTime = mStartTime + mTime; 76 | mActive = State.Active; 77 | } 78 | 79 | /// 80 | /// Get the current fading value. 81 | /// 82 | public float get(Time aCurrentTime) 83 | { 84 | if (mActive == State.LFO) 85 | { 86 | // LFO mode 87 | if (mStartTime > aCurrentTime) 88 | // Time rolled over. 89 | mStartTime = aCurrentTime; 90 | double t = aCurrentTime - mStartTime; 91 | return (float)(Math.Sin(t * mEndTime) * mDelta + (mFrom + mDelta)); 92 | 93 | } 94 | if (mStartTime > aCurrentTime) 95 | { 96 | // Time rolled over. 97 | // Figure out where we were.. 98 | float p = (mCurrent - mFrom) / mDelta; // 0..1 99 | mFrom = mCurrent; 100 | mStartTime = aCurrentTime; 101 | mTime = mTime * (1 - p); // time left 102 | mDelta = mTo - mFrom; 103 | mEndTime = mStartTime + mTime; 104 | } 105 | if (aCurrentTime > mEndTime) 106 | { 107 | mActive = State.Inactive; 108 | return mTo; 109 | } 110 | mCurrent = (float)(mFrom + mDelta * ((aCurrentTime - mStartTime) / mTime)); 111 | return mCurrent; 112 | } 113 | 114 | public enum State 115 | { 116 | Inactive = -1, 117 | Disabled = 0, 118 | Active = 1, 119 | LFO = 2, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /LoudPizza/Core/Handle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace LoudPizza.Core 5 | { 6 | [DebuggerDisplay("{" + nameof(GetDebuggerDisplay) + "(),nq}")] 7 | public readonly struct Handle : IEquatable 8 | { 9 | public uint Value { get; } 10 | 11 | public Handle(uint value) 12 | { 13 | Value = value; 14 | } 15 | 16 | public bool Equals(Handle other) 17 | { 18 | return Value == other.Value; 19 | } 20 | 21 | public override int GetHashCode() 22 | { 23 | return Value.GetHashCode(); 24 | } 25 | 26 | public override bool Equals(object? obj) 27 | { 28 | return obj is Handle other && Equals(other); 29 | } 30 | 31 | public static bool operator ==(Handle left, Handle right) 32 | { 33 | return left.Equals(right); 34 | } 35 | 36 | public static bool operator !=(Handle left, Handle right) 37 | { 38 | return !(left == right); 39 | } 40 | 41 | public override string ToString() 42 | { 43 | return $"0x{Value:x}"; 44 | } 45 | 46 | private string GetDebuggerDisplay() 47 | { 48 | return ToString(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LoudPizza/Core/SoLoud.BasicOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Modifiers; 3 | using LoudPizza.Sources; 4 | 5 | namespace LoudPizza.Core 6 | { 7 | public unsafe partial class SoLoud : IAudioBus 8 | { 9 | /// 10 | /// Start playing a sound. 11 | /// Returns voice handle, which can be ignored or used to alter the playing sound's parameters. 12 | /// Negative volume means to use default. 13 | /// 14 | /// 15 | /// The source was not constructed from this library instance. 16 | /// 17 | public Handle play(AudioSource aSound, float aVolume = -1.0f, float aPan = 0.0f, bool aPaused = false, Handle aBus = default) 18 | { 19 | if (aSound.SoLoud != this) 20 | { 21 | throw new InvalidOperationException("The source was not constructed from this library instance."); 22 | } 23 | 24 | if ((aSound.mFlags & AudioSource.Flags.SingleInstance) != 0) 25 | { 26 | // Only one instance allowed, stop others 27 | aSound.Stop(); 28 | } 29 | 30 | // Creation of an audio instance may take significant amount of time, 31 | // so let's not do it inside the audio thread mutex. 32 | AudioSourceInstance instance = aSound.CreateInstance(); 33 | 34 | lock (mAudioThreadMutex) 35 | { 36 | int ch = findFreeVoice_internal(); 37 | if (ch < 0) 38 | { 39 | instance.Dispose(); 40 | return new Handle((uint)SoLoudStatus.PoolExhausted); 41 | } 42 | mVoice[ch] = instance; 43 | instance.mBusHandle = aBus; 44 | instance.Initialize(mPlayIndex); 45 | m3dData[ch].init(aSound); 46 | 47 | mPlayIndex++; 48 | 49 | // 20 bits, skip the last one (top bits full = voice group) 50 | if (mPlayIndex == 0xfffff) 51 | { 52 | mPlayIndex = 0; 53 | } 54 | 55 | if (aPaused) 56 | { 57 | instance.mFlags |= AudioSourceInstance.Flags.Paused; 58 | } 59 | 60 | setVoicePan_internal(ch, aPan); 61 | if (aVolume < 0) 62 | { 63 | setVoiceVolume_internal(ch, aSound.mVolume); 64 | } 65 | else 66 | { 67 | setVoiceVolume_internal(ch, aVolume); 68 | } 69 | 70 | // Fix initial voice volume ramp up 71 | for (uint i = 0; i < MaxChannels; i++) 72 | { 73 | instance.mCurrentChannelVolume[i] = instance.mChannelVolume[i] * instance.mOverallVolume; 74 | } 75 | 76 | setVoiceRelativePlaySpeed_internal(ch, 1); 77 | 78 | ReadOnlySpan filters = aSound.GetFilters(); 79 | for (int i = 0; i < filters.Length; i++) 80 | { 81 | AudioFilter? filter = filters[i]; 82 | if (filter != null) 83 | { 84 | instance.SetFilter(i, filter.CreateInstance()); 85 | } 86 | } 87 | 88 | mActiveVoiceDirty = true; 89 | 90 | Handle handle = getHandleFromVoice_internal(ch); 91 | return handle; 92 | } 93 | } 94 | 95 | VoiceHandle IAudioBus.Play(AudioSource source, float volume, float pan, bool paused) 96 | { 97 | Handle handle = play(source, volume, pan, paused, default); 98 | return new VoiceHandle(this, handle); 99 | } 100 | 101 | /// 102 | /// Start playing a sound delayed in relation to other sounds called via this function. 103 | /// Negative volume means to use default. 104 | /// 105 | public Handle playClocked(Time aSoundTime, AudioSource aSound, float aVolume = -1.0f, float aPan = 0.0f, Handle aBus = default) 106 | { 107 | Handle h = play(aSound, aVolume, aPan, true, aBus); 108 | Time lasttime; 109 | lock (mAudioThreadMutex) 110 | { 111 | // mLastClockedTime is cleared to zero at start of every output buffer 112 | lasttime = mLastClockedTime; 113 | if (lasttime == 0) 114 | { 115 | mLastClockedTime = aSoundTime; 116 | lasttime = aSoundTime; 117 | } 118 | } 119 | int samples = (int)Math.Floor((aSoundTime - lasttime) * mSamplerate); 120 | // Make sure we don't delay too much (or overflow) 121 | if (samples < 0 || samples > 2048) 122 | samples = 0; 123 | setDelaySamples(h, (uint)samples); 124 | setPause(h, false); 125 | return h; 126 | } 127 | 128 | VoiceHandle IAudioBus.PlayClocked(AudioSource source, Time soundTime, float volume, float pan) 129 | { 130 | Handle handle = playClocked(soundTime, source, volume, default); 131 | return new VoiceHandle(this, handle); 132 | } 133 | 134 | /// 135 | /// Start playing a sound without any panning. 136 | /// 137 | /// 138 | /// It will be played at full volume. 139 | /// 140 | public Handle playBackground(AudioSource aSound, float aVolume = 1.0f, bool aPaused = false, Handle aBus = default) 141 | { 142 | Handle h = play(aSound, aVolume, 0.0f, aPaused, aBus); 143 | setPanAbsolute(h, 1.0f, 1.0f); 144 | return h; 145 | } 146 | 147 | VoiceHandle IAudioBus.PlayBackground(AudioSource source, float volume, bool paused) 148 | { 149 | Handle handle = playBackground(source, volume, default); 150 | return new VoiceHandle(this, handle); 151 | } 152 | 153 | /// 154 | /// Seek the audio stream to certain point in time. 155 | /// 156 | /// 157 | /// The audio stream may not support seeking, or may only allow seeking forward. 158 | /// 159 | /// Flags that affect seek behavior. 160 | public SoLoudStatus seek(Handle aVoiceHandle, ulong aSamplePosition, AudioSeekFlags flags) 161 | { 162 | lock (mAudioThreadMutex) 163 | { 164 | SoLoudStatus res = SoLoudStatus.Ok; 165 | 166 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 167 | foreach (Handle h in h_) 168 | { 169 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 170 | if (ch != null) 171 | { 172 | SoLoudStatus singleres = ch.Seek(aSamplePosition, mScratch.AsSpan(), flags, out _); 173 | if (singleres != SoLoudStatus.Ok) 174 | res = singleres; 175 | } 176 | } 177 | return res; 178 | } 179 | } 180 | 181 | /// 182 | /// Stop the voice. 183 | /// 184 | public void stop(Handle aVoiceHandle) 185 | { 186 | lock (mAudioThreadMutex) 187 | { 188 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 189 | foreach (Handle h in h_) 190 | { 191 | int ch = getVoiceFromHandle_internal(h); 192 | if (ch != -1) 193 | { 194 | stopVoice_internal(ch); 195 | } 196 | } 197 | } 198 | } 199 | 200 | /// 201 | /// Stop all voices that are playing from the given audio source. 202 | /// 203 | public void stopAudioSource(AudioSource aSound) 204 | { 205 | lock (mAudioThreadMutex) 206 | { 207 | ReadOnlySpan highVoices = mVoice.AsSpan(0, mHighestVoice); 208 | for (int i = 0; i < highVoices.Length; i++) 209 | { 210 | AudioSourceInstance? voice = highVoices[i]; 211 | if (voice != null && voice.Source == aSound) 212 | { 213 | stopVoice_internal(i); 214 | } 215 | } 216 | } 217 | } 218 | 219 | /// 220 | /// Stop all voices. 221 | /// 222 | public void stopAll() 223 | { 224 | lock (mAudioThreadMutex) 225 | { 226 | for (int i = 0; i < mHighestVoice; i++) 227 | { 228 | stopVoice_internal(i); 229 | } 230 | } 231 | } 232 | 233 | /// 234 | /// Gets the amount of voices that play the given audio source. 235 | /// 236 | public int countAudioSource(AudioSource aSound) 237 | { 238 | int count = 0; 239 | lock (mAudioThreadMutex) 240 | { 241 | ReadOnlySpan highVoices = mVoice.AsSpan(0, mHighestVoice); 242 | for (int i = 0; i < highVoices.Length; i++) 243 | { 244 | AudioSourceInstance? voice = highVoices[i]; 245 | if (voice != null && voice.Source == aSound) 246 | { 247 | count++; 248 | } 249 | } 250 | } 251 | return count; 252 | } 253 | 254 | /// 255 | /// Move a live sound to the given bus. 256 | /// 257 | public void AnnexSound(Handle voiceHandle, Handle busHandle) 258 | { 259 | lock (mAudioThreadMutex) 260 | { 261 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref voiceHandle); 262 | foreach (Handle h in h_) 263 | { 264 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 265 | if (ch != null) 266 | { 267 | ch.mBusHandle = busHandle; 268 | } 269 | } 270 | } 271 | } 272 | 273 | /// 274 | public void AnnexSound(Handle voiceHandle) 275 | { 276 | AnnexSound(voiceHandle, default); 277 | } 278 | 279 | /// 280 | public Handle GetBusHandle() 281 | { 282 | return default; 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /LoudPizza/Core/SoLoud.FaderOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Sources; 3 | 4 | namespace LoudPizza.Core 5 | { 6 | public unsafe partial class SoLoud 7 | { 8 | /// 9 | /// Schedule a stream to pause. 10 | /// 11 | public void schedulePause(Handle aVoiceHandle, Time aTime) 12 | { 13 | if (aTime <= 0) 14 | { 15 | setPause(aVoiceHandle, true); 16 | return; 17 | } 18 | 19 | lock (mAudioThreadMutex) 20 | { 21 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 22 | foreach (Handle h in h_) 23 | { 24 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 25 | if (ch != null) 26 | { 27 | ch.mPauseScheduler.set(1, 0, aTime, ch.mStreamTime); 28 | } 29 | } 30 | } 31 | } 32 | 33 | /// 34 | /// Schedule a stream to stop. 35 | /// 36 | public void scheduleStop(Handle aVoiceHandle, Time aTime) 37 | { 38 | if (aTime <= 0) 39 | { 40 | stop(aVoiceHandle); 41 | return; 42 | } 43 | 44 | lock (mAudioThreadMutex) 45 | { 46 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 47 | foreach (Handle h in h_) 48 | { 49 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 50 | if (ch != null) 51 | { 52 | ch.mStopScheduler.set(1, 0, aTime, ch.mStreamTime); 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// 59 | /// Set up volume fader. 60 | /// 61 | public void fadeVolume(Handle aVoiceHandle, float aTo, Time aTime) 62 | { 63 | lock (mAudioThreadMutex) 64 | { 65 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 66 | foreach (Handle h in h_) 67 | { 68 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 69 | if (ch != null) 70 | { 71 | ch.mVolumeFader.set(ch.mSetVolume, aTo, aTime, ch.mStreamTime); 72 | } 73 | } 74 | } 75 | } 76 | 77 | /// 78 | /// Set up panning fader. 79 | /// 80 | public void fadePan(Handle aVoiceHandle, float aTo, Time aTime) 81 | { 82 | lock (mAudioThreadMutex) 83 | { 84 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 85 | foreach (Handle h in h_) 86 | { 87 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 88 | if (ch != null) 89 | { 90 | ch.mPanFader.set(ch.mPan, aTo, aTime, ch.mStreamTime); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /// 97 | /// Set up relative play speed fader. 98 | /// 99 | public void fadeRelativePlaySpeed(Handle aVoiceHandle, float aTo, Time aTime) 100 | { 101 | lock (mAudioThreadMutex) 102 | { 103 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 104 | foreach (Handle h in h_) 105 | { 106 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 107 | if (ch != null) 108 | { 109 | ch.mRelativePlaySpeedFader.set(ch.mSetRelativePlaySpeed, aTo, aTime, ch.mStreamTime); 110 | } 111 | } 112 | } 113 | } 114 | 115 | /// 116 | /// Set up global volume fader. 117 | /// 118 | public void fadeGlobalVolume(float aTo, Time aTime) 119 | { 120 | float from = getGlobalVolume(); 121 | if (aTime <= 0 || aTo == from) 122 | { 123 | setGlobalVolume(aTo); 124 | return; 125 | } 126 | mGlobalVolumeFader.set(from, aTo, aTime, mStreamTime); 127 | } 128 | 129 | /// 130 | /// Set up volume oscillator. 131 | /// 132 | public void oscillateVolume(Handle aVoiceHandle, float aFrom, float aTo, Time aTime) 133 | { 134 | if (aTime <= 0 || aTo == aFrom) 135 | { 136 | setVolume(aVoiceHandle, aTo); 137 | return; 138 | } 139 | 140 | lock (mAudioThreadMutex) 141 | { 142 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 143 | foreach (Handle h in h_) 144 | { 145 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 146 | if (ch != null) 147 | { 148 | ch.mVolumeFader.setLFO(aFrom, aTo, aTime, ch.mStreamTime); 149 | } 150 | } 151 | } 152 | } 153 | 154 | /// 155 | /// Set up panning oscillator. 156 | /// 157 | public void oscillatePan(Handle aVoiceHandle, float aFrom, float aTo, Time aTime) 158 | { 159 | if (aTime <= 0 || aTo == aFrom) 160 | { 161 | setPan(aVoiceHandle, aTo); 162 | return; 163 | } 164 | 165 | lock (mAudioThreadMutex) 166 | { 167 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 168 | foreach (Handle h in h_) 169 | { 170 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 171 | if (ch != null) 172 | { 173 | ch.mPanFader.setLFO(aFrom, aTo, aTime, ch.mStreamTime); 174 | } 175 | } 176 | } 177 | } 178 | 179 | /// 180 | /// Set up relative play speed oscillator. 181 | /// 182 | public void oscillateRelativePlaySpeed(Handle aVoiceHandle, float aFrom, float aTo, Time aTime) 183 | { 184 | if (aTime <= 0 || aTo == aFrom) 185 | { 186 | setRelativePlaySpeed(aVoiceHandle, aTo); 187 | return; 188 | } 189 | 190 | lock (mAudioThreadMutex) 191 | { 192 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 193 | foreach (Handle h in h_) 194 | { 195 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 196 | if (ch != null) 197 | { 198 | ch.mRelativePlaySpeedFader.setLFO(aFrom, aTo, aTime, ch.mStreamTime); 199 | } 200 | } 201 | } 202 | } 203 | 204 | /// 205 | /// Set up global volume oscillator. 206 | /// 207 | public void oscillateGlobalVolume(float aFrom, float aTo, Time aTime) 208 | { 209 | if (aTime <= 0 || aTo == aFrom) 210 | { 211 | setGlobalVolume(aTo); 212 | return; 213 | } 214 | mGlobalVolumeFader.setLFO(aFrom, aTo, aTime, mStreamTime); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /LoudPizza/Core/SoLoud.FilterOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Modifiers; 3 | using LoudPizza.Sources; 4 | 5 | namespace LoudPizza.Core 6 | { 7 | public unsafe partial class SoLoud 8 | { 9 | /// 10 | /// Set global filters. Set to to clear the filter. 11 | /// 12 | /// is invalid. 13 | public void setGlobalFilter(int aFilterId, AudioFilter? aFilter) 14 | { 15 | if ((uint)aFilterId >= FiltersPerStream) 16 | { 17 | throw new ArgumentOutOfRangeException(nameof(aFilterId)); 18 | } 19 | 20 | lock (mAudioThreadMutex) 21 | { 22 | mFilterInstance[aFilterId]?.Dispose(); 23 | mFilterInstance[aFilterId] = null; 24 | 25 | mFilter[aFilterId] = aFilter; 26 | if (aFilter != null) 27 | { 28 | mFilterInstance[aFilterId] = aFilter.CreateInstance(); 29 | } 30 | } 31 | } 32 | 33 | /// 34 | /// Get a live filter parameter. Use 0 for the global filters. 35 | /// 36 | /// is invalid. 37 | public float getFilterParameter(Handle aVoiceHandle, int aFilterId, int aAttributeId) 38 | { 39 | if ((uint)aFilterId >= FiltersPerStream) 40 | { 41 | throw new ArgumentOutOfRangeException(nameof(aFilterId)); 42 | } 43 | 44 | lock (mAudioThreadMutex) 45 | { 46 | float ret = (int)SoLoudStatus.InvalidParameter; 47 | if (aVoiceHandle == default) 48 | { 49 | AudioFilterInstance? filterInstance = mFilterInstance[aFilterId]; 50 | if (filterInstance != null) 51 | { 52 | ret = filterInstance.GetFilterParameter(aAttributeId); 53 | } 54 | return ret; 55 | } 56 | 57 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(aVoiceHandle); 58 | if (ch != null) 59 | { 60 | AudioFilterInstance? filterInstance = ch.GetFilter(aFilterId); 61 | if (filterInstance != null) 62 | { 63 | ret = filterInstance.GetFilterParameter(aAttributeId); 64 | } 65 | } 66 | return ret; 67 | } 68 | } 69 | 70 | /// 71 | /// Set a live filter parameter. Use 0 for the global filters. 72 | /// 73 | /// is invalid. 74 | public void setFilterParameter(Handle aVoiceHandle, int aFilterId, int aAttributeId, float aValue) 75 | { 76 | if ((uint)aFilterId >= FiltersPerStream) 77 | { 78 | throw new ArgumentOutOfRangeException(nameof(aFilterId)); 79 | } 80 | 81 | lock (mAudioThreadMutex) 82 | { 83 | if (aVoiceHandle == default) 84 | { 85 | AudioFilterInstance? filterInstance = mFilterInstance[aFilterId]; 86 | if (filterInstance != null) 87 | { 88 | filterInstance.SetFilterParameter(aAttributeId, aValue); 89 | } 90 | return; 91 | } 92 | 93 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 94 | foreach (Handle h in h_) 95 | { 96 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 97 | if (ch != null) 98 | { 99 | AudioFilterInstance? filterInstance = ch.GetFilter(aFilterId); 100 | if (filterInstance != null) 101 | { 102 | filterInstance.SetFilterParameter(aAttributeId, aValue); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | /// 110 | /// Fade a live filter parameter. Use 0 for the global filters. 111 | /// 112 | /// is invalid. 113 | public void fadeFilterParameter( 114 | Handle aVoiceHandle, int aFilterId, int aAttributeId, float aTo, Time aTime) 115 | { 116 | if ((uint)aFilterId >= FiltersPerStream) 117 | { 118 | throw new ArgumentOutOfRangeException(nameof(aFilterId)); 119 | } 120 | 121 | lock (mAudioThreadMutex) 122 | { 123 | if (aVoiceHandle == default) 124 | { 125 | AudioFilterInstance? filterInstance = mFilterInstance[aFilterId]; 126 | if (filterInstance != null) 127 | { 128 | filterInstance.FadeFilterParameter(aAttributeId, aTo, aTime, mStreamTime); 129 | } 130 | return; 131 | } 132 | 133 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 134 | foreach (Handle h in h_) 135 | { 136 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 137 | if (ch != null) 138 | { 139 | AudioFilterInstance? filterInstance = ch.GetFilter(aFilterId); 140 | if (filterInstance != null) 141 | { 142 | filterInstance.FadeFilterParameter(aAttributeId, aTo, aTime, ch.mStreamTime); 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | /// 150 | /// Oscillate a live filter parameter. Use 0 for the global filters. 151 | /// 152 | /// is invalid. 153 | public void oscillateFilterParameter( 154 | Handle aVoiceHandle, int aFilterId, int aAttributeId, float aFrom, float aTo, Time aTime) 155 | { 156 | if ((uint)aFilterId >= FiltersPerStream) 157 | { 158 | throw new ArgumentOutOfRangeException(nameof(aFilterId)); 159 | } 160 | 161 | lock (mAudioThreadMutex) 162 | { 163 | if (aVoiceHandle == default) 164 | { 165 | AudioFilterInstance? filterInstance = mFilterInstance[aFilterId]; 166 | if (filterInstance != null) 167 | { 168 | filterInstance.OscillateFilterParameter(aAttributeId, aFrom, aTo, aTime, mStreamTime); 169 | } 170 | return; 171 | } 172 | 173 | ReadOnlySpan h_ = VoiceGroupHandleToSpan(ref aVoiceHandle); 174 | foreach (Handle h in h_) 175 | { 176 | AudioSourceInstance? ch = getVoiceRefFromHandle_internal(h); 177 | if (ch != null) 178 | { 179 | AudioFilterInstance? filterInstance = ch.GetFilter(aFilterId); 180 | if (filterInstance != null) 181 | { 182 | filterInstance.OscillateFilterParameter(aAttributeId, aFrom, aTo, aTime, ch.mStreamTime); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /LoudPizza/Core/SoLoud.VoiceGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace LoudPizza.Core 5 | { 6 | // Voice group operations 7 | public unsafe partial class SoLoud 8 | { 9 | /// 10 | /// Create a voice group. Returns 0 if unable (out of voice groups / out of memory). 11 | /// 12 | public Handle createVoiceGroup() 13 | { 14 | lock (mAudioThreadMutex) 15 | { 16 | int index; 17 | { 18 | // Check if there's any deleted voice groups and re-use if found 19 | Handle[][] voiceGroups = mVoiceGroup; 20 | for (int i = 0; i < voiceGroups.Length; i++) 21 | { 22 | if (voiceGroups[i].Length == 0) 23 | { 24 | Handle[] groupa = new Handle[16]; 25 | if (groupa == null) 26 | { 27 | return default; 28 | } 29 | mVoiceGroup[i] = groupa; 30 | return new Handle(0xfffff000 | (uint)i); 31 | } 32 | } 33 | if (voiceGroups.Length == 4096) 34 | { 35 | return default; 36 | } 37 | 38 | Handle[][] vg = new Handle[Math.Max(4, voiceGroups.Length * 2)][]; 39 | if (vg == null) 40 | { 41 | return default; 42 | } 43 | voiceGroups.CopyTo(vg.AsSpan()); 44 | for (int i = voiceGroups.Length; i < vg.Length; i++) 45 | { 46 | vg[i] = Array.Empty(); 47 | } 48 | 49 | mVoiceGroup = vg; 50 | index = voiceGroups.Length; 51 | } 52 | 53 | Handle[] groupb = new Handle[16]; 54 | if (groupb == null) 55 | { 56 | return default; 57 | } 58 | mVoiceGroup[index] = groupb; 59 | return new Handle(0xfffff000 | (uint)index); 60 | } 61 | } 62 | 63 | /// 64 | /// Destroy a voice group. 65 | /// 66 | public SoLoudStatus destroyVoiceGroup(Handle aVoiceGroupHandle) 67 | { 68 | if (!isVoiceGroup(aVoiceGroupHandle)) 69 | return SoLoudStatus.InvalidParameter; 70 | 71 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 72 | 73 | lock (mAudioThreadMutex) 74 | { 75 | //delete[] mVoiceGroup[c]; 76 | mVoiceGroup[c] = Array.Empty(); 77 | return SoLoudStatus.Ok; 78 | } 79 | } 80 | 81 | /// 82 | /// Add a voice handle to a voice group. 83 | /// 84 | public SoLoudStatus addVoiceToGroup(Handle aVoiceGroupHandle, Handle aVoiceHandle) 85 | { 86 | if (!isVoiceGroup(aVoiceGroupHandle)) 87 | return SoLoudStatus.InvalidParameter; 88 | 89 | // Don't consider adding invalid voice handles as an error, since the voice may just have ended. 90 | if (!isValidVoiceHandle(aVoiceHandle)) 91 | return SoLoudStatus.Ok; 92 | 93 | trimVoiceGroup_internal(aVoiceGroupHandle); 94 | 95 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 96 | 97 | lock (mAudioThreadMutex) 98 | { 99 | Handle[] group = mVoiceGroup[c]; 100 | for (int i = 0; i < group.Length; i++) 101 | { 102 | if (group[i] == aVoiceHandle) 103 | { 104 | return SoLoudStatus.Ok; // already there 105 | } 106 | 107 | if (group[i] == default) 108 | { 109 | group[i] = aVoiceHandle; 110 | return SoLoudStatus.Ok; 111 | } 112 | } 113 | 114 | // Full group, allocate more memory 115 | int newLength = Math.Max(16, group.Length * 2); 116 | Handle[] newGroup = new Handle[newLength]; 117 | if (newGroup == null) 118 | { 119 | return SoLoudStatus.OutOfMemory; 120 | } 121 | 122 | group.CopyTo(newGroup.AsSpan()); 123 | newGroup[group.Length] = aVoiceHandle; 124 | 125 | mVoiceGroup[c] = newGroup; 126 | return SoLoudStatus.Ok; 127 | } 128 | } 129 | 130 | /// 131 | /// Get whether the given handle is a valid voice group. 132 | /// 133 | public bool isVoiceGroup(Handle aVoiceGroupHandle) 134 | { 135 | if ((aVoiceGroupHandle.Value & 0xfffff000) != 0xfffff000) 136 | return false; 137 | 138 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 139 | 140 | lock (mAudioThreadMutex) 141 | { 142 | Handle[][] voiceGroups = mVoiceGroup; 143 | if (c >= voiceGroups.Length) 144 | return false; 145 | 146 | bool res = voiceGroups[c].Length != 0; 147 | return res; 148 | } 149 | } 150 | 151 | /// 152 | /// Get whether the given voice group is empty. 153 | /// 154 | public bool isVoiceGroupEmpty(Handle aVoiceGroupHandle) 155 | { 156 | // If not a voice group, yeah, we're empty alright.. 157 | if (!isVoiceGroup(aVoiceGroupHandle)) 158 | return true; 159 | 160 | trimVoiceGroup_internal(aVoiceGroupHandle); 161 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 162 | 163 | lock (mAudioThreadMutex) 164 | { 165 | bool res = mVoiceGroup[c].Length != 0; 166 | return res; 167 | } 168 | } 169 | 170 | /// 171 | /// Remove all non-active voices from group. 172 | /// 173 | internal void trimVoiceGroup_internal(Handle aVoiceGroupHandle) 174 | { 175 | if (!isVoiceGroup(aVoiceGroupHandle)) 176 | return; 177 | 178 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 179 | 180 | lock (mAudioThreadMutex) 181 | { 182 | Handle[] group = mVoiceGroup[c]; 183 | 184 | for (int i = 0; i < group.Length; i++) 185 | { 186 | // If we hit a voice in the group that's not set, we're done 187 | if (group[i] == default) 188 | { 189 | return; 190 | } 191 | 192 | while (!isValidVoiceHandle(group[i])) 193 | { 194 | // current index is an invalid handle, move all following handles backwards 195 | for (int j = i; j < group.Length - 1; j++) 196 | { 197 | group[j] = group[j + 1]; 198 | // not a full group, we can stop copying 199 | if (group[j] == default) 200 | break; 201 | } 202 | 203 | // did we end up with an empty group? we're done then 204 | if (group[i] == default) 205 | { 206 | return; 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | /// 214 | /// Gets a span to the zero-terminated array of voice handles in a voice group. 215 | /// 216 | internal ReadOnlySpan VoiceGroupHandleToSpan(ref Handle aVoiceGroupHandle) 217 | { 218 | if ((aVoiceGroupHandle.Value & 0xfffff000) != 0xfffff000) 219 | return MemoryMarshal.CreateReadOnlySpan(ref aVoiceGroupHandle, 1); 220 | 221 | int c = (int)(aVoiceGroupHandle.Value & 0xfff); 222 | 223 | Handle[][] voiceGroups = mVoiceGroup; 224 | if (c >= voiceGroups.Length) 225 | return MemoryMarshal.CreateReadOnlySpan(ref aVoiceGroupHandle, 1); 226 | 227 | Handle[] group = voiceGroups[c]; 228 | return group; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /LoudPizza/Core/SoLoud.VoiceOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using LoudPizza.Sources; 5 | 6 | namespace LoudPizza.Core 7 | { 8 | // Direct voice operations (no mutexes - called from other functions) 9 | public unsafe partial class SoLoud 10 | { 11 | [Conditional("DEBUG")] 12 | private void Validate(int voice) 13 | { 14 | Debug.Assert((uint)voice < MaxVoiceCount); 15 | Debug.Assert(Monitor.IsEntered(mAudioThreadMutex)); 16 | } 17 | 18 | /// 19 | /// Set voice (not handle) relative play speed. 20 | /// 21 | internal void setVoiceRelativePlaySpeed_internal(int aVoice, float aSpeed) 22 | { 23 | Validate(aVoice); 24 | Debug.Assert(aSpeed > 0); 25 | 26 | AudioSourceInstance? voice = mVoice[aVoice]; 27 | if (voice != null) 28 | { 29 | voice.mSetRelativePlaySpeed = aSpeed; 30 | updateVoiceRelativePlaySpeed_internal(aVoice); 31 | } 32 | } 33 | 34 | /// 35 | /// Set voice (not handle) pause state. 36 | /// 37 | internal void setVoicePause_internal(int aVoice, bool aPause) 38 | { 39 | Validate(aVoice); 40 | 41 | mActiveVoiceDirty = true; 42 | AudioSourceInstance? voice = mVoice[aVoice]; 43 | if (voice != null) 44 | { 45 | voice.mPauseScheduler.mActive = 0; 46 | 47 | if (aPause) 48 | { 49 | voice.mFlags |= AudioSourceInstance.Flags.Paused; 50 | } 51 | else 52 | { 53 | voice.mFlags &= ~AudioSourceInstance.Flags.Paused; 54 | } 55 | } 56 | } 57 | 58 | /// 59 | /// Set voice (not handle) pan. 60 | /// 61 | internal void setVoicePan_internal(int aVoice, float aPan) 62 | { 63 | Validate(aVoice); 64 | 65 | AudioSourceInstance? voice = mVoice[aVoice]; 66 | if (voice != null) 67 | { 68 | voice.mPan = aPan; 69 | (float r, float l) = MathF.SinCos((aPan + 1) * MathF.PI / 4); 70 | voice.mChannelVolume[0] = l; 71 | voice.mChannelVolume[1] = r; 72 | if (voice.Channels == 4) 73 | { 74 | voice.mChannelVolume[2] = l; 75 | voice.mChannelVolume[3] = r; 76 | } 77 | if (voice.Channels == 6) 78 | { 79 | voice.mChannelVolume[2] = SQRT2RECP; 80 | voice.mChannelVolume[3] = 1; 81 | voice.mChannelVolume[4] = l; 82 | voice.mChannelVolume[5] = r; 83 | } 84 | if (voice.Channels == 8) 85 | { 86 | voice.mChannelVolume[2] = SQRT2RECP; 87 | voice.mChannelVolume[3] = 1; 88 | voice.mChannelVolume[4] = l; 89 | voice.mChannelVolume[5] = r; 90 | voice.mChannelVolume[6] = l; 91 | voice.mChannelVolume[7] = r; 92 | } 93 | } 94 | } 95 | 96 | /// 97 | /// Set voice (not handle) volume. 98 | /// 99 | internal void setVoiceVolume_internal(int aVoice, float aVolume) 100 | { 101 | Validate(aVoice); 102 | 103 | mActiveVoiceDirty = true; 104 | AudioSourceInstance? voice = mVoice[aVoice]; 105 | if (voice != null) 106 | { 107 | voice.mSetVolume = aVolume; 108 | updateVoiceVolume_internal(aVoice); 109 | } 110 | } 111 | 112 | /// 113 | /// Stop voice (not handle). 114 | /// 115 | internal void stopVoice_internal(int aVoice) 116 | { 117 | Validate(aVoice); 118 | 119 | mActiveVoiceDirty = true; 120 | AudioSourceInstance? voice = mVoice[aVoice]; 121 | if (voice != null) 122 | { 123 | // Delete via temporary variable to avoid recursion 124 | AudioSourceInstance? v = mVoice[aVoice]; 125 | mVoice[aVoice] = null; 126 | 127 | Span resampleDataOwners = mResampleDataOwners.AsSpan(); 128 | for (int i = 0; i < resampleDataOwners.Length; i++) 129 | { 130 | if (resampleDataOwners[i] == v) 131 | { 132 | resampleDataOwners[i] = null; 133 | } 134 | } 135 | 136 | v?.Dispose(); 137 | } 138 | } 139 | 140 | /// 141 | /// Update overall relative play speed from set and 3D speeds. 142 | /// 143 | internal void updateVoiceRelativePlaySpeed_internal(int aVoice) 144 | { 145 | Validate(aVoice); 146 | 147 | AudioSourceInstance? voice = mVoice[aVoice]; 148 | Debug.Assert(voice != null); 149 | 150 | voice.mOverallRelativePlaySpeed = m3dData[aVoice].mDopplerValue * voice.mSetRelativePlaySpeed; 151 | voice.mSamplerate = voice.mBaseSamplerate * voice.mOverallRelativePlaySpeed; 152 | } 153 | 154 | /// 155 | /// Update overall volume from set and 3D volumes. 156 | /// 157 | internal void updateVoiceVolume_internal(int aVoice) 158 | { 159 | Validate(aVoice); 160 | 161 | AudioSourceInstance? voice = mVoice[aVoice]; 162 | Debug.Assert(voice != null); 163 | 164 | voice.mOverallVolume = voice.mSetVolume * m3dData[aVoice].m3dVolume; 165 | if ((voice.mFlags & AudioSourceInstance.Flags.Paused) != 0) 166 | { 167 | for (int i = 0; i < MaxChannels; i++) 168 | { 169 | voice.mCurrentChannelVolume[i] = voice.mChannelVolume[i] * voice.mOverallVolume; 170 | } 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /LoudPizza/Core/TinyAlignedFloatBuffer.cs: -------------------------------------------------------------------------------- 1 | namespace LoudPizza.Core 2 | { 3 | /// 4 | /// Handles small aligned buffer to support vectorized operations. 5 | /// 6 | internal unsafe struct TinyAlignedFloatBuffer 7 | { 8 | public const int Length = sizeof(float) * 16 + 16; 9 | 10 | public fixed byte mData[Length]; 11 | 12 | public static float* align(byte* basePtr) 13 | { 14 | return (float*)((long)basePtr + 15 & ~15); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.3D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Numerics; 4 | using LoudPizza.Core; 5 | using LoudPizza.Sources; 6 | 7 | namespace LoudPizza 8 | { 9 | public readonly partial struct SoLoudHandle 10 | { 11 | /// 12 | public void UpdateAudio3D() 13 | { 14 | SoLoud.update3dAudio(); 15 | } 16 | 17 | /// 18 | public VoiceHandle Play3D( 19 | AudioSource source, 20 | Vector3 position, 21 | Vector3 velocity = default, 22 | float volume = -1.0f, 23 | bool paused = false, 24 | Handle bus = default) 25 | { 26 | Handle handle = SoLoud.play3d(source, position, velocity, volume, paused, bus); 27 | return new VoiceHandle(SoLoud, handle); 28 | } 29 | 30 | VoiceHandle IAudioBus.Play3D(AudioSource source, Vector3 position, Vector3 velocity, float volume, bool paused) 31 | { 32 | return Play3D(source, position, velocity, volume, paused, default); 33 | } 34 | 35 | /// 36 | public VoiceHandle PlayClocked3D( 37 | AudioSource source, 38 | Time soundTime, 39 | Vector3 position, 40 | Vector3 velocity = default, 41 | float volume = -1.0f, 42 | Handle bus = default) 43 | { 44 | Handle handle = SoLoud.play3dClocked(soundTime, source, position, velocity, volume, bus); 45 | return new VoiceHandle(SoLoud, handle); 46 | } 47 | 48 | VoiceHandle IAudioBus.PlayClocked3D(AudioSource source, Time soundTime, Vector3 position, Vector3 velocity, float volume) 49 | { 50 | return PlayClocked3D(source, soundTime, position, velocity, volume, default); 51 | } 52 | 53 | /// 54 | /// The speed is less than or equal to zero. 55 | public void SetSoundSpeed(float aSpeed) 56 | { 57 | if (aSpeed <= 0) 58 | { 59 | throw new ArgumentOutOfRangeException(nameof(aSpeed)); 60 | } 61 | 62 | SoLoudStatus status = SoLoud.set3dSoundSpeed(aSpeed); 63 | Debug.Assert(status == SoLoudStatus.Ok); 64 | } 65 | 66 | /// 67 | public float GetSoundSpeed() 68 | { 69 | return SoLoud.get3dSoundSpeed(); 70 | } 71 | 72 | /// 73 | public void SetListenerParameters( 74 | Vector3 position, 75 | Vector3 at, 76 | Vector3 up, 77 | Vector3 velocity) 78 | { 79 | SoLoud.set3dListenerParameters(position, at, up, velocity); 80 | } 81 | 82 | /// 83 | public void SetListenerPosition(Vector3 position) 84 | { 85 | SoLoud.set3dListenerPosition(position); 86 | } 87 | 88 | /// 89 | public void SetListenerAt(Vector3 at) 90 | { 91 | SoLoud.set3dListenerAt(at); 92 | } 93 | 94 | /// 95 | public void SetListenerUp(Vector3 up) 96 | { 97 | SoLoud.set3dListenerUp(up); 98 | } 99 | 100 | /// 101 | public void SetListenerVelocity(Vector3 velocity) 102 | { 103 | SoLoud.set3dListenerVelocity(velocity); 104 | } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.BasicOps.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | using LoudPizza.Sources; 3 | 4 | namespace LoudPizza 5 | { 6 | public readonly partial struct SoLoudHandle 7 | { 8 | /// 9 | public VoiceHandle Play(AudioSource source, float volume = -1.0f, float pan = 0.0f, bool paused = false, Handle bus = default) 10 | { 11 | Handle handle = SoLoud.play(source, volume, pan, paused, bus); 12 | return new VoiceHandle(SoLoud, handle); 13 | } 14 | 15 | VoiceHandle IAudioBus.Play(AudioSource source, float volume, float pan, bool paused) 16 | { 17 | return Play(source, volume, pan, paused, default); 18 | } 19 | 20 | /// 21 | public VoiceHandle PlayClocked(AudioSource source, Time soundTime, float volume = -1.0f, float pan = 0.0f, Handle bus = default) 22 | { 23 | Handle handle = SoLoud.playClocked(soundTime, source, volume, pan, bus); 24 | return new VoiceHandle(SoLoud, handle); 25 | } 26 | 27 | VoiceHandle IAudioBus.PlayClocked(AudioSource source, Time soundTime, float volume, float pan) 28 | { 29 | return PlayClocked(source, soundTime, volume, pan, default); 30 | } 31 | 32 | /// 33 | public VoiceHandle PlayBackground(AudioSource source, float volume = 1.0f, bool paused = false, Handle bus = default) 34 | { 35 | Handle handle = SoLoud.playBackground(source, volume, paused, bus); 36 | return new VoiceHandle(SoLoud, handle); 37 | } 38 | 39 | VoiceHandle IAudioBus.PlayBackground(AudioSource source, float volume, bool paused) 40 | { 41 | return PlayBackground(source, volume, paused, default); 42 | } 43 | 44 | /// 45 | public void StopAudioSource(AudioSource source) 46 | { 47 | SoLoud.stopAudioSource(source); 48 | } 49 | 50 | /// 51 | public void StopAll() 52 | { 53 | SoLoud.stopAll(); 54 | } 55 | 56 | /// 57 | public int CountAudioSource(AudioSource source) 58 | { 59 | return SoLoud.countAudioSource(source); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.FaderOps.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza 4 | { 5 | public readonly partial struct SoLoudHandle 6 | { 7 | /// 8 | public void FadeGlobalVolume(float to, Time time) 9 | { 10 | SoLoud.fadeGlobalVolume(to, time); 11 | } 12 | 13 | /// 14 | public void OscillateGlobalVolume(float from, float to, Time time) 15 | { 16 | SoLoud.oscillateGlobalVolume(from, to, time); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.FilterOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | using LoudPizza.Modifiers; 4 | 5 | namespace LoudPizza 6 | { 7 | public readonly partial struct SoLoudHandle 8 | { 9 | /// 10 | public void SetGlobalFilter(int filterId, AudioFilter? filter) 11 | { 12 | SoLoud.setGlobalFilter(filterId, filter); 13 | } 14 | 15 | /// 16 | /// Get a global live filter parameter. 17 | /// 18 | /// is invalid. 19 | public float GetFilterParameter(int filterId, int attributeId) 20 | { 21 | return SoLoud.getFilterParameter(default, filterId, attributeId); 22 | } 23 | 24 | /// 25 | /// Set a global live filter parameter. 26 | /// 27 | /// is invalid. 28 | public void SetFilterParameter(int filterId, int attributeId, float value) 29 | { 30 | SoLoud.setFilterParameter(default, filterId, attributeId, value); 31 | } 32 | 33 | /// 34 | /// Fade a global live filter parameter. 35 | /// 36 | /// is invalid. 37 | public void FadeFilterParameter(int filterId, int attributeId, float to, Time time) 38 | { 39 | SoLoud.fadeFilterParameter(default, filterId, attributeId, to, time); 40 | } 41 | 42 | /// 43 | /// Oscillate a global live filter parameter. 44 | /// 45 | /// is invalid. 46 | public void OscillateFilterParameter(int filterId, int attributeId, float from, float to, Time time) 47 | { 48 | SoLoud.oscillateFilterParameter(default, filterId, attributeId, from, to, time); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.Getters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using LoudPizza.Core; 4 | using LoudPizza.Modifiers; 5 | 6 | namespace LoudPizza 7 | { 8 | public readonly partial struct SoLoudHandle 9 | { 10 | /// 11 | public uint GetVersion() 12 | { 13 | return SoLoud.getVersion(); 14 | } 15 | 16 | /// 17 | public float GetPostClipScaler() 18 | { 19 | return SoLoud.getPostClipScaler(); 20 | } 21 | 22 | /// 23 | public AudioResampler GetResampler() 24 | { 25 | return SoLoud.GetResampler(); 26 | } 27 | 28 | /// 29 | public float GetGlobalVolume() 30 | { 31 | return SoLoud.getGlobalVolume(); 32 | } 33 | 34 | /// 35 | public int GetMaxActiveVoiceCount() 36 | { 37 | return SoLoud.getMaxActiveVoiceCount(); 38 | } 39 | 40 | /// 41 | public int GetActiveVoiceCount() 42 | { 43 | return SoLoud.GetActiveVoiceCount(); 44 | } 45 | 46 | /// 47 | public int GetVoiceCount() 48 | { 49 | return SoLoud.getVoiceCount(); 50 | } 51 | 52 | /// 53 | public string? GetBackendString() 54 | { 55 | return SoLoud.getBackendString(); 56 | } 57 | 58 | /// 59 | public uint GetBackendChannels() 60 | { 61 | return SoLoud.getBackendChannels(); 62 | } 63 | 64 | /// 65 | public uint GetBackendSampleRate() 66 | { 67 | return SoLoud.getBackendSamplerate(); 68 | } 69 | 70 | /// 71 | public uint GetBackendBufferSize() 72 | { 73 | return SoLoud.getBackendBufferSize(); 74 | } 75 | 76 | /// 77 | public void GetSpeakerPosition(uint channel, out Vector3 position) 78 | { 79 | SoLoud.getSpeakerPosition(channel, out position); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.Setters.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using LoudPizza.Core; 3 | using LoudPizza.Modifiers; 4 | 5 | namespace LoudPizza 6 | { 7 | public readonly partial struct SoLoudHandle 8 | { 9 | /// 10 | public void SetPostClipScaler(float scaler) 11 | { 12 | SoLoud.setPostClipScaler(scaler); 13 | } 14 | 15 | /// 16 | public void SetResampler(AudioResampler resampler) 17 | { 18 | SoLoud.SetResampler(resampler); 19 | } 20 | 21 | /// 22 | public void SetGlobalVolume(float volume) 23 | { 24 | SoLoud.setGlobalVolume(volume); 25 | } 26 | 27 | /// 28 | public void SetMaxActiveVoiceCount(int voiceCount) 29 | { 30 | SoLoud.setMaxActiveVoiceCount(voiceCount); 31 | } 32 | 33 | /// 34 | public void SetPauseAll(bool pause) 35 | { 36 | SoLoud.setPauseAll(pause); 37 | } 38 | 39 | /// 40 | public void SetVisualizationEnabled(bool enable) 41 | { 42 | SoLoud.SetVisualizationEnabled(enable); 43 | } 44 | 45 | /// 46 | public bool GetVisualizationEnabled() 47 | { 48 | return SoLoud.GetVisualizationEnabled(); 49 | } 50 | 51 | /// 52 | public void SetClipRoundoff(bool enable) 53 | { 54 | SoLoud.SetClipRoundoff(enable); 55 | } 56 | 57 | /// 58 | public bool GetClipRoundoff() 59 | { 60 | return SoLoud.GetClipRoundoff(); 61 | } 62 | 63 | /// 64 | public void SetLeftHanded3D(bool enable) 65 | { 66 | SoLoud.SetLeftHanded3D(enable); 67 | } 68 | 69 | /// 70 | public bool GetLeftHanded3D() 71 | { 72 | return SoLoud.GetLeftHanded3D(); 73 | } 74 | 75 | /// 76 | public void SetSpeakerPosition(uint channel, Vector3 position) 77 | { 78 | SoLoud.setSpeakerPosition(channel, position); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.VoiceGroup.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza 4 | { 5 | public readonly partial struct SoLoudHandle 6 | { 7 | /// 8 | public VoiceHandle CreateVoiceGroup() 9 | { 10 | Handle handle = SoLoud.createVoiceGroup(); 11 | return new VoiceHandle(SoLoud, handle); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LoudPizza/Handles/SoLoudHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | using LoudPizza.Modifiers; 4 | using LoudPizza.Sources; 5 | 6 | namespace LoudPizza 7 | { 8 | /// 9 | /// Represents a wrapper handle around the library. 10 | /// 11 | public readonly partial struct SoLoudHandle : IAudioBus 12 | { 13 | /// 14 | /// Gets the main instance of the library associated with this handle. 15 | /// 16 | public SoLoud SoLoud { get; } 17 | 18 | public SoLoudHandle(SoLoud soLoud) 19 | { 20 | SoLoud = soLoud; 21 | } 22 | 23 | /// 24 | public void InitializeFromBackend(uint sampleRate, uint bufferSize, uint channels) 25 | { 26 | SoLoud.postinit_internal(sampleRate, bufferSize, channels); 27 | } 28 | 29 | /// 30 | public void Shutdown() 31 | { 32 | SoLoud.deinit(); 33 | } 34 | 35 | /// 36 | /// 37 | /// length is not a multiple of . 38 | /// 39 | public unsafe void Mix(Span buffer, uint samples) 40 | { 41 | fixed (float* bufferPtr = buffer) 42 | { 43 | SoLoud.mix(bufferPtr, samples); 44 | } 45 | } 46 | 47 | /// 48 | /// 49 | /// length is not a multiple of . 50 | /// 51 | public unsafe void MixSigned16(Span buffer, uint samples) 52 | { 53 | if (buffer.Length % samples != 0) 54 | { 55 | throw new ArgumentException(); 56 | } 57 | 58 | fixed (short* bufferPtr = buffer) 59 | { 60 | SoLoud.mixSigned16(bufferPtr, samples); 61 | } 62 | } 63 | 64 | /// 65 | public void CalcFFT(out Buffer256 buffer) 66 | { 67 | SoLoud.CalcFFT(out buffer); 68 | } 69 | 70 | /// 71 | public void GetWave(out Buffer256 buffer) 72 | { 73 | SoLoud.GetWave(out buffer); 74 | } 75 | 76 | /// 77 | public float GetApproximateVolume(uint channel) 78 | { 79 | return SoLoud.GetApproximateVolume(channel); 80 | } 81 | 82 | /// 83 | public void GetApproximateVolumes(out ChannelBuffer buffer) 84 | { 85 | SoLoud.GetApproximateVolumes(out buffer); 86 | } 87 | 88 | /// 89 | public void AnnexSound(Handle voiceHandle) 90 | { 91 | SoLoud.AnnexSound(voiceHandle); 92 | } 93 | 94 | /// 95 | public Handle GetBusHandle() 96 | { 97 | return default; 98 | } 99 | 100 | /// 101 | /// Gets or sets the resampler for the main bus. 102 | /// 103 | public AudioResampler Resampler 104 | { 105 | get => GetResampler(); 106 | set => SetResampler(value); 107 | } 108 | 109 | /// 110 | /// Gets or sets the global volume. 111 | /// 112 | public float GlobalVolume 113 | { 114 | get => GetGlobalVolume(); 115 | set => SetGlobalVolume(value); 116 | } 117 | 118 | public static implicit operator SoLoud(SoLoudHandle soLoudHandle) 119 | { 120 | return soLoudHandle.SoLoud; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.3D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using LoudPizza.Core; 4 | using LoudPizza.Modifiers; 5 | 6 | namespace LoudPizza 7 | { 8 | public readonly partial struct VoiceHandle 9 | { 10 | /// 11 | public void Set3DParameters(Vector3 position, Vector3 velocity) 12 | { 13 | SoLoud.set3dSourceParameters(Handle, position, velocity); 14 | } 15 | 16 | /// 17 | public void SetPosition(Vector3 position) 18 | { 19 | SoLoud.set3dSourcePosition(Handle, position); 20 | } 21 | 22 | /// 23 | public void SetVelocity(Vector3 velocity) 24 | { 25 | SoLoud.set3dSourceVelocity(Handle, velocity); 26 | } 27 | 28 | /// 29 | public void SetMinMaxDistance(float minDistance, float maxDistance) 30 | { 31 | SoLoud.set3dSourceMinMaxDistance(Handle, minDistance, maxDistance); 32 | } 33 | 34 | /// 35 | public void SetCollider(AudioCollider? collider, IntPtr userData = default) 36 | { 37 | SoLoud.set3dSourceCollider(Handle, collider, userData); 38 | } 39 | 40 | /// 41 | public void SetAttenuationRolloffFactor(float attenuationRolloffFactor) 42 | { 43 | SoLoud.set3dSourceAttenuationRolloffFactor(Handle, attenuationRolloffFactor); 44 | } 45 | 46 | /// 47 | public void SetAttenuator(AudioAttenuator? attenuator) 48 | { 49 | SoLoud.set3dSourceAttenuator(Handle, attenuator); 50 | } 51 | 52 | /// 53 | public void SetDopplerFactor(float dopplerFactor) 54 | { 55 | SoLoud.set3dSourceDopplerFactor(Handle, dopplerFactor); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.FaderOps.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza 4 | { 5 | public readonly partial struct VoiceHandle 6 | { 7 | /// 8 | public void SchedulePause(Time time) 9 | { 10 | SoLoud.schedulePause(Handle, time); 11 | } 12 | 13 | /// 14 | public void ScheduleStop(Time time) 15 | { 16 | SoLoud.scheduleStop(Handle, time); 17 | } 18 | 19 | /// 20 | public void FadeVolume(float to, Time time) 21 | { 22 | SoLoud.fadeVolume(Handle, to, time); 23 | } 24 | 25 | /// 26 | public void FadePan(float to, Time time) 27 | { 28 | SoLoud.fadePan(Handle, to, time); 29 | } 30 | 31 | /// 32 | public void FadeRelativePlaySpeed(float to, Time time) 33 | { 34 | SoLoud.fadeRelativePlaySpeed(Handle, to, time); 35 | } 36 | 37 | /// 38 | public void OscillateVolume(float from, float to, Time time) 39 | { 40 | SoLoud.oscillateVolume(Handle, from, to, time); 41 | } 42 | 43 | /// 44 | public void OscillatePan(float from, float to, Time time) 45 | { 46 | SoLoud.oscillatePan(Handle, from, to, time); 47 | } 48 | 49 | /// 50 | public void OscillateRelativePlaySpeed(float from, float to, Time time) 51 | { 52 | SoLoud.oscillateRelativePlaySpeed(Handle, from, to, time); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.FilterOps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza 5 | { 6 | public readonly partial struct VoiceHandle 7 | { 8 | /// 9 | /// Get a live filter parameter. 10 | /// 11 | /// is invalid. 12 | public float GetFilterParameter(int filterId, int attributeId) 13 | { 14 | return SoLoud.getFilterParameter(Handle, filterId, attributeId); 15 | } 16 | 17 | /// 18 | /// Set a live filter parameter. 19 | /// 20 | /// is invalid. 21 | public void SetFilterParameter(int filterId, int attributeId, float value) 22 | { 23 | SoLoud.setFilterParameter(Handle, filterId, attributeId, value); 24 | } 25 | 26 | /// 27 | /// Fade a live filter parameter. 28 | /// 29 | /// is invalid. 30 | public void FadeFilterParameter(int filterId, int attributeId, float to, Time time) 31 | { 32 | SoLoud.fadeFilterParameter(Handle, filterId, attributeId, to, time); 33 | } 34 | 35 | /// 36 | /// Oscillate a live filter parameter. 37 | /// 38 | /// is invalid. 39 | public void OscillateFilterParameter(int filterId, int attributeId, float from, float to, Time time) 40 | { 41 | SoLoud.oscillateFilterParameter(Handle, filterId, attributeId, from, to, time); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.Getters.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza 4 | { 5 | public readonly partial struct VoiceHandle 6 | { 7 | /// 8 | public float OverallVolume => GetOverallVolume(); 9 | 10 | /// 11 | public Time StreamTime => GetStreamTime(); 12 | 13 | /// 14 | public Time StreamTimePosition => GetStreamTimePosition(); 15 | 16 | /// 17 | public ulong LoopCount => GetLoopCount(); 18 | 19 | /// 20 | public bool IsValidVoiceHandle => SoLoud.isValidVoiceHandle(Handle); 21 | 22 | /// 23 | public ulong GetLoopPoint() 24 | { 25 | return SoLoud.getLoopPoint(Handle); 26 | } 27 | 28 | /// 29 | public bool GetLooping() 30 | { 31 | return SoLoud.getLooping(Handle); 32 | } 33 | 34 | /// 35 | public bool GetAutoStop() 36 | { 37 | return SoLoud.getAutoStop(Handle); 38 | } 39 | 40 | /// 41 | public float GetInfo(uint infoKey) 42 | { 43 | return SoLoud.getInfo(Handle, infoKey); 44 | } 45 | 46 | /// 47 | public float GetVolume() 48 | { 49 | return SoLoud.getVolume(Handle); 50 | } 51 | 52 | /// 53 | public float GetOverallVolume() 54 | { 55 | return SoLoud.getOverallVolume(Handle); 56 | } 57 | 58 | /// 59 | public float GetPan() 60 | { 61 | return SoLoud.getPan(Handle); 62 | } 63 | 64 | /// 65 | public Time GetStreamTime() 66 | { 67 | return SoLoud.getStreamTime(Handle); 68 | } 69 | 70 | /// 71 | public ulong GetStreamSamplePosition() 72 | { 73 | return SoLoud.getStreamSamplePosition(Handle); 74 | } 75 | 76 | /// 77 | public Time GetStreamTimePosition() 78 | { 79 | return SoLoud.getStreamTimePosition(Handle); 80 | } 81 | 82 | /// 83 | public float GetRelativePlaySpeed() 84 | { 85 | return SoLoud.getRelativePlaySpeed(Handle); 86 | } 87 | 88 | /// 89 | public float GetSampleRate() 90 | { 91 | return SoLoud.getSamplerate(Handle); 92 | } 93 | 94 | /// 95 | public bool GetPause() 96 | { 97 | return SoLoud.getPause(Handle); 98 | } 99 | 100 | /// 101 | public bool GetProtectVoice() 102 | { 103 | return SoLoud.getProtectVoice(Handle); 104 | } 105 | 106 | /// 107 | public uint GetLoopCount() 108 | { 109 | return SoLoud.getLoopCount(Handle); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.Setters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using LoudPizza.Core; 4 | 5 | namespace LoudPizza 6 | { 7 | public readonly partial struct VoiceHandle 8 | { 9 | /// 10 | /// is less than or equal to zero. 11 | public void SetRelativePlaySpeed(float speed) 12 | { 13 | if (!(speed > 0)) 14 | { 15 | throw new ArgumentOutOfRangeException(nameof(speed)); 16 | } 17 | 18 | SoLoudStatus status = SoLoud.setRelativePlaySpeed(Handle, speed); 19 | Debug.Assert(status == SoLoudStatus.Ok); 20 | } 21 | 22 | /// 23 | /// is less than zero. 24 | public void SetSampleRate(float sampleRate) 25 | { 26 | if (float.IsNegative(sampleRate)) 27 | { 28 | throw new ArgumentOutOfRangeException(nameof(sampleRate)); 29 | } 30 | 31 | SoLoud.setSamplerate(Handle, sampleRate); 32 | } 33 | 34 | /// 35 | public void SetPause(bool pause) 36 | { 37 | SoLoud.setPause(Handle, pause); 38 | } 39 | 40 | /// 41 | public void SetProtectVoice(bool protect) 42 | { 43 | SoLoud.setProtectVoice(Handle, protect); 44 | } 45 | 46 | /// 47 | public void SetPan(float pan) 48 | { 49 | SoLoud.setPan(Handle, pan); 50 | } 51 | 52 | /// 53 | public void SetChannelVolume(uint channel, float volume) 54 | { 55 | SoLoud.setChannelVolume(Handle, channel, volume); 56 | } 57 | 58 | /// 59 | public void SetPanAbsolute(float leftVolume, float rightVolume) 60 | { 61 | SoLoud.setPanAbsolute(Handle, leftVolume, rightVolume); 62 | } 63 | 64 | /// 65 | public void SetInaudibleBehavior(bool mustTick, bool kill) 66 | { 67 | SoLoud.setInaudibleBehavior(Handle, mustTick, kill); 68 | } 69 | 70 | /// 71 | public void SetLoopPoint(ulong loopPoint) 72 | { 73 | SoLoud.setLoopPoint(Handle, loopPoint); 74 | } 75 | 76 | /// 77 | public void SetLooping(bool looping) 78 | { 79 | SoLoud.setLooping(Handle, looping); 80 | } 81 | 82 | /// 83 | public void SetAutoStop(bool autoStop) 84 | { 85 | SoLoud.setAutoStop(Handle, autoStop); 86 | } 87 | 88 | /// 89 | public void SetVolume(float volume) 90 | { 91 | SoLoud.setVolume(Handle, volume); 92 | } 93 | 94 | /// 95 | public void SetDelaySamples(uint samples) 96 | { 97 | SoLoud.setDelaySamples(Handle, samples); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.VoiceGroup.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza 4 | { 5 | public readonly partial struct VoiceHandle 6 | { 7 | /// 8 | public bool IsVoiceGroup => SoLoud.isVoiceGroup(Handle); 9 | 10 | /// 11 | public bool IsVoiceGroupEmpty => SoLoud.isVoiceGroupEmpty(Handle); 12 | 13 | /// 14 | public SoLoudStatus AddVoiceToGroup(Handle voiceHandle) 15 | { 16 | return SoLoud.addVoiceToGroup(Handle, voiceHandle); 17 | } 18 | 19 | /// 20 | public SoLoudStatus DestroyVoiceGroup() 21 | { 22 | return SoLoud.destroyVoiceGroup(Handle); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LoudPizza/Handles/VoiceHandle.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza 5 | { 6 | /// 7 | /// Represents a wrapper handle around a voice handle. 8 | /// 9 | public readonly partial struct VoiceHandle 10 | { 11 | /// 12 | /// Gets the main instance of the library associated with this handle. 13 | /// 14 | public SoLoud SoLoud { get; } 15 | 16 | /// 17 | /// Gets the internal handle of the voice used by the library. 18 | /// 19 | public Handle Handle { get; } 20 | 21 | public VoiceHandle(SoLoud soLoud, Handle handle) 22 | { 23 | SoLoud = soLoud; 24 | Handle = handle; 25 | } 26 | 27 | /// 28 | public SoLoudStatus Seek(ulong samplePosition, AudioSeekFlags flags) 29 | { 30 | return SoLoud.seek(Handle, samplePosition, flags); 31 | } 32 | 33 | /// 34 | public void Stop() 35 | { 36 | SoLoud.stop(Handle); 37 | } 38 | 39 | /// 40 | /// Gets or sets whether the voice is looping. 41 | /// 42 | /// 43 | /// The audio source may not support looping (seeking). 44 | /// 45 | public bool IsLooping 46 | { 47 | get => GetLooping(); 48 | set => SetLooping(value); 49 | } 50 | 51 | /// 52 | /// Gets or sets whether the voice is protected. 53 | /// 54 | /// 55 | /// Protected voices are not stopped when many voices are playing. 56 | /// 57 | public bool IsProtected 58 | { 59 | get => GetProtectVoice(); 60 | set => SetProtectVoice(value); 61 | } 62 | 63 | /// 64 | /// Gets or sets the loop point of the voice. 65 | /// 66 | public ulong LoopPoint 67 | { 68 | get => GetLoopPoint(); 69 | set => SetLoopPoint(value); 70 | } 71 | 72 | /// 73 | /// Gets or sets whether the voice is paused. 74 | /// 75 | public bool IsPaused 76 | { 77 | get => GetPause(); 78 | set => SetPause(value); 79 | } 80 | 81 | /// 82 | /// Gets or sets the relative play speed of the voice. 83 | /// 84 | public float RelativePlaySpeed 85 | { 86 | get => GetRelativePlaySpeed(); 87 | set => SetRelativePlaySpeed(value); 88 | } 89 | 90 | /// 91 | /// Gets or sets the channel pan of the voice. 92 | /// 93 | public float Pan 94 | { 95 | get => GetPan(); 96 | set => SetPan(value); 97 | } 98 | 99 | /// Gets or sets the volume of the voice. 100 | public float Volume 101 | { 102 | get => GetVolume(); 103 | set => SetVolume(value); 104 | } 105 | 106 | /// Gets or sets the sample rate of the voice. 107 | public float SampleRate 108 | { 109 | get => GetSampleRate(); 110 | set => SetSampleRate(value); 111 | } 112 | 113 | /// 114 | /// Gets or sets whether the voice will automatically stop when it ends. 115 | /// 116 | public bool AutoStop 117 | { 118 | get => GetAutoStop(); 119 | set => SetAutoStop(value); 120 | } 121 | 122 | /// 123 | /// Gets or sets the sample position within the stream. 124 | /// 125 | /// 126 | /// The audio stream may not support seeking, or may only allow seeking forward. 127 | /// 128 | /// Failed to seek. 129 | public ulong StreamSamplePosition 130 | { 131 | get => GetStreamSamplePosition(); 132 | set 133 | { 134 | SoLoudStatus status = Seek(value, AudioSeekFlags.NonBlocking); 135 | if (status != SoLoudStatus.Ok && 136 | status != SoLoudStatus.EndOfStream) 137 | { 138 | throw new IOException(status.ToString()); 139 | } 140 | } 141 | } 142 | 143 | public static implicit operator Handle(VoiceHandle voiceHandle) 144 | { 145 | return voiceHandle.Handle; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /LoudPizza/LoudPizza.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LoudPizza/Mat3.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Numerics; 3 | 4 | namespace LoudPizza 5 | { 6 | public struct Mat3 7 | { 8 | public Vector3 M0; 9 | public Vector3 M1; 10 | public Vector3 M2; 11 | 12 | public Vector3 mul(Vector3 a) 13 | { 14 | Vector3 r; 15 | 16 | r.X = M0.X * a.X + M0.Y * a.Y + M0.Z * a.Z; 17 | r.Y = M1.X * a.X + M1.Y * a.Y + M1.Z * a.Z; 18 | r.Z = M2.X * a.X + M2.Y * a.Y + M2.Z * a.Z; 19 | 20 | return r; 21 | } 22 | 23 | public void lookatRH(Vector3 at, Vector3 up) 24 | { 25 | Vector3 z = Vector3Extensions.SafeNormalize(at); 26 | Vector3 x = Vector3Extensions.SafeNormalize(Vector3.Cross(up, z)); 27 | Vector3 y = Vector3.Cross(z, x); 28 | M0 = x; 29 | M1 = y; 30 | M2 = z; 31 | } 32 | 33 | public void lookatLH(Vector3 at, Vector3 up) 34 | { 35 | Vector3 z = Vector3Extensions.SafeNormalize(at); 36 | Vector3 x = Vector3Extensions.SafeNormalize(Vector3.Cross(up, z)); 37 | Vector3 y = Vector3.Cross(z, x); 38 | x = Vector3.Negate(x); // flip x 39 | M0 = x; 40 | M1 = y; 41 | M2 = z; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/AudioAttenuator.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace LoudPizza.Modifiers 3 | { 4 | public abstract class AudioAttenuator 5 | { 6 | public abstract float Attenuate(float distance, float minDistance, float maxDistance, float rolloffFactor); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/AudioCollider.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | using LoudPizza.Sources; 3 | 4 | namespace LoudPizza.Modifiers 5 | { 6 | public abstract class AudioCollider 7 | { 8 | /// 9 | /// Calculate volume multiplier. Assumed to return value between 0 and 1. 10 | /// 11 | public abstract float Collide(SoLoud soLoud, in AudioSourceInstance3dData audioInstance3dData); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/AudioFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Modifiers 4 | { 5 | public abstract class AudioFilter : IDisposable 6 | { 7 | public enum ParamType 8 | { 9 | Float = 0, 10 | Int, 11 | Bool, 12 | } 13 | 14 | public bool IsDisposed { get; private set; } 15 | 16 | public virtual int GetParamCount() 17 | { 18 | return 1; // there's always WET 19 | } 20 | 21 | public virtual string GetParamName(uint paramIndex) 22 | { 23 | return "Wet"; 24 | } 25 | 26 | public virtual ParamType GetParamType(uint paramIndex) 27 | { 28 | return ParamType.Float; 29 | } 30 | 31 | public virtual float GetParamMax(uint paramIndex) 32 | { 33 | return 1; 34 | } 35 | 36 | public virtual float GetParamMin(uint paramIndex) 37 | { 38 | return 0; 39 | } 40 | 41 | public abstract AudioFilterInstance CreateInstance(); 42 | 43 | protected virtual void Dispose(bool disposing) 44 | { 45 | if (!IsDisposed) 46 | { 47 | IsDisposed = true; 48 | } 49 | } 50 | 51 | public void Dispose() 52 | { 53 | Dispose(disposing: true); 54 | GC.SuppressFinalize(this); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/AudioFilterInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Modifiers 5 | { 6 | public abstract class AudioFilterInstance : IDisposable 7 | { 8 | protected uint mNumParams; 9 | protected uint mParamChanged; 10 | protected float[] mParam; 11 | protected Fader[] mParamFader; 12 | 13 | public bool IsDisposed { get; private set; } 14 | 15 | public AudioFilterInstance(int paramCount) 16 | { 17 | mNumParams = (uint)paramCount; 18 | mParam = new float[mNumParams]; 19 | mParamFader = new Fader[mNumParams]; 20 | 21 | for (uint i = 0; i < mNumParams; i++) 22 | { 23 | mParam[i] = 0; 24 | mParamFader[i].mActive = 0; 25 | } 26 | mParam[0] = 1; // set 'wet' to 1 27 | } 28 | 29 | public virtual void UpdateParams(Time time) 30 | { 31 | for (uint i = 0; i < mNumParams; i++) 32 | { 33 | if (mParamFader[i].mActive > 0) 34 | { 35 | mParamChanged |= 1u << (int)i; 36 | mParam[i] = mParamFader[i].get(time); 37 | } 38 | } 39 | } 40 | 41 | public virtual void Filter(Span buffer, uint samples, uint bufferSize, uint channels, float sampleRate, Time time) 42 | { 43 | for (uint i = 0; i < channels; i++) 44 | { 45 | FilterChannel( 46 | buffer.Slice((int)(i * bufferSize), (int)samples), sampleRate, time, i, channels); 47 | } 48 | } 49 | 50 | public abstract void FilterChannel(Span buffer, float sampleRate, Time time, uint channel, uint channels); 51 | 52 | public virtual float GetFilterParameter(int attributeId) 53 | { 54 | if ((uint)attributeId >= mNumParams) 55 | return 0; 56 | 57 | return mParam[attributeId]; 58 | } 59 | 60 | public virtual void SetFilterParameter(int attributeId, float value) 61 | { 62 | if ((uint)attributeId >= mNumParams) 63 | return; 64 | 65 | mParamFader[attributeId].mActive = 0; 66 | mParam[attributeId] = value; 67 | mParamChanged |= 1u << (int)attributeId; 68 | } 69 | 70 | public virtual void FadeFilterParameter(int attributeId, float to, Time time, Time startTime) 71 | { 72 | if ((uint)attributeId >= mNumParams || time <= 0 || to == mParam[attributeId]) 73 | return; 74 | 75 | mParamFader[attributeId].set(mParam[attributeId], to, time, startTime); 76 | } 77 | 78 | public virtual void OscillateFilterParameter(int attributeId, float from, float to, Time time, Time startTime) 79 | { 80 | if ((uint)attributeId >= mNumParams || time <= 0 || from == to) 81 | return; 82 | 83 | mParamFader[attributeId].setLFO(from, to, time, startTime); 84 | } 85 | 86 | protected virtual void Dispose(bool disposing) 87 | { 88 | if (!IsDisposed) 89 | { 90 | IsDisposed = true; 91 | } 92 | } 93 | 94 | public void Dispose() 95 | { 96 | Dispose(disposing: true); 97 | GC.SuppressFinalize(this); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/AudioResampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Modifiers 4 | { 5 | public unsafe abstract class AudioResampler 6 | { 7 | public abstract void Resample( 8 | ReadOnlySpan src0, 9 | ReadOnlySpan src1, 10 | Span dst, 11 | int srcOffset, 12 | int stepFixed); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/CatmullRomAudioResampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Modifiers 5 | { 6 | public class CatmullRomAudioResampler : AudioResampler 7 | { 8 | public static CatmullRomAudioResampler Instance { get; } = new(); 9 | 10 | public override unsafe void Resample( 11 | ReadOnlySpan src0, 12 | ReadOnlySpan src1, 13 | Span dst, 14 | int srcOffset, 15 | int stepFixed) 16 | { 17 | fixed (float* src0Ptr = src0) 18 | fixed (float* src1Ptr = src1) 19 | fixed (float* dstPtr = dst) 20 | { 21 | SoLoud.resample_catmullrom(src0Ptr, src1Ptr, dstPtr, srcOffset, dst.Length, stepFixed); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/ExponentialDistanceAudioAttenuator.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza.Modifiers 4 | { 5 | /// 6 | /// Exponential distance attenuation model. 7 | /// 8 | public class ExponentialDistanceAudioAttenuator : AudioAttenuator 9 | { 10 | public static ExponentialDistanceAudioAttenuator Instance { get; } = new ExponentialDistanceAudioAttenuator(); 11 | 12 | public override float Attenuate(float distance, float minDistance, float maxDistance, float rolloffFactor) 13 | { 14 | return SoLoud.attenuateExponentialDistance(distance, minDistance, maxDistance, rolloffFactor); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/InverseDistanceAudioAttenuator.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza.Modifiers 4 | { 5 | /// 6 | /// Inverse distance attenuation model. 7 | /// 8 | public class InverseDistanceAudioAttenuator : AudioAttenuator 9 | { 10 | public static InverseDistanceAudioAttenuator Instance { get; } = new InverseDistanceAudioAttenuator(); 11 | 12 | public override float Attenuate(float distance, float minDistance, float maxDistance, float rolloffFactor) 13 | { 14 | return SoLoud.attenuateInvDistance(distance, minDistance, maxDistance, rolloffFactor); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/LinearAudioResampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Modifiers 5 | { 6 | public class LinearAudioResampler : AudioResampler 7 | { 8 | public static LinearAudioResampler Instance { get; } = new(); 9 | 10 | public override unsafe void Resample( 11 | ReadOnlySpan src0, 12 | ReadOnlySpan src1, 13 | Span dst, 14 | int srcOffset, 15 | int stepFixed) 16 | { 17 | fixed (float* src0Ptr = src0) 18 | fixed (float* src1Ptr = src1) 19 | fixed (float* dstPtr = dst) 20 | { 21 | SoLoud.resample_linear(src0Ptr, src1Ptr, dstPtr, srcOffset, dst.Length, stepFixed); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/LinearDistanceAudioAttenuator.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza.Modifiers 4 | { 5 | /// 6 | /// Linear distance attenuation model. 7 | /// 8 | public class LinearDistanceAudioAttenuator : AudioAttenuator 9 | { 10 | public static LinearDistanceAudioAttenuator Instance { get; } = new LinearDistanceAudioAttenuator(); 11 | 12 | public override float Attenuate(float distance, float minDistance, float maxDistance, float rolloffFactor) 13 | { 14 | return SoLoud.attenuateLinearDistance(distance, minDistance, maxDistance, rolloffFactor); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LoudPizza/Modifiers/PointAudioResampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Modifiers 5 | { 6 | public class PointAudioResampler : AudioResampler 7 | { 8 | public static PointAudioResampler Instance { get; } = new(); 9 | 10 | public override unsafe void Resample( 11 | ReadOnlySpan src0, 12 | ReadOnlySpan src1, 13 | Span dst, 14 | int srcOffset, 15 | int stepFixed) 16 | { 17 | fixed (float* src0Ptr = src0) 18 | fixed (float* src1Ptr = src1) 19 | fixed (float* dstPtr = dst) 20 | { 21 | SoLoud.resample_point(src0Ptr, src1Ptr, dstPtr, srcOffset, dst.Length, stepFixed); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LoudPizza/SoLoudStatus.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace LoudPizza 3 | { 4 | public enum SoLoudStatus 5 | { 6 | /// 7 | /// No error. 8 | /// 9 | Ok = 0, 10 | 11 | /// 12 | /// Some parameter is invalid. 13 | /// 14 | InvalidParameter = 1, 15 | 16 | /// 17 | /// File not found. 18 | /// 19 | FileNotFound = 2, 20 | 21 | /// 22 | /// File found, but could not be loaded. 23 | /// 24 | FileLoadFailed = 3, 25 | 26 | /// 27 | /// DLL not found, or wrong DLL. 28 | /// 29 | DllNotFound = 4, 30 | 31 | /// 32 | /// Out of memory. 33 | /// 34 | OutOfMemory = 5, 35 | 36 | /// 37 | /// Feature not implemented. 38 | /// 39 | NotImplemented = 6, 40 | 41 | /// 42 | /// Other error. 43 | /// 44 | UnknownError = 7, 45 | 46 | EndOfStream = 8, 47 | 48 | PoolExhausted = 9, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Sources 5 | { 6 | public unsafe class AudioBuffer : AudioSource 7 | { 8 | //result loadwav(MemoryFile* aReader); 9 | //result loadogg(MemoryFile* aReader); 10 | //result loadmp3(MemoryFile* aReader); 11 | //result loadflac(MemoryFile* aReader); 12 | //result testAndLoadFile(MemoryFile* aReader); 13 | 14 | internal float[] mData; 15 | internal uint mSampleCount; 16 | 17 | public AudioBuffer(SoLoud soLoud) : base(soLoud) 18 | { 19 | } 20 | 21 | //SOLOUD_ERRORS load(const char* aFilename); 22 | //SOLOUD_ERRORS loadMem(const unsigned char* aMem, uint aLength, bool aCopy = false, bool aTakeOwnership = true); 23 | //SOLOUD_ERRORS loadFile(File* aFile); 24 | 25 | public SoLoudStatus LoadRawWave8(ReadOnlySpan memory, float sampleRate, uint channels) 26 | { 27 | if (memory.Length == 0 || sampleRate <= 0 || channels < 1) 28 | return SoLoudStatus.InvalidParameter; 29 | 30 | DeleteData(); 31 | float[] data = new float[memory.Length]; 32 | mData = data; 33 | mSampleCount = (uint)memory.Length / channels; 34 | mChannels = channels; 35 | mBaseSamplerate = sampleRate; 36 | for (int i = 0; i < data.Length; i++) 37 | { 38 | data[i] = (memory[i] - 128) / (float)0x80; 39 | } 40 | return SoLoudStatus.Ok; 41 | } 42 | 43 | public SoLoudStatus LoadRawWave16(ReadOnlySpan memory, float sampleRate, uint channels) 44 | { 45 | if (memory.Length == 0 || sampleRate <= 0 || channels < 1) 46 | return SoLoudStatus.InvalidParameter; 47 | 48 | DeleteData(); 49 | float[] data = new float[memory.Length]; 50 | mData = data; 51 | mSampleCount = (uint)memory.Length / channels; 52 | mChannels = channels; 53 | mBaseSamplerate = sampleRate; 54 | for (int i = 0; i < data.Length; i++) 55 | { 56 | data[i] = memory[i] / (float)0x8000; 57 | } 58 | return SoLoudStatus.Ok; 59 | } 60 | 61 | public SoLoudStatus LoadRawWave(ReadOnlySpan memory, float sampleRate, uint channels) 62 | { 63 | if (memory.Length == 0 || sampleRate <= 0 || channels < 1) 64 | return SoLoudStatus.InvalidParameter; 65 | 66 | DeleteData(); 67 | mData = memory.ToArray(); 68 | 69 | mSampleCount = (uint)memory.Length / channels; 70 | mChannels = channels; 71 | mBaseSamplerate = sampleRate; 72 | return SoLoudStatus.Ok; 73 | } 74 | 75 | public override AudioBufferInstance CreateInstance() 76 | { 77 | return new AudioBufferInstance(this); 78 | } 79 | 80 | public Time GetLength() 81 | { 82 | if (mBaseSamplerate == 0) 83 | return 0; 84 | return mSampleCount / (double)mBaseSamplerate; 85 | } 86 | 87 | private void DeleteData() 88 | { 89 | Stop(); 90 | //delete[] mData; 91 | } 92 | 93 | protected override void Dispose(bool disposing) 94 | { 95 | DeleteData(); 96 | 97 | base.Dispose(disposing); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioBufferInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace LoudPizza.Sources 5 | { 6 | public unsafe class AudioBufferInstance : AudioSourceInstance 7 | { 8 | public new AudioBuffer Source => Unsafe.As(base.Source); 9 | 10 | protected uint mOffset; 11 | 12 | public AudioBufferInstance(AudioBuffer source) : base(source) 13 | { 14 | mOffset = 0; 15 | } 16 | 17 | public override uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 18 | { 19 | if (Source.mData == null) 20 | return 0; 21 | 22 | uint dataleft = Source.mSampleCount - mOffset; 23 | uint copylen = dataleft; 24 | if (copylen > samplesToRead) 25 | copylen = samplesToRead; 26 | 27 | for (uint i = 0; i < Channels; i++) 28 | { 29 | Span destination = buffer.Slice((int)(i * channelStride), (int)copylen); 30 | Span source = Source.mData.AsSpan((int)(mOffset + i * Source.mSampleCount), (int)copylen); 31 | 32 | source.CopyTo(destination); 33 | } 34 | 35 | mOffset += copylen; 36 | return copylen; 37 | } 38 | 39 | /// 40 | /// Seek to certain place in the buffer. 41 | /// 42 | /// 43 | public override SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition) 44 | { 45 | long offset = (long)(samplePosition - mStreamPosition); 46 | if (offset <= 0) 47 | { 48 | mOffset = 0; 49 | mStreamPosition = 0; 50 | offset = (long)samplePosition; 51 | } 52 | ulong samples_to_discard = (ulong)offset; 53 | 54 | uint dataleft = Source.mSampleCount - mOffset; 55 | uint copylen = dataleft; 56 | if (copylen > samples_to_discard) 57 | copylen = (uint)samples_to_discard; 58 | 59 | mOffset += copylen; 60 | mStreamPosition += copylen; 61 | 62 | resultPosition = mStreamPosition; 63 | return SoLoudStatus.Ok; 64 | } 65 | 66 | public override bool HasEnded() 67 | { 68 | return (mFlags & Flags.Looping) == 0 && mOffset >= Source.mSampleCount; 69 | } 70 | 71 | public override bool CanSeek() 72 | { 73 | return true; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | using LoudPizza.Core; 5 | using LoudPizza.Modifiers; 6 | 7 | namespace LoudPizza.Sources 8 | { 9 | public unsafe class AudioBus : AudioSource, IAudioBus 10 | { 11 | private AudioBusInstance? mInstance; 12 | internal Handle mChannelHandle; 13 | private AudioResampler mResampler; 14 | 15 | public AudioBus(SoLoud soLoud) : base(soLoud) 16 | { 17 | mChannelHandle = default; 18 | mInstance = null; 19 | mChannels = 2; 20 | mResampler = SoLoud.DefaultResampler; 21 | } 22 | 23 | /// 24 | public override AudioBusInstance CreateInstance() 25 | { 26 | if (mChannelHandle != default) 27 | { 28 | Stop(); 29 | mChannelHandle = default; 30 | mInstance = null; 31 | } 32 | mInstance = new AudioBusInstance(this); 33 | return mInstance; 34 | } 35 | 36 | /// 37 | public override void SetFilter(int filterId, AudioFilter? filter) 38 | { 39 | base.SetFilter(filterId, filter); 40 | 41 | if (mInstance != null) 42 | { 43 | lock (SoLoud.mAudioThreadMutex) 44 | { 45 | mInstance.SetFilter(filterId, filter?.CreateInstance()); 46 | } 47 | } 48 | } 49 | 50 | /// 51 | public VoiceHandle Play(AudioSource source, float volume = -1.0f, float pan = 0.0f, bool paused = false) 52 | { 53 | Handle busHandle = GetBusHandle(); 54 | if (busHandle == default) 55 | { 56 | return default; 57 | } 58 | 59 | Handle handle = SoLoud.play(source, volume, pan, paused, busHandle); 60 | return new VoiceHandle(SoLoud, handle); 61 | } 62 | 63 | /// 64 | public VoiceHandle PlayClocked(AudioSource source, Time soundTime, float volume = -1.0f, float pan = 0.0f) 65 | { 66 | Handle busHandle = GetBusHandle(); 67 | if (busHandle == default) 68 | { 69 | return default; 70 | } 71 | 72 | Handle handle = SoLoud.playClocked(soundTime, source, volume, pan, busHandle); 73 | return new VoiceHandle(SoLoud, handle); 74 | } 75 | 76 | /// 77 | public VoiceHandle Play3D( 78 | AudioSource source, 79 | Vector3 position, 80 | Vector3 velocity = default, 81 | float volume = -1.0f, 82 | bool paused = false) 83 | { 84 | Handle busHandle = GetBusHandle(); 85 | if (busHandle == default) 86 | { 87 | return default; 88 | } 89 | 90 | Handle handle = SoLoud.play3d(source, position, velocity, volume, paused, busHandle); 91 | return new VoiceHandle(SoLoud, handle); 92 | } 93 | 94 | /// 95 | public VoiceHandle PlayClocked3D( 96 | AudioSource source, 97 | Time soundTime, 98 | Vector3 position, 99 | Vector3 velocity = default, 100 | float volume = -1.0f) 101 | { 102 | Handle busHandle = GetBusHandle(); 103 | if (busHandle == default) 104 | { 105 | return default; 106 | } 107 | 108 | Handle handle = SoLoud.play3dClocked(soundTime, source, position, velocity, volume, busHandle); 109 | return new VoiceHandle(SoLoud, handle); 110 | } 111 | 112 | /// 113 | public VoiceHandle PlayBackground(AudioSource source, float volume = 1.0f, bool paused = false) 114 | { 115 | Handle busHandle = GetBusHandle(); 116 | if (busHandle == default) 117 | { 118 | return default; 119 | } 120 | 121 | Handle handle = SoLoud.playBackground(source, volume, paused, busHandle); 122 | return new VoiceHandle(SoLoud, handle); 123 | } 124 | 125 | /// 126 | /// Set number of channels for the bus (default 2). 127 | /// 128 | public SoLoudStatus SetChannels(uint channels) 129 | { 130 | if (channels == 0 || channels == 3 || channels == 5 || channels == 7 || channels > SoLoud.MaxChannels) 131 | return SoLoudStatus.InvalidParameter; 132 | 133 | mChannels = channels; 134 | return SoLoudStatus.Ok; 135 | } 136 | 137 | /// 138 | public void SetVisualizationEnabled(bool enable) 139 | { 140 | if (enable) 141 | { 142 | mFlags |= Flags.VisualizationData; 143 | } 144 | else 145 | { 146 | mFlags &= ~Flags.VisualizationData; 147 | } 148 | } 149 | 150 | /// 151 | public bool GetVisualizationEnabled() 152 | { 153 | return (mFlags & Flags.VisualizationData) != 0; 154 | } 155 | 156 | /// 157 | public void AnnexSound(Handle voiceHandle) 158 | { 159 | Handle busHandle = GetBusHandle(); 160 | 161 | SoLoud.AnnexSound(voiceHandle, busHandle); 162 | } 163 | 164 | /// 165 | [SkipLocalsInit] 166 | public void CalcFFT(out Buffer256 data) 167 | { 168 | float* temp = stackalloc float[1024]; 169 | 170 | if (mInstance != null && SoLoud != null) 171 | { 172 | lock (SoLoud.mAudioThreadMutex) 173 | { 174 | for (int i = 0; i < 256; i++) 175 | { 176 | temp[i * 2] = mInstance.mVisualizationWaveData[i]; 177 | temp[i * 2 + 1] = 0; 178 | temp[i + 512] = 0; 179 | temp[i + 768] = 0; 180 | } 181 | } 182 | 183 | FFT.fft1024(temp); 184 | 185 | for (int i = 0; i < 256; i++) 186 | { 187 | float real = temp[i * 2]; 188 | float imag = temp[i * 2 + 1]; 189 | data[i] = MathF.Sqrt(real * real + imag * imag); 190 | } 191 | } 192 | } 193 | 194 | /// 195 | public void GetWave(out Buffer256 data) 196 | { 197 | if (mInstance != null && SoLoud != null) 198 | { 199 | lock (SoLoud.mAudioThreadMutex) 200 | { 201 | data = mInstance.mVisualizationWaveData; 202 | } 203 | } 204 | else 205 | { 206 | data = default; 207 | } 208 | } 209 | 210 | /// 211 | public float GetApproximateVolume(uint channel) 212 | { 213 | if (channel > mChannels) 214 | return 0; 215 | float vol = 0; 216 | if (mInstance != null && SoLoud != null) 217 | { 218 | lock (SoLoud.mAudioThreadMutex) 219 | { 220 | vol = mInstance.mVisualizationChannelVolume[channel]; 221 | } 222 | } 223 | return vol; 224 | } 225 | 226 | /// 227 | public void GetApproximateVolumes(out ChannelBuffer buffer) 228 | { 229 | if (mInstance != null && SoLoud != null) 230 | { 231 | lock (SoLoud.mAudioThreadMutex) 232 | { 233 | buffer = mInstance.mVisualizationChannelVolume; 234 | return; 235 | } 236 | } 237 | 238 | buffer = default; 239 | } 240 | 241 | /// 242 | public int GetActiveVoiceCount() 243 | { 244 | Handle busHandle = GetBusHandle(); 245 | lock (SoLoud.mAudioThreadMutex) 246 | { 247 | int count = 0; 248 | foreach (AudioSourceInstance? voice in SoLoud.mVoice) 249 | { 250 | if (voice != null && voice.mBusHandle == busHandle) 251 | count++; 252 | } 253 | return count; 254 | } 255 | } 256 | 257 | /// 258 | public AudioResampler GetResampler() 259 | { 260 | return mResampler; 261 | } 262 | 263 | /// 264 | public void SetResampler(AudioResampler resampler) 265 | { 266 | mResampler = resampler ?? throw new ArgumentNullException(nameof(resampler)); 267 | } 268 | 269 | /// 270 | public Handle GetBusHandle() 271 | { 272 | SoLoud s = SoLoud; 273 | if (mInstance == null || s == null) 274 | { 275 | return default; 276 | } 277 | 278 | // Find the channel the bus is playing on to calculate handle.. 279 | ReadOnlySpan highVoices = s.mVoice.AsSpan(0, s.mHighestVoice); 280 | for (int i = 0; mChannelHandle == default && i < highVoices.Length; i++) 281 | { 282 | if (highVoices[i] == mInstance) 283 | { 284 | mChannelHandle = s.getHandleFromVoice_internal(i); 285 | } 286 | } 287 | 288 | return mChannelHandle; 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioBusInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using LoudPizza.Core; 4 | 5 | namespace LoudPizza.Sources 6 | { 7 | public unsafe class AudioBusInstance : AudioSourceInstance 8 | { 9 | public new AudioBus Source => Unsafe.As(base.Source); 10 | 11 | protected uint mScratchSize; 12 | protected AlignedFloatBuffer mScratch; 13 | 14 | /// 15 | /// Approximate volume for channels. 16 | /// 17 | internal ChannelBuffer mVisualizationChannelVolume; 18 | 19 | /// 20 | /// Mono-mixed wave data for visualization and for visualization FFT input. 21 | /// 22 | internal Buffer256 mVisualizationWaveData; 23 | 24 | public AudioBusInstance(AudioBus source) : base(source) 25 | { 26 | mFlags |= Flags.Protected | Flags.InaudibleTick; 27 | mVisualizationChannelVolume = default; 28 | mVisualizationWaveData = default; 29 | mScratchSize = SoLoud.SampleGranularity; 30 | mScratch.init(mScratchSize * SoLoud.MaxChannels, SoLoud.VECTOR_SIZE); 31 | } 32 | 33 | /// 34 | public override uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 35 | { 36 | AudioBus mParent = Source; 37 | uint channels = Channels; 38 | 39 | Span bufferSlice = buffer.Slice(0, (int)(channelStride * channels)); 40 | 41 | Handle handle = mParent.mChannelHandle; 42 | if (handle == default) 43 | { 44 | // Avoid reuse of scratch data if this bus hasn't played anything yet 45 | bufferSlice.Clear(); 46 | 47 | return samplesToRead; 48 | } 49 | 50 | SoLoud s = mParent.SoLoud; 51 | s.mixBus_internal( 52 | bufferSlice, samplesToRead, channelStride, mScratch.mData, handle, mSamplerate, channels, mParent.GetResampler()); 53 | 54 | if ((mParent.mFlags & AudioSource.Flags.VisualizationData) != 0) 55 | { 56 | fixed (float* aBufferPtr = bufferSlice) 57 | { 58 | mVisualizationChannelVolume = default; 59 | 60 | if (samplesToRead > 255) 61 | { 62 | for (uint i = 0; i < 256; i++) 63 | { 64 | mVisualizationWaveData[i] = 0; 65 | for (uint j = 0; j < channels; j++) 66 | { 67 | float sample = aBufferPtr[i + channelStride * j]; 68 | float absvol = MathF.Abs(sample); 69 | if (absvol > mVisualizationChannelVolume[j]) 70 | mVisualizationChannelVolume[j] = absvol; 71 | mVisualizationWaveData[i] += sample; 72 | } 73 | } 74 | } 75 | else 76 | { 77 | // Very unlikely failsafe branch 78 | for (uint i = 0; i < 256; i++) 79 | { 80 | mVisualizationWaveData[i] = 0; 81 | for (uint j = 0; j < channels; j++) 82 | { 83 | float sample = aBufferPtr[(i % samplesToRead) + channelStride * j]; 84 | float absvol = MathF.Abs(sample); 85 | if (absvol > mVisualizationChannelVolume[j]) 86 | mVisualizationChannelVolume[j] = absvol; 87 | mVisualizationWaveData[i] += sample; 88 | } 89 | } 90 | } 91 | } 92 | } 93 | return samplesToRead; 94 | } 95 | 96 | /// 97 | /// Busses are not seekable. 98 | /// 99 | /// Always . 100 | public override SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition) 101 | { 102 | resultPosition = 0; 103 | return SoLoudStatus.NotImplemented; 104 | } 105 | 106 | /// 107 | /// Busses never stop for fear of going under 50mph. 108 | /// 109 | public override bool HasEnded() 110 | { 111 | return false; 112 | } 113 | 114 | /// 115 | /// Busses are not seekable. 116 | /// 117 | /// Always . 118 | public override bool CanSeek() 119 | { 120 | return false; 121 | } 122 | 123 | protected override void Dispose(bool disposing) 124 | { 125 | AudioBus mParent = Source; 126 | SoLoud s = mParent.SoLoud; 127 | 128 | ReadOnlySpan highVoices = s.mVoice.AsSpan(0, s.mHighestVoice); 129 | for (int i = 0; i < highVoices.Length; i++) 130 | { 131 | AudioSourceInstance? voice = highVoices[i]; 132 | if (voice != null && voice.mBusHandle == mParent.mChannelHandle) 133 | { 134 | s.stopVoice_internal(i); 135 | } 136 | } 137 | 138 | base.Dispose(disposing); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Sources 5 | { 6 | public class AudioQueue : AudioSource 7 | { 8 | internal uint mReadIndex; 9 | internal uint mWriteIndex; 10 | internal uint mCount; 11 | internal IAudioStream?[] mSource; 12 | internal AudioQueueInstance? mInstance; 13 | internal Handle mQueueHandle; 14 | 15 | public AudioQueue(SoLoud soLoud, int capacity) : base(soLoud) 16 | { 17 | mQueueHandle = default; 18 | mInstance = null; 19 | mReadIndex = 0; 20 | mWriteIndex = 0; 21 | mCount = 0; 22 | mSource = new IAudioStream[capacity]; 23 | } 24 | 25 | public override AudioQueueInstance CreateInstance() 26 | { 27 | if (mInstance != null) 28 | { 29 | Stop(); 30 | mInstance = null; 31 | } 32 | mInstance = new AudioQueueInstance(this); 33 | return mInstance; 34 | } 35 | 36 | /// 37 | /// Get whether the queue can currently play a audio. 38 | /// 39 | public SoLoudStatus CanPlay() 40 | { 41 | if (SoLoud == null) 42 | return SoLoudStatus.InvalidParameter; 43 | 44 | Handle queueHandle = FindQueueHandle(); 45 | if (queueHandle == default) 46 | return SoLoudStatus.InvalidParameter; 47 | 48 | if (mCount >= mSource.Length) 49 | return SoLoudStatus.OutOfMemory; 50 | 51 | return SoLoudStatus.Ok; 52 | } 53 | 54 | /// 55 | /// Play the audio source through the queue. 56 | /// 57 | public SoLoudStatus Play(AudioSource source) 58 | { 59 | SoLoudStatus status = CanPlay(); 60 | if (status == SoLoudStatus.Ok) 61 | { 62 | AudioSourceInstance instance = source.CreateInstance(); 63 | instance.Initialize(0); 64 | Enqueue(instance); 65 | } 66 | return status; 67 | } 68 | 69 | /// 70 | /// Play the audio stream through the queue. 71 | /// 72 | public SoLoudStatus Play(IAudioStream stream) 73 | { 74 | SoLoudStatus status = CanPlay(); 75 | if (status == SoLoudStatus.Ok) 76 | { 77 | Enqueue(stream); 78 | } 79 | return SoLoudStatus.Ok; 80 | } 81 | 82 | private void Enqueue(IAudioStream stream) 83 | { 84 | lock (SoLoud.mAudioThreadMutex) 85 | { 86 | mSource[mWriteIndex] = stream; 87 | mWriteIndex = (mWriteIndex + 1) % (uint)mSource.Length; 88 | mCount++; 89 | } 90 | } 91 | 92 | /// 93 | /// Get the number of audio sources queued for replay. 94 | /// 95 | public uint GetQueueCount() 96 | { 97 | if (SoLoud == null) 98 | { 99 | return 0; 100 | } 101 | 102 | lock (SoLoud.mAudioThreadMutex) 103 | { 104 | uint count = mCount; 105 | return count; 106 | } 107 | } 108 | 109 | /// 110 | /// Get whether the given audio source currently playing. 111 | /// 112 | public bool IsCurrentlyPlaying(AudioSource source) 113 | { 114 | if (SoLoud == null || mCount == 0) 115 | return false; 116 | 117 | lock (SoLoud.mAudioThreadMutex) 118 | { 119 | if (mSource[mReadIndex] is AudioSourceInstance audioInstance) 120 | { 121 | bool res = audioInstance.Source == source; 122 | return res; 123 | } 124 | return false; 125 | } 126 | } 127 | 128 | /// 129 | /// Get whether the given audio stream currently playing. 130 | /// 131 | public bool IsCurrentlyPlaying(IAudioStream stream) 132 | { 133 | if (SoLoud == null || mCount == 0) 134 | return false; 135 | 136 | lock (SoLoud.mAudioThreadMutex) 137 | { 138 | bool res = mSource[mReadIndex]! == stream; 139 | return res; 140 | } 141 | } 142 | 143 | /// 144 | /// Set params by reading them from the given audio source. 145 | /// 146 | public SoLoudStatus SetParamsFromAudioSource(AudioSource source) 147 | { 148 | mChannels = source.mChannels; 149 | mBaseSamplerate = source.mBaseSamplerate; 150 | 151 | return SoLoudStatus.Ok; 152 | } 153 | 154 | /// 155 | /// Set params manually. 156 | /// 157 | public SoLoudStatus SetParams(float sampleRate, uint channels = 2) 158 | { 159 | if (channels < 1 || channels > SoLoud.MaxChannels) 160 | return SoLoudStatus.InvalidParameter; 161 | 162 | mChannels = channels; 163 | mBaseSamplerate = sampleRate; 164 | return SoLoudStatus.Ok; 165 | } 166 | 167 | /// 168 | /// Find the channel the queue is playing on to calculate handle. 169 | /// 170 | internal Handle FindQueueHandle() 171 | { 172 | SoLoud s = SoLoud; 173 | ReadOnlySpan highVoices = s.mVoice.AsSpan(0, s.mHighestVoice); 174 | for (int i = 0; mQueueHandle == default && i < highVoices.Length; i++) 175 | { 176 | if (highVoices[i] == mInstance) 177 | { 178 | mQueueHandle = s.getHandleFromVoice_internal(i); 179 | } 180 | } 181 | return mQueueHandle; 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioQueueInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace LoudPizza.Sources 5 | { 6 | public class AudioQueueInstance : AudioSourceInstance 7 | { 8 | public new AudioQueue Source => Unsafe.As(base.Source); 9 | 10 | public AudioQueueInstance(AudioQueue source) : base(source) 11 | { 12 | mFlags |= Flags.Protected; 13 | } 14 | 15 | /// 16 | public override uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 17 | { 18 | AudioQueue parent = Source; 19 | if (parent.mCount == 0) 20 | { 21 | return 0; 22 | } 23 | 24 | uint copycount = samplesToRead; 25 | uint copyofs = 0; 26 | while (copycount != 0 && parent.mCount != 0) 27 | { 28 | IAudioStream source = parent.mSource[parent.mReadIndex]!; 29 | uint readcount = source.GetAudio(buffer.Slice((int)copyofs), copycount, channelStride); 30 | copyofs += readcount; 31 | copycount -= readcount; 32 | if (source.HasEnded()) 33 | { 34 | source.Dispose(); 35 | parent.mSource[parent.mReadIndex] = null; 36 | parent.mReadIndex = (parent.mReadIndex + 1) % (uint)parent.mSource.Length; 37 | parent.mCount--; 38 | mLoopCount++; 39 | } 40 | } 41 | return copyofs; 42 | } 43 | 44 | /// 45 | public override SoLoudStatus Seek(ulong aSamplePosition, Span mScratch, AudioSeekFlags flags, out ulong resultPosition) 46 | { 47 | resultPosition = 0; 48 | return SoLoudStatus.NotImplemented; 49 | } 50 | 51 | /// 52 | public override bool HasEnded() 53 | { 54 | return mLoopCount != 0 && Source.mCount == 0; 55 | } 56 | 57 | /// 58 | /// Queues are not seekable. 59 | /// 60 | /// Always . 61 | public override bool CanSeek() 62 | { 63 | return false; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioSourceInstance3dData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using LoudPizza.Core; 4 | using LoudPizza.Modifiers; 5 | 6 | namespace LoudPizza.Sources 7 | { 8 | public struct AudioSourceInstance3dData 9 | { 10 | /// 11 | /// Set settings from an . 12 | /// 13 | public void init(AudioSource aSource) 14 | { 15 | m3dAttenuationRolloff = aSource.m3dAttenuationRolloff; 16 | m3dDopplerFactor = aSource.m3dDopplerFactor; 17 | m3dMaxDistance = aSource.m3dMaxDistance; 18 | m3dMinDistance = aSource.m3dMinDistance; 19 | mCollider = aSource.mCollider; 20 | mColliderData = aSource.mColliderData; 21 | mAttenuator = aSource.mAttenuator; 22 | m3dVolume = 1.0f; 23 | mDopplerValue = 1.0f; 24 | 25 | mFlags = 0; 26 | mHandle = default; 27 | m3dVelocity = default; 28 | m3dPosition = default; 29 | mChannelVolume = default; 30 | } 31 | 32 | /// 33 | /// 3D position. 34 | /// 35 | public Vector3 m3dPosition; 36 | 37 | /// 38 | /// 3D velocity. 39 | /// 40 | public Vector3 m3dVelocity; 41 | 42 | // 3D cone direction 43 | /* 44 | float m3dConeDirection[3]; 45 | // 3D cone inner angle 46 | float m3dConeInnerAngle; 47 | // 3D cone outer angle 48 | float m3dConeOuterAngle; 49 | // 3D cone outer volume multiplier 50 | float m3dConeOuterVolume; 51 | */ 52 | 53 | /// 54 | /// 3D min distance. 55 | /// 56 | public float m3dMinDistance; 57 | 58 | /// 59 | /// 3D max distance. 60 | /// 61 | public float m3dMaxDistance; 62 | 63 | /// 64 | /// 3D attenuation rolloff factor. 65 | /// 66 | public float m3dAttenuationRolloff; 67 | 68 | /// 69 | /// 3D doppler factor. 70 | /// 71 | public float m3dDopplerFactor; 72 | 73 | /// 74 | /// Custom audio collider object. 75 | /// 76 | public AudioCollider? mCollider; 77 | 78 | /// 79 | /// Custom audio attenuator object. 80 | /// 81 | public AudioAttenuator? mAttenuator; 82 | 83 | /// 84 | /// User data related to audio collider. 85 | /// 86 | public IntPtr mColliderData; 87 | 88 | /// 89 | /// Doppler sample rate multiplier. 90 | /// 91 | public float mDopplerValue; 92 | 93 | /// 94 | /// Overall 3D volume. 95 | /// 96 | public float m3dVolume; 97 | 98 | /// 99 | /// Channel volume. 100 | /// 101 | public ChannelBuffer mChannelVolume; 102 | 103 | /// 104 | /// Copy of flags. 105 | /// 106 | public AudioSourceInstance.Flags mFlags; 107 | 108 | /// 109 | /// Latest handle for this voice. 110 | /// 111 | public Handle mHandle; 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoudPizza.Core; 3 | 4 | namespace LoudPizza.Sources 5 | { 6 | public class AudioStream : AudioSource 7 | { 8 | private IAudioStream? _audioStream; 9 | private AudioStreamInstance? _instance; 10 | 11 | public uint mSampleCount; 12 | 13 | public AudioStream(SoLoud soLoud, IAudioStream audioStream) : base(soLoud) 14 | { 15 | _audioStream = audioStream ?? throw new ArgumentNullException(nameof(audioStream)); 16 | 17 | mBaseSamplerate = _audioStream.SampleRate; 18 | } 19 | 20 | public override AudioStreamInstance CreateInstance() 21 | { 22 | if (_instance != null) 23 | { 24 | Stop(); 25 | _instance = null; 26 | } 27 | 28 | if (_audioStream == null) 29 | { 30 | ThrowObjectDisposed(); 31 | } 32 | 33 | _instance = new AudioStreamInstance(this, _audioStream); 34 | _audioStream = null; 35 | return _instance; 36 | } 37 | 38 | public Time GetLength() 39 | { 40 | if (mBaseSamplerate == 0) 41 | return 0; 42 | return mSampleCount / mBaseSamplerate; 43 | } 44 | 45 | internal void ReturnAudioStream(AudioStreamInstance instance) 46 | { 47 | if (_instance != instance) 48 | { 49 | throw new InvalidOperationException("The given instance does not originate from this source."); 50 | } 51 | 52 | _audioStream = instance.DataStream; 53 | } 54 | 55 | protected override void Dispose(bool disposing) 56 | { 57 | base.Dispose(disposing); 58 | 59 | _audioStream?.Dispose(); 60 | _audioStream = null; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /LoudPizza/Sources/AudioStreamInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using LoudPizza.Sources.Streaming; 4 | 5 | namespace LoudPizza.Sources 6 | { 7 | public unsafe class AudioStreamInstance : AudioSourceInstance 8 | { 9 | private IRelativePlaybackRateChangeListener? _playbackRateChangeListener; 10 | 11 | public new AudioStream Source => Unsafe.As(base.Source); 12 | 13 | /// 14 | /// Get the audio stream that this instance wraps around. 15 | /// 16 | public IAudioStream DataStream { get; private set; } 17 | 18 | /// 19 | public override uint Channels => DataStream.Channels; 20 | 21 | /// 22 | public override float SampleRate => DataStream.SampleRate; 23 | 24 | public AudioStreamInstance(AudioStream source, IAudioStream dataStream) : base(source) 25 | { 26 | DataStream = dataStream; 27 | _playbackRateChangeListener = DataStream as IRelativePlaybackRateChangeListener; 28 | } 29 | 30 | /// 31 | public override uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 32 | { 33 | _playbackRateChangeListener?.RelativePlaybackRateChanged(RelativePlaybackSpeed); 34 | 35 | return DataStream.GetAudio(buffer, samplesToRead, channelStride); 36 | } 37 | 38 | /// 39 | public override SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition) 40 | { 41 | SoLoudStatus status = DataStream.Seek(samplePosition, scratch, flags, out resultPosition); 42 | mStreamPosition = resultPosition; 43 | if (status == SoLoudStatus.Ok || 44 | status == SoLoudStatus.EndOfStream) 45 | { 46 | mStreamPosition = resultPosition; 47 | } 48 | return status; 49 | } 50 | 51 | /// 52 | public override bool HasEnded() 53 | { 54 | return DataStream.HasEnded(); 55 | } 56 | 57 | /// 58 | public override bool CanSeek() 59 | { 60 | return DataStream.CanSeek(); 61 | } 62 | 63 | protected override void Dispose(bool disposing) 64 | { 65 | if (!IsDisposed) 66 | { 67 | Source.ReturnAudioStream(this); 68 | DataStream = null!; 69 | } 70 | base.Dispose(disposing); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LoudPizza/Sources/IAudioBus.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using LoudPizza.Core; 3 | using LoudPizza.Modifiers; 4 | 5 | namespace LoudPizza.Sources 6 | { 7 | public interface IAudioBus 8 | { 9 | /// 10 | /// Start playing a sound from an audio source. 11 | /// 12 | /// Negative volume means to use the default from . 13 | /// The voice handle, which can be ignored or used to alter the playing sound's parameters. 14 | VoiceHandle Play(AudioSource source, float volume = -1.0f, float pan = 0.0f, bool paused = false); 15 | 16 | /// 17 | /// Start playing a sound from an audio source, delayed in relation to other sounds called via this function. 18 | /// 19 | /// Negative volume means to use the default from . 20 | /// The voice handle, which can be ignored or used to alter the playing sound's parameters. 21 | VoiceHandle PlayClocked(AudioSource source, Time soundTime, float volume = -1.0f, float pan = 0.0f); 22 | 23 | /// 24 | /// Start playing a sound without any panning. 25 | /// 26 | /// 27 | /// It will be played at full volume. 28 | /// 29 | /// Negative volume means to use the default from . 30 | /// The voice handle, which can be ignored or used to alter the playing sound's parameters. 31 | VoiceHandle PlayBackground(AudioSource source, float volume = 1.0f, bool paused = false); 32 | 33 | /// 34 | /// Start playing a 3D sound from an audio source. 35 | /// 36 | /// Negative volume means to use the default from . 37 | /// The voice handle, which can be ignored or used to alter the playing sound's parameters. 38 | VoiceHandle Play3D( 39 | AudioSource source, 40 | Vector3 position, 41 | Vector3 velocity = default, 42 | float volume = -1.0f, 43 | bool paused = false); 44 | 45 | /// 46 | /// Start playing a 3D sound from an audio source, delayed in relation to other sounds called via this function. 47 | /// 48 | /// Negative volume means to use the default from . 49 | /// The voice handle, which can be ignored or used to alter the playing sound's parameters. 50 | VoiceHandle PlayClocked3D( 51 | AudioSource source, 52 | Time soundTime, 53 | Vector3 position, 54 | Vector3 velocity = default, 55 | float volume = -1.0f); 56 | 57 | /// 58 | /// Enable or disable visualization data gathering. 59 | /// 60 | public void SetVisualizationEnabled(bool enable); 61 | 62 | /// 63 | /// Get whether visualization data gathering is enabled. 64 | /// 65 | public bool GetVisualizationEnabled(); 66 | 67 | /// 68 | /// Move a live sound to this bus. 69 | /// 70 | void AnnexSound(Handle voiceHandle); 71 | 72 | /// 73 | /// Calculate and get 256 floats of FFT data for visualization. 74 | /// 75 | /// 76 | /// Visualization has to be enabled before use. 77 | /// 78 | void CalcFFT(out Buffer256 data); 79 | 80 | /// 81 | /// Get 256 floats of wave data for visualization. 82 | /// 83 | /// 84 | /// Visualization has to be enabled before use. 85 | /// 86 | void GetWave(out Buffer256 data); 87 | 88 | /// 89 | /// Get approximate volume for output channel for visualization. 90 | /// 91 | /// 92 | /// Visualization has to be enabled before use. 93 | /// 94 | float GetApproximateVolume(uint channel); 95 | 96 | /// 97 | /// Get approximate volumes for all output channels for visualization. 98 | /// 99 | /// 100 | /// Visualization has to be enabled before use. 101 | /// 102 | void GetApproximateVolumes(out ChannelBuffer buffer); 103 | 104 | /// 105 | /// Get the current number of busy voices. 106 | /// 107 | int GetActiveVoiceCount(); 108 | 109 | /// 110 | /// Get current the resampler for this bus. 111 | /// 112 | AudioResampler GetResampler(); 113 | 114 | /// 115 | /// Set the resampler for this bus. 116 | /// 117 | void SetResampler(AudioResampler resampler); 118 | 119 | /// 120 | /// Get the handle for this playing bus. 121 | /// 122 | Handle GetBusHandle(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /LoudPizza/Sources/IAudioStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Sources 4 | { 5 | public interface IAudioStream : IDisposable 6 | { 7 | /// 8 | /// Gets the amount of channels in the stream. 9 | /// 10 | uint Channels { get; } 11 | 12 | /// 13 | /// Gets the sample rate of the stream. 14 | /// 15 | float SampleRate { get; } 16 | 17 | /// 18 | /// Gets the relative playback speed of the stream. 19 | /// 20 | float RelativePlaybackSpeed { get; } 21 | 22 | /// 23 | /// Reads non-interleaved samples into the specified buffer. 24 | /// 25 | /// 26 | /// The buffer to read the samples into, 27 | /// of which length must be a multiple of . 28 | /// 29 | /// The amount of samples to read per channel. 30 | /// The offset in values between each channel in the buffer. 31 | /// The amount of samples read. 32 | /// 33 | /// The is not interleaved 34 | /// (Left, Left, Left, Right, Right, Right). 35 | /// 36 | uint GetAudio(Span buffer, uint samplesToRead, uint channelStride); 37 | 38 | /// 39 | /// Get whether the has stream ended. 40 | /// 41 | bool HasEnded(); 42 | 43 | /// 44 | /// Get whether the stream is seekable both backwards and forwards. 45 | /// 46 | bool CanSeek(); 47 | 48 | /// 49 | /// Attempt to seek to the given position in the stream. 50 | /// 51 | /// The target position to seek to. 52 | /// Scratch buffer for seek implementations. 53 | /// Flags that affect seek behavior. 54 | /// The position that the stream could seek to. 55 | /// 56 | /// The status of the operation. 57 | /// and are considered non-errors. 58 | /// 59 | SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.AudioBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Sources.Streaming 4 | { 5 | public partial class AudioStreamer 6 | { 7 | internal class AudioBuffer 8 | { 9 | public float[] Buffer; 10 | public uint Length; 11 | public uint Start; 12 | 13 | public Span AsSpan() 14 | { 15 | return Buffer.AsSpan(); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.ReadWorker.cs: -------------------------------------------------------------------------------- 1 | namespace LoudPizza.Sources.Streaming 2 | { 3 | public partial class AudioStreamer 4 | { 5 | private sealed class ReadWorker : Worker 6 | { 7 | public ReadWorker(AudioStreamer streamer) : base(streamer) 8 | { 9 | } 10 | 11 | protected override bool ShouldWork(StreamedAudioStream stream) 12 | { 13 | return stream.NeedsToRead; 14 | } 15 | 16 | protected override void Work(StreamedAudioStream stream) 17 | { 18 | stream.ReadWork(); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.SeekToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace LoudPizza.Sources.Streaming 5 | { 6 | public partial class AudioStreamer 7 | { 8 | internal class SeekToken 9 | { 10 | public readonly ManualResetEventSlim WaitHandle; 11 | 12 | public ulong TargetPosition; 13 | public AudioSeekFlags Flags; 14 | 15 | public ulong ResultPosition; 16 | public SoLoudStatus ResultStatus; 17 | public Exception? Exception; 18 | 19 | public SeekToken() 20 | { 21 | WaitHandle = new ManualResetEventSlim(); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.SeekWorker.cs: -------------------------------------------------------------------------------- 1 | using LoudPizza.Core; 2 | 3 | namespace LoudPizza.Sources.Streaming 4 | { 5 | public partial class AudioStreamer 6 | { 7 | private sealed class SeekWorker : Worker 8 | { 9 | private AlignedFloatBuffer _scratch; 10 | 11 | public SeekWorker(AudioStreamer streamer) : base(streamer) 12 | { 13 | _scratch.init(SoLoud.SampleGranularity * 2 * SoLoud.MaxChannels, SoLoud.VECTOR_SIZE); 14 | } 15 | 16 | protected override bool ShouldWork(StreamedAudioStream stream) 17 | { 18 | return stream.NeedsToSeek; 19 | } 20 | 21 | protected override void Work(StreamedAudioStream stream) 22 | { 23 | stream.SeekWork(_scratch.AsSpan()); 24 | } 25 | 26 | protected override void Dispose(bool disposing) 27 | { 28 | base.Dispose(disposing); 29 | 30 | _scratch.destroy(); 31 | } 32 | 33 | ~SeekWorker() 34 | { 35 | Dispose(disposing: false); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.StreamHolder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoudPizza.Sources.Streaming 4 | { 5 | public partial class AudioStreamer 6 | { 7 | private readonly struct StreamHolder : IEquatable 8 | { 9 | public StreamedAudioStream Stream { get; } 10 | public object Mutex { get; } 11 | 12 | public StreamHolder(StreamedAudioStream stream, object mutex) 13 | { 14 | Stream = stream; 15 | Mutex = mutex; 16 | } 17 | 18 | public bool Equals(StreamHolder other) 19 | { 20 | return ReferenceEquals(Stream, other.Stream); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | using System.Threading; 6 | 7 | namespace LoudPizza.Sources.Streaming 8 | { 9 | public partial class AudioStreamer 10 | { 11 | private abstract class Worker : IDisposable 12 | { 13 | private Thread _thread; 14 | private ManualResetEventSlim _resetEvent; 15 | private bool _disposed; 16 | 17 | public AudioStreamer Streamer { get; } 18 | 19 | public Worker(AudioStreamer streamer) 20 | { 21 | Streamer = streamer ?? throw new ArgumentNullException(nameof(streamer)); 22 | 23 | _thread = new Thread(WorkerThread) 24 | { 25 | IsBackground = true 26 | }; 27 | _resetEvent = new ManualResetEventSlim(false, 0); 28 | } 29 | 30 | protected abstract bool ShouldWork(StreamedAudioStream stream); 31 | 32 | protected abstract void Work(StreamedAudioStream stream); 33 | 34 | public void Start() 35 | { 36 | _thread.Start(); 37 | } 38 | 39 | public void Notify() 40 | { 41 | // We do not need a pulse for every call; 42 | // checking is cheap and being set means the worker will run soon regardless. 43 | if (!_resetEvent.IsSet) 44 | { 45 | _resetEvent.Set(); 46 | } 47 | } 48 | 49 | private void WorkerThread() 50 | { 51 | Stopwatch watch = new(); 52 | List holders = new(); 53 | 54 | while (true) 55 | { 56 | _resetEvent.Wait(); 57 | _resetEvent.Reset(); 58 | 59 | watch.Restart(); 60 | 61 | Streamer.ProcessStreamChanges(); 62 | 63 | Streamer._streamLock.EnterReadLock(); 64 | try 65 | { 66 | foreach (ref StreamHolder holder in CollectionsMarshal.AsSpan(Streamer._streams)) 67 | { 68 | if (ShouldWork(holder.Stream)) 69 | { 70 | holders.Add(holder); 71 | } 72 | } 73 | } 74 | finally 75 | { 76 | Streamer._streamLock.ExitReadLock(); 77 | } 78 | 79 | foreach (ref StreamHolder holder in CollectionsMarshal.AsSpan(holders)) 80 | { 81 | // TryEnter allows other workers to steal work 82 | if (Monitor.TryEnter(holder.Mutex)) 83 | { 84 | try 85 | { 86 | Work(holder.Stream); 87 | } 88 | finally 89 | { 90 | Monitor.Exit(holder.Mutex); 91 | } 92 | } 93 | } 94 | holders.Clear(); 95 | 96 | watch.Stop(); 97 | } 98 | } 99 | 100 | protected virtual void Dispose(bool disposing) 101 | { 102 | if (!_disposed) 103 | { 104 | if (disposing) 105 | { 106 | _resetEvent.Dispose(); 107 | } 108 | 109 | _disposed = true; 110 | } 111 | } 112 | 113 | public void Dispose() 114 | { 115 | Dispose(disposing: true); 116 | GC.SuppressFinalize(this); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/AudioStreamer.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | 6 | namespace LoudPizza.Sources.Streaming 7 | { 8 | public partial class AudioStreamer 9 | { 10 | private enum StreamChangeKind 11 | { 12 | Add, 13 | Remove, 14 | } 15 | 16 | private readonly record struct StreamChange(StreamChangeKind Kind, StreamedAudioStream Stream); 17 | 18 | private ReadWorker[] _readers; 19 | private SeekWorker[] _seekers; 20 | 21 | private ReaderWriterLockSlim _streamLock; 22 | private List _streams; 23 | private ConcurrentQueue _streamChanges; 24 | 25 | private Queue _seekTokenPool; 26 | private Queue _audioBufferPool; 27 | private ArrayPool _audioBufferArrayPool; 28 | 29 | public int ReadBufferCount { get; set; } = 3; 30 | public float SecondsPerBuffer { get; set; } = 1 / 12f; 31 | 32 | public AudioStreamer() 33 | { 34 | _readers = new ReadWorker[1]; 35 | _seekers = new SeekWorker[1]; 36 | 37 | _streamLock = new ReaderWriterLockSlim(); 38 | _streams = new List(); 39 | _streamChanges = new ConcurrentQueue(); 40 | 41 | _seekTokenPool = new Queue(); 42 | _audioBufferPool = new Queue(); 43 | _audioBufferArrayPool = ArrayPool.Create(); 44 | 45 | for (int i = 0; i < _readers.Length; i++) 46 | { 47 | _readers[i] = new ReadWorker(this); 48 | } 49 | 50 | for (int i = 0; i < _seekers.Length; i++) 51 | { 52 | _seekers[i] = new SeekWorker(this); 53 | } 54 | } 55 | 56 | public void Start() 57 | { 58 | foreach (ReadWorker worker in _readers) 59 | { 60 | worker.Start(); 61 | } 62 | 63 | foreach (SeekWorker worker in _seekers) 64 | { 65 | worker.Start(); 66 | } 67 | } 68 | 69 | private void ProcessStreamChanges() 70 | { 71 | if (!_streamChanges.TryDequeue(out StreamChange change)) 72 | { 73 | return; 74 | } 75 | 76 | _streamLock.EnterWriteLock(); 77 | try 78 | { 79 | do 80 | { 81 | switch (change.Kind) 82 | { 83 | case StreamChangeKind.Add: 84 | _streams.Add(new StreamHolder(change.Stream, new object())); 85 | break; 86 | 87 | case StreamChangeKind.Remove: 88 | int index = _streams.IndexOf(new StreamHolder(change.Stream, null!)); 89 | if (index != -1) 90 | { 91 | int lastIndex = _streams.Count - 1; 92 | _streams[index] = _streams[lastIndex]; 93 | _streams.RemoveAt(lastIndex); 94 | } 95 | break; 96 | } 97 | } 98 | while (_streamChanges.TryDequeue(out change)); 99 | } 100 | finally 101 | { 102 | _streamLock.ExitWriteLock(); 103 | } 104 | } 105 | 106 | public void RegisterStream(StreamedAudioStream stream) 107 | { 108 | _streamChanges.Enqueue(new StreamChange(StreamChangeKind.Add, stream)); 109 | } 110 | 111 | public void UnregisterStream(StreamedAudioStream stream) 112 | { 113 | _streamChanges.Enqueue(new StreamChange(StreamChangeKind.Remove, stream)); 114 | } 115 | 116 | public void NotifyForRead() 117 | { 118 | foreach (ReadWorker worker in _readers) 119 | { 120 | worker.Notify(); 121 | } 122 | } 123 | 124 | public void NotifyForSeek() 125 | { 126 | foreach (SeekWorker worker in _seekers) 127 | { 128 | worker.Notify(); 129 | } 130 | } 131 | 132 | internal SeekToken RentSeekToken(ulong targetSamplePosition, AudioSeekFlags flags) 133 | { 134 | SeekToken? token; 135 | lock (_seekTokenPool) 136 | { 137 | _seekTokenPool.TryDequeue(out token); 138 | } 139 | 140 | if (token != null) 141 | { 142 | token.WaitHandle.Reset(); 143 | } 144 | else 145 | { 146 | token = new SeekToken(); 147 | } 148 | 149 | token.TargetPosition = targetSamplePosition; 150 | token.Flags = flags; 151 | 152 | return token; 153 | } 154 | 155 | internal void ReturnSeekToken(SeekToken token) 156 | { 157 | if (_seekTokenPool.Count < 16) 158 | { 159 | token.ResultPosition = default; 160 | token.ResultStatus = default; 161 | token.Exception = default; 162 | 163 | lock (_seekTokenPool) 164 | { 165 | _seekTokenPool.Enqueue(token); 166 | } 167 | } 168 | else 169 | { 170 | token.WaitHandle.Dispose(); 171 | } 172 | } 173 | 174 | internal AudioBuffer RentAudioBuffer(uint minimumLength) 175 | { 176 | int length = checked((int)minimumLength); 177 | 178 | return new AudioBuffer() 179 | { 180 | Buffer = _audioBufferArrayPool.Rent(length) 181 | }; 182 | } 183 | 184 | internal void ReturnAudioBuffer(AudioBuffer audioBuffer) 185 | { 186 | _audioBufferArrayPool.Return(audioBuffer.Buffer); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/IRelativePlaybackRateChangeListener.cs: -------------------------------------------------------------------------------- 1 | namespace LoudPizza.Sources.Streaming 2 | { 3 | // TODO: expose a general-purpose property change listener? 4 | internal interface IRelativePlaybackRateChangeListener 5 | { 6 | void RelativePlaybackRateChanged(float relativePlaybackSpeed); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LoudPizza/Sources/Streaming/StreamedAudioStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using LoudPizza.Core; 5 | 6 | namespace LoudPizza.Sources.Streaming 7 | { 8 | public class StreamedAudioStream : IAudioStream, IRelativePlaybackRateChangeListener 9 | { 10 | private volatile int _disposed; 11 | private bool _hasEnded; 12 | private Queue _seekQueue; 13 | private Queue _audioQueue; 14 | private AudioStreamer.AudioBuffer? _currentBuffer; 15 | private bool _discardCurrentBuffer; 16 | 17 | public AudioStreamer Streamer { get; } 18 | public IAudioStream BaseStream { get; } 19 | 20 | public bool NeedsToRead { get; private set; } 21 | public bool NeedsToSeek => _seekQueue.Count > 0; 22 | 23 | public bool IsDisposed => _disposed != 0; 24 | 25 | /// 26 | public uint Channels => BaseStream.Channels; 27 | 28 | /// 29 | public float SampleRate => BaseStream.SampleRate; 30 | 31 | /// 32 | public float RelativePlaybackSpeed { get; set; } 33 | 34 | public StreamedAudioStream(AudioStreamer streamer, IAudioStream baseStream) 35 | { 36 | Streamer = streamer ?? throw new ArgumentNullException(nameof(streamer)); 37 | BaseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); 38 | 39 | _seekQueue = new Queue(); 40 | _audioQueue = new Queue(); 41 | } 42 | 43 | void IRelativePlaybackRateChangeListener.RelativePlaybackRateChanged(float relativePlaybackSpeed) 44 | { 45 | RelativePlaybackSpeed = relativePlaybackSpeed; 46 | } 47 | 48 | /// 49 | public bool CanSeek() 50 | { 51 | return BaseStream.CanSeek(); 52 | } 53 | 54 | public void ReadWork() 55 | { 56 | if (!NeedsToRead) 57 | { 58 | return; 59 | } 60 | NeedsToRead = false; 61 | 62 | while (_audioQueue.Count < Streamer.ReadBufferCount) 63 | { 64 | if (!ReadNewBuffer()) 65 | { 66 | break; 67 | } 68 | } 69 | } 70 | 71 | private bool ReadNewBuffer() 72 | { 73 | if (_hasEnded) 74 | { 75 | return false; 76 | } 77 | 78 | uint channels = BaseStream.Channels; 79 | float playbackRate = RelativePlaybackSpeed * BaseStream.SampleRate; 80 | uint toRead = Math.Max(SoLoud.SampleGranularity, (uint)(playbackRate * Streamer.SecondsPerBuffer)); 81 | AudioStreamer.AudioBuffer audioBuffer = Streamer.RentAudioBuffer(toRead * channels); 82 | 83 | uint samplesRead = BaseStream.GetAudio(audioBuffer.AsSpan(), toRead, toRead); 84 | if (samplesRead > 0) 85 | { 86 | audioBuffer.Start = 0; 87 | audioBuffer.Length = samplesRead; 88 | 89 | lock (_audioQueue) 90 | { 91 | _audioQueue.Enqueue(audioBuffer); 92 | } 93 | } 94 | else 95 | { 96 | Streamer.ReturnAudioBuffer(audioBuffer); 97 | 98 | if (BaseStream.HasEnded()) 99 | { 100 | _hasEnded = true; 101 | return false; 102 | } 103 | } 104 | return true; 105 | } 106 | 107 | private void PrimeForMoreAudio() 108 | { 109 | if (_audioQueue.Count < Streamer.ReadBufferCount) 110 | { 111 | NeedsToRead = true; 112 | Streamer.NotifyForRead(); 113 | } 114 | } 115 | 116 | private bool GetCurrentBuffer(out AudioStreamer.AudioBuffer? audioBuffer) 117 | { 118 | lock (_audioQueue) 119 | { 120 | bool discard = _discardCurrentBuffer; 121 | _discardCurrentBuffer = false; 122 | 123 | if (_currentBuffer != null) 124 | { 125 | if (_currentBuffer.Start == _currentBuffer.Length) 126 | { 127 | // The buffer has been consumed, discard it. 128 | discard = true; 129 | } 130 | 131 | if (discard) 132 | { 133 | Streamer.ReturnAudioBuffer(_currentBuffer); 134 | _currentBuffer = null; 135 | } 136 | } 137 | 138 | if (_currentBuffer == null) 139 | { 140 | if (!_audioQueue.TryDequeue(out _currentBuffer) && _hasEnded) 141 | { 142 | audioBuffer = null; 143 | return false; 144 | } 145 | } 146 | 147 | audioBuffer = _currentBuffer; 148 | return true; 149 | } 150 | } 151 | 152 | /// 153 | public uint GetAudio(Span buffer, uint samplesToRead, uint channelStride) 154 | { 155 | uint channels = BaseStream.Channels; 156 | 157 | uint totalRead = 0; 158 | do 159 | { 160 | if (!GetCurrentBuffer(out AudioStreamer.AudioBuffer? audioBuffer)) 161 | { 162 | // Only return less than the requested amount of samples when the stream ends. 163 | return totalRead; 164 | } 165 | 166 | if (audioBuffer == null) 167 | { 168 | // Fill the rest of the destination with zeroes to pad the total read amount. 169 | for (uint i = 0; i < channels; i++) 170 | { 171 | Span dst = buffer.Slice((int)(totalRead + i * channelStride), (int)samplesToRead); 172 | dst.Clear(); 173 | } 174 | 175 | totalRead += samplesToRead; 176 | samplesToRead = 0; 177 | break; 178 | } 179 | 180 | // C 181 | uint toCopy = Math.Min(samplesToRead, audioBuffer.Length - audioBuffer.Start); 182 | ReadOnlySpan totalSrc = audioBuffer.AsSpan(); 183 | 184 | for (uint i = 0; i < channels; i++) 185 | { 186 | ReadOnlySpan src = totalSrc.Slice((int)(audioBuffer.Start + i * audioBuffer.Length), (int)toCopy); 187 | Span dst = buffer.Slice((int)(totalRead + i * channelStride), src.Length); 188 | src.CopyTo(dst); 189 | } 190 | 191 | audioBuffer.Start += toCopy; 192 | totalRead += toCopy; 193 | samplesToRead -= toCopy; 194 | } 195 | while (samplesToRead > 0); 196 | 197 | PrimeForMoreAudio(); 198 | 199 | return totalRead; 200 | } 201 | 202 | /// 203 | public bool HasEnded() 204 | { 205 | return _hasEnded; 206 | } 207 | 208 | public void SeekWork(Span scratch) 209 | { 210 | do 211 | { 212 | AudioStreamer.SeekToken? token; 213 | lock (_seekQueue) 214 | { 215 | if (!_seekQueue.TryDequeue(out token)) 216 | { 217 | return; 218 | } 219 | } 220 | 221 | lock (_audioQueue) 222 | { 223 | while (_audioQueue.TryDequeue(out AudioStreamer.AudioBuffer? buffer)) 224 | { 225 | Streamer.ReturnAudioBuffer(buffer); 226 | } 227 | 228 | _discardCurrentBuffer = true; 229 | } 230 | 231 | try 232 | { 233 | SoLoudStatus status = BaseStream.Seek( 234 | token.TargetPosition, scratch, AudioSeekFlags.None, out token.ResultPosition); 235 | 236 | token.ResultStatus = status; 237 | 238 | if (status == SoLoudStatus.EndOfStream) 239 | { 240 | _hasEnded = true; 241 | } 242 | else if (status == SoLoudStatus.Ok) 243 | { 244 | _hasEnded = false; 245 | 246 | if (_seekQueue.Count == 0) 247 | { 248 | PrimeForMoreAudio(); 249 | } 250 | } 251 | } 252 | catch (Exception ex) 253 | { 254 | token.Exception = ex; 255 | token.ResultStatus = SoLoudStatus.UnknownError; 256 | 257 | _hasEnded = true; 258 | } 259 | 260 | if ((token.Flags & AudioSeekFlags.NonBlocking) == 0) 261 | { 262 | token.WaitHandle.Set(); 263 | } 264 | else 265 | { 266 | // TODO: bubble up exceptions 267 | Streamer.ReturnSeekToken(token); 268 | } 269 | } 270 | while (true); 271 | } 272 | 273 | /// 274 | public SoLoudStatus Seek(ulong samplePosition, Span scratch, AudioSeekFlags flags, out ulong resultPosition) 275 | { 276 | AudioStreamer.SeekToken token = Streamer.RentSeekToken(samplePosition, flags); 277 | lock (_seekQueue) 278 | { 279 | _seekQueue.Enqueue(token); 280 | } 281 | Streamer.NotifyForSeek(); 282 | 283 | if ((flags & AudioSeekFlags.NonBlocking) != 0) 284 | { 285 | // TODO: try to get stream length from BaseStream and truncate the resultPosition 286 | resultPosition = samplePosition; 287 | 288 | return SoLoudStatus.Ok; 289 | } 290 | 291 | token.WaitHandle.Wait(); 292 | 293 | resultPosition = token.ResultPosition; 294 | SoLoudStatus resultStatus = token.ResultStatus; 295 | Exception? exception = token.Exception; 296 | 297 | Streamer.ReturnSeekToken(token); 298 | if (exception != null) 299 | { 300 | throw exception; 301 | } 302 | return resultStatus; 303 | } 304 | 305 | protected virtual void Dispose(bool disposing) 306 | { 307 | int disposed = Interlocked.Exchange(ref _disposed, 1); 308 | if (disposed != 0) 309 | { 310 | return; 311 | } 312 | 313 | Streamer.UnregisterStream(this); 314 | 315 | if (disposing) 316 | { 317 | } 318 | } 319 | 320 | /// 321 | public void Dispose() 322 | { 323 | Dispose(disposing: true); 324 | GC.SuppressFinalize(this); 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /LoudPizza/Time.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace LoudPizza 5 | { 6 | [DebuggerDisplay("{" + nameof(GetDebuggerDisplay) + "(),nq}")] 7 | public readonly struct Time 8 | { 9 | public double Seconds { get; } 10 | 11 | public Time(double seconds) 12 | { 13 | Seconds = seconds; 14 | } 15 | 16 | public static implicit operator Time(TimeSpan timeSpan) 17 | { 18 | return new Time(timeSpan.TotalSeconds); 19 | } 20 | 21 | public static implicit operator Time(double seconds) 22 | { 23 | return new Time(seconds); 24 | } 25 | 26 | public static implicit operator double(Time time) 27 | { 28 | return time.Seconds; 29 | } 30 | 31 | public override string ToString() 32 | { 33 | return $"{Seconds}s"; 34 | } 35 | 36 | private string GetDebuggerDisplay() 37 | { 38 | return ToString(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LoudPizza/Vector3Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace LoudPizza 6 | { 7 | public static class Vector3Extensions 8 | { 9 | /// Returns a vector with the same direction as the specified vector, but with a length of one. 10 | /// The vector to normalize. 11 | /// The normalized vector. 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public static Vector3 SafeNormalize(Vector3 value) 14 | { 15 | float length = value.LengthSquared(); 16 | if (length == 0) 17 | { 18 | return Vector3.Zero; 19 | } 20 | return value / MathF.Sqrt(length); 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------