【Unity】セガサターンのCD再生画面を再現する - その4

f:id:amayoshitqs:20201117010429g:plain

セガサターンのCD再生画面をUnityで再現する第4回です。 今回は左右の音を聞き分ける処理を行います。
それに伴ってキューブも2つになります。

左右の音量差をなるべく分かりやすくするために、簡単な曲を作っています(Youtube著作権対策にもなる)
イントロだけみたいな、20秒にも満たないものですけどね。
コードも小室進行そのままです。

前回はこちら。 amayoshitqs.hatenablog.com

モデルをコピペ

f:id:amayoshitqs:20201120001656p:plain

モデルをコピー&ペーストして左右に設置します。
特に設定の必要もありません。

ついでにUIテキストも2つにしましょう。

この2つのモデルを操作できるようにしましょう。
SerializeFieldを2つ用意します。

[SerializeField] SkinnedMeshRenderer left = default;
[SerializeField] SkinnedMeshRenderer right = default;
[SerializeField] Text leftAudioVolume = default;
[SerializeField] Text rightAudioVolume = default;

続いて一旦2つのモデルに同じ処理をさせてみます。
今まで「skinnedMeshRenderer」で定義していた処理をほぼ2つ分用意しましょう。

まずはStartメソッド。

void Start()
{
    m_index = left.sharedMesh.GetBlendShapeIndex("スフィア");

    left.material.color = new Color(0, 1, 0, 1);
    right.material.color = new Color(0, 1, 0, 1);

    originalScale = left.transform.localScale;

    StartCoroutine(Rotate(left.transform));
    StartCoroutine(Rotate(right.transform));
}

初期状態の保存などは片方あれば十分なのでleftのみ行ってます。
続いてUpdateメソッド。

void Update()
{
    audioSource.GetOutputData(waveArrayData, 1);
    var volumeLeft = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
    leftAudioVolume.text = (volumeLeft * 600).ToString("0.00");
    OnChangeValue(volumeLeft * 600, left);

    audioSource.GetOutputData(waveArrayData, 1);
    var volumeRight = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
    rightAudioVolume.text = (volumeRight * 600).ToString("0.00");
    OnChangeValue(volumeRight * 600, right);
}

それぞれ同じ処理をさせています。

お次はRotateコルーチン。

IEnumerator Rotate(Transform trans)
{
    var count = 0;
    var x = 0.0f;
    var y = 0.0f;
    var z = 0.0f;

    while(true)
    {
        if(count <= 0)
        {
            x = Random.Range(0.0f, 5f);
            y = Random.Range(0.0f, 5f);
            z = Random.Range(0.0f, 5f);
            count = 150;
        }
        else
        {
            trans.Rotate(x, y, z);
            count--;
            yield return null;
        }
    }
}

こちらは引数で受け取ったTransformを回転させるようにしてます。
2つがそれぞれランダムの動きをしてもらうためですね。

最後にOnChangeValueメソッド。

public void OnChangeValue(float value, SkinnedMeshRenderer target)
{
    target.SetBlendShapeWeight(m_index, value);

    var temp = value / 50;
    target.material.color = new Color(Mathf.Clamp(temp, 0.0f, 1.0f), Mathf.Clamp(2-temp, 0.0f, 1.0f), 0, 1);

    target.transform.localScale = originalScale * (value / 100 + 1);
}

引数でSkinnedMeshRendererを受け取り、それに対して処理を行っています。

では、このまま実行してみましょう。

f:id:amayoshitqs:20201120002633g:plain

回転以外同じ動きをしていれば成功です。

左右それぞれの音の強さを取得するには?

それでは左右の音を別々に取得する処理をしていきます、が。
明確に左右の音の強弱が異なる曲を用意するのは難しいと思いますので、今回はこちらで用意させていただきました。

https://www.dropbox.com/s/ui7k5043xjg2ssi/Mixdown.wav?dl=1

ベタ打ちの小室進行です。
最初は左が強めのギターソロ、その後に右から大きめのギターが単音で。
それ以降はドラムとベースを含めてにぎやかな感じ、というよくあるイントロ的な構成です。
片耳イヤホンで左右それぞれ聞いてみると分かりやすいかと思います。

これを使って確認するために、Updateメソッドを下記のように変更します。

void Update()
{
    audioSource.GetOutputData(waveArrayData, 0);
    var volumeLeft = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
    leftAudioVolume.text = (volumeLeft * 600).ToString("0.00");
    OnChangeValue(volumeLeft * 600, left);

    audioSource.GetOutputData(waveArrayData, 1);
    var volumeRight = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
    rightAudioVolume.text = (volumeRight * 600).ToString("0.00");
    OnChangeValue(volumeRight * 600, right);
}

AudioSource.GetOutputData()の第二引数ですが、
0の場合は左、1の場合は右から聞こえる音を取得することができます。

本当にこれだけです。

結果

www.youtube.com

このように、左右の音の強弱を取得することができました(イヤホン推奨)

最後に、本シリーズの初回で見せたセガサターンのCD再生画面をもう一度見てみましょう。

www.youtube.com

...本質は似てるからヨシ!

本シリーズは一旦ここで打ち止めとなります。
サターンのCD再生には他にも様々な機能がありますが、今回やりたかった左右のモデルの変形はそこそこ上手くできたと思います。

後は追加機能を作ったり、背景を宇宙にしたり、飛行機が飛んだりと、独自の再現を行っていただければと思います。
私も執筆に当たって改めて色々知れて良かったです、Blenderとも仲良くなれましたし。

今回までのソースコード

SaturnManager.cs

using System.Collections;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class SaturnManager : MonoBehaviour
{
    [SerializeField] SkinnedMeshRenderer left = default;
    [SerializeField] SkinnedMeshRenderer right = default;
    [SerializeField] AudioSource audioSource = default;
    [SerializeField] Text leftAudioVolume = default;
    [SerializeField] Text rightAudioVolume = default;
    int m_index;
    Vector3 originalScale;
    float[] waveArrayData = new float[2048];

    void Start()
    {
        m_index = left.sharedMesh.GetBlendShapeIndex("スフィア");

        left.material.color = new Color(0, 1, 0, 1);
        right.material.color = new Color(0, 1, 0, 1);

        originalScale = left.transform.localScale;

        StartCoroutine(Rotate(left.transform));
        StartCoroutine(Rotate(right.transform));
    }

    void Update()
    {
        audioSource.GetOutputData(waveArrayData, 1);
        var volumeLeft = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
        leftAudioVolume.text = (volumeLeft * 600).ToString("0.00");
        OnChangeValue(volumeLeft * 600, left);

        audioSource.GetOutputData(waveArrayData, 1);
        var volumeRight = waveArrayData.Select(x => Mathf.Abs(x)).Sum() / waveArrayData.Length;
        rightAudioVolume.text = (volumeRight * 600).ToString("0.00");
        OnChangeValue(volumeRight * 600, right);
    }

    IEnumerator Rotate(Transform trans)
    {
        var count = 0;
        var x = 0.0f;
        var y = 0.0f;
        var z = 0.0f;

        while(true)
        {
            if(count <= 0)
            {
                x = Random.Range(0.0f, 5f);
                y = Random.Range(0.0f, 5f);
                z = Random.Range(0.0f, 5f);
                count = 150;
            }
            else
            {
                trans.Rotate(x, y, z);
                count--;
                yield return null;
            }
        }
    }

    public void OnChangeValue(float value, SkinnedMeshRenderer target)
    {
        target.SetBlendShapeWeight(m_index, value);

        var temp = value / 50;
        target.material.color = new Color(Mathf.Clamp(temp, 0.0f, 1.0f), Mathf.Clamp(2-temp, 0.0f, 1.0f), 0, 1);

        target.transform.localScale = originalScale * (value / 100 + 1);
    }

    public void OnPushPlayButton()
    {
        if(audioSource.isPlaying)
        {
            audioSource.Pause();
        }
        else
        {
            audioSource.Play();
        }
    }

    public void OnPushStopButton()
    {
        audioSource.Stop();
    }
}