最近我在一個隊伍裏參加SOLVEFOR TOMORROM探知未來2014年全國青年科普創新實踐暨作品大賽。從廣州賽區毫無懸念地進入到北京賽區決賽,然後決賽的規則更加噁心,這個時候隊長提出要使用聲音傳遞信號,網上是有個開源的軟件:https://github.com/JesseGu/SinVoice
隊長是我們專業裏的大牛,把這個開源軟件改寫了,但是發現聲音傳送的的速度無法滿足要求,於是轉而研究音頻傳輸文件。好了,我說了隊長是大牛,可是在研究的時候遇到一個問題,就是要將聲音信號採集,然後將聲音的時域信號轉化爲頻域信號,這個時候就要用到FFT(快速傅里葉變換),隊長見我一直在打醬油,就把這個任務交給了我。
我當時心裏就一個靠,哥去年的高數都差點掛了,傅里葉我去你大爺的,爺們兒不會啊。於是乎便上百度,找到的FFT算法採樣點都特麼要是2的整數次方,隊長給我的卻是4410個採樣點,這尼瑪我在百度裏一番搗騰,要麼就是寫得太渣,要我自己去研究高數還不如弄死我。最後翻牆谷歌找到一個不錯的FFT的Java算法,之後對其進行了修改。
好的,現在來談談,我解決的問題。我要在0.1秒鐘裏用聲音傳輸2個字節的數據,首先要對這段兩個字節進行編碼,例如我要傳送的是0x31,0xA7,首先將其編碼,片段如下:
private final static double sampling_rate = 4410;
private static double[] encode = new double[4410]; //定義編碼數組
private static boolean[] single = new boolean[16]; //定義標記數組
private static double[] t = new double[4410];
public static double[] t_encode(){ //初始化t
for(int i = 0; i < t.length; i++){
t[i] = (double) (1.0/sampling_rate*i);
}
return t;
}
public static boolean[] single_encode(byte[] message){ //編碼single
String ZERO="00000000";
String[] s_ = new String[2];
for(int i = 0; i < message.length; i++){
String s = Integer.toBinaryString(message[i]);
if(s.length() > 8){
s = s.substring(s.length() - 8);
}
else if(s.length() < 8){
s = ZERO.substring(s.length()) + s;
}
// System.out.println(s);
s_[i] = s;
}
char[] c = new char[16];
String se = s_[0] + s_[1];
c = se.toCharArray();
// System.out.println(c);
for(int i = 0; i < 16; i++){
if( c[i] == '1'){
single[i] = true;
}
else{
single[i] = false;
}
}
return single;
}
public static double[] encode_(boolean[] single){ //初始化encode
for(int i = 0; i < encode.length; i++){
for(int j = 0; j < single.length; j++){
if(!single[j]) continue;
int rate = 4000 + 1000 * j;
encode[i] += 8 * Math.sin(2 * Math.PI * rate * t[i]);
}
}
return encode;
}
上面的三個方法完成了對byte數組的編碼,然後再通過聲音發射出去,然後重點來了,下一個問題就是對採集的聲音信號經過FFT處理,將頻率散佈在0 -- 44100HZ之間,在3000HZ到20000HZ之間取1000HZ爲間隔設立信號點,3000HZ有峯值就代表start,4000HZ,5000HZ,6000HZ,......19000HZ剛好構成了兩個字節,20000HZ代表end。FFT經過變換後是一個對稱的圖形,我們去前面2205個點判斷峯值,並進行解碼得到數組。廢話不多說,上代碼。
FFT:
public class FFT {
public static Complex[] fft(Complex[] x){
int N = x.length;
Complex[] y = new Complex[N];
y = x;
double[] real = new double[N];
double[] imag = new double[N];
for(int i = 0; i < N; i++){
real[i] = x[i].re;
imag[i] = x[i].im;
}
transform(real,imag);
for(int i = 0; i < N; i++){
y[i].re = real[i];
y[i].im = imag[i];
}
return y;
}
/*
* Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
* The vector can have any length. This is a wrapper function.
*/
public static void transform(double[] real, double[] imag) {
if (real.length != imag.length)
throw new IllegalArgumentException("Mismatched lengths");
int n = real.length;
if (n == 0)
return;
else if ((n & (n - 1)) == 0) //N是2的整數次方
transformRadix2(real, imag);
else //N不是2的整數次方,跳轉到更爲複雜的計算方法
transformBluestein(real, imag);
}
/*
* Computes the inverse discrete Fourier transform (IDFT) of the given complex vector, storing the result back into the vector.
* The vector can have any length. This is a wrapper function. This transform does not perform scaling, so the inverse is not a true inverse.
*/
public static void inverseTransform(double[] real, double[] imag) {
transform(imag, real);
}
/*
* Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
* The vector's length must be a power of 2. Uses the Cooley-Tukey decimation-in-time radix-2 algorithm.
*/
public static void transformRadix2(double[] real, double[] imag) {
// Initialization
if (real.length != imag.length)
throw new IllegalArgumentException("Mismatched lengths");
int n = real.length;
int levels = 31 - Integer.numberOfLeadingZeros(n); // Equal to floor(log2(n))
if (1 << levels != n)
throw new IllegalArgumentException("Length is not a power of 2");
double[] cosTable = new double[n / 2];
double[] sinTable = new double[n / 2];
for (int i = 0; i < n / 2; i++) {
cosTable[i] = Math.cos(2 * Math.PI * i / n);
sinTable[i] = Math.sin(2 * Math.PI * i / n);
}
// Bit-reversed addressing permutation
for (int i = 0; i < n; i++) {
int j = Integer.reverse(i) >>> (32 - levels);
if (j > i) {
double temp = real[i];
real[i] = real[j];
real[j] = temp;
temp = imag[i];
imag[i] = imag[j];
imag[j] = temp;
}
}
// Cooley-Tukey decimation-in-time radix-2 FFT
for (int size = 2; size <= n; size *= 2) {
int halfsize = size / 2;
int tablestep = n / size;
for (int i = 0; i < n; i += size) {
for (int j = i, k = 0; j < i + halfsize; j++, k += tablestep) {
double tpre = real[j+halfsize] * cosTable[k] + imag[j+halfsize] * sinTable[k];
double tpim = -real[j+halfsize] * sinTable[k] + imag[j+halfsize] * cosTable[k];
real[j + halfsize] = real[j] - tpre;
imag[j + halfsize] = imag[j] - tpim;
real[j] += tpre;
imag[j] += tpim;
}
}
if (size == n) // Prevent overflow in 'size *= 2'
break;
}
}
/*
* Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
* The vector can have any length. This requires the convolution function, which in turn requires the radix-2 FFT function.
* Uses Bluestein's chirp z-transform algorithm.
*/
public static void transformBluestein(double[] real, double[] imag) {
// Find a power-of-2 convolution length m such that m >= n * 2 + 1
if (real.length != imag.length)
throw new IllegalArgumentException("Mismatched lengths");
int n = real.length;
if (n >= 0x20000000)
throw new IllegalArgumentException("Array too large");
int m = Integer.highestOneBit(n * 2 + 1) << 1;
// Trignometric tables
double[] cosTable = new double[n];
double[] sinTable = new double[n];
for (int i = 0; i < n; i++) {
int j = (int)((long)i * i % (n * 2)); // This is more accurate than j = i * i
cosTable[i] = Math.cos(Math.PI * j / n);
sinTable[i] = Math.sin(Math.PI * j / n);
}
// Temporary vectors and preprocessing
double[] areal = new double[m];
double[] aimag = new double[m];
for (int i = 0; i < n; i++) {
areal[i] = real[i] * cosTable[i] + imag[i] * sinTable[i];
aimag[i] = -real[i] * sinTable[i] + imag[i] * cosTable[i];
}
double[] breal = new double[m];
double[] bimag = new double[m];
breal[0] = cosTable[0];
bimag[0] = sinTable[0];
for (int i = 1; i < n; i++) {
breal[i] = breal[m - i] = cosTable[i];
bimag[i] = bimag[m - i] = sinTable[i];
}
// Convolution
double[] creal = new double[m];
double[] cimag = new double[m];
convolve(areal, aimag, breal, bimag, creal, cimag);
// Postprocessing
for (int i = 0; i < n; i++) {
real[i] = creal[i] * cosTable[i] + cimag[i] * sinTable[i];
imag[i] = -creal[i] * sinTable[i] + cimag[i] * cosTable[i];
}
}
/*
* Computes the circular convolution of the given real vectors. Each vector's length must be the same.
*/
public static void convolve(double[] x, double[] y, double[] out) {
if (x.length != y.length || x.length != out.length)
throw new IllegalArgumentException("Mismatched lengths");
int n = x.length;
convolve(x, new double[n], y, new double[n], out, new double[n]);
}
/*
* Computes the circular convolution of the given complex vectors. Each vector's length must be the same.
*/
public static void convolve(double[] xreal, double[] ximag, double[] yreal, double[] yimag, double[] outreal, double[] outimag) {
if (xreal.length != ximag.length || xreal.length != yreal.length || yreal.length != yimag.length || xreal.length != outreal.length || outreal.length != outimag.length)
throw new IllegalArgumentException("Mismatched lengths");
int n = xreal.length;
xreal = (double[]) xreal.clone();
ximag = (double[]) ximag.clone();
yreal = (double[]) yreal.clone();
yimag = (double[]) yimag.clone();
transform(xreal, ximag);
transform(yreal, yimag);
for (int i = 0; i < n; i++) {
double temp = xreal[i] * yreal[i] - ximag[i] * yimag[i];
ximag[i] = ximag[i] * yreal[i] + xreal[i] * yimag[i];
xreal[i] = temp;
}
inverseTransform(xreal, ximag);
for (int i = 0; i < n; i++) { // Scaling (because this FFT implementation omits it)
outreal[i] = xreal[i] / n;
outimag[i] = ximag[i] / n;
}
}
// display an array of Complex numbers to standard output
public static void show(Complex[] x, String title) {
System.out.println(title);
System.out.println("-------------------");
for (int i = 0; i < x.length; i++) {
System.out.println(x[i]);
}
System.out.println();
}
public static void main(String[] args) {
int N = Integer.parseInt("5");
Complex[] x = new Complex[N];
// original data
for (int i = 0; i < N; i++) {
x[i] = new Complex(i, 0);
x[i] = new Complex(-2*Math.random() + 1, 0);
}
show(x, "x");
// FFT of original data
Complex[] y = fft(x);
show(y, "y = fft(x)");
}
}
接下來是Complex:
/*************************************************************************
* Compilation: javac Complex.java
* Execution: java Complex
*
* Data type for complex numbers.
*
* The data type is "immutable" so once you create and initialize
* a Complex object, you cannot change it. The "final" keyword
* when declaring re and im enforces this rule, making it a
* compile-time error to change the .re or .im fields after
* they've been initialized.
*
* % java Complex
* a = 5.0 + 6.0i
* b = -3.0 + 4.0i
* Re(a) = 5.0
* Im(a) = 6.0
* b + a = 2.0 + 10.0i
* a - b = 8.0 + 2.0i
* a * b = -39.0 + 2.0i
* b * a = -39.0 + 2.0i
* a / b = 0.36 - 1.52i
* (a / b) * b = 5.0 + 6.0i
* conj(a) = 5.0 - 6.0i
* |a| = 7.810249675906654
* tan(a) = -6.685231390246571E-6 + 1.0000103108981198i
*
*************************************************************************/
public class Complex {
public double re; // the real part
public double im; // the imaginary part
// create a new object with the given real and imaginary parts
public Complex(double real, double imag) {
re = real;
im = imag;
}
// return a string representation of the invoking Complex object
public String toString() {
if (im == 0) return re + "";
if (re == 0) return im + "i";
if (im < 0) return re + " - " + (-im) + "i";
return re + " + " + im + "i";
}
// return abs/modulus/magnitude and angle/phase/argument
public double abs() { return Math.hypot(re, im); } // Math.sqrt(re*re + im*im)
public double phase() { return Math.atan2(im, re); } // between -pi and pi
// return a new Complex object whose value is (this + b)
public Complex plus(Complex b) {
Complex a = this; // invoking object
double real = a.re + b.re;
double imag = a.im + b.im;
return new Complex(real, imag);
}
// return a new Complex object whose value is (this - b)
public Complex minus(Complex b) {
Complex a = this;
double real = a.re - b.re;
double imag = a.im - b.im;
return new Complex(real, imag);
}
// return a new Complex object whose value is (this * b)
public Complex times(Complex b) {
Complex a = this;
double real = a.re * b.re - a.im * b.im;
double imag = a.re * b.im + a.im * b.re;
return new Complex(real, imag);
}
// scalar multiplication
// return a new object whose value is (this * alpha)
public Complex times(double alpha) {
return new Complex(alpha * re, alpha * im);
}
// return a new Complex object whose value is the conjugate of this
public Complex conjugate() { return new Complex(re, -im); }
// return a new Complex object whose value is the reciprocal of this
public Complex reciprocal() {
double scale = re*re + im*im;
return new Complex(re / scale, -im / scale);
}
// return the real or imaginary part
public double re() { return re; }
public double im() { return im; }
// return a / b
public Complex divides(Complex b) {
Complex a = this;
return a.times(b.reciprocal());
}
// return a new Complex object whose value is the complex exponential of this
public Complex exp() {
return new Complex(Math.exp(re) * Math.cos(im), Math.exp(re) * Math.sin(im));
}
// return a new Complex object whose value is the complex sine of this
public Complex sin() {
return new Complex(Math.sin(re) * Math.cosh(im), Math.cos(re) * Math.sinh(im));
}
// return a new Complex object whose value is the complex cosine of this
public Complex cos() {
return new Complex(Math.cos(re) * Math.cosh(im), -Math.sin(re) * Math.sinh(im));
}
// return a new Complex object whose value is the complex tangent of this
public Complex tan() {
return sin().divides(cos());
}
// a static version of plus
public static Complex plus(Complex a, Complex b) {
double real = a.re + b.re;
double imag = a.im + b.im;
Complex sum = new Complex(real, imag);
return sum;
}
}
最後求峯值我是直接設定一個高度,將FFT變換後的點取模值,然後設定一個高度,比如90000,大於10000的爲1,就是峯值,其他的爲0。這樣就解碼出來了。
如果考慮到雜音的干擾,可以調高0bit的峯值,比如講0bit的峯值改成1bit峯值的三分之一。