第三次作业
截止时间: 2016年11月20日 09:00
这次作业的内容是用 shazam算法 实现音频检索(听歌识曲)。 shazam算法一共可以分成4个模块: 音频文件读取、指纹提取(hashing)、指纹存取、和指纹匹配。 关于这四个模块到底做什么,以及它们之间的关系,已经在群文件 ppt 中详细说明。
建议大家组队完成项目,一队3-4人为宜,最多4人。分工自行协商,每个人至少负责一个完整的模块。
知识性的内容已经在ppt中做了充分的介绍。在网页中主要是介绍提交的注意事项。
团队要求
- 只需队长建库,其他人不需建库。队长需在根目录下放一个成员名单,说明成员(含队长)的姓名、学号和分工。
- 每个人至少2次commit,否则此人不给分。
- 远程仓库的目录结构要和本地intellij/eclipse项目的目录结构一致。 不得在项目外再套文件夹。 换句话说,git根目录就是项目根目录。
- 所有文档(截图,txt等)放在项目根目录的doc文件夹下。
- 由于音频文件过大,不得放在项目目录中。请自行在程序入口放置Scanner来读取音频文件(夹)的路径。
- 一个模块一个package。
- 有
merge commit
的团队,可以加团队分。
两个建议:
- 多人合作的项目总要有一个初始版本,这样才方便其他人在这个版本上一点一点地推进项目。 音频文件读取是最固定的模块,因此适合作为初始版本。
- 每次打开项目之前一定别忘了先pull一下!!!
音频读取
- 音频文件一律采用【PCM WAV格式存储,位深度16bit,采样率44100Hz,单声道,不携带元信息。】
- 样例音频库 提取码 4zk2
- 写一个测试类: 任挑一首歌或其片段,把它的时序信息输出到txt。 之后把txt中内容粘贴到matlab,同时用matlab读取原始音频,比较两个波形。
指纹提取
这个模块没有有效的单元测试手段,只能看最终结果。
加分项:用O(n)复杂度实现”在4096个频率中找到能量最大的m个频率”。这是腾讯面试题,希望大家引起重视。 参考书:《算法导论》第三版,9.3节。 参考博文:http://blog.csdn.net/v_JULY_v/article/details/6370650
匹配打分
匹配至少5个歌曲片段,结果截图。
指纹存取(即数据库模块)
- 允许的数据库: MySQL,PostgreSQL。严禁使用Oracle,因为它会给队友带来很多麻烦。
- 在lib中附加jdbc引擎,并提交到git。
- 写一个测试类: 查询并输出当前时间。
- 保留建表SQL。
提交内容小结
- 项目代码。
- 建表sql,放在doc目录下。
- 波形对比截图。
- 查询当前时间截图。
- 至少5个匹配结果截图。
需要的工具小结
除编程工具(jdk、intellij/eclipse、数据库)外,可能还需要UltraEdit、Adobe Audition、Matlab。
附录
如何生成PCM WAV文件?
以Adobe Audition为例,
批量转换:
保存选区:
如何在matlab中验证音频解析的结果?
建议自己截一段几秒十几秒的音乐来验证,不要用整首歌,太大了。
** 2016-11-04 更新 **
首先,把算出来的double数组打印到csv文件。
PrintWriter out = new PrintWriter(new FileWriter(new File("test.csv")));
for (int i=0; i < sequence.length; ++i) {
out.printf("%.2f,\n", sequence[i]);
}
打出来的内容就是Matlab中的一个数组变量. 之后在Matlab中输入
a = csvread('test.csv');
a = a(:,1);
之后在Matlab中直接读取原音频,生成变量b。
[b sr] = audioread('音频目录');
b是时序数据,sr是采样率。
最后用Matlab绘制a和b的波形图。
subplot(2,1,1), plot(a./32768); subplot(2,1,2), plot(b);
会出现这样的一个图表:
上面是你自己算出来的,下面是Matlab读出来的。如果你的和Matlab的波形是一样的,就说明文件读取成功了。
傅里叶代码(2016-11-04)
表扬提早入手的同学,也恳请大家原谅我的疏忽:没有检查网上代码的正确性而给大家带来不便。
这次的代码是完全忠于算法导论实现的。有兴趣的可以参照算法导论的中英讲解。
复数类
public class Complex {
private double a, b;
public Complex(double _a, double _b) {
a = _a;
b = _b;
}
public Complex() {
a = 0;
b = 0;
}
public Complex add(final Complex e) {
return new Complex(a + e.a, b + e.b);
}
public Complex sub(final Complex e) {
return new Complex(a - e.a, b - e.b);
}
public Complex mul(final Complex e) {
return new Complex (a * e.a - b * e.b, a * e.b + b * e.a);
}
public double abs() {
// use hypot instead of sqrt(pow(a, 2), pow(b, 2))
// to improve performance
return Math.hypot(a, b);
}
}
Bit Reverse函数
做【基-2】傅里叶变换之前,要先按照(无符号)逆比特序交换下标。具体原因请查阅算法导论。
这里简单说一下什么是逆比特序:就是把下标为无符号整数a和b的两个数组元素交换位置,其中b的比特是a的倒序比特。
private static void bit_reverse(Complex[] y) {
int i, j, k;
for (i = 1, j = y.length / 2; i < y.length - 1; ++i) {
if (i < j) {
Complex temp = y[i];
y[i] = y[j];
y[j] = temp;
}
k = y.length / 2;
while (j >= k) {
j -= k; // eliminate leading 1.
k /= 2;
if (k == 0) // cutting branches.
break;
}
if (j < k) // add leading 1.
j += k;
}
}
FFT主体函数
关键词:旋转因子、蝴蝶变换
private static Complex[] fft(Complex[] y){
bit_reverse(y);
int j, k, h;
for (h = 2; h <= y.length; h <<= 1) {
// twiddle factor 旋转因子
Complex omega_n = new Complex(Math.cos(-2 * Math.PI / h), Math.sin(-2 * Math.PI / h));
for (j = 0; j < y.length; j += h) {
Complex omega = new Complex(1, 0);
// butterfly transformation
for (k = j; k < j + h / 2; ++k) {
Complex u = y[k];
Complex t = omega.mul(y[k + h / 2]);
y[k] = u.add(t);
y[k + h/2] = u.sub(t);
omega = omega.mul(omega_n);
}
}
}
return y;
}
FFT最后一步
每个复数的模(绝对值)和振幅是正相关的
public static final WINDOW_SIZE = 4096;
public static double[] fft(double[] slice) {
if (slice.length != WINDOW_SIZE)
throw new RuntimeException("FFT::fft(double[] slice) - " +
"The window size is not equal to the required window size (" + WINDOW_SIZE + ")");
Complex[] x = new Complex[WINDOW_SIZE];
/**
* Convert the time-domain series as Complex series whose imaginary parts are zeros.
*/
for (int i = 0; i < WINDOW_SIZE; ++i) {
x[i] = new Complex(slice[i], 0);
}
Complex[] res = fft(x);
double[] ret = new double[WINDOW_SIZE];
for (int i = 0; i < WINDOW_SIZE; ++i) {
/**
* The magnitude of each frequency.
*/
ret[i] = res[i].abs();
}
return ret;
}
指纹提取的部分实现
代码中append函数内部有一个TODO,这一块是找出每一帧音频中能量较高的N个频率,要自己实现。
** 2016-11-04 补充说明 **
既可以使用N最值法,也可以使用分块最值法。采用N最值法的如果用O(n)复杂度算法,有分数奖励。
public class Fingerprint {
// DO NOT write FFT.WINDOW_SIZE / 44100, it equals to 0 forever!!
public static final double scaling = FFT.WINDOW_SIZE / 44100.0;
public static final int N = 3;
private ArrayList<int[]> constel_data = new ArrayList<>();
private int id;
/**
* For songs about to add into DB
* @param id
*/
public Fingerprint(int id) {
this.id = id;
}
/**
* For songs about to be searched
*/
public Fingerprint() {
this.id = -1;
}
/**
* Append a column of frequency peaks to the constellation map.
* A frequency peak is a frequency value whose amplitude is the highest among
* all frequencies in a frequency interval.
*
* @param freqDomain The frequency domain generated by FFT.
*/
public void append(double[] freqDomain) {
int[] freqPeaks = new int[N];
/**
* TODO: Either find N frequencies with the highest amplitude(energy),
* or find the frequency with the max energy within each interval.
*/
constel_data.add(freqPeaks);
}
/**
* Generate fingerprints using Combinational Hash.
* For each frequency peak, generate 6 fingerprints with its 6 successors.
*
* @return
*/
public ArrayList<ShazamHash> combineHash() {
if (constel_data.size() < 3)
throw new RuntimeException("Too few frequency peaks");
ArrayList<ShazamHash> hashes = new ArrayList<>();
for (int i = 0; i < constel_data.size() - 2; ++i) {
for (int k = 0; k < interval_num; ++k) {
// "Combine" with all peak frequencies inside its next two frames.
for (int j = 1; j <= 2; ++j) {
for (int kk = 1; kk < interval_num; ++kk) {
ShazamHash hash = new ShazamHash();
hash.f1 = (short) constel_data.get(i)[k];
hash.f2 = (short) constel_data.get(i + j)[kk];
hash.dt = (short) j;
hash.offset = i;
hash.id = id;
hashes.add(hash);
}
}
}
}
return hashes;
}
}