目录

  • 要解决的问题
  • 音律
    • 五度相生律与32\frac{3}{2}23​
    • 纯率与54\frac{5}{4}45​
    • 十二平均律与21122^\frac{1}{12}2121​
    • 88键钢琴各键键位与音高
  • Unity GetSpectrumData获取的音频数据与88键钢琴各键的映射
  • 全部代码与测试结果
  • 未解决的问题

要解决的问题

我在上一篇系列文章的结尾段提到了一个可以利用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)之间的相对音高比率。在古代,这些相关的知识就已经形成了音乐领域中的一种学科,设计这些音高比率的过程叫做定律,从中总结出的规则叫做音律。

音律

五度相生律与32\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,所以五度听起来非常协和。

纯率与54\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调都能唱不会有什么问题,但是用纯律或五度相生率写的歌转调后是有可能会跑调的(转调后音的相对关系无法保持一致导致旋律被破坏了)。

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

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

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

88键钢琴各键键位与音高

钢琴各键的音高既是通过十二平均律计算出来的。

(图1:88键钢琴键位与音高,原图地址)

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

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

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

    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 8192for (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 maxif (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]]);//testTestResult(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 pianofloat[] Herz_PianoKeys = new float[88];//A4键440标准赫兹,键位49//A4 at key 49 with 440Hzfloat Herz_A4 = 440f;int Pos_A4 = 49;//12平均律//twelve-tone equal temperamentfloat ET12 = 1f / 12f;//过滤当前音乐杂音阈值//filter threadshold of current music cliipfloat 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.oggprivate AudioSource thisAudioSource;private float[] spectrumData = new float[8192];//the value denpended on pc, sould be updated in runtimeprivate float MaxHerzOfSpectrumData = 22050;//钢琴键位<-->spectrumData位置//piano key position<-->spectrumData positionprivate 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 8192for (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 maxif (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]]);//testTestResult(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  40List<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 updatevoid Start(){//例如:44100/2=22050//eg:44100/2=22050MaxHerzOfSpectrumData = AudioSettings.outputSampleRate / 2;thisAudioSource = gameObject.GetComponent<AudioSource>();InitAllPianoKeysHerz();BindKeysAndSpectrumData();thisAudioSource.Play();Invoke("DebugTestResults", thisAudioSource.clip.length);}// Update is called once per framevoid Update(){thisAudioSource.GetSpectrumData(spectrumData, 0, FFTWindow.BlackmanHarris);AnalyzeMusic();}//debug functionvoid 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
音乐理论基础 --李重光

用Unity的GetSpectrumData方法识别钢琴曲中的钢琴琴键相关推荐

  1. python 如何识别字符串中的人名 ,如何识别一串拼音字符串以及韵母的提取 (一些方法整理)

    一.识别字符串中的人名或特定名词 笔者所用的是百度智能云提供的词法分析接口,它可以对字符串分词,并且能够识别句子中的人名,地名,等等.好,接下来教你们如何使用这个接口. 官网:https://clou ...

  2. echarts怎么控制一个点沿着折线移动_计算机怎么识别图像中的直线?

    1 直线检测问题 在纸上画一条直线,用手机拍下照片,把照片交给计算机识别. 计算机是如何知道这张照片中的这条直线的? 存在直线吗? 直线在哪里? 点.线.面是基本的几何元素. 欧几里得在<几何原 ...

  3. (转)[EntLib]微软企业库5.0 学习之路——第十步、使用Unity解耦你的系统—PART2——了解Unity的使用方法(1)...

    原文地址:http://www.cnblogs.com/kyo-yo/archive/2010/11/01/Learning-EntLib-Tenth-Decoupling-Your-System-U ...

  4. OpenCV3 识别图中表格-JAVA 实现

    2019独角兽企业重金招聘Python工程师标准>>> 关于 JAVA 学习 OpenCV 的内容,函数讲解.内容我均整理在 GitHubd的OpenCV3-Study-JAVA O ...

  5. IE浏览器解决无法识别js中getElementsByClassName问题

    关于ie浏览器无法识别js中getElementsByClassName问题,现通过以下方法,引用如下js /***打印js对象详细信息*/ function alertObj(obj) {var d ...

  6. 一文详解深度学习在命名实体识别(NER)中的应用

    近几年来,基于神经网络的深度学习方法在计算机视觉.语音识别等领域取得了巨大成功,另外在自然语言处理领域也取得了不少进展.在NLP的关键性基础任务-命名实体识别(Named Entity Recogni ...

  7. linux怎样自动检查link文件_怎样理解和识别 Linux 中的文件类型 | Linux 中国

    众所周知,在 Linux 中一切皆为文件,包括硬盘和显卡等.在 Linux 中导航时,大部分的文件都是普通文件和目录文件.但是也有其他的类型,对应于 5 类不同的作用.因此,理解 Linux 中的文件 ...

  8. Facebook AI牛津提出带“轨迹注意力”的Video Transformer,在视频动作识别任务中性能SOTA!...

    关注公众号,发现CV技术之美 ▊ 写在前面 在视频Transformer中,时间维度通常与两个空间维度(W和H)的处理方式相同.然而,在对象或摄影机可以移动的场景中,在第t帧中的一个位置处的物体可能与 ...

  9. 绒毛动物探测器:通过TensorFlow.js中的迁移学习识别浏览器中的自定义对象

    目录 起点 MobileNet v1体系结构上的迁移学习 修改模型 训练新模式 运行物体识别 终点线 下一步是什么?我们可以检测到脸部吗? 下载TensorFlowJS-Examples-master ...

  10. dll放在unity哪个文件夹下_程序丨如何将你的Unity代码整理到一个DLL中?

    原标题:程序丨如何将你的Unity代码整理到一个DLL中? 翻译:林政(玄猫大人) 审校:沈晓霖 代码复用的重要性 这里有一则故事也是你听过类似的: 你下载好Unity,看完Youtube上的一些教程 ...

最新文章

  1. python 之 Pandas (五)导入导出
  2. 看漫画学python电子书-看漫画学Python(有趣有料好玩好用全彩版)
  3. 别把项目成功当目标 (转)
  4. 直播电商要处理好五个关系
  5. MySQL数据库与Oracle数据库在存储中文字符以字节或字符存储的区别
  6. CNN卷积神经网络原理讲解+图片识别应用(附源码)
  7. linux下WPS并安装字体
  8. 【DirectX】D3D中基本图形绘制函数形参意义(总结)
  9. 组态王中Modbus字节顺序的调整
  10. Storage LUN connected on Emulex / Qlogica HBA not detecting during boot time.
  11. LVGL lv_msgbox消息对话框(22)
  12. 什么运动耳机好用,六款运动耳机值得推荐
  13. 【MySQL】黑马教程MySQL数据库 MySQL基础(一)
  14. 用于光导耦合的倾斜光栅的分析
  15. shader Cg 基本数据类型
  16. php实现两个数相乘,最高效的乘法:两个非常大的数字相乘迄今最快算法
  17. FCRP-D---帆软官网模拟题,kettle模块
  18. 大数据的中的数据是从哪里来的?
  19. R语言:接受拒绝法(Acceptance-Rejection Method)生成随机数
  20. java if作业_19201528- JAVA所有作业总结

热门文章

  1. 用Nodejs爬取Matrix67的博客
  2. Hack the box靶机 Blunder
  3. 程序查询方式、程序中断方式和DMA方式
  4. 仿新浪微博布局学习——妙用TabHost
  5. html灯箱效果代码,WordPress纯代码实现图片灯箱lightbox效果
  6. 后盾网php微博系统,后盾网thinkphp5.0 博客系统实现
  7. 网络共享中心的计算机名,网络和共享中心在哪?教你打开Windows电脑系统网络和共享中心5大方法...
  8. 三星为Ativ S发布WP8更新
  9. 生物计算机 量子计算机,生物计算机光子计算机量子计算机哪个更先进
  10. Matplotlib绘制半圆形