Skip to content

Commit

Permalink
NAudio plugin ready for release, I think!
Browse files Browse the repository at this point in the history
  • Loading branch information
BenMakesGames committed Mar 31, 2024
1 parent 037d1b3 commit 68a3451
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Company>Ben Hendel-Doying</Company>
<Description>Get seamless looping music in your MonoGame-PlayPlayMini game using NAudio.</Description>
<Copyright>2024 Ben Hendel-Doying</Copyright>
<Version>1.0.0</Version>
<Version>0.1.0</Version>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageTags>monogame playplaymini naudio music</PackageTags>
Expand Down
27 changes: 22 additions & 5 deletions BenMakesGames.PlayPlayMini.NAudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions BenMakesGames.PlayPlayMini.NAudio/Services/NAudioFileLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using NAudio.Wave;

namespace BenMakesGames.PlayPlayMini.NAudio.Services;

public sealed class NAudioFileLoader
{
private Func<string, WaveStream> Loader { get; }
public string Extension { get; }

public NAudioFileLoader(string extension, Func<string, WaveStream> loader)
{
Loader = loader;
Extension = extension.ToLower();
}

public WaveStream Load(string path) => Loader(path);
}
56 changes: 30 additions & 26 deletions BenMakesGames.PlayPlayMini.NAudio/Services/NAudioMusicPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<NAudioMusicPlayer> Logger { get; }
private ILifetimeScope IocContainer { get; }

public Dictionary<string, WaveStream> Songs { get; } = new();

private Dictionary<string, FadeInOutSampleProvider> PlayingSongs { get; } = new();
private Dictionary<string, DateTimeOffset> FadingSongs { get; } = new();

public NAudioMusicPlayer(ILogger<NAudioMusicPlayer> logger)
public NAudioMusicPlayer(ILogger<NAudioMusicPlayer> logger, ILifetimeScope iocContainer)
{
Logger = logger;

PlaybackEngine = new NAudioPlaybackEngine();
IocContainer = iocContainer;
}

public void LoadContent(GameStateManager gsm)
Expand Down Expand Up @@ -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)
Expand All @@ -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<IEnumerable<NAudioFileLoader>>()
.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);
}

/// <summary>
Expand All @@ -125,7 +129,7 @@ private static WaveStream TryLoad(string typeName, string filePath)
/// <returns></returns>
public NAudioMusicPlayer SetVolume(float volume)
{
PlaybackEngine.SetVolume(volume);
PlaybackEngine?.SetVolume(volume);
return this;
}

Expand Down Expand Up @@ -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;
Expand All @@ -189,7 +193,7 @@ public NAudioMusicPlayer StopAllSongs(int fadeOutMilliseconds = 0)
{
if(fadeOutMilliseconds <= 0)
{
PlaybackEngine.RemoveAll();
PlaybackEngine?.RemoveAll();
PlayingSongs.Clear();
FadingSongs.Clear();
return this;
Expand Down Expand Up @@ -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;
Expand All @@ -284,7 +288,7 @@ public void Dispose()

Songs.Clear();

PlaybackEngine.Dispose();
PlaybackEngine?.Dispose();
}

public void Update(GameTime gameTime)
Expand All @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions BenMakesGames.PlayPlayMini.NAudio/Services/NAudioPlaybackEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;

Expand All @@ -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();
Expand All @@ -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);
}

Expand Down

0 comments on commit 68a3451

Please sign in to comment.