├── .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 |
--------------------------------------------------------------------------------