用Unity的GetSpectrumData方法识别钢琴曲中的钢琴琴键
Posted liu_if_else
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用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 \\frac32 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 \\frac54 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^\\frac112 2121
十二平均律是将一个八度(从声波频率倍数1到2的距离)分为了十二个半音,这十二个半音的音高的比率从数学上看是一个等比队列,每个半音之间的比率是2的1/12次方,例如以A4=440Hz为标准,那么#A4=440* 2 1 12 2^\\frac112 2121=466.164Hz。代入所有键的位置参数,其他键的音高频率可通过一个通用公式 f ( n ) = ( 2 1 12 ) n − 49 ∗ 440 H z f(n)=(2^\\frac112)^n-49*440Hz f(n)=(2121)n−49∗440Hz得出,其中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^\\frac112)^n-49*440Hz f(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 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++)以上是关于用Unity的GetSpectrumData方法识别钢琴曲中的钢琴琴键的主要内容,如果未能解决你的问题,请参考以下文章
用Unity的GetSpectrumData方法识别钢琴曲中的钢琴琴键