WAVファイルにループポイントを埋め込む

WAVファイルにループポイントを埋め込むことで、Unity上でイントロ付きBGMを実装してみました。

<details><summary>wavファイルにループポイントを埋め込む（抜粋）</summary>

```
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
        }
    }
}
```

</details>

出力されたWAVをUnityに読み込み、Loop ON かつ PlayOnAwaake ON
にするとちゃんとループはしてくれるのですが、
プレビューすると音の最後にノイズがのってしまい、ループしませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/6dd26f28-cecb-48ef-b247-c6d561441fd9.png)

またスクリプトからPlay()した時もループポイントを無視してしまう（音の最後から最初にループ）ようなので、```audioSource.timeSamples```を参照するなどして素直にイントロループを行うスクリプトを用意するのがよさそうです。

<details open><summary>イントロ付きループスクリプト</summary>

```
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);
        }
    }

}
```
</details>

ちなみに、Unity上で直接AudioClipを加工し、部分切り出しやループ音の作成等が可能なツールをアセットストアで販売中です（ちゃっかり宣伝）。

<a href="https://assetstore.unity.com/packages/tools/audio/ambient-looper-203078"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/05900c25-f874-475f-a116-cb1f90a4785c.png">