前記:
前段時間公司沒事幹,突發奇想想做一個語音識別系統,看起來應該很簡單的,但做起來卻是各種問題,這個對電氣畢業的我,卻是挺爲難的。谷姐已經離我們而去,感謝度娘,感謝CSDN各位大神,好歹也做的是那麼回事了,雖然還是不好用,但基本功能實現了。
該軟件使用VS2008C++/CLR開發,由於科大訊飛提供的是C的API接口,結果到這邊就是各種不兼容,CLR是基於託管堆運行的,而這個API有是非託管堆的,使用了各種指針,原本打算使用C#來做,最後門外漢的我也沒能做到C#和C指針完美結合,真懷戀單片機寫代碼的年代啊。還有錄音方面需要directX是的支持。軟件下載地址:http://download.csdn.net/detail/liucheng5037/7509003
軟件運行界面如下圖所示:
左邊實現文字轉語音,需要在文本框中輸入文字,然後根據需要配置好聲音,音量,速度選項,點擊播放,軟件會先通過訊飛的API獲取語音,然後以設定的方式播放出來。
右邊實現語音轉文字,直接按住button說話,鬆開後軟件通過訊飛的API將語音信息傳遞給語音雲,最後返回文字顯示在文本框。
看起來很簡單的東西折騰了我不少時間啊!!!
系統組成
該系統由4部分組成:訊飛雲、語音錄入、語音播放、系統控制。語音播放和訊飛雲封裝在一個類XunFeiSDK裏面,語音錄入使用的是網上找的基於DirectX的類SoundRecord,基於C#寫的,本想把錄入也寫到訊飛雲那個類裏去,結果說什麼非託管的類不能有基於託管堆的成員,無奈只有單獨出來,作爲一個dll文件存在。系統控制是在form類裏面。
訊飛語音雲:
(先複製一段官方說法)
訊飛移動語音平臺是基於訊飛公司已有的ISP 和IMS 產品,開發出的一款符合移動互聯網用戶使用的語音應用開發平臺,提供語音合成、語音聽寫、語音識別、聲紋識別等服務,爲語音應用開發愛好者提供方便易用的開發接口,使得用戶能夠基於該開發接口進行多種語音應用開發。其主要功能有:
1)實現基於HTTP 協議的語音應用服務器,集成訊飛公司最新的語音引擎,支持語音合成、語音聽寫、語音識別、聲紋識別等服務;
2)提供基於移動平臺和PC 上的語音客戶端子系統,內部集成音頻處理和音頻編解碼模塊,提供關於語音合成、語音聽寫、語音識別和聲紋識別完善的API。
(複製完畢)
由於只想寫的玩玩,沒有太多時間,故直接把官方C語言寫的demo複製過來,轉變成一個類。官方提供了一個dll文件一個lib文件還有一堆H文件。具體執行的代碼時封裝在dll文件裏的,我們看不到,我們需要引入lib文件來間接調用語音函數。引入lib的方式如下:
#ifdef _WIN64
#pragma comment(lib,"../lib/msc_x64.lib")//x64
#else
#pragma comment(lib,"../lib/msc.lib")//x86
#endif
然後需要include下面幾個H文件:
#include "../include/qisr.h"
#include "../include/qtts.h"
#include "../include/msp_cmn.h"
#include "../include/msp_errors.h"
類XunFeiSDK不能使用ref來修飾,不然又是各種託管堆和非託管堆不能互通之類的報錯。訊飛語音一個轉換來回如下:
訊飛語音詳細的說明可以到這裏下載http://open.voicecloud.cn/index.php/services/voicebase?type=tts&tab_index=1
選擇windowsSDK開發包,裏面有一些簡單的demo和說明,不過需要事先註冊才能下載。
有一點要注意的是,語音返回的音頻格式是PCM這種格式和wav很像,一般支持WAV的播放器都支持PCM。不同的語音播放方式如普通話女聲和東北話使用的語音引擎不同,具體可參考類SoundType。
登錄可以在軟件打開時執行,登出可以在軟件關閉時執行,中間的轉換每次需要執行一次,因爲每次執行的sessionID不一樣,每次需要重新發起會話。
語音錄入:
這一部分花的時間比較長,剛開始時什麼都不知道啊,一點錄入的概念都沒有,完全不知道該調用什麼API,用什麼控件,只有到處百度,試了各種辦法,最後,果然CSDN是大神出沒的地方,被我找到了,地址如下:C#中使用DirectSound錄音。
這個類封裝的很好,就只有3個函數。
SetFileName():錄音文件存放位置和名稱
RecStart():開始錄音
RecStop():結束錄音
整個錄音過程是在一個單獨線程上運行的,不會影響主程序運行。
C#DLL文件移植到C++的方法:
1、使用#using把文件包含進來#using "SoundRecord.dll";
2、增加命名空間usingnamespace VoiceRecord;
3、聲明一個對象,注意類名不可以和命名空間名一致,這樣雖然聲稱dll時不會出錯,但編譯會出錯, SoundRecord^ recorder;
語音播放
該部分比較簡單,直接使用了System::Media命名空間下的類SoundPlayer,在使用時直接gcnew一個對象,然後load(),然後play(),當然,load可以不要的。這個play可以支持播放PCM和WAV格式語音,其他格式未試驗。
系統控制部分
在這一部分聲明瞭一個靜態的XunFeiSDK類指針,還有一個錄音類的託管對象,還有轉換進程等。
static XunFeiSDK* xunfei;
static SoundRecord^ recorder;
Thread^ xunfei_thread;
音頻轉文字部分採用了單獨線程,由於子線程不可以訪問主線程的form控件,無奈又加了個定時器和標誌位來檢測子線程是否完成,網上說可以採用委託的方式來訪問控件,但本人實在弄不懂委託,只有放棄,這一部分做的很單片機的style。
在文字轉語音部分沒有采用進程,會有在這裏卡一會。
知識點
1、由於使用的是訊飛的C庫,又用到了C++/CLR的form,託管堆和非託管堆的鴻溝很麻煩。
本程序使用了微軟提供的轉換函數。需要include的內容:
#include <windows.h>
#include <string>
#include <iostream>
#include <sstream>
include <msclr\marshal.h>
a、std::string轉const char *
const char *strp=str.c_str();
b、System::String^轉 string 和 const char*
Stringstd_str = (constchar*)(Marshal::StringToHGlobalAnsi(nowTime.ToString(Sys_str))).ToPointer();
c、char* 轉 System::String^
Sys_str = Marshal::PtrToStringAnsi((IntPtr)char_str);
d、int 轉 std::String
ostringstreamoss1;
oss1<<int_num;
std_str = oss1.str();
2、同一文件下,若一個類需要使用另一個類,則需要在前面聲明一下,這和C函數類似。
Eg:refclass SoundType;
3、在非託管類下,不能使用託管類作爲成員;實例化託管對象需要使用gcnew,實例化非託管對象直接使用new。
4、對List之類的對象,可以直接添加任何對象,包括form上的List,比如ComboBox,顯示是顯示該對象的ToString方法。
TOString方法重載:
virtual System::String^ ToString() override//重載ToString方法
{
return voice;
}
5、switchcase 不支持string類型的值輸入。
部分源代碼如下:
//類XunFeiSDK
/*
string str("hello");
const char *strp=str.c_str(); string轉const char*
*/
//#using "Microsoft.DirectX.DirectSound.dll"
//#using "Microsoft.DirectX.dll"
#include "../SoundTest/stdafx.h"
//#include "stdafx.h"
#include "stdlib.h"
#include "stdio.h"
#include <windows.h>
#include <conio.h>
#include <errno.h>
#include <iostream>
#include <sstream>
#include <fstream>
#include <time.h>
#include <string>
#include <msclr\marshal.h>
using namespace std;
#include "../include/qisr.h"
#include "../include/qtts.h"
#include "../include/msp_cmn.h"
#include "../include/msp_errors.h"
#ifdef _WIN64
#pragma comment(lib,"../lib/msc_x64.lib")//x64
#else
#pragma comment(lib,"../lib/msc.lib")//x86
#endif
#define DebugPrint(str_x,msg_y) fprintf(out_file,(str_x),(msg_y))
typedef int SR_DWORD;
typedef short int SR_WORD ;
//音頻頭部格式
struct wave_pcm_hdr
{
char riff[4]; // = "RIFF"
SR_DWORD size_8; // = FileSize - 8
char wave[4]; // = "WAVE"
char fmt[4]; // = "fmt "
SR_DWORD dwFmtSize; // = 下一個結構體的大小: 16
SR_WORD format_tag; // = PCM : 1
SR_WORD channels; // = 通道數: 1
SR_DWORD samples_per_sec; // = 採樣率: 8000 | 6000 | 11025 | 16000
SR_DWORD avg_bytes_per_sec; // = 每秒字節數: dwSamplesPerSec * wBitsPerSample / 8
SR_WORD block_align; // = 每採樣點字節數: wBitsPerSample / 8
SR_WORD bits_per_sample; // = 量化比特數: 8 | 16
char data[4]; // = "data";
SR_DWORD data_size; // = 純數據長度: FileSize - 44
} ;
//默認音頻頭部數據
const struct wave_pcm_hdr default_pcmwavhdr =
{
{ 'R', 'I', 'F', 'F' },
0,
{'W', 'A', 'V', 'E'},
{'f', 'm', 't', ' '},
16,
1,
1,
16000,
32000,
2,
16,
{'d', 'a', 't', 'a'},
0
};
namespace SoundTest {
using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::Media;
using namespace msclr::interop;
ref class SoundType;
public class XunFeiSDK{
private: FILE* out_file;//輸出log文件
string appid;
int ret;
string pcm_path;//存儲音頻文件的文件名
string user;
string password;
string voice_type;//語言類型
string volunm;//音量0-10
string engin;//引擎
string voice_speed;//語速-10
public: XunFeiSDK()
{
DateTime nowTime = DateTime::Now;
string nowTimes = (const char*)(Marshal::StringToHGlobalAnsi(nowTime.ToString("yyyy-MM-dd HH:mm:ss"))).ToPointer();
fopen_s(&out_file,"log.txt","at+");
if(out_file == NULL)
{
ret = -1;
return;
}
fseek(out_file, 0, 2);
fprintf(out_file,"begin Time:%s \n",nowTimes.c_str());
appid = "";
user = "";
password = "53954218";//可以上官網註冊專屬自己的ID
pcm_path = "PCM_SPEED.pcm";
voice_type = "xiaoyan";
volunm = "7";
voice_speed = "5";
engin = "intp65";
}
~XunFeiSDK()
{
string nowTimes = (const char*)(Marshal::StringToHGlobalAnsi(DateTime::Now.ToString("yyyy-MM-dd HH:mm:ss"))).ToPointer();
fprintf(out_file,"Time:%s end\n",nowTimes.c_str());
fclose(out_file);
}
public: int status()
{
return ret;
}
bool Login()//登錄
{
string logins = "appid = " + appid + ",work_dir = . ";
ret = MSPLogin(user.c_str(), password.c_str(), logins.c_str());
if ( ret != MSP_SUCCESS )
{
fprintf(out_file,"MSPLogin failed , Error code %d.\n",ret);
return false;
}
return true;
}
void Logout()
{
MSPLogout();//退出登錄
}
int TextToSpeed(System::String^ Ssrc_text)//字符串轉音頻,音頻存放在PCM_SPEED.pcm下
{
#pragma region 字符串轉音頻
struct wave_pcm_hdr pcmwavhdr = default_pcmwavhdr;
const char* sess_id = NULL;
unsigned int text_len = 0;
char* audio_data = NULL;
unsigned int audio_len = 0;
int synth_status = MSP_TTS_FLAG_STILL_HAVE_DATA;
FILE* fp = NULL;
string params = "vcn=xiaoyan, spd = 50, vol = 50";//參數可參考可設置參數列表
ret = -1;//失敗
//參數配置
params = "vcn=" + voice_type + ", spd = " + voice_speed + ", vol = " + volunm + ", ent = "+engin;
const char* src_text = (const char*)(Marshal::StringToHGlobalAnsi(Ssrc_text)).ToPointer();
pcm_path = "PCM_SPEED.pcm";
fprintf(out_file,"begin to synth source = %s\n",src_text);
if (NULL == src_text)
{
fprintf(out_file,"params is null!\n");
return ret;
}
text_len = strlen(src_text);//獲取文本長度
fopen_s(&fp,pcm_path.c_str(),"wb");//打開PCM文件
if (NULL == fp)
{
fprintf(out_file,"open PCM file %s error\n",pcm_path);
return ret;
}
sess_id = QTTSSessionBegin(params.c_str(), &ret);//開始一個會話
if ( ret != MSP_SUCCESS )
{
fprintf(out_file,"QTTSSessionBegin: qtts begin session failed Error code %d.\n",ret);
return ret;
}
fprintf(out_file,"sess_id = %s\n",sess_id);
ret = QTTSTextPut(sess_id, src_text, text_len, NULL );//發送txt信息
if ( ret != MSP_SUCCESS )
{
fprintf(out_file,"QTTSTextPut: qtts put text failed Error code %d.\n",ret);
QTTSSessionEnd(sess_id, "TextPutError");//異常,結束
return ret;
}
fwrite(&pcmwavhdr, sizeof(pcmwavhdr) ,1, fp);//把開始文件寫到最前面
while (1)//循環讀取音頻文件並存儲
{
const void *data = QTTSAudioGet(sess_id, &audio_len, &synth_status, &ret);
if (NULL != data)
{
fwrite(data, audio_len, 1, fp);
pcmwavhdr.data_size += audio_len;//修正pcm數據的大小
}
if (synth_status == MSP_TTS_FLAG_DATA_END || ret != 0)
break;
}//合成狀態synth_status取值可參考開發文檔
//修正pcm文件頭數據的大小
pcmwavhdr.size_8 += pcmwavhdr.data_size + 36;
//將修正過的數據寫回文件頭部
fseek(fp, 4, 0);
fwrite(&pcmwavhdr.size_8,sizeof(pcmwavhdr.size_8), 1, fp);
fseek(fp, 40, 0);
fwrite(&pcmwavhdr.data_size,sizeof(pcmwavhdr.data_size), 1, fp);
fclose(fp);
ret = QTTSSessionEnd(sess_id, NULL);
if ( ret != MSP_SUCCESS )
{
fprintf(out_file,"QTTSSessionEnd: qtts end failed Error code %d.\n",ret);
}
fprintf(out_file,"program end");
return ret;
#pragma endregion
}
System::String^ GetPcmName()//獲取音頻文件路徑
{
return gcnew String(pcm_path.c_str());
}
int Play(System::String^ text)//播放音頻文件
{
if(text == "") return -1;
SoundPlayer^ player = (gcnew SoundPlayer(text));//音頻播放器
player->SoundLocation = text;
player->Load();
player->Play();
return 0;
}
int StartRecord()//開始錄音
{
}
int EndRecord()//結束錄音
{
}
System::String^ SpeedToText(System::String^ text)//語音轉文字,輸入語音文件名,返回文字信息
{
System::String^ Sys_value = "No data return";
const char* src_wav_filename = (const char*)(Marshal::StringToHGlobalAnsi(text)).ToPointer();
//test = Marshal::PtrToStringAnsi((IntPtr)(char *)src_text);
//return test;
char rec_result[1024] = {0};//存放返回結果
const char *sessionID = NULL;
FILE *f_pcm = NULL;//
char *pPCM = NULL;//存放音頻文件緩存
int lastAudio = 0 ;
int audStat = MSP_AUDIO_SAMPLE_CONTINUE ;
int epStatus = MSP_EP_LOOKING_FOR_SPEECH;
int recStatus = MSP_REC_STATUS_SUCCESS ;
long pcmCount = 0;
long pcmSize = 0;//音頻文件大小
int errCode = 10 ;
string param = "sub=iat,auf=audio/L16;rate=16000,aue=speex-wb,ent=sms16k,rst=plain,rse=gb2312";
fprintf(out_file,"Start iat...\n");
sessionID = QISRSessionBegin(NULL, param.c_str(), &errCode);//開始一路會話
fopen_s(&f_pcm,src_wav_filename, "rb");
if (NULL != f_pcm) {
fseek(f_pcm, 0, SEEK_END);
pcmSize = ftell(f_pcm);//獲取音頻大小
fseek(f_pcm, 0, SEEK_SET);
pPCM = (char *)malloc(pcmSize);//分配內存存放音頻
fread((void *)pPCM, pcmSize, 1, f_pcm);
fclose(f_pcm);
f_pcm = NULL;
}//讀取音頻文件,讀到pPCM中
else
{
fprintf(out_file,"media %s not found\n",src_wav_filename);
return Sys_value;
}
while (1) {//開始往服務器寫音頻數據
unsigned int len = 6400;
int ret = 0;
if (pcmSize < 12800) {
len = pcmSize;
lastAudio = 1;//音頻長度小於
}
audStat = MSP_AUDIO_SAMPLE_CONTINUE;//有後繼音頻
if (pcmCount == 0)
audStat = MSP_AUDIO_SAMPLE_FIRST;
if (len<=0)
{
break;
}
fprintf(out_file,"csid=%s,count=%d,aus=%d,",sessionID,pcmCount/len,audStat);
ret = QISRAudioWrite(sessionID, (const void *)&pPCM[pcmCount], len, audStat, &epStatus, &recStatus);//寫音頻
fprintf(out_file,"eps=%d,rss=%d,ret=%d\n",epStatus,recStatus,errCode);
if (ret != 0)
break;
pcmCount += (long)len;
pcmSize -= (long)len;
if (recStatus == MSP_REC_STATUS_SUCCESS) {
const char *rslt = QISRGetResult(sessionID, &recStatus, 0, &errCode);//服務端已經有識別結果,可以獲取
fprintf(out_file,"csid=%s,rss=%d,ret=%d\n",sessionID,recStatus,errCode);
if (NULL != rslt)
strcat_s(rec_result,rslt);
}
if (epStatus == MSP_EP_AFTER_SPEECH)
break;
Sleep(150);//模擬人說話時間間隙
}
QISRAudioWrite(sessionID, (const void *)NULL, 0, MSP_AUDIO_SAMPLE_LAST, &epStatus, &recStatus);//寫入結束
free(pPCM);
pPCM = NULL;
while (recStatus != MSP_REC_STATUS_COMPLETE && 0 == errCode) {
const char *rslt = QISRGetResult(sessionID, &recStatus, 0, &errCode);//獲取結果
fprintf(out_file,"csid=%s,rss=%d,ret=%d\n",sessionID,recStatus,errCode);
if (NULL != rslt)
{
strcat_s(rec_result,rslt);
}
Sleep(150);
}
QISRSessionEnd(sessionID, NULL);
fprintf(out_file,"The result is: %s\n",rec_result);
if(NULL != rec_result)//不爲空時返回正確值
Sys_value = Marshal::PtrToStringAnsi((IntPtr)rec_result);//數值轉換
return Sys_value;
}
void set_tts_params(System::String^ e_voice_type , System::String^ e_engin , int e_volunm , int e_speed)
{
const char* src_text = (const char*)(Marshal::StringToHGlobalAnsi(e_voice_type)).ToPointer();
voice_type = src_text;
src_text = (const char*)(Marshal::StringToHGlobalAnsi(e_engin)).ToPointer();
engin = src_text;
ostringstream oss1;
ostringstream oss2;
oss1<<e_volunm;
volunm = oss1.str();//音量
oss2<<e_speed;
voice_speed = oss2.str();//語速
}
};
public ref class SoundType{
public: System::String^ engin;//語音引擎
System::String^ voice_type;//說話類型
System::String^ voice;//顯示
SoundType(System::String^ e_voice)//switch case 不支持string的輸入
{
voice = e_voice;
if (e_voice == "普通話女聲") {engin = "intp65";voice_type = "xiaoyan";}
else if(e_voice == "普通話男聲") {engin = "intp65";voice_type = "xiaoyu";}
else if(e_voice == "英文女聲") {engin = "intp65_en";voice_type = "Catherine";}
else if(e_voice == "英文男聲") {engin = "intp65_en";voice_type = "henry";}
else if(e_voice == "粵語") {engin = "vivi21";voice_type = "vixm";}
else if(e_voice == "臺灣話") {engin = "vivi21";voice_type = "vixl";}
else if(e_voice == "四川話") {engin = "vivi21";voice_type = "vixr";}
else if(e_voice == "東北話") {engin = "vivi21";voice_type = "vixyun";}
else {engin = "intp65";voice_type = "xiaoyan";voice = "普通話女聲";}
}
SoundType()
{
engin = "intp65";voice_type = "xiaoyan";voice = "普通話女聲";
}
virtual System::String^ ToString() override//重載ToString方法
{
return voice;
}
};
}
FORM類:
局部變量:
private: static XunFeiSDK* xunfei;
private: Thread^ xunfei_thread;
static int end_flag;
static String^ end_result;
ArrayList^ voice_types;
private: static SoundRecord^ recorder;
#pragma region 控件觸發函數
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {
xunfei = (new XunFeiSDK());
end_flag = 0;
if(-1 == xunfei->status())
{
MessageBox::Show("初始化失敗");
this->Close();//關閉窗體
return;
}
if(!(xunfei->Login()))
{
MessageBox::Show("登錄失敗");
this->Close();//關閉窗體
return;
}
volunm_lab->Text = "音量 " + volunm_bar->Value;
speed_lab->Text = "速度 " + speed_bar->Value;
}
private: System::Void Form1_FormClosing(System::Object^ sender, System::Windows::Forms::FormClosingEventArgs^ e) {
xunfei->Logout();//登出
delete xunfei;//必須釋放纔會調用析構函數
delete recorder;
}
private: System::Void play_tts_btn_Click(System::Object^ sender, System::EventArgs^ e) {
// tts_status_lab->Text = "先轉換,再播放語音";
set_xunfei_param();//參數設置
if(-1 == xunfei->TextToSpeed(txt_speak->Text))
{
MessageBox::Show("轉換失敗");
}
else
{
xunfei->Play(xunfei->GetPcmName());
}
}
private: System::Void speak_btn_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
StartRecord();//開始錄音線程
status_lab->Text = "錄音中.....";
}
private: System::Void speak_btn_MouseUp(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
status_lab->Text = "結束錄音,轉換中...";
xunfei_thread = (gcnew Thread(gcnew ThreadStart(EndRecord)));
xunfei_thread->Start();
}
private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) {
if(1 == end_flag)
{
end_flag = 0;
result_box->Text = end_result;
status_lab->Text = "轉換結束";
}
}
private: System::Void volunm_bar_Scroll(System::Object^ sender, System::EventArgs^ e) {
volunm_lab->Text = "音量 " + volunm_bar->Value;
}
private: System::Void speed_bar_Scroll(System::Object^ sender, System::EventArgs^ e) {
speed_lab->Text = "速度 " + speed_bar->Value;
}
#pragma endregion
#pragma region 自定義函數
private: void set_xunfei_param()//訊飛語音參數設置
{
SoundType^ sound_type;
sound_type = (SoundType^)(voice_type->SelectedItem);//獲取選中的對象
xunfei->set_tts_params(sound_type->voice_type , sound_type->engin , volunm_bar->Value , speed_bar->Value);
}
private: static void StartRecord()
{
recorder = (gcnew SoundRecord());
recorder->SetFileName("record.wav");
recorder->RecStart(); //開始錄音
}
private:static void EndRecord()
{
// String text;
recorder->RecStop();
delete recorder;
end_result = xunfei->SpeedToText("record.wav");//錄音結束,顯示語音轉換結果
end_flag = 1;
}
#pragma endregion