滤波器

1.滤波器的概念

为了阐述滤波器的概念,我们首先来看两个场景。

在这两个场景中,为了排除不同频段信号(声音)的干扰,我们需要使用一种工具将不需要的频段滤除出去,这种工具就是滤波器。

2.高通、低通和带通

假定我们有三个不同频率的正弦信号,当他们相互叠加之后,如何将我们想要的信号分离出来呢?

我们可以使用高通,低通和带通滤波器分别得到我们想要的部分。

3.使用 Matlab 生成滤波器

1
2
hd = design(fdesign.bandpass('N,F3dB1,F3dB2',6,7500,8500,fs),'butter');
res = filter(hd,data);

其中,data 是输入被滤波的信号,res 是输出的结果。如果对 matlab 中滤波器感兴趣,可以输入

1
> help fdesign.bandpass

下面我们用 matlab 生成的滤波器进行滤波。

滤波的结果如下:

傅里叶变换

本文是 如果看了此文你还不懂傅里叶变换,那就过来掐死我吧 的精华版。目的就是要让你在不看任何数学公式的情况下理解傅里叶分析。如果有兴趣且有耐心的话可以点击阅览原文。

首先 cite 一下原作者:

1
2
3
4
5
6
作 者:韩 昊
知 乎:Heinrich
微 博:@花生油工人
知乎专栏:与时间无关的故事
谨以此文献给大连海事大学的吴楠老师,柳晓鸣老师,王新年老师以及张晶泊老师。
转载的同学请保留上面这句话,谢谢。如果还能保留文章来源就更感激不尽了。

任何一种波形,都可以看作若干幅度、相位、频率不同的正弦波的叠加。我们首先用方波举个例子:

如果我们把叠加方波所需的若干个正弦波展开,会得到如下所示的图像。

我们从时间方向去观察这个波形,就会得到信号的时域图像(也就是方波)。如果我们从频率方向去观察这个信号,就会得到信号的频域图像。

不过,上图的表示其实并不全面。每个频率的信号对应的信息不仅有强度,而且还有相位。从下图来看,如果箭头所指向的信号的相位发生了变化(即向左或向右移动),那么叠加合成的波形也将发生变化。

所以,傅里叶变换的结果是一个复数的数组,其中每一个复数代表某一频率上信号的强度和相位的信息。

复数的模长表示强度,复数的角度表示相位。

信号的时域表示和频域表示是等价的,得到其中一个就可以得到另一个。如果有时域信号,可以使用傅里叶变换得到频域信号。如果有频域信号,则可以使用逆傅里叶变换得到时域信号。

TarsosDSP 简介

1.什么是 TarsosDSP?

  • TarsosDSP 是一个用于音频处理的Java库。
  • 它的目标是提供一个易于使用的界面,以便在纯Java中尽可能简单地实现实用的音频处理算法,而无需任何其他外部依赖。

  • 这个库试图在能够完成任务,但足够简洁。

  • TarsosDSP 的特点是实现了一个冲击起始检测器和一些间距检测算法:YIN,Mcleod Pitch方法和一个“动态小波算法间距跟踪”算法。
  • 它还实现了Goertzel DTMF解码算法,时间拉伸算法(WSOLA),重采样,滤波器,简单合成,一些音频效果和变调算法。

2.TarsosDSP 都能实现哪些功能?

  • 声音检测器:显示如何响度计算可以完成。当输入声音超过限定的事件被触发。

  • 音高检测器:这个演示应用程序显示实时音高检测。当检测到音调时,赫兹值以概率一起打印。

  • 敲击检测器:显示敲击的检测。拍手会导致一个敲击事件。这个演示应用程序还显示了这两个参数对算法的影响。

  • UtterAsterisk:是一个唱与目标尽可能接近旋律的游戏。从技术上讲,它使用YIN或MPM实时检测音高。

  • Goertzel DTMF(双音多频):解码Goertzel算法的实现。

  • 音频时间拉伸 : 在纯 Java 中实现使用WSOLA实现一个时间拉伸算法。 WSOLA可以在不改变音高的情况下改变音频的播放速度。播放速度可以随时改变,即使有音频正在播放。
  • 音频特征提取:一个命令行应用程序来做简单的特征提取。
  • 音频合成:一个命令行应用程序来做简单的音频合成。
  • 音高移动:一个示例应用程序,无论是实时麦克风输入还是录制的音频,都可以进行音高移位。还包括一个命令行应用程序来做音高转换。

3.TarsosDSP 是如何工作的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AudioDispatcher dispatcher = AudioDispatcherFactory.fromDefaultMicrophone(22050,1024,0);
PitchDetectionHandler pdh = new PitchDetectionHandler() {
@Override
public void handlePitch(PitchDetectionResult result,AudioEvent e) {
final float pitchInHz = result.getPitch();
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView text = (TextView) findViewById(R.id.textView1);
text.setText("" + pitchInHz);
}
});
}
};
AudioProcessor p = new PitchProcessor(PitchEstimationAlgorithm.FFT_YIN, 22050, 1024, pdh);
dispatcher.addAudioProcessor(p);
new Thread(dispatcher,"Audio Dispatcher").start();

4.如何获取和使用 TarsosDSP

点击此链接跳转至官方网站

声音信号的播放与录制

在 Andriod 系统下,一般使用 AudioTrack 和 AudioRecord 来实现声音的播放与录制。

1.声音信号的播放

播放声音一般用 AudioTrack 实现,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import android.content.pm.PackageManager;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.jar.Manifest;
public class MainActivity extends AppCompatActivity implements View.OnClickListener , ActivityCompat.OnRequestPermissionsResultCallback{
//private static final int AUDIO_REQUEST = 0;
public static final String TAG = "PCMSample";
//是否在录制
private boolean isRecording = false;
//开始录音
private Button startAudio;
//结束录音
private Button stopAudio;
//播放录音
private Button playAudio;
//删除文件
private Button deleteAudio;
private ScrollView mScrollView;
private TextView tv_audio_succeess;
//pcm文件
private File file;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
//初始化View
private void initView() {
mScrollView = (ScrollView) findViewById(R.id.mScrollView);
tv_audio_succeess = (TextView) findViewById(R.id.tv_audio_succeess);
printLog("初始化成功");
startAudio = (Button) findViewById(R.id.startAudio);
startAudio.setOnClickListener(this);
stopAudio = (Button) findViewById(R.id.stopAudio);
stopAudio.setOnClickListener(this);
playAudio = (Button) findViewById(R.id.playAudio);
playAudio.setOnClickListener(this);
deleteAudio = (Button) findViewById(R.id.deleteAudio);
deleteAudio.setOnClickListener(this);
}
//点击事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.startAudio:
/*在此处插入运行时权限获取的代码*/
if (ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.RECORD_AUDIO) !=
PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
new String[]{android.Manifest.permission.RECORD_AUDIO,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
StartRecord();
Log.e(TAG,"start");
}
});
thread.start();
printLog("开始录音");
break;
case R.id.stopAudio:
isRecording = false;
printLog("停止录音");
break;
case R.id.playAudio:
PlayRecord();
ButtonEnabled(true, false, false);
printLog("播放录音");
break;
case R.id.deleteAudio:
deleFile();
break;
}
}
//打印log
private void printLog(final String resultString) {
tv_audio_succeess.post(new Runnable() {
@Override
public void run() {
tv_audio_succeess.append(resultString + "\n");
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
//获取/失去焦点
private void ButtonEnabled(boolean start, boolean stop, boolean play) {
startAudio.setEnabled(start);
stopAudio.setEnabled(stop);
playAudio.setEnabled(play);
}
//开始录音
public void StartRecord() {
Log.i(TAG,"开始录音");
//设置采样频率
int frequency = 48000;
//设置音频采样声道,CHANNEL_IN_STEREO代表双声道,CHANNEL_IN_MONO代表单声道
int channelConfiguration = AudioFormat.CHANNEL_IN_STEREO;
//设置音频数据格式,每个采样数据占16bit
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
//生成PCM文件
file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/myaudio.pcm");
Log.i(TAG,"生成文件");
//如果存在,就先删除再创建
if (file.exists())
file.delete();
Log.i(TAG,"删除文件");
try {
file.createNewFile();
Log.i(TAG,"创建文件");
} catch (IOException e) {
Log.i(TAG,"未能创建");
throw new IllegalStateException("未能创建" + file.toString());
}
try {
//输出流
OutputStream os = new FileOutputStream(file);
int bufferSize = AudioRecord.getMinBufferSize(frequency,
channelConfiguration, audioEncoding);
AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
frequency, channelConfiguration, audioEncoding, bufferSize);
byte[] buffer = new byte[bufferSize];
audioRecord.startRecording();
Log.i(TAG, "开始录音");
isRecording = true;
while (isRecording) {
int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
os.write(buffer);
}
audioRecord.stop();
os.close();
} catch (Throwable t) {
Log.e(TAG, "录音失败");
}
}
//播放文件
public void PlayRecord() {
if(file == null){
return;
}
int musicLength = (int) (file.length() / 2);
byte[] music = new byte[musicLength];
try {
InputStream is = new FileInputStream(file);
//设置播放参数
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
48000, AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT,
musicLength * 2,
AudioTrack.MODE_STREAM);
audioTrack.play();
//从文件中读取并写入audioTrack
while(is.read(music)!= -1)
{
audioTrack.write(music, 0, musicLength);
}
is.close();
audioTrack.stop();
} catch (Throwable t) {
Log.e(TAG, "播放失败");
}
}
//删除文件
private void deleFile() {
if(file == null){
return;
}
file.delete();
printLog("文件删除成功");
}
/*private short[] test_Noise(short[] buf, int nb_sample)
{
int i = 0;
for (i = 0; i < nb_sample; i++)
{
buf[i] >>= 2;
}
return buf;
}*/
}

2.声音信号的录制

播放声音一般用 AudioRecord 来实现,实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package com.Javen;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.text.format.Time;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.Javen44100.R;
public class AudioRecordActivity extends Activity {
private static final String TAG = "AudioRecordActivity";
private Button startButton, stopButton, playButton;
private int bufferSizeInBytes = 0;//缓冲区大小
//音频获取来源
private int audioSource = MediaRecorder.AudioSource.MIC;
//设置音频的采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
private static int sampleRateInHz = 44100;
//设置音频的录制声道,CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
private static int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
//设置音频数据格式:PCM 16位每个样本,保证设备支持。PCM 8位每个样本,不一定能得到设备的支持。
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//AudioName裸音频数据文件
private static final String AudioName = "/sdcard/love.raw";
//NewAudioName可播放的音频文件
private String NewAudioName = "/sdcard/new.wav";
private AudioRecord audioRecord;
//播放音频
private MediaPlayer mediaPlayer;
private boolean isRecord = false;//设置录制状态
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//获取当前时间
Time time=new Time();
time.setToNow();
String times= time.year+"."+(time.month+1)+"."+time.monthDay+"_"+time.hour+":"+time.minute+":"+time.second;
NewAudioName = "/sdcard/"+times+".wav";
init();
}
private void init()
{
startButton = (Button)findViewById(R.id.startButton);
stopButton = (Button)findViewById(R.id.stopButton);
playButton = (Button)findViewById(R.id.playButton);
creatAudioRecord();
startButton.setOnClickListener(new AudioRecordLinstener());
stopButton.setOnClickListener(new AudioRecordLinstener());
playButton.setOnClickListener(new AudioRecordLinstener());
}
private void creatAudioRecord()
{
//根据AudioRecord的音频采样率、音频录制声道、音频数据格式获取缓冲区大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
Log.d(TAG, "creatAudioRecord-->bufferSizeInBytes="+bufferSizeInBytes);
//根据音频获取来源、音频采用率、音频录制声道、音频数据格式和缓冲区大小来创建AudioRecord对象
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
//创建播放实例
mediaPlayer = new MediaPlayer();
}
class AudioRecordLinstener implements OnClickListener
{
@Override
public void onClick(View v)
{
if(v == startButton)
{
startAudioRecord();
}
if(v == stopButton)
{
stopAudioRecord();
}
if(v == playButton)
{
playMusic();
}
}
}
private final class PrepareListener implements OnPreparedListener
{
@Override
public void onPrepared(MediaPlayer mp)
{
// TODO Auto-generated method stub
mediaPlayer.start();//开始播放
}
}
/**
* 播放录制的音频
*/
private void playMusic()
{
File file = new File(NewAudioName);
if(file.exists())
{
try
{
mediaPlayer.reset();
mediaPlayer.setDataSource(NewAudioName);
mediaPlayer.prepare();//进行数据缓冲
mediaPlayer.setOnPreparedListener(new PrepareListener());
} catch (IllegalArgumentException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 开始录制音频
*/
private void startAudioRecord()
{
audioRecord.startRecording();//开始录制
isRecord = true;
new AudioRecordThread().start();//开启线程来把录制的音频数据保留下来
}
/**
* 停止录制音频
*/
private void stopAudioRecord()
{
close();
}
private void close()
{
if (audioRecord != null)
{
System.out.println("stopRecord");
isRecord = false;//停止文件写入
audioRecord.stop();
audioRecord.release();//释放资源
audioRecord = null;
}
}
/**
* 音频数据写入线程
* @author Administrator
*
*/
class AudioRecordThread extends Thread
{
@Override
public void run()
{
super.run();
writeDataToFile();//把录制的音频裸数据写入到文件中去
copyWaveFile(AudioName, NewAudioName);//给裸数据加上头文件
}
}
/**
* 把录制的音频裸数据写入到文件中去
* 这里将数据写入文件,但是并不能播放,因为AudioRecord获得的音频是原始的裸音频,
* 如果需要播放就必须加入一些格式或者编码的头信息。但是这样的好处就是你可以对音频的 裸数据进行处理,比如你要做一个爱说话的TOM
* 猫在这里就进行音频的处理,然后重新封装 所以说这样得到的音频比较容易做一些音频的处理。
*/
private void writeDataToFile()
{
// new一个byte数组用来存一些字节数据,大小为缓冲区大小
byte[] audioData = new byte[bufferSizeInBytes];
int readSize = 0;
FileOutputStream fos = null;
File file = new File(AudioName);
if(file.exists())
file.delete();
try
{
fos = new FileOutputStream(file);//获取一个文件的输出流
} catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
while(isRecord == true)
{
readSize = audioRecord.read(audioData, 0, bufferSizeInBytes);
Log.d(TAG, "readSize ="+readSize);
if(AudioRecord.ERROR_INVALID_OPERATION != readSize)
{
try
{
fos.write(audioData);
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
try
{
fos.close();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void copyWaveFile(String inFileName, String outFileName)
{
FileInputStream in = null;
FileOutputStream out = null;
long totalAudioLen = 0;
long totalDataLen = totalAudioLen + 36;
long longSampleRate = sampleRateInHz;
int channels = 2;
long byteRate = 16 * sampleRateInHz * channels / 8;
byte[] data = new byte[bufferSizeInBytes];
try
{
in = new FileInputStream(inFileName);
out = new FileOutputStream(outFileName);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
while(in.read(data) != -1)
{
out.write(data);
}
in.close();
out.close();
} catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 这里提供一个头信息。插入这些信息就可以得到可以播放的文件。
* 音频的文件,可以发现前面的头文件可以说基本一样哦。每种格式的文件都有
* 自己特有的头文件。
*/
private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels, long byteRate)
throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) (2 * 16 / 8); // block align
header[33] = 0;
header[34] = 16; // bits per sample
header[35] = 0;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
@Override
protected void onDestroy()
{
close();
if(mediaPlayer != null)
{
mediaPlayer.release();
mediaPlayer = null;
}
super.onDestroy();
}
}

概率概念

1.互斥事件(Mutually exclusive events)和周全事件(Exhaustive events)

  • 互斥事件(Mutually exclusive events)指的是不可能同时发生的事件。
  • 周全事件(Exhaustive events)指的是覆盖了所有结果的时间集合。

2.先验概率(prior probability)和后验概率(empirical probability)

  • 先验概率(prior probability)指的是依靠推测和推理获得的概率,如抛硬币等。
  • 后验概率(empirical probability)指的是依靠分析过去的数据得到的概率。

3.协方差(Covariance)

1
Cov(Ri,Rj) = E{[Ri - E(Ri)] × [Rj - E(Rj)]}

值得一提的是:

1
Cov(RA,RA) = Var(RA)

4.相关系数(correlation coefficient)

1
Corr(Ri,Rj) = Cov(Ri,Rj) / [σ(Ri) × σ(Rj)]

注意,相关系数有时也被写成:

1
ρ(Ri,Rj) = Corr(Ri,Rj)

5.投资组合的方差(Portfolio variance)

1
Var(Rp) = ∑∑ wi × wj × Cov(Ri,Rj)

6.贝叶斯方程(Bayes‘ formula)

1
P(Bi|A) = [P(Bi) × P(A|Bi)] / [ ∑ P(Bj) × P(A|Bj)]

国内 CRM 系统调研小结

1.界面设计

1.1.Salesforce

如果让我给我使用过的所有 CRM 软件的界面做个排名,SalesForce 的界面是稳稳能占据前两名的。它的界面看上去给人的感觉就是两个字:专业

1.1.1.主页

1.1.2.客户列表

1.1.3.新建客户

1.1.4.新建任务

1.2.爱客

爱客的整体画风也比较简洁,虽然看上去像是套用了某个前端的模板,但总的来说还是比较专业的。

1.2.1.工作台

1.2.2.客户列表

1.2.3.客户详情

1.2.4.新增客户

1.2.5.跟进

1.2.6.写跟进

1.3.Xtools

如果说谁能在前端界面上能够跟 Salesforce 媲美的话,那一定就是 Xtools。Xtools 的界面在细节的处理上特别完善,使用起来也很流畅。

1.3.1.工作台

1.3.2.客户列表

1.3.3.客户详情

1.3.4.新建客户

1.4.悟空

悟空的使用感受给人的印象就是:流畅。无论是开通试用的过程还是软件的使用过程,流畅到让你觉得你可能落入了一个精心设计的陷阱。开玩笑地说,真是如丝般顺滑(笑)。

1.4.1.主页

1.4.2.客户列表

1.4.3.新建客户

1.4.4.跟进

1.5.亿客

亿客采用纯蓝色“复古”画风,看起来比它的竞争对手落后了一个时代。不过它的功能还是比较完善的。

1.5.1.首页

1.5.2.客户列表

1.5.3.新建客户

1.5.4.记录

1.6.八百客

八百客的界面风格实在是太古老了,虽然它的功能也很完善,但是我们这里就不详细描述了。

2.用户数量

大多数网站没有公布自己的用户数量信息,不过我们可以通过流量来进行推测。由于网上的数据并不是很全面,这里我们只展示出我们能拿到的数据。

2.1.分享销客 VS 爱客

我们可以看到,分享销客的数量更多一点,但也只有 700 K / 月。我们做个大概的估计,假定每个用户平均三天上线一次(对于“活”的用户而言这已经是比较低的使用频率了),那么一个月就是 10 次。所以纷享销客的越活用户应该不会超过 70 万。如果按照每天上线一次来计算,那么它的用户数量大概在 10 万左右。

2.2.泛微 VS Salesforce

泛微的月流量大概是 500 K 左右,跟纷享销客大概是一个量级。不过 Salesforce 这也太多了吧? 100 多M?仔细核对了一下,force.com 下面有很多个子域名,其中最有可能和 Salesforce 相关的是 :lightning.force.com。

即使是这样流量还是很多,不过与其他 CRM 不同,它的流量主要来自于北美。这似乎违背了我们调研国内 CRM 系统的初衷。不过我在使用 Salesforce 的时候,觉得它的汉化已经很完善了。而且 SEO(搜索引擎优化)也做到了百度的前列。所以在我看来算作国内市场的 CRM 软件应该也不过分吧。

3.价格

各家 CRM 软件的价格都差不多,可能因为 Salesforce 习惯了用美元的缘故,折算成人民币要贵一点。以下是各家的价格:

3.1.Salesforce 价格

3.2.Xtools 价格

3.1.亿客价格

4.总结

从目前的 CRM 软件市场来看,显然已经是一片红海。市场上已经进化出了比较完备的产品,而且在国内的价格也在可接收的范围之内。如果想要在这一领域划分出一块领地,就必须有比较大的创新点。否则可能就是死路一条。

统计概念与市场回报

1.描述统计(descriptive statics)与推论统计(inferential statistics)

描述统计(descriptive statics)指的是对总体(population)进行统计特征,而推论统计指的是基于一个样本(sample)来进行判断。

2.测量尺度类型

  • 定类型数据(Nominal Scale):如两种不同类型的基金。
  • 定序型数据(Ordinal Scale):如老年、中年、青年、幼年。
  • 定距型数据(Interval Scale):如温度。
  • 定比型数据(Ratio Scale):如货币。

3.总体均值(μ)和样本均值(X’)

总体均值是总体中所有观测值的平均值,记作μ:

1
μ = (X1 + X2 + ... + XN) / N

其中,N 是总体中的观测值数量。

样本均值是样本中所有观测值的平均值,记作X‘(应该是 X 拔,即 X 上面有一道横线,打不出来所以用 X‘代替):

1
X' = (X1 + X2 + ... + Xn) / n

其中,n 是样本中观测值的数量。

4.直方图(Histogram)和折线图(Frequency Polygon)

5.中位数(median)和众数(mode)

  • 中位数(median)就是将数据集排序(升序或者降序)之后,最中间的样本的值。
  • 众数(mode)就是数据集中出现频次最高的值。如果只有一个众数,那么数据集为单峰(unimodal)的,如果是两个或者三个众数,那么称为双峰(bimodal)或者三峰(trimodal)的。

6.几何平均数(geometric median)

几何平均数(geometric median)的计算有两种方式,一种是最基本的计算方式:

1
G = (X1 × X2 × ... × Xn) ^ (1 / n)

另一种计算方式是对回报数据集(returns data set)的几何平均数:

1
1 + RG = [(1 + R1) × (1 + R2) × ... × (1 + R2)] ^ (1 / n)

7.调谐平均数(harmonic mean)

调谐平均数(harmonic mean)的计算方法为:

1
XH = N / [(1 / X1) + (1 / X2) + ...+ (1 / XN)]

8.分位数(Quantile)

分位数(Quantile) 说的是比例所处或者低于该值的术语。

  • 四分位数(Quartiles) 低于四分之一的数值。
  • 五分位数(Quintile)低于五分之一的数值。
  • 十分位数(Decile)低于十分之一的数值。
  • 百分位数(Percentile)低于百分之一的数值。

以百分位数为例,假设给出的百分数为y,计算公式为:

1
Ly = (n + 1) × y / 100

其中,n 为样本总数。

9.范围(range)

1
range = max value - min value

10.平均绝对偏差(MAD,mean absolute deviation)

1
MAD = (|X1 - X'| + |X2 - X'| + ... + |Xn - X'|) / n

其中,X’ 表示样本均值。

11.总体方差(population variance)和总体标准差(population standard deviation)

  • 总体方差(population variance)
1
σ2 = [(X1 - μ) ^ 2 + (X2 - μ) ^ 2 + ... + (XN - μ) ^ 2 ] / N

其中,μ 为总体均值, N 为总体数量。

  • 总体标准差(population standard deviation)
1
σ = {[(X1 - μ) ^ 2 + (X2 - μ) ^ 2 + ... + (XN - μ) ^ 2 ] / N}^ (1 / 2)

12.样本方差(sample variance)和样本标准差(sample standard deviation)

  • 样本方差(sample variance)
1
s2 = [(X1 - X') ^ 2 + (X2 - X') ^ 2 + ... + (XN - X') ^ 2] / (n - 1)

其中,X’ 为样本均值,n 为样本数量。

  • 样本标准差(sample standard deviation)
1
s = {[(X1 - X') ^ 2 + (X2 - X') ^ 2 + ... + (XN - X') ^ 2] / (n - 1)} ^ (1 / 2)

13.切比雪夫不等式(Chebyshev’s inequality)

切比雪夫不等式的内容为:无论样本分布如何,对于 k > 1 的情况,距离均值 k 倍方差以内的样本占比至少为:

1
1 - 1 / k ^2

14.变异系数(coefficient of variation)

变异系数(coefficient of variation)是用来描述相对分布的情况的,其计算公式为:

1
CV = s / X'

其中,s 为样本标准差,X’ 为样本均值。

15.夏普比率(Sharpe Ratio)

夏普比率(Sharpe Ratio)是用来衡量基金绩效(即收益与风险关系)的一个指标。其计算公式为:

1
Sharpe ratio = (rp - rf) / σ

其中,rp 表示投资组合的回报,rf 表示无风险的投资的回报,σ 表示投资组合的标准差。

16.偏度(Skewness)

偏度(Skewness) 用来表示非对称分布的非对称的方向。

  • 正偏(positively skewed)表示极端值(Outliers)存在于向上的区域,或者说分布在右侧有长尾(long tail)。这种情况下:

    1
    均值 > 中值 > 众数
  • 正偏(negatively skewed)表示极端值(Outliers)存在于向下的区域,或者说分布在左侧有长尾(long tail)。这种情况下:

    1
    均值 < 中值 < 众数

偏度计算公式:

1
sk = (1 / n) × [(X1 - X') ^ 3 + (X2 - X') ^ 3 + ... + (Xn - X') ^ 3] / (s ^ 3)

其中,X’ 是样本均值,s 是样本标准差。

17.峰度(kurtosis)

峰度(kurtosis)是用来表示一个分布是否比正态分布更“尖峰”的一侧参数。

  • 尖峰态(leptokurtic)表示比正态分布更“尖峰”。
  • 低峰态(platykurtic)表示比正态分布更“平坦”。
  • 常峰态(mesokurtic)表示比正态分布一致。

峰度计算公式:

1
k = (1 / n) × [(X1 - X') ^ 4 + (X2 - X') ^ 4 + ... + (Xn - X') ^ 4] / (s ^ 4)

其中,X’是样本均值,s 是样本标准差。

贴现现金流应用

1.净现值(NPV)

净现值(NPV)是考虑资金收支净额,以及按照一定的折现率折现之后的现值。

1
NPV = CF1/[(1 + r)^1] + CF2/[(1 + r)^2] + ... + CFn/[(1 + r)^n]

其中 CFi 是第 i 年的现金流,r 是折现率,n 是投资的时间。

2.内回报率(IRR)

内部回报率(IRR),指项目投资实际可望达到的收益率。实质上,它是能使项目的净现值等于零时的折现率。

1
0 = CF0 + CF1/[(1 + IRR)^1] + CF2/[(1 + IRR)^2] + ... + + CFn/[(1 + IRR)^n]

其中 CFi 是第 i 年的现金流,r 是折现率,n 是投资的时间,IRR 是内回报率。

3.接受和拒绝项目的标准

  • 对于独立(independent)项目,接受所有净现值(NPV)为正的项目。因为净现值为 0 意味着内回报率(IRR)等于投资者预期的回报率。所以这也意味着接收所有内回报率(IRR)大于预期回报率的项目。
  • 对于互斥(mutually exclusive)项目,接受净现值(NPV)最高的项目。

4.持有期汇报(Holding Period Return)

持有期汇报(HPR)等于利润(最终价值(ending value)与所有现金流(cash flow received)的总和与初始价值(beginning value)的差值)与初始价值的商。

1
HPR = (ending value + cash flow received - beginning value) / beginning value

如果没有现金流,则持有期回报(HPR)等于:

1
HPR = (ending value - beginning value) / beginning value

5.价值加权回报率(money-weighted return)与时间加权回报率(time-weighted return)

  • 价值加权回报率(money-weighted return)相当于对投资组合(investment portfolio)的内回报率(IRR)。即当:

    1
    PV-inflows = PV-outflows

    时得到的内回报率(IRR)。

  • 时间加权回报率(time-weighted return)相当于每个复利周期的回报率的几何平均值。即

    1
    time-weighted return = [(1 + HPR1) × (1 + HPR2) × ... × (1 + HPR2)] ^ (1/n)

6.银行折现收益率(bank discount yield)、持有期收益率(holding period yield)、实际年收益(effective annual yield)和货币市场收益率(money market yield)

  • 银行折现收益率(bank discount yield,r-BD)等于:

    1
    r-BD = [(F - P) / F] × (360 / t)

    其中,F 为纸面价格(即期末支付价格),P 为买入价格,t 为持有时间,一年以 360 天计。

  • 持有期收益率(holding period yield,HPY)等于:

    1
    HPY = [(F - P) / P]

    其中,F 为纸面价格(即期末支付价格),P 为购买价格,t 为持有时间,一年以 360 天计。与银行折现收益率(BDY)相比,持有期收益率(HPY)的分母是买入价格(P),更接近我们平常对收益率的认知。

  • 实际年收益率(effective annual yield,EAY)等于:

    1
    EAY = (1 + HPY) ^ (365 / t) = [(F - P) / P] ^ (365 / t)

    实际年收益率使用了复利的形式,而且以 365 天为一年。

  • 货币市场收益率(money market yield,CD equivalent yield,r-MM)等于:

    1
    r-MM = HPY × (360 / t) = [(F - P) / P] × (360 / t)

    货币时长收益率使用了单利的形式,以 360 天为一年。

除以本金 / 纸面 单利 / 复利 一年 360 天 / 365 天
银行折现收益率 纸面 单利 360
货币市场收益率 本金 单利 360
持有期收益 本金
实际年收益 本金 复利 365

7.债券等值收益率(bond equivalent yield,BEY)

债券等值收益率(bond equivalent yield,BEY)等于半年度收益率(半年度收益率按照复利计算)的两倍。

货币的时间价值

1.复利

货币的时间价值是建立在复利(compound interest)的基础之上的。所谓复利,就是复利计算中利息按照约定的计息周期参与计息。也就是我们平常说的“利滚利”或者“驴打滚”。

举个例子,一个人欠了 100 元的债务,年利率 10 %。如果按照复利计算,那么如果一年以后还清,则需要归还 :

1
100 × (100 % + 10 %) = 110 元。

如果两年之后还清,则不仅需要支付当年本金产生的利息 :

1
100 × 10 % = 10 元

还要支付上一年利息所产生的利息:

1
10 × 10 % = 1 元

总共需要归还 :

1
100(本金) + 10(第一年本金产生的利息) + 10 (第二年本金产生的利息)+ 1(第一年利息产生的利息)= 121 元。

2.金钱的现值(PV)和未来值(FV)

一个人欠了 100 元的债务,年利率 10 %。两年之后需要归还 121 元。 100 元就是这笔债务的现值价值(PV)。两年之后需要归还的 121 元就是这笔债务的未来值(FV)。

3.复利周期数(N)和每周期复利利率(I/Y)

一个人欠了 100 元的债务,年利率 10 %,约定两年之后归还。这里的复利周期数(N)就是 2,每周期复利利率(I/Y)就是 10 %。

4.每期现金流(PMT)

一个人欠了 100 元的债务,年利率 10 %。如果这个人选择分期付款,每年归还 57.62 元。那么这个 57.62 元就是每期现金流(PMT)。

5.货币的时间价值(TVM)

理想情况下,如果利率恒定为正,那么货币的价值随着时间的前进而增加,随着时间的倒退而减少。金钱的现值(PV)和未来值(FV)存在着如下关系:

1
FV = PV × (1 + I/Y) ^ N

如果考虑每期现金流(PMT),则总的关系式如下:

1
FV = PV × (1 + I/Y) ^ N + PMT × {[(1 - I/Y) ^ N] / (1 - I/Y)}

6.风险类型

  1. 默认风险:借贷者不能按期支付现金流的风险。
  2. 流动性风险:为了使资产快速变现而贬值产生的风险。
  3. 到期风险:由借贷时间带来的风险。一般而言,长期借贷的风险更高。

7.有效年利率(EAR)

一个人欠了 100 元的债务,年利率 10 %。但是复利周期不是按年结算,而是按半年结算。则每一年中有两个复利周期,每个周期利率 5 %。那么一年之后,这个人需要偿还:

1
100 × (100 % + 5 %)×(100 % + 5 %) = 110.25 元。

所以,这个人实际每年支付的利率其实是:

1
10.25 / 100 = 10.25 %

这个 10.25 % 就是有效年利率(EAR)。假定年利率不变,则一年之内复利周期越多,那么有效年利率(EAR)就越高。

8.普通年金(ordinary annuity)与即付年金(annuity due)

年金(annuity)是在一定时间内的等间隔等值的持续现金流。一个人为了还清 年利率 10 % 的100 元的债务。两年中每年归还 57.62 元。那么他在这两年中每年归还的 57.62 元就是年金。

年金有两种结算方式,一种是普通年金(ordinary annuity),也就是每年年末支付。另一种是即付年金(annuity due),每年年初支付。普通年金每年的现金流(PMT)需要比即付年金多支付一年的利息。

9.永续年金(Perpetuity)

永续年金是是在无限时间内的等间隔等值的持续现金流。比如一个人买了一些股票,在理想情况下,这只股票将给他每年带来一定数额的收益,那么这支股票带来的收益就是永续年金(Perpetuity)。

永续年金的价值计算方式为:

1
PV = PMT / (I/Y)

10.计算器的使用方式

我们已经知道:

1
FV = PV × (1 + I/Y) ^ N + PMT × {[(1 - I/Y) ^ N] / (1 - I/Y)}

也就是说,PV、FV、I/Y、PMT、N 这五个参数组成了一个恒等式,只要知道其中的四个,就可以知道剩下的那个。下面举个例子通过计算器求 N:

1
一个人欠了 100 元的债务,年利率 10 %。如果这个人选择分期付款,每年归还 57.62 元。请问几年还清?

下面是输入的步骤:

1
2
3
4
5
100 [PV]
10 [I/Y]
0 [FV]
57.62 [+|-] [PMT]
[CPT] [N]

计算结果为:

1
N = 2.00

11.净现值(NPV)计算

对于非恒定现金流(即每期的现金流的数量是不等的),可以采用净现值(NPV)计算,计算出该现金流折算成当前价值的数额:

1
一个现金流的 PMT 为:-1000, 500, 0, 4000,3500, 2000,求净现值。

则计算器输入的顺序为:

1
2
3
4
5
6
7
8
9
10
[CF] [2nd] [CLR WORK]
0 [ENTER]
[↓] 1000 [+|-] [ENTER]
[↓] [↓] 500 [+|-] [ENTER]
[↓] [↓] 0 [ENTER]
[↓] [↓] 4000 [ENTER]
[↓] [↓] 3500 [ENTER]
[↓] [↓] 2000 [ENTER]
[NPV] 10 [ENTER]
[↓] [CPT]

输出结果为:

1
NPV = 4711.91226

AndroidStudio安装教程

1.下载 Android Studio 安装包

下载地址:http://www.android-studio.org/

2.安装 Android Studio

2.1.打开安装程序

2.2.选择安装内容

进入到选择安装内容界面,除了 Android Studio 之外,我们还需要安装 Android SDKAndroid Virtual Device

2.3.选择 Android Studio 和 SDK 的安装位置

2.4.开始安装

2.5.安装完成

3.基本配置

3.1. 设置 SDK 路径

打开 Android Studio,会弹出设置 SDK 路径的对话框。选择刚才安装的 SDK 路径。

如果没有弹出或者路径设置错误,也可以自己在【项目结构】里面自行设定和修改。

3.2.新建项目

【文件】→【新建】→【新项目】

3.3.配置 AVD

运行项目,弹出建立安卓虚拟机对话框。

建立安卓虚拟机。

用新建的安卓虚拟机运行程序。