Skip to content

Instantly share code, notes, and snippets.

@Yamayamada0924
Last active September 14, 2023 12:13
Show Gist options
  • Save Yamayamada0924/932689cab9f2c52032016eacf29ab700 to your computer and use it in GitHub Desktop.
Save Yamayamada0924/932689cab9f2c52032016eacf29ab700 to your computer and use it in GitHub Desktop.
using System.Collections.Generic;
using UnityEngine;
namespace InteractiveMusic
{
/// <summary>
/// # VerticalMixMusic
/// WebGLで縦の遷移を行うためのComponent
///
/// こちらの方が後発で上位互換です -> https://gist.github.com/Yamayamada0924/d3fe20835d2e6bb696c82007b5e6de26
///
/// ## 準備
/// musicClipsにAudioClipを設定する
/// 縦の遷移を行う場合はsizeを2以上にする
/// 更にmodesにはこのAudioClipのボリュームの組み合わせを設定する
/// volumesの数はmusicClipsの数と同じにする必要がある
/// modesの数は1以上であればいくつでも良い
///
/// ## 使い方
/// Prepareを呼んだ後にPlayを呼ぶことで再生する
/// Prepareには完了まで数フレームかかるのであらかじめ呼んでおくことが望ましい
/// prepareOnAwakeをtrueにすることでAwake時にPrepareを呼ぶことができる
/// ChangeModeを呼ぶことであらかじめ設定したボリュームの組み合わせに変更できる
///
/// ## 縦の遷移を行う場合
/// 同じ長さで楽器パートが違うAudioClipを複数用意して、musicClipsに設定する
/// 例えばメロディのみのAudioClipと打楽器のAudioClipを用意する
/// modesの1つ目はvolumesの0番目を1.0に、1番目を0.0にする
/// modesの2つ目はvolumesの0番目を1.0と1番目を1.0にする
/// ChangeMode(0)を呼ぶとメロディのみのAudioClipが再生され、ChangeMode(1)を呼ぶとメロディと打楽器のAudioClipが再生される
///
/// ## 注意事項
/// ループポイントを設定したwavファイルを用意してもWebGLではループポイントが無視され、全体がループする
/// AudioClip はロードタイプを `Decompress On Load` にして `Preload Audio Data` をオフにする設定を推奨する
///
/// ## 仕組み
/// 最初に一瞬再生することで読み込みを予め行い、その後に再生することで2つ以上のAudioClipをほぼ同じタイミングで再生する
/// 片方のAudioClipを無音で再生しておくことで、そのボリュームの変化で縦の遷移を実現している
///
/// </summary>
public class VerticalMixMusic : MonoBehaviour
{
[SerializeField, Range(0.0f, 1.0f)] private float volume;
[SerializeField] private List<AudioClip> musicClips;
[SerializeField] private bool loop;
[SerializeField] private bool prepareOnAwake;
[SerializeField] private bool playOnAwake;
[System.SerializableAttribute]
private struct ModeParam
{
[SerializeField] private string modeName;
public string ModeName => modeName;
[SerializeField, Range(0.0f, 1.0f)] private List<float> volumes;
public IReadOnlyList<float> Volumes => volumes;
[SerializeField] private float modeChangeTime;
public float ModeChangeTime => this.modeChangeTime;
}
[SerializeField] private List<ModeParam> modes = new List<ModeParam>();
private readonly List<AudioSource> _audioSources = new List<AudioSource>();
private bool _prepared;
private bool _waitPrepare;
private bool _reservedPlay;
private float _inputVolume;
private int _mode;
private int _nextMode;
private readonly List<float> _modeChangeVolumes = new List<float>();
private float _modeChangeTime;
private void Awake()
{
_prepared = false;
_waitPrepare = false;
_reservedPlay = false;
_inputVolume = 1.0f;
_mode = 0;
_nextMode = 0;
_modeChangeTime = 0.0f;
CreateAudioSource();
if (playOnAwake)
{
Play();
}
else if (prepareOnAwake)
{
Prepare();
}
#if DEBUG
foreach (var modeParam in modes)
{
if(modeParam.Volumes.Count < musicClips.Count)
{
Debug.LogWarning(
$"{nameof(modes)} {modeParam.ModeName} の volumes が足りていません。足りないボリュームは 0 として扱われます。");
}
if(modeParam.Volumes.Count > musicClips.Count)
{
Debug.LogWarning(
$"{nameof(modes)} {modeParam.ModeName} の volumes が多いです。");
}
}
#endif
}
private void CreateAudioSource()
{
if (musicClips.Count == 0)
{
Debug.LogWarning($"{nameof(musicClips)} が設定されていません。");
return;
}
#if DEBUG
var samples = musicClips[0].samples;
#endif
foreach (var musicClip in musicClips)
{
var audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = musicClip;
audioSource.loop = loop;
audioSource.volume = 0.0f;
_audioSources.Add(audioSource);
_modeChangeVolumes.Add(0.0f);
#if DEBUG
if(musicClip.samples != samples)
{
Debug.LogWarning($"{nameof(musicClips)} のsample数(音楽の長さ)が一致しません。");
}
#endif
}
}
private void Update()
{
if (_waitPrepare)
{
var isAllPlaying = true;
foreach (var audioSource in _audioSources)
{
if( !audioSource.isPlaying )
{
isAllPlaying = false;
}
}
if (isAllPlaying)
{
foreach (var audioSource in _audioSources)
{
audioSource.Stop();
}
_waitPrepare = false;
_prepared = true;
}
return;
}
if(_reservedPlay)
{
_reservedPlay = false;
for (var index = 0; index < _audioSources.Count; index++)
{
var audioSource = _audioSources[index];
audioSource.volume = volume * _inputVolume * GetModeVolume(_mode, index);
audioSource.Play();
}
return;
}
if(_mode != _nextMode)
{
_modeChangeTime -= Time.deltaTime;
if(_modeChangeTime <= 0.0f)
{
_mode = _nextMode;
_modeChangeTime = 0.0f;
for (var index = 0; index < _audioSources.Count; index++)
{
var audioSource = _audioSources[index];
audioSource.volume = volume * _inputVolume * GetModeVolume(_mode, index);
}
}
else
{
var t = _modeChangeTime / GetModeChangeTime(_mode);
for (var index = 0; index < _audioSources.Count; index++)
{
var audioSource = _audioSources[index];
audioSource.volume = volume * _inputVolume * Mathf.Lerp(GetModeVolume(_nextMode, index), _modeChangeVolumes[index], t);
}
}
}
}
public void Play( int mode = 0, float inputVolume = 1.0f)
{
_reservedPlay = true;
_inputVolume = inputVolume;
_mode = _nextMode = mode;
if(!_prepared)
{
Prepare();
}
}
public void Stop()
{
_reservedPlay = false;
foreach (var audioSource in _audioSources)
{
audioSource.Stop();
}
}
public void ChangeMode(string modeName)
{
for (int i = 0; i < modes.Count; i++)
{
if (modes[i].ModeName == modeName)
{
ChangeMode(i);
return;
}
}
Debug.LogWarning($"mode {modeName} は見つかりませんでした。");
}
public void ChangeMode(int mode)
{
_nextMode = mode;
_modeChangeTime = GetModeChangeTime(_mode);
for (int i = 0; i < _audioSources.Count; i++)
{
_modeChangeVolumes[i] = _audioSources[i].volume;
}
}
public void Prepare()
{
if(_prepared)
{
return;
}
if(_waitPrepare)
{
return;
}
foreach (var audioSource in _audioSources)
{
audioSource.volume = 0.0f;
audioSource.Play();
}
_waitPrepare = true;
}
public void SetVolume(float inputVolume)
{
_inputVolume = inputVolume;
if(_mode != _nextMode)
{
// Updateですぐに変わるので何もしない
}
else
{
for (var index = 0; index < _audioSources.Count; index++)
{
var audioSource = _audioSources[index];
audioSource.volume = volume * _inputVolume * GetModeVolume(_mode, index);
}
}
}
private float GetModeVolume(int mode, int index)
{
if (mode < 0 || mode >= modes.Count)
{
Debug.LogWarning($"モード {mode} は範囲外です。");
return 0.0f;
}
var modeParam = modes[mode];
if (index < 0 || index >= modeParam.Volumes.Count)
{
return 0.0f;
}
return modeParam.Volumes[index];
}
private float GetModeChangeTime(int mode)
{
if (mode < 0 || mode >= modes.Count)
{
Debug.LogWarning($"モード {mode} は範囲外です。");
return 0.0f;
}
var modeParam = modes[mode];
return modeParam.ModeChangeTime;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment