From 68a34515ee2d02314530d7b2b13a575b3bea5c30 Mon Sep 17 00:00:00 2001 From: Ben H Date: Sat, 30 Mar 2024 23:31:23 -0400 Subject: [PATCH] NAudio plugin ready for release, I think! --- .../BenMakesGames.PlayPlayMini.NAudio.csproj | 2 +- BenMakesGames.PlayPlayMini.NAudio/README.md | 27 +++++++-- .../Services/NAudioFileLoader.cs | 18 ++++++ .../Services/NAudioMusicPlayer.cs | 56 ++++++++++--------- .../Services/NAudioPlaybackEngine.cs | 16 ++++++ 5 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 BenMakesGames.PlayPlayMini.NAudio/Services/NAudioFileLoader.cs diff --git a/BenMakesGames.PlayPlayMini.NAudio/BenMakesGames.PlayPlayMini.NAudio.csproj b/BenMakesGames.PlayPlayMini.NAudio/BenMakesGames.PlayPlayMini.NAudio.csproj index b8ddd36..7c95310 100644 --- a/BenMakesGames.PlayPlayMini.NAudio/BenMakesGames.PlayPlayMini.NAudio.csproj +++ b/BenMakesGames.PlayPlayMini.NAudio/BenMakesGames.PlayPlayMini.NAudio.csproj @@ -5,7 +5,7 @@ Ben Hendel-Doying Get seamless looping music in your MonoGame-PlayPlayMini game using NAudio. 2024 Ben Hendel-Doying - 1.0.0 + 0.1.0 true monogame playplaymini naudio music diff --git a/BenMakesGames.PlayPlayMini.NAudio/README.md b/BenMakesGames.PlayPlayMini.NAudio/README.md index 52bbebc..6f1a194 100644 --- a/BenMakesGames.PlayPlayMini.NAudio/README.md +++ b/BenMakesGames.PlayPlayMini.NAudio/README.md @@ -25,14 +25,31 @@ Seamlessy-looping music is important for many games, but MonoGame's built-in mus * For example, `new NAudioSongMeta("TitleTheme", "Content/Music/TitleTheme.mp3")`. 4. In your startup state, where you wait for content loaders to finish loading, wait for `NAudioMusicPlayer.FullyLoaded`, too. +**Note:** All songs you load must have the same sample rate (typically 44.1khz) and channel count (typically 2). When songs are loading, an error will be logged if they do not all match, and not all songs will be loaded. + ### Optional Setup -`.mp3`, `.aiff`, and `.wav` files are supported out of the box. For other formats, you will need to install additional NAudio packages: +`.mp3`, `.aiff`, and `.wav` files are supported out of the box. For other formats, you will need to install additional NAudio packages and do a little extra configuration: + +#### `.ogg` + +Add the `NAudio.Vorbis` package to your project. + +In your game's `.AddServices(...)` configuration, add the following line: + +```c# +s.RegisterInstance(new NAudioFileLoader("ogg", f => new VorbisWaveReader(f))); +``` + +#### `.flac` + +Add the `NAudio.Flac` package to your project. -| Extension | Package | -| --------- | ------- | -| `.ogg` | `NAudio.Vorbis` | -| `.flac` | `NAudio.Flac` | +In your game's `.AddServices(...)` configuration, add the following line: + +```c# +s.RegisterInstance(new NAudioFileLoader("flac", f => new FlacReader(f))); +``` ### Use diff --git a/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioFileLoader.cs b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioFileLoader.cs new file mode 100644 index 0000000..536a4a5 --- /dev/null +++ b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioFileLoader.cs @@ -0,0 +1,18 @@ +using System; +using NAudio.Wave; + +namespace BenMakesGames.PlayPlayMini.NAudio.Services; + +public sealed class NAudioFileLoader +{ + private Func Loader { get; } + public string Extension { get; } + + public NAudioFileLoader(string extension, Func loader) + { + Loader = loader; + Extension = extension.ToLower(); + } + + public WaveStream Load(string path) => Loader(path); +} diff --git a/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioMusicPlayer.cs b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioMusicPlayer.cs index 43d994b..1694a9f 100644 --- a/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioMusicPlayer.cs +++ b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioMusicPlayer.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Autofac; using BenMakesGames.PlayPlayMini.Attributes.DI; using BenMakesGames.PlayPlayMini.NAudio.Model; using Microsoft.Extensions.Logging; @@ -37,19 +38,19 @@ namespace BenMakesGames.PlayPlayMini.NAudio.Services; [AutoRegister] public sealed class NAudioMusicPlayer: IServiceLoadContent, IServiceUpdate, IDisposable { - private NAudioPlaybackEngine PlaybackEngine { get; } + private NAudioPlaybackEngine? PlaybackEngine { get; set; } private ILogger Logger { get; } + private ILifetimeScope IocContainer { get; } public Dictionary Songs { get; } = new(); private Dictionary PlayingSongs { get; } = new(); private Dictionary FadingSongs { get; } = new(); - public NAudioMusicPlayer(ILogger logger) + public NAudioMusicPlayer(ILogger logger, ILifetimeScope iocContainer) { Logger = logger; - - PlaybackEngine = new NAudioPlaybackEngine(); + IocContainer = iocContainer; } public void LoadContent(GameStateManager gsm) @@ -85,6 +86,18 @@ private void LoadSong(string name, string filePath) { var stream = CreateWaveStream(filePath); + if(PlaybackEngine is null) + PlaybackEngine = new NAudioPlaybackEngine(stream.WaveFormat.SampleRate, stream.WaveFormat.Channels); + else if(stream.WaveFormat.SampleRate != PlaybackEngine.SampleRate || stream.WaveFormat.Channels != PlaybackEngine.Channels) + { + Logger.LogError( + "All songs must have the same sample rate and channel count. Song {Name} has a sample rate of {StreamSampleRate} and channel count of {StreamChannelCount}, but other songs have a sample rate of {PlayerSampleRate} and channel count of {PlayerChannelCount}.", + name, stream.WaveFormat.SampleRate, stream.WaveFormat.Channels, PlaybackEngine.SampleRate, PlaybackEngine.Channels + ); + + return; + } + Songs.Add(name, stream); } catch(Exception e) @@ -93,26 +106,17 @@ private void LoadSong(string name, string filePath) } } - private static WaveStream CreateWaveStream(string filePath) => Path.GetExtension(filePath).ToLower() switch + private WaveStream CreateWaveStream(string filePath) { - ".ogg" => TryLoad("NAudio.Vorbis.VorbisWaveReader", filePath), - ".flac" => TryLoad("NAudio.Flac.FlacReader", filePath), - _ => new AudioFileReader(filePath), - }; - - private static WaveStream TryLoad(string typeName, string filePath) - { - var type = Type.GetType(typeName); - - if(type == null) - throw new InvalidOperationException($"Could not find class of type {typeName}"); + var extension = Path.GetExtension(filePath)[1..].ToLower(); - var instance = Activator.CreateInstance(type, filePath); + var loader = IocContainer.Resolve>() + .FirstOrDefault(l => l.Extension == extension); - if (instance is not WaveStream waveStream) - throw new InvalidOperationException($"Failed to instantiate class of type {typeName}"); + if(loader is not null) + return loader.Load(filePath); - return waveStream; + return new AudioFileReader(filePath); } /// @@ -125,7 +129,7 @@ private static WaveStream TryLoad(string typeName, string filePath) /// public NAudioMusicPlayer SetVolume(float volume) { - PlaybackEngine.SetVolume(volume); + PlaybackEngine?.SetVolume(volume); return this; } @@ -171,7 +175,7 @@ public NAudioMusicPlayer PlaySong(string name, int fadeInMilliseconds = 0) if(fadeInMilliseconds > 0) sample.BeginFadeIn(fadeInMilliseconds); - PlaybackEngine.AddSample(sample); + PlaybackEngine?.AddSample(sample); PlayingSongs.Add(name, sample); return this; @@ -189,7 +193,7 @@ public NAudioMusicPlayer StopAllSongs(int fadeOutMilliseconds = 0) { if(fadeOutMilliseconds <= 0) { - PlaybackEngine.RemoveAll(); + PlaybackEngine?.RemoveAll(); PlayingSongs.Clear(); FadingSongs.Clear(); return this; @@ -260,7 +264,7 @@ public NAudioMusicPlayer StopSong(string name, int fadeOutMilliseconds = 0) if (fadeOutMilliseconds <= 0) { - PlaybackEngine.RemoveSample(sample); + PlaybackEngine?.RemoveSample(sample); PlayingSongs.Remove(name); FadingSongs.Remove(name); return this; @@ -284,7 +288,7 @@ public void Dispose() Songs.Clear(); - PlaybackEngine.Dispose(); + PlaybackEngine?.Dispose(); } public void Update(GameTime gameTime) @@ -296,7 +300,7 @@ public void Update(GameTime gameTime) { var sample = PlayingSongs[name]; - PlaybackEngine.RemoveSample(sample); + PlaybackEngine?.RemoveSample(sample); PlayingSongs.Remove(name); FadingSongs.Remove(name); diff --git a/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioPlaybackEngine.cs b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioPlaybackEngine.cs index 2bd5d0b..2ce7a3d 100644 --- a/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioPlaybackEngine.cs +++ b/BenMakesGames.PlayPlayMini.NAudio/Services/NAudioPlaybackEngine.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; using NAudio.Wave; using NAudio.Wave.SampleProviders; @@ -10,6 +11,9 @@ public sealed class NAudioPlaybackEngine: IDisposable private IWavePlayer OutputDevice { get; } private MixingSampleProvider Mixer { get; } + public int SampleRate => Mixer.WaveFormat.SampleRate; + public int Channels => Mixer.WaveFormat.Channels; + public NAudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2) { OutputDevice = new WaveOutEvent(); @@ -26,6 +30,18 @@ public void SetVolume(float volume) public void AddSample(ISampleProvider sample) { + if(sample.WaveFormat.SampleRate != Mixer.WaveFormat.SampleRate) + { + Console.WriteLine($"Warning: Sample's sample rate ({sample.WaveFormat.SampleRate}) does not match mixer sample rate ({Mixer.WaveFormat.SampleRate})"); + return; + } + + if(sample.WaveFormat.Channels != Mixer.WaveFormat.Channels) + { + Console.WriteLine($"Warning: Sample's channel count ({sample.WaveFormat.Channels}) does not match mixer channel count ({Mixer.WaveFormat.Channels})"); + return; + } + Mixer.AddMixerInput(sample); }