用Unity的GetSpectrumData方法识别钢琴曲中的钢琴琴键

要解决的问题

我在上一篇系列文章的结尾段提到了一个可以利用GetSpectrumData方法检测乐曲中的音高的思路,本文既是对此进行尝试与验证。

那篇文章中已经写过,从物理的角度看,声音是一种波动,音乐则是一种有规则的声音波动(无规则的被称为噪音)。不管是旋律(一个声波接着另一个声波)还是声(声波的叠加),人类听觉总是偏喜欢“更和谐”的音高组合,从数学上看,这个音高组合问题其实是一个相对比率问题,既是C(do) D(re) E(mi) F(fa) G(sol) A(la) B(si) C(do) #C(升do) #D(升re) #F(升fa) #G(升sol) #A(升la)之间的相对音高比率。在古代,这些相关的知识就已经形成了音乐领域中的一种学科,设计这些音高比率的过程叫做定律,从中总结出的规则叫做音律。

音律

五度相生律与 3 2 \frac{3}{2} 23

世界上很多文明从不同时期都独立的发现了类似的音律思想,在中国叫五度相生律,它的核心是一个3:2的比率。假如我们定一个基准比率C4=1,通过3:2找到它的上方五度音(度可以理解为五线谱上的线和间,例如do到mi一共经过两线一间,所以它们是三度)G4=3/2,继续通过3:2找到G4的上方五度D5=9/4,任何一个音的八度都是二倍比率,降一个八度找到D4=9/8,D4的五度A4=27/16,A4的五度E5=81/32,降八度E4=81/64,E4的五度B4=243/128…以此类推找出所有音高的比率,再通过一个标准音高值进行相乘即可得出所有音高值。五度相生律的优点是由于所有五度(例如do和so)的频率比都是3比2,所以五度听起来非常协和。

纯率与 5 4 \frac{5}{4} 45

在五度相生律的基础上引入一个5:4的比率去找一个音的大三度音。例如已知标准c4=1,通过3/2比率找出纯五度g4=3/2,再通过c4的5/4比率找出大三度e4=5/4,通过c4的3/2比率找出纯五度f3=2/3,f4=4/3,再通过f3的5/4比率找出大三度a3=5/6,a4=5/3,通过g4的3/2比率找出d5=9/4,d4=9/8,通过g4的5/4比率找出b4=15/8…纯律的优点是和弦中比较重要的三度音听起来更协和。

纯律和五度相生律最大的问题是不可转调,(转调简单来说就是原曲是C->D->E,转某个调后变成F->G->A,由于每个音之间的音程(相对距离关系)没变,所以听着旋律还是对的)。例如现在的流行音乐(使用十二平均律)有时用C调或降E调都能唱不会有什么问题,但是用纯律或五度相生率写的歌转调后是有可能会跑调的(转调后音的相对关系无法保持一致导致旋律被破坏了)。

十二平均律与 2 1 12 2^\frac{1}{12} 2121

十二平均律是将一个八度(从声波频率倍数1到2的距离)分为了十二个半音,这十二个半音的音高的比率从数学上看是一个等比队列,每个半音之间的比率是2的1/12次方,例如以A4=440Hz为标准,那么#A4=440* 2 1 12 2^\frac{1}{12} 2121=466.164Hz。代入所有键的位置参数,其他键的音高频率可通过一个通用公式 f ( n ) = ( 2 1 12 ) n − 49 ∗ 440 H z f(n)={(2^\frac{1}{12})}^{n-49}*440Hz f(n)=(2121)n49440Hz得出,其中n是半音的键位。

十二平均律的优点是可以灵活转调,缺点是从数值上看关键音程的协和度没有上面两种更纯粹,比如某些音程比率在五度相生率中比例是3:2,但在十二平均律中可能是3:2.1,但是这些差距对于人类听觉来讲影响不大。

88键钢琴各键键位与音高

钢琴各键的音高既是通过十二平均律计算出来的。
在这里插入图片描述
(图1:88键钢琴键位与音高,原图地址

Unity GetSpectrumData获取的音频数据与88键钢琴各键的映射

理解好了理论后,我们接下来进行实践。

第一步将图1中的钢琴各键标准频率保存在一个数组中,可利用上面提到的公式“ f ( n ) = ( 2 1 12 ) n − 49 ∗ 440 H z f(n)={(2^\frac{1}{12})}^{n-49}*440Hz f(n)=(2121)n49440Hz”进行计算:

    float[] Herz_PianoKeys = new float[88];
    void InitAllPianoKeysHerz()
    {
        for(int i = 0; i < Herz_PianoKeys.Length; i++)
        {
            Herz_PianoKeys[i] = Mathf.Pow(Mathf.Pow(2, ET12), ((i + 1) - Pos_A4))*Herz_A4;
        }
    }

第二步,将每个钢琴键标准频率数组的index与GetSpectrumData获取的频谱数组中的最接近它的频率的index,一同插入到一个字典中:

    private float[] spectrumData = new float[8192];
    private Dictionary<int, int> KeysDataMap = new Dictionary<int, int>();
    
    void BindKeysAndSpectrumData()
    {
    	//由于官网上缺乏说明,其实这里对GetSpectrumData的返回数组中的每个成员的所代表的频率只是猜测,不过从测试结果来看应该是猜对了
        float interval = MaxHerzOfSpectrumData / spectrumData.Length;
        //尝试找到精确的映射。与88和8192两个参数有关。
        //try to find the precise mapping.the algorithm depending on the parameters of 88 and 8192
        for (int i = 0; i < spectrumData.Length; i++)
        {
            for (int j = 0; j < Herz_PianoKeys.Length; j++) {
                if (Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 0.05f)
                {
                    KeysDataMap[j] = i;
                }
                else if (KeysDataMap.ContainsKey(j) == false && Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 1f)
                {
                    KeysDataMap[j] = i;
                }
                else if (KeysDataMap.ContainsKey(j) == false && Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 2f)
                {
                    KeysDataMap[j] = i;
                }
            }
        }
    }

最后一步,在update()中实时分析spectrumData中对应钢琴键的频率的成员,找出在当前帧它们之中最大音量的频率:

    void AnalyzeMusic() 
    {
        float maxValue = 0;
        int maxKey = 0;
        foreach (var key in KeysDataMap.Keys)
        {
            //find max
            if (spectrumData[KeysDataMap[key]] > maxValue && spectrumData[KeysDataMap[key]] > threadshold)
            {
                maxValue = spectrumData[KeysDataMap[key]];
                maxKey = key;
            }
        }

        if (maxValue>0){
            Debug.Log(maxKey + 1);
            Debug.Log(spectrumData[KeysDataMap[maxKey]]);
            //test
            TestResult(maxKey+1);
        }
    }

全部代码与测试结果

分析一个从中央C爬音到B再降回C的一个简单音频(
C4(键位40)->C#4(键位41)->D4(键位42)->D#4(键位43)->E4(键位44)->F4(键位45)->F#4(键位46)->G4(键位47)->G#4(键位48)->A4(键位49)->A#4(键位50)->B4(键位51)->A5(键位52)->B4(键位51)->A#4(键位50)->A4(键位49)->G#4(键位48)->G4(键位47)->F#4(键位46)->F4(键位45)->E4(键位44)->D#4(键位43)->D4(键位42)->C#4(键位41)->C4(键位40)

结果(Debug打印AnalyzeMusic方法检测出的每个键的键位):
在这里插入图片描述
(图2:爬音音频测试结果正确)

全部代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PianoKeysDetector : MonoBehaviour
{
    //88键钢琴
    //88 keys piano
    float[] Herz_PianoKeys = new float[88];

    //A4键440标准赫兹,键位49
    //A4 at key 49 with 440Hz
    float Herz_A4 = 440f;
    int Pos_A4 = 49;

    //12平均律
    //twelve-tone equal temperament
    float ET12 = 1f / 12f;

    //过滤当前音乐杂音阈值
    //filter threadshold of current music cliip
    float threadshold = 0.002f;

    //被分析的音乐来自:https://upload.wikimedia.org/wikipedia/commons/f/f0/ChromaticScaleUpDown.ogg
    //music clip from:https://upload.wikimedia.org/wikipedia/commons/f/f0/ChromaticScaleUpDown.ogg
    private AudioSource thisAudioSource;
    private float[] spectrumData = new float[8192];
    //the value denpended on pc, sould be updated in runtime
    private float MaxHerzOfSpectrumData = 22050;

    //钢琴键位<-->spectrumData位置
    //piano key position<-->spectrumData position
    private Dictionary<int, int> KeysDataMap = new Dictionary<int, int>();

    void InitAllPianoKeysHerz()
    {
        for(int i = 0; i < Herz_PianoKeys.Length; i++)
        {
            Herz_PianoKeys[i] = Mathf.Pow(Mathf.Pow(2, ET12), ((i + 1) - Pos_A4))*Herz_A4;
        }
    }

    void BindKeysAndSpectrumData()
    {
        float interval = MaxHerzOfSpectrumData / spectrumData.Length;
        //尝试找到精确的映射。与88和8192两个参数有关。
        //try to find the precise mapping.the algorithm depending on the parameters of 88 and 8192
        for (int i = 0; i < spectrumData.Length; i++)
        {
            for (int j = 0; j < Herz_PianoKeys.Length; j++) {
                if (Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 0.05f)
                {
                    KeysDataMap[j] = i;
                }
                else if (KeysDataMap.ContainsKey(j) == false && Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 1f)
                {
                    KeysDataMap[j] = i;
                }
                else if (KeysDataMap.ContainsKey(j) == false && Mathf.Abs((i + 1) * interval - Herz_PianoKeys[j]) <= 2f)
                {
                    KeysDataMap[j] = i;
                }
            }
        }
    }

    void AnalyzeMusic() 
    {
        float maxValue = 0;
        int maxKey = 0;
        foreach (var key in KeysDataMap.Keys)
        {
            //find max
            if (spectrumData[KeysDataMap[key]] > maxValue && spectrumData[KeysDataMap[key]] > threadshold)
            {
                maxValue = spectrumData[KeysDataMap[key]];
                maxKey = key;
            }
        }

        if (maxValue>0){
            Debug.Log(maxKey + 1);
            Debug.Log(spectrumData[KeysDataMap[maxKey]]);
            //test
            TestResult(maxKey+1);
        }
    }

    //should be:C4 C#4 D4 D#4 E4 F4 F#4 G4 G#4 A4 A#4 B4 C5 B4 A#4 A4 G#4 G4 F#4 F4 E4 D#4 D4 C#4 C4
    //          40 41  42 43  44 45 46  47 48  49 50  51 52 51 50  49 48  47 46  45 44 43  42 41  40
    List<int> testResults = new List<int>();
    void TestResult(int val)
    {
        if (testResults.Count > 0 && val != testResults[testResults.Count - 1])
        {
            testResults.Add(val);
        }
        else if (testResults.Count == 0)
        {
            testResults.Add(val);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //例如:44100/2=22050
        //eg:44100/2=22050
        MaxHerzOfSpectrumData = AudioSettings.outputSampleRate / 2;
        thisAudioSource = gameObject.GetComponent<AudioSource>();
        InitAllPianoKeysHerz();
        BindKeysAndSpectrumData();
        thisAudioSource.Play();
        Invoke("DebugTestResults", thisAudioSource.clip.length);
    }

    // Update is called once per frame
    void Update()
    {
        thisAudioSource.GetSpectrumData(spectrumData, 0, FFTWindow.BlackmanHarris);
        AnalyzeMusic();
    }

    //debug function

    void DebugAllPianoKeysHerz()
    {
        for (int i = 0; i < Herz_PianoKeys.Length; i++)
        {
            Debug.Log(i);
            Debug.Log("Herz:" + Herz_PianoKeys[i]);
        }
    }
    void DebugKeysDataMap()
    {
        foreach (var key in KeysDataMap.Keys)
        {
            Debug.Log(key);
            Debug.Log(KeysDataMap[key]);
        }
    }
    void DebugTestResults()
    {
        string result = "";
        for(int i = 0; i < testResults.Count; i++)
        {
            result += testResults[i].ToString() + " ";
        }
        Debug.Log(result);
    }
}

未解决的问题

观察图1的钢琴按键的频率分布,低音区的数值间隔很小,例如按键2和3的频率只相差不到2,而Unity GetSpectrumData获取的数据的最大频率间隔(使用最长的数组8192)是2.6916Hz,这样在映射低音区时会有些精确度问题,虽然那部分超低音键在实际演奏中比较少见。

另外,音乐的发声体的震动所产生的声波并不是单一频率波,例如拉小提琴的弦或敲击钢琴的音板都会导致有规律的共振,一个钢琴键按下会产生一个基音和多个泛音,在一个简单的声音环境中(例如上面测试中分析的简单钢琴爬音音频)我们可以通过一些阈值变量(例如代码中的threadshold变量)滤掉泛音,但在一个复杂的各种音色的基音和泛音叠加的快速变化的环境中,简单的通过上面的寻找最大值配合过滤的算法是很难分析出正确的基音的,需要调整FFT window参数并配合一些更好的更适合于被分析数据的插值和过滤算法。


传送门:
上一篇系列文章----用Unity3D内部频谱分析方法做音乐视觉特效的原理说明
英文版:编辑中…


GitHub项目链接:
https://github.com/liu-if-else/UnitySpectrumDataDetectsPianoKeys


参考:
各种维基百科 --wikipedia
音乐理论基础 --李重光

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 要将Unity3D与电钢琴连接,首先需要一台电脑,并确保其已安装Unity3D软件。然后,需准备一根MIDI连接线,该线可将电钢琴与电脑连接起来。 下面是连接步骤: 1. 将一端的MIDI连接线插入电钢琴的MIDI输出插孔。 2. 将另一端的MIDI连接线插入电脑的MIDI输入插孔。 3. 打开Unity3D软件,选择“文件”菜单,点击“新建项目”。 4. 在新项目,创建一个新的场景或打开已有项目的场景。 5. 在Unity3D,选择“编辑”菜单,点击“项目设置”。 6. 在项目设置对话框,选择“输入”选项卡。 7. 在输入选项卡,点击“检测输入设备”按钮。 8. Unity3D会自动检测并显示可用的MIDI设备列表。 9. 选择正确的电钢琴的MIDI设备,并关闭项目设置对话框。 10. 在场景,创建一个空物体,用于处理从电钢琴接收到的MIDI消息。 11. 为该空物体添加一个脚本,该脚本将监听并处理MIDI消息。 12. 在脚本,你可以根据接收到的MIDI消息,编写代码来控制Unity3D的游戏逻辑或音频播放。 通过以上连接步骤,我们可以让Unity3D接收到来自电钢琴的MIDI消息,并根据这些消息进行相应的游戏逻辑或音频控制。注意,在进行连接和设置之前,请确保在电钢琴上已正确配置MIDI设置,并在Unity3D正确设置输入设备,以确保正常工作。 ### 回答2: Unity3D是一款功能强大的游戏开发引擎,也可以用于连接电钢琴。要连接电钢琴,首先需要确保电钢琴正确连接到计算机。可以通过USB线将电钢琴连接到计算机的USB接口上。然后,在Unity3D使用C#脚本来控制电钢琴。 在Unity3D连接电钢琴的第一步是创建一个脚本来处理连接和控制电钢琴的功能。可以使用Unity的脚本编辑器来创建一个新的脚本文件。在脚本,需要引用Unity的输入系统来获取电钢琴的输入信号。 接下来,需要编写代码来获取电钢琴的键盘输入信号。使用Unity的输入系统可以轻松地获取电钢琴上每个键盘的输入状态。代码可以设置一个函数来处理电钢琴的输入,例如按下、释放或持续按住一个键。 在代码,可以使用脚本定义的函数来控制音符的播放。通过使用Unity3D的音频管理系统,可以加载和播放音符的音频文件。可以根据电钢琴输入的信号来触发正确的音频文件。 最后,需要将脚本附加到Unity3D场景的对象上。可以选择一个空对象,然后将脚本拖放到对象上。这样,脚本将被加载并在游戏运行时控制电钢琴。 总之,通过在Unity3D编写脚本来连接电钢琴,可以实现与电钢琴的交互,使其成为游戏音乐元素。这种连接方式可以为游戏增加更多的乐趣和创新。 ### 回答3: 要将Unity3D连接到电钢琴,您需要完成以下几个步骤: 首先,确保您已经安装了Unity3D开发环境,并且您的电钢琴具备与计算机连接的功能。通常,电钢琴可以通过USB接口与计算机连接。 接下来,在Unity3D创建一个新的项目或打开您现有的项目。确保您是以管理员权限运行Unity3D,以便对连接硬件进行操作。 然后,导入相关的插件或脚本,以便能够与电钢琴进行通信。通常,有一些第三方插件可以帮助实现这一功能。您可以在Unity Asset Store搜索“钢琴连接插件”或“MIDI插件”来获取与您的电钢琴兼容的插件。 安装并导入所选的插件后,您需要在Unity3D配置和设置相关的参数。您可以通过阅读插件的文档或参考在线教程来了解如何正确设置这些参数。通常,您需要指定连接电钢琴的端口和速度。 完成上述步骤后,您可以使用Unity3D的脚本编写代码来与电钢琴进行交互。您可以使用脚本控制琴键的按下和释放动作,并根据电钢琴发送的信号触发相应的音频或图形效果。 最后,测试连接是否成功。在运行Unity3D项目之前,请确保您的电钢琴已经连接到计算机上,并且正确地显示在Unity3D的设备列表。您可以通过在Unity3D项目按下电钢琴上的琴键并观察是否能够在Unity3D的场景触发相应的动作来进行测试。 总之,通过选择合适的插件、配置相关参数并编写适当的代码,您可以实现Unity3D与电钢琴的连接,并且在游戏或应用程序实现互动的音乐效果。希望以上回答能为您提供帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值