WAVファイルにループポイントを埋め込むことで、Unity上でイントロ付きBGMを実装してみました。
wavファイルにループポイントを埋め込む(抜粋)
public static void WriteSmplChunk(FileStream outputFile, int loopStart, int loopEnd)
{
using (BinaryWriter writer = new BinaryWriter(outputFile))
{
// "smpl" chunk ID
writer.Write(new char[] { 's', 'm', 'p', 'l' });
// Chunk size (36 bytes for header + 24 bytes per loop)
writer.Write(60); // Fixed size for one loop
// Manufacturer, Product, Sample Period
writer.Write(0); // Manufacturer
writer.Write(0); // Product
writer.Write(22675); // 1000000000ns/44.1kHz
// MIDI Unity Note, Pitch Fraction, SMPTE format/offset
writer.Write(60); // MIDI Unity Note (Middle C)
writer.Write(0); // Pitch Fraction
writer.Write(0); // SMPTE Format
writer.Write(0); // SMPTE Offset
// Number of Loops, Sampler Data
writer.Write(1); // One loop
writer.Write(0); // Sampler Data
// Loop information
writer.Write(0); // Cue Point ID
writer.Write(0); // Loop type (Forward)
writer.Write(loopStart); // Loop start (in samples)
writer.Write(loopEnd); // Loop end (in samples)
writer.Write(0); // Fraction
writer.Write(0xFFFFFFFF); // Infinite loop
}
}
}
出力されたWAVをUnityに読み込み、Loop ON かつ PlayOnAwaake ON
にするとちゃんとループはしてくれるのですが、
プレビューすると音の最後にノイズがのってしまい、ループしませんでした。
またスクリプトからPlay()した時もループポイントを無視してしまう(音の最後から最初にループ)ようなので、audioSource.timeSamples
を参照するなどして素直にイントロループを行うスクリプトを用意するのがよさそうです。
イントロ付きループスクリプト
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace ELIX
{
[RequireComponent(typeof(AudioSource))]
public class IntroLoopPlayer : MonoBehaviour
{
[SerializeField, Tooltip("the end of Intro point"), FormerlySerializedAs("m_LoopSttPoint")] int m_loopSttPoint=-1;
[SerializeField, Tooltip("the start of Outro point"), FormerlySerializedAs("m_LoopEndPoint")] int m_loopEndPoint=-1;
[SerializeField, Tooltip("number of loops")] int m_loopNum = 2;
[SerializeField] UnityEngine.UI.Slider m_slider;
AudioSource m_audioSrc;
AudioClip m_ac;
int m_loopCnt = 0;
bool m_isLoop = false;
// Start is called before the first frame update
void Start()
{
m_audioSrc = gameObject.GetComponent<AudioSource>();
m_isLoop = m_audioSrc.loop;
m_audioSrc.loop = false;
m_ac = m_audioSrc.clip;
if (m_ac == null)
return;
if (m_loopSttPoint < 0) { m_loopSttPoint = 0; }
if (m_loopEndPoint < 0) { m_loopEndPoint = m_ac.samples; }
}
// Update is called once per frame
void Update()
{
if (m_ac == null)
return;
if (m_loopCnt < m_loopNum)
{
//Debug.Log("timeSamples:" + m_audioSrc.timeSamples);
if (m_audioSrc.timeSamples >= m_loopEndPoint)
{
if(m_loopEndPoint >= m_audioSrc.timeSamples)
{
m_audioSrc.Play();
}
m_audioSrc.timeSamples = m_loopSttPoint;
m_loopCnt++;
}
}
else if (m_isLoop)
{
if (m_audioSrc.timeSamples >= m_ac.samples)
{
m_loopCnt = 0;
m_audioSrc.Play();
}
}
if(m_slider != null)
{
m_slider.SetValueWithoutNotify((float)m_audioSrc.timeSamples / (float)m_ac.samples);
}
}
private void OnGUI()
{
GUI.Label(new Rect(10, 10, 1000, 30), $"Loops {m_loopNum} times between {m_loopSttPoint} and {m_loopEndPoint}. {m_audioSrc.timeSamples}/{m_ac.samples}");
}
//private void OnAudioFilterRead(float[] data, int channels) { }
public void PlayDelayed(float _delaySec=0f)
{
m_audioSrc.PlayDelayed(_delaySec);
}
}
}
ちなみに、Unity上で直接AudioClipを加工し、部分切り出しやループ音の作成等が可能なツールをアセットストアで販売中です(ちゃっかり宣伝)。