Skip to content

Commit

Permalink
feat: Adds support for .ogg files without loop markers
Browse files Browse the repository at this point in the history
  • Loading branch information
ViMaSter committed Feb 3, 2024
1 parent 4dcc6f1 commit c483631
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 119 deletions.
182 changes: 105 additions & 77 deletions unity-ggjj/Assets/Scripts/SceneLoading/LoopableMusicClip.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
using System;
using System.Collections;
using System.IO;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Networking;

[assembly: InternalsVisibleTo("PlayModeTests")]
/// <summary>
/// Handles playing back .ogg files with loop markers
/// Handles playing back .ogg files with optional loop markers
/// </summary>
/// <remarks>
/// Music with loop markers consist of up to 3 sections:
/// 1. Intro
/// 2. Loop
/// 3. Outro (optional)
///
/// Inside the .ogg file, the loop markers are defined as tags with the names LOOP_START and LOOP_END in the format HH:MM:SS.mmm.
///
/// If ContinueLooping is true, a track will start with the intro and continue looping the loop section. If ContinueLooping is set to false during playback, it will play finish the current loop and then play the outro.
/// If ContinueLooping is false, a track will start with the intro, loop the loop section once, and then play the outro.
/// If no outro exists, the track will simply end after the last loop.
///
/// Usage:
/// 1. Instantiate an instance of this class
/// 2. Call `.Initialize(pathToOGGWithinMusicDirectory)` to load an .ogg file with loop markers.
/// 3. Assign the `.Clip` property to an `AudioSource.clip` of your choice.
/// 4. Call `AudioSource.Play()` to start playback. By default, the track will loop indefinitely.
/// 5. Change the `.ContinueLooping` property to false to finish the current loop and play the outro, if it exists.
/// </remarks>
public class LoopableMusicClip
{
private readonly string _clipName;
Expand All @@ -36,80 +19,44 @@ public class LoopableMusicClip
private int _sampleRate;
private int _channelCount;

public int TimeToSamples(double time)
/// <summary>
/// Creates a new instance of LoopableMusicClip.<br />
/// Use `this.Initialize(oggFilePath)` to await and load an .ogg file. After awaiting, use the `Clip` property to get an AudioClip for playback.
/// </summary>
/// <seealso cref="Initialize"/>
/// <seealso cref="Clip"/>
/// <seealso cref="ContinueLooping"/>"/>
public LoopableMusicClip()
{

}

internal int TimeToSamples(double time)
{
return (int) (time * _sampleRate * _channelCount);
}

public double SamplesToTime(int samples)
internal double SamplesToTime(int samples)
{
return (double) samples / _sampleRate / _channelCount;
}

/// <summary>
/// If true (default), the track will loop indefinitely.<br />
/// If false, the track will finish the current loop, optionally play an outro, if data after the LOOP_END point exists and then stop.
/// </summary>
public bool ContinueLooping { get; set; } = true;

private AudioClip _clip;
public AudioClip Clip
{
get
{
if (_rawData == null || _rawData.Length == 0)
{
throw new InvalidOperationException($"Clip has not been initialized yet; call {nameof(Initialize)}(pathToOGGWithinMusicDirectory) first");
}
if (_clip != null)
{
return _clip;
}

_clip = AudioClip.Create("preLoop", _preLoopSampleLength + _loopSampleLength, _channelCount, _sampleRate, true, GenerateStream, SetCurrentPlaybackHead);
return _clip;
}
}

public IEnumerator Initialize(string pathToOGGWithinMusicDirectory)
{
if (!pathToOGGWithinMusicDirectory.EndsWith(".ogg"))
{
throw new NotSupportedException("Only ogg files are supported");
}

using var www = UnityWebRequestMultimedia.GetAudioClip(Application.streamingAssetsPath + "/Music/" + pathToOGGWithinMusicDirectory, AudioType.OGGVORBIS);
yield return www.SendWebRequest();

using var vorbis = new NVorbis.VorbisReader(new MemoryStream(www.downloadHandler.data));

_rawData = new float[vorbis.TotalSamples * vorbis.Channels];
vorbis.ReadSamples(_rawData, 0, (int) vorbis.TotalSamples * vorbis.Channels);
var loopStart = vorbis.Tags.GetTagSingle("LOOP_START");
var loopEnd = vorbis.Tags.GetTagSingle("LOOP_END");
Debug.Log("loopStart: " + loopStart);
Debug.Log("loopEnd: " + loopEnd);
var loopStartSeconds = TimeSpan.Parse(loopStart).TotalSeconds;
var loopEndSeconds = TimeSpan.Parse(loopEnd).TotalSeconds;
Debug.Log("loopStartSeconds: " + loopStartSeconds);
Debug.Log("loopEndSeconds: " + loopEndSeconds);
Debug.Log("loopSeconds: " + (loopEndSeconds - loopStartSeconds));
_sampleRate = vorbis.SampleRate;
_channelCount = vorbis.Channels;

_preLoopSampleLength = TimeToSamples(loopStartSeconds);
_loopSampleLength = TimeToSamples(loopEndSeconds - loopStartSeconds);
_fullTrackSampleLength = TimeToSamples(vorbis.TotalTime.TotalSeconds);

Debug.Log("preLoopSampleLength: " + _preLoopSampleLength);
Debug.Log("loopSampleLength: " + _loopSampleLength);
Debug.Log("totalSamples: " + vorbis.TotalSamples);
}

public void SetCurrentPlaybackHead(int position)

internal void SetCurrentPlaybackHead(int position)
{
_currentPlaybackHead = position;
}

private int _currentPlaybackHead;

public void GenerateStream(float[] data)
internal void GenerateStream(float[] data)
{
int dataIndex = 0;

Expand Down Expand Up @@ -165,4 +112,85 @@ public void GenerateStream(float[] data)
Array.Clear(data, dataIndex, data.Length - dataIndex);
}
}

/// <summary>
/// Retrieves an AudioClip for the .ogg file, that will loop indefinitely by default.
/// </summary>
/// <remarks>
/// An .ogg file can optionally specify a section to loop inside the file's metadata:<br />
/// `LOOP_START`: the start of the loop in the format`HH:MM:SSmmm<br />
/// `LOOP_END`: the end of the loop in the format`HH:MM:SSmmm<br />
/// If no loop markers are specified, the entire file will be looped.<br />
/// </remarks>
/// <seealso cref="ContinueLooping"/>
/// <exception cref="InvalidOperationException">Thrown when this.Initialize(oggFile) has not been called</exception>
public AudioClip Clip
{
get
{
if (_rawData == null || _rawData.Length == 0)
{
throw new InvalidOperationException($"Clip has not been initialized yet; call {nameof(Initialize)}(pathToOGGWithinMusicDirectory) first");
}
if (_clip != null)
{
return _clip;
}

_clip = AudioClip.Create("preLoop", _preLoopSampleLength + _loopSampleLength, _channelCount, _sampleRate, true, GenerateStream, SetCurrentPlaybackHead);
return _clip;
}
}

/// <summary>
/// Assigns an .ogg file, reads the loop markers and prepares `Clip`
/// </summary>
/// <returns>An IEnumerator to be used with `yield return` to wait loading and processing of the .ogg file</returns>
/// <exception cref="NotSupportedException">Thrown when the file is not an `.ogg` file</exception>
/// <seealso cref="Clip"/>
public IEnumerator Initialize(string pathToOGGWithinMusicDirectory)
{
if (!pathToOGGWithinMusicDirectory.EndsWith(".ogg"))
{
throw new NotSupportedException("Only ogg files are supported");
}

using var www = UnityWebRequestMultimedia.GetAudioClip(Application.streamingAssetsPath + "/Music/" + pathToOGGWithinMusicDirectory, AudioType.OGGVORBIS);
yield return www.SendWebRequest();

using var vorbis = new NVorbis.VorbisReader(new MemoryStream(www.downloadHandler.data));

_rawData = new float[vorbis.TotalSamples * vorbis.Channels];
vorbis.ReadSamples(_rawData, 0, (int) vorbis.TotalSamples * vorbis.Channels);
_sampleRate = vorbis.SampleRate;
_channelCount = vorbis.Channels;
_fullTrackSampleLength = TimeToSamples(vorbis.TotalTime.TotalSeconds);

var loopStart = vorbis.Tags.GetTagSingle("LOOP_START");
var loopEnd = vorbis.Tags.GetTagSingle("LOOP_END");
Debug.Log("loopStart: " + loopStart);
Debug.Log("loopEnd: " + loopEnd);

if (string.IsNullOrEmpty(loopStart) || string.IsNullOrEmpty(loopEnd))
{
_preLoopSampleLength = TimeToSamples(0);
_loopSampleLength = TimeToSamples(vorbis.TotalTime.TotalSeconds);
Debug.Log("no loop markers");
}
else
{
var loopStartSeconds = TimeSpan.Parse(loopStart).TotalSeconds;
var loopEndSeconds = TimeSpan.Parse(loopEnd).TotalSeconds;
Debug.Log("loopStartSeconds: " + loopStartSeconds);
Debug.Log("loopEndSeconds: " + loopEndSeconds);
Debug.Log("loopSeconds: " + (loopEndSeconds - loopStartSeconds));
_preLoopSampleLength = TimeToSamples(loopStartSeconds);
_loopSampleLength = TimeToSamples(loopEndSeconds - loopStartSeconds);
}

Debug.Log("preLoopSampleLength: " + _preLoopSampleLength);
Debug.Log("loopSampleLength: " + _loopSampleLength);
Debug.Log("totalSamples: " + vorbis.TotalSamples);
}

}
Git LFS file not shown

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c483631

Please sign in to comment.