Skip to content

Commit

Permalink
PlayPlayMini.NAudio WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
BenMakesGames committed Mar 30, 2024
1 parent 73e8010 commit 037d1b3
Show file tree
Hide file tree
Showing 9 changed files with 590 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Authors>Ben Hendel-Doying</Authors>
<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>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageTags>monogame playplaymini naudio music</PackageTags>
<PackageProjectUrl>https://github.com/BenMakesGames/PlayPlayMini</PackageProjectUrl>
<RepositoryUrl>https://github.com/BenMakesGames/PlayPlayMini</RepositoryUrl>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>package-icon.png</PackageIcon>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenMakesGames.PlayPlayMini" Version="4.5.0" />
<PackageReference Include="NAudio" Version="2.2.1" />
</ItemGroup>

<ItemGroup>
<None Include="..\LICENSE.md">
<Pack>True</Pack>
<PackagePath />
</None>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath />
</None>
<None Include="package-icon.png">
<Pack>True</Pack>
<PackagePath />
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
69 changes: 69 additions & 0 deletions BenMakesGames.PlayPlayMini.NAudio/Model/LoopStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using NAudio.Wave;

namespace BenMakesGames.PlayPlayMini.NAudio.Model;

public sealed class LoopStream : WaveStream
{
private WaveStream SourceStream { get; }

/// <summary>
/// Creates a new Loop stream
/// </summary>
/// <param name="sourceStream">The stream to read from. Note: the Read method of this stream should return 0 when it reaches the end
/// or else we will not loop to the start again.</param>
public LoopStream(WaveStream sourceStream)
{
SourceStream = sourceStream;
EnableLooping = true;
}

/// <summary>
/// Use this to turn looping on or off
/// </summary>
public bool EnableLooping { get; set; }

/// <summary>
/// Return source stream's wave format
/// </summary>
public override WaveFormat WaveFormat => SourceStream.WaveFormat;

/// <summary>
/// LoopStream simply returns
/// </summary>
public override long Length => SourceStream.Length;

/// <summary>
/// LoopStream simply passes on positioning to source stream
/// </summary>
public override long Position
{
get => SourceStream.Position;
set => SourceStream.Position = value;
}

public override int Read(byte[] buffer, int offset, int count)
{
int totalBytesRead = 0;

while (totalBytesRead < count)
{
int bytesRead = SourceStream.Read(buffer, offset + totalBytesRead, count - totalBytesRead);

if (bytesRead == 0)
{
if (SourceStream.Position == 0 || !EnableLooping)
{
// something wrong with the source stream
break;
}

// loop
SourceStream.Position = 0;
}

totalBytesRead += bytesRead;
}

return totalBytesRead;
}
}
8 changes: 8 additions & 0 deletions BenMakesGames.PlayPlayMini.NAudio/Model/NAudioSong.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using BenMakesGames.PlayPlayMini.Model;

namespace BenMakesGames.PlayPlayMini.NAudio.Model;

/// <param name="Key">Name that uniquely identifies this song</param>
/// <param name="Path">Relative path to image, excluding file extension (ex: "Music/TownTheme")</param>
/// <param name="PreLoaded">Whether or not to load this resource BEFORE entering the first GameState</param>
public sealed record NAudioSongMeta(string Key, string Path, bool PreLoaded = false) : IAsset;
99 changes: 99 additions & 0 deletions BenMakesGames.PlayPlayMini.NAudio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# What Is It?

Seamlessy-looping music is important for many games, but MonoGame's built-in music player isn't able to consistently loop music - more often than not, it lags, adding a short, but noticeable delay before looping.

`PlayPlayMini.NAudio` allows you to use NAudio to play music, resolving this issue, and adding support for cross-fading songs!

[![Buy Me a Coffee at ko-fi.com](https://raw.githubusercontent.com/BenMakesGames/AssetsForNuGet/main/buymeacoffee.png)](https://ko-fi.com/A0A12KQ16)

## How To Use

### Required Setup

1. Install this package.
* `dotnet add package BenMakesGames.PlayPlayMini.NAudio`
2. Do NOT add your songs to the MCGB content pipeline tool (remove them if they're already there); instead, ensure your songs are set to "copy if newer" in the project's properties.
* You can add something like the following to your `.csproj` file to automatically include all songs; change the path as needed, of course:
```xml
<ItemGroup>
<None Update="Content\Music\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
```
3. When adding assets to your game's `GameStateManagerBuilder`, use `new NAudioSongMeta(...)` instead of `new SongMeta(...)`. When using `new NAudioSongMeta(...)`, you must specify the extension of the song.
* 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.

### Optional Setup

`.mp3`, `.aiff`, and `.wav` files are supported out of the box. For other formats, you will need to install additional NAudio packages:

| Extension | Package |
| --------- | ------- |
| `.ogg` | `NAudio.Vorbis` |
| `.flac` | `NAudio.Flac` |

### Use

In your game state or services, get an `NAudioMusicPlayer` via the constructor (just as you would any other service), and use it to play and stop songs.

Example:

```c#
NAudioMusicPlayer
.StopAllSongs(1000) // stop all songs, fading them out over 1 second
.PlaySong("TitleTheme", 0); // start the TitleTheme with no fade-in time
```

Refer to the reference, below, for a list of available methods.

## `NAudioMusicPlayer` Method Reference

Note: negative fade-in and fade-out times are treated as 0.

### `NAudioMusicPlayer PlaySong(string name, int fadeInMilliseconds = 0)`

Starts playing the specific song, fading it in over the specified number of milliseconds.

Songs which are already playing will not be stopped! You must explicitly stop them using `StopAllSongs` or `StopSong` (below).

If the song is already playing, it will not be played again (you cannot play two copies of the song playing at the same time). If the song is fading in, its fade-in time will not be changed.

### `NAudioMusicPlayer StopAllSongs(int fadeOutMilliseconds = 0)`

Stops all songs, fading them out over the specified number of milliseconds.

Songs which are already fading out will not have their fade-out time changed. A fade-out time of 0 will always immediately stops all songs, however.

To cross-fade between songs, you can chain `StopSongs` and `PlaySong` calls. For example:

```c#
NAudioMusicPlayer
.StopAllSongs(1000)
.PlaySong("TitleTheme");
```

### `NAudioMusicPlayer StopAllSongsExcept(string[] songsToContinue, int fadeOutMilliseconds = 0)`

Works like `StopAllSongs` (above), but does NOT stop the songs named in `songsToContinue`.

### `NAudioMusicPlayer StopAllSongsExcept(string name, int fadeOutMilliseconds = 0)`

Works like `StopAllSongs` (above), but does NOT stop the named song.

### `NAudioMusicPlayer StopSong(string name, int fadeOutMilliseconds = 0)`

Like `StopAllSongs` (above), but stops only the named song.

### `NAudioMusicPlayer SetVolume(float volume)`

Changes the volume for all songs.

### `bool IsPlaying(string name)`

Returns `true` if the specific song is currently playing.

### `string[] GetPlayingSongs()`

Returns an array of the names of all songs currently playing.
Loading

0 comments on commit 037d1b3

Please sign in to comment.