博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Orangpi Zero和Linux ALSA实现WIFI无线音箱(二)
阅读量:5965 次
发布时间:2019-06-19

本文共 13216 字,大约阅读时间需要 44 分钟。

作品已经完成,先上源码:

全文包含三篇,这是第二篇,主要讲述发送端程序的原理和过程。

第一篇:

第三篇:

 

以下是正文:

  发送端程序基于MFC的对话框类实现,开发环境Visual Studio 2012,主要实现了5个功能,下面逐个讲述:

  1、软件启动检查互斥体,防止程序重复启动。

  2、读取上一次启动的配置文件,初始化socket、获取本机ip地址。

  3、读取用户输入的接收端IP地址,利用Core Audio APIs初始化loopback(环回录音)模式,启动录音子线程。

  4、在子线程不断读取音频缓冲区数据,每0.1s将录制的数据打包以PCM格式,通过socket发送到接收端。

  5、最小化到系统托盘

一、检查互斥体

  创建互斥体是防止应用程序重复启动最常用的方式,本作品使用Core Audio APIs读取声卡音频数据,只能实例化一次。这是因为,这个作品完成后,作者在使用的过程中,发送端软件在运行一段时间后,总是不定期莫名其妙地出现“appcrash”错误,然后程序莫名崩溃,后来发现是因为作者之前使用过一个叫“wifiaudio”的程序,这个程序也是一样利用Core Audio APIs实现声卡的环回录音,而且它老是开机自启动,这样当我也运行这个作品的时候,两个程序就出现冲突,导致本作品运行不稳定,在解决了这个问题之后,作者也在作品中增加检查互斥体的功能,防止程序重复启动。

  以下是在应用程序实例化时增加的代码。

//创建互斥体,防止应用程序重复启动,by Hecan    HANDLE hMutex = ::CreateMutex(NULL, FALSE, "WifiSpeaker by Hecan");    DWORD dwRet = ::GetLastError();    if (hMutex)    {        if (ERROR_ALREADY_EXISTS == dwRet)        {            AfxMessageBox("应用程序已经运行,请关闭后重试!!!");            CloseHandle(hMutex);  // should be closed            return FALSE;        }    }    else        AfxMessageBox("创建互斥体错误,请检查源代码WiFiSpeaker.cpp");

  最后建议在dlg.DoModal()返回后增加关闭句柄的代码,虽然这工作在软件退出时系统会自动完成,但不建议由系统来做。

// 关闭互斥体句柄    CloseHandle(hMutex);

 二、读取上一次启动的配置文件,初始化socket

   上一次启动的配置文件默认保存在可执行文件当前的目录下,后缀名为bin,这个文件只有一个作用,就是保存用户上一次退出时设定的接收端IP地址,减少用户每次打开程序都要设置IP的麻烦,这个文件固定16个字节,实际就是m_ClientAddr这个成员变量以2进制形式保存在bin文件中,m_ClientAddr成员变量的类型为SOCKADDR_IN结构体。

  代码中注意一下:

  1、发送端配置的端口为12320,接收端端为12321,这个是在程序中固化的,没有提供给用户做修改,这个值只能在源代码中修改后重新编译。修改后,接收端对应的本机端口也要同步修改。

  2、初始化中使用ioctlsocket函数把socket配置为非阻塞模式,这样后面调用sendto函数后,函数会立即返回。因为是UDP协议,数据发送后不需要关心接收端有没有收到,直接返回即可,提高程序的执行效率。

  3、BuffDuration_millisec是成员变量,表示初始化音频客户端请求的数据缓冲区大小,以毫秒为单位。后面会讲到。

  初始化代码如下:

BOOL CWiFiSpeakerDlg::OnInitDialog(){    CDialogEx::OnInitDialog();    // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动    //  执行此操作    SetIcon(m_hIcon, TRUE);            // 设置大图标    SetIcon(m_hIcon, FALSE);        // 设置小图标    // TODO: 在此添加额外的初始化代码/*--------------------------------------------------------------------------------------------------------*/    //读取初始化文件,如果没有,则按照默认192.168.1.100的ip地址初始化客户端ip,客户端口设为12321    CFile iniFile;    //iniFile.Open("./WiFiSpeaker.bin",CFile::modeReadWrite |CFile::modeCreate|CFile::modeNoTruncate);    volatile BOOL resul = iniFile.Open("./WiFiSpeaker.bin",CFile::modeReadWrite |CFile::modeCreate|CFile::modeNoTruncate);        if(iniFile.GetLength() == sizeof(m_ClientAddr))        iniFile.Read(&m_ClientAddr,sizeof(m_ClientAddr));    else    {        m_ClientAddr.sin_family = AF_INET;        m_ClientAddr.sin_port =  htons(12321);        m_ClientAddr.sin_addr.S_un.S_addr =inet_addr("192.168.1.100");      }    iniFile.Close();    //初始化服务器IP地址,获取本机IP地址,服务器端口设置设为12320    m_ServerAddr.sin_family = AF_INET;    m_ServerAddr.sin_port = htons(12320);    m_ServerAddr.sin_addr = GetLocalIPAddr();    //把IP地址转为字符串并显示在编辑框中    char a[15];    sprintf_s(a,"%d.%d.%d.%d",m_ServerAddr.sin_addr.S_un.S_un_b.s_b1,m_ServerAddr.sin_addr.S_un.S_un_b.s_b2,m_ServerAddr.sin_addr.S_un.S_un_b.s_b3,m_ServerAddr.sin_addr.S_un.S_un_b.s_b4);    this->SetDlgItemText(IDC_EDIT1,a);//服务器(本机)ip    sprintf_s(a,"%d.%d.%d.%d",m_ClientAddr.sin_addr.S_un.S_un_b.s_b1,m_ClientAddr.sin_addr.S_un.S_un_b.s_b2,m_ClientAddr.sin_addr.S_un.S_un_b.s_b3,m_ClientAddr.sin_addr.S_un.S_un_b.s_b4);    this->SetDlgItemText(IDC_EDIT2,a);//客户端ip    this->GetDlgItem(IDC_BUTTON2)->EnableWindow(FALSE);//停止按钮禁用    //初始化socket并绑定到主机地址,UDP模式    m_socket = socket(AF_INET,SOCK_DGRAM,0);    bind(m_socket,(SOCKADDR*)&m_ServerAddr,sizeof(SOCKADDR));//绑定套接字      u_long mode = 1;    ioctlsocket(m_socket,FIONBIO,&mode);//设置为非阻塞模式(sendto函数立即返回)/*---------------------------------------------------------------------------------------------------------*/    //设置0.1s时长的音频缓冲区    BuffDuration_millisec = 100;        //初始化成员变量    pAudioClient = NULL;    pCaptureClient = NULL;    pwfx =NULL;/*---------------------------------------------------------------------------------------------------------*/    //对话框初始化在屏幕右下角位置    CRect dlg_windows,sysWorkArea;     SystemParametersInfo(SPI_GETWORKAREA,0,&sysWorkArea,0);    GetWindowRect(&dlg_windows);     SetWindowPos(NULL,sysWorkArea.right-dlg_windows.right, sysWorkArea.bottom-dlg_windows.bottom, 0, 0, SWP_NOSIZE | SWP_NOZORDER);    return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE}

三、启动按钮——读取用户输入的接收端IP地址,初始化loopback(环回录音)模式,启动录音子线程

  点击启动按钮后,首先读取用户输入的接收端IP地址,并存放在m_ClientAddr成员变量中。

  初始化音频客户端为loopback模式,这部分代码是参考msdn上的:,主要有两个地方要注意:

  1、IMMDeviceEnumerator::GetDefaultAudioEndpoint函数的第一个参数必须为eRender。

  2、IAudioClient::Initialize函数第二个参数需配置为AUDCLNT_STREAMFLAGS_LOOPBACK。

  下面主要讲述IAudioClient::Initialize函数,这个函数的声明如下:

HRESULT Initialize(  [in]       AUDCLNT_SHAREMODE ShareMode,  [in]       DWORD             StreamFlags,  [in]       REFERENCE_TIME    hnsBufferDuration,  [in]       REFERENCE_TIME    hnsPeriodicity,  [in] const WAVEFORMATEX      *pFormat,  [in]       LPCGUID           AudioSessionGuid);

  全部都是输入参数,

  ShareMode:共享模式独占还是共享,AUDCLNT_SHAREMODE_EXCLUSIVE或者AUDCLNT_SHAREMODE_SHARED,一般设置为AUDCLNT_SHAREMODE_SHARED。涉及知识产权问题时才使用独占模式。

  StreamFlags:流标志,本程序必须设为环回录音模式,AUDCLNT_STREAMFLAGS_LOOPBACK。

  pFormat:指定格式描述符,在程序中,我们先调用IAudioClient::GetMixFormat函数,获取声卡默认的录音格式,再做适当修改,例如把采样位深度修改由32位调整为16位,有助于减少录制的音频数据量。

  hnsBufferDuration:申请的buff持续时间,以100ns为单位,这个参数很重要,它指定了我们存放录音数据缓冲区的大小,它是以时间为单位的。举个例子,如果pFormat指定的音频格式为48kHz、双通道、16位深、无压缩的音频数据,那1s的数据量是48000×2×2=192000字节。如果把这个参数指定为1s,那么函数就会给程序分配192k字节的空间。在本程序中,设定每0.05s发送一次音频数据,所以把这个参数设定为0.1s,即两倍大小的缓冲区。

  hnsPeriodicity、AudioSessionGuid:未使用,置为空即可。

  调用该函数初始化音频客户端之后,必须使用IAudioClient::GetBufferSize获取系统分配给程序的缓冲区大小:

HRESULT GetBufferSize(  [out] UINT32 *pNumBufferFrames);

  这个函数只有一个参数,指向UINT32类型变量的指针,这个变量用来存放系统给程序分配的缓冲区大小,以帧为单位。这里解释一下帧的含义,采样一次即为一帧。2通道、32位深的音频数据,一帧就有2×4=8个字节。看回上面的例子,48kHz、2通道、16位深的音频数据,调用IAudioClient::Initialize函数申请0.1s的缓冲区,正常情况下,IAudioClient::GetBufferSize函数会返回4800,表示系统分配了4800帧、19200字节的缓冲区。

  申请内存后,就可以调用AfxBeginThread函数启动录音及发送音频数据子线程。以下为点击启动按钮的处理代码:

void CWiFiSpeakerDlg::OnBnClickedButton1(){    // TODO: 在此添加控件通知处理程序代码    //读取设定的客户端IP地址并存放到m_ClientAddr成员变量中    CString strIP;    this->GetDlgItemText(IDC_EDIT2,strIP);    m_ClientAddr.sin_addr.S_un.S_addr = inet_addr(strIP.GetBuffer(strIP.GetLength()));      //检测输入的IP地址是否有误    if(m_ClientAddr.sin_addr.S_un.S_addr == 0xffffffff)        {        AfxMessageBox("客户端IP地址输入有误!!!");        return;    }/*----------------------------------------------------------------------------------*///以下为实现系统录音的代码,大部分都是参考MSDN的例程//捕获(录音)例程:https://msdn.microsoft.com/en-us/library/windows/desktop/dd370800(v=vs.85).aspx//环回录音()系统录音例程:https://msdn.microsoft.com/en-us/library/windows/desktop/dd316551(v=vs.85).aspx    HRESULT hr;    IMMDeviceEnumerator *pEnumerator = NULL;    IMMDevice *pDevice = NULL;    //指定初始化函数分配100ms的缓冲区,音频设备的初始化函数只接受时间参数来分配内存空间,不能直接指定要多少字节    //例如44100Hz的音频,0.1s就有4410帧数据(1帧就是一次采样的数据量),如果是2通道,16位的话,那1帧数据就是4个字节,0.1s共17640字节    REFERENCE_TIME hnsRequestedDuration = BuffDuration_millisec*REFTIMES_PER_MILLISEC;    //系统分配给我们的缓冲区,和上面的参数有关,以帧为单位,一般情况下我们申请的多长时间,按照采样率就给我们分配多少帧的音频缓冲区    UINT32 bufferFrameCount;    //临时的字符串变量    CString tempstr;    //获取设备枚举器    hr = CoCreateInstance(           CLSID_MMDeviceEnumerator, NULL,           CLSCTX_ALL, IID_IMMDeviceEnumerator,           (void**)&pEnumerator);    //获取默认音频设备,注意,后面要初始化环回录音模式,这里必须是eRender参数,不能使用eCapture    hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice );    //激活音频客户端    hr = pDevice->Activate( IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&pAudioClient);    SAFE_RELEASE(pEnumerator);//pEnumerator已使用完,释放掉    SAFE_RELEASE(pDevice);    if (FAILED(hr)) {
this->SetDlgItemText(IDC_EDIT3,"初始化设备失败code:1!");return;} //错误退出 //获取默认的音频格式 hr = pAudioClient->GetMixFormat(&pwfx); //调整为16位,PCM格式 AdjustFormatTo16Bits(pwfx); //音频客户端初始化,共享模式、换回录音模式、申请0.1s的缓冲区 hr = pAudioClient->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, hnsRequestedDuration, 0, pwfx, NULL); if (FAILED(hr)) {
this->SetDlgItemText(IDC_EDIT3,"初始化设备失败code:2!");ErrorProcess();return;} //错误处理 //查看系统实际给我们分配多少的缓冲区 hr = pAudioClient->GetBufferSize(&bufferFrameCount); tempstr.Format("目标ip:%s\r\n%d采样率%d通道%d位深\r\n实际系统分配缓冲区%d帧\r\n",strIP,pwfx->nSamplesPerSec,pwfx->nChannels,pwfx->wBitsPerSample,bufferFrameCount); this->SetDlgItemText(IDC_EDIT3,tempstr); //以下直接启动录音线程,因为pAudioClient->GetService和release()必须在同一个线程使用,所以只能在新线程里获取服务和启动录音。 //启动录音处理线程,所有的音频数据的读取、打包、发送都在这个线程完成 AfxBeginThread(RecordAndSendAudioStreamThread,this); bThreadisRunning = TRUE;/*----------------------------------------------------------------------------------------*/ this->GetDlgItem(IDC_EDIT2)->EnableWindow(FALSE);//编辑框只读。 this->GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE);//开始按钮禁用 this->GetDlgItem(IDC_BUTTON2)->EnableWindow(TRUE);//停止按钮恢复 return;}

 

四、录音及发送音频数据子线程

   子线程的工作就是启动录音,然后在循环中不断读取之前设置的音频缓冲区,再通过socket发送出去。这里有4点需要注意的:

  1、用来存放音频数据的缓冲区,作者在程序中是定义了一个long型的全局数组,有5000个数据大小。这个数组非常大,不能在子线程里面定义这个数组,因为系统为子线程分配的堆栈空间有限,所以如果在子线程里定义这么大的数组,会导致软件运行崩溃。

  2、设定每0.05s发送一次音频数据,但是0.05s的音频数据无法一次全部读出来,只能通过while循环,重复读取系统缓冲区,直至全部读出来为止。实际在测试中,可能由于线程调度导致延迟的关系,每0.05s的数据量有时会多一点,有时会少一点,所以之前初始化申请的缓冲区是按照0.05s的两倍来申请的,防止数据溢出被覆盖。

  3、双通道、16位深的音频数据,一帧数据是4个字节,所以程序中以long型数据代表一帧数据,这样在后续调用mencopy函数时就不用考虑字节对齐的问题了,相对比较方便。

  4、数据包的格式问题,作者人为地设定数据包的前40个字节为数据格式描述,实际就是把pwfx这个变量的内容,作为包头附到数据包中。这样,在接收端就可以根据数据包的包头获取数据的分辨率、位深等信息了。

//启动录音处理线程,所有的音频数据的读取、打包、发送都在这个线程完成UINT RecordAndSendAudioStreamThread(LPVOID pParam ){    CWiFiSpeakerDlg* dlg=(CWiFiSpeakerDlg*) pParam;    HRESULT hr;    //缓冲区的下一个数据包的长度,以帧为单位    UINT32 packetLength = 0;    //缓冲区一次可以读取的帧数量,这个参数和上面那个的数值是一样的    //至于为什么要设两个,是因为使用的情况不一样    //上面那个是以函数返回值的形式返回,这个是以形参的形式跟缓冲区起始地址一起返回的    UINT32 numFramesAvailable = 0;    //标志位,指示静音什么的,这里不用    DWORD flags;    //这个是数据缓冲区,传递给函数的指针变量    BYTE *pData;    //计数器,记录读了多少数据帧数据    UINT32 Counter=0;    //把音频格式结构体复制到DataToSend中,占40个字节,真正的音频数据从第41个字节开始    if(dlg->pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE)        memcpy(DataToSend,dlg->pwfx,sizeof(WAVEFORMATEXTENSIBLE));    else        memcpy(DataToSend,dlg->pwfx,sizeof(WAVEFORMATEX));    //初始化定时器    LARGE_INTEGER FirstTime;    HANDLE hTimerWakeUp = CreateWaitableTimer(NULL, FALSE, NULL);    FirstTime.QuadPart = -dlg->BuffDuration_millisec * REFTIMES_PER_MILLISEC/2;    //获取音频捕获(录音)客户端    hr = dlg->pAudioClient->GetService( IID_IAudioCaptureClient, (void**)(&(dlg->pCaptureClient)));    //启动捕获(录音)    hr = dlg->pAudioClient->Start();     if (FAILED(hr)) {dlg->SetDlgItemText(IDC_EDIT3,"初始化设备失败code:3!");dlg->ErrorProcess();return 0;}//错误处理    //配置定时器,第一次信号定时0.05s,时间间隔0.05s,即每隔0.05把数据读出来并发送    SetWaitableTimer(hTimerWakeUp,&FirstTime,(dlg->BuffDuration_millisec *5) /10,NULL, NULL, FALSE);    //输出重定向到txt文件的方法,在命令行启动就可以看到调试信息,请参考https://blog.csdn.net/benkaoya/article/details/5935626    //printf("/-------------------------------------------------------------------------------/\n");    //主循环共有两层,这是因为数据缓冲区共有两个,    //一个是音频客户端内部硬件的缓冲区(比较小,简称小buff,即下面pData指针),另一个是我们之前在初始化客户端申请的缓冲区(比较大,简称大buff)    //小buff我在自己计算机上测试48kHz的情况下,每次只能读到480帧,可是我申请的大buff有0.1s,能装4800帧    //所以需要多一层循环,把0.05s的数据以每次480的数量全部读出来后,再发送出去。    //为什么不直接把每次480的小buff直接发出去,而多弄一个大Buff?因为这样的话会发送太频繁,会造成网络资源浪费    while (bThreadisRunning == TRUE)    {        Counter =sizeof(WAVEFORMATEXTENSIBLE)>>2;    //计数器从置,从第41个字节开始写音频数据        //线程休眠,一直录音,这里设置的时间要比BuffDuration_millisec短,因为后面复制数据也是需要时间的        //官方给的例程是大buff时间的一半。        //Sleep((dlg->BuffDuration_millisec * 5) / 10);        WaitForSingleObject(hTimerWakeUp,INFINITE);        hr = dlg->pCaptureClient->GetNextPacketSize(&packetLength);    //获取包长度,以帧为单位,这里获取的是小buff的数据包长度        //输出重定向到txt文件的方法,在命令行启动就可以看到调试信息,请参考https://blog.csdn.net/benkaoya/article/details/5935626        //printf("\nCounter:numFA: ");        while (packetLength != 0)        {            //获取小buff的地址,同时获取帧数量,这个帧数量和上面的包长度数值是一样的            hr = dlg->pCaptureClient->GetBuffer(&pData,&numFramesAvailable,&flags, NULL, NULL);            //输出重定向到文件的方法,可以看到调试信息,请参考https://blog.csdn.net/benkaoya/article/details/5935626            //printf("%04d:%d; ",Counter,numFramesAvailable);            //保存音频数据            memcpy(&(DataToSend[Counter]),pData,numFramesAvailable*dlg->pwfx->nBlockAlign);            //计数总共读了多少帧            Counter += numFramesAvailable;            //释放小buff,并读取下一个数据包长度            hr = dlg->pCaptureClient->ReleaseBuffer(numFramesAvailable);            hr = dlg->pCaptureClient->GetNextPacketSize(&packetLength);        }        //这里跳出循环,如果是48kHz采样率的话,此时的Counter就应该为0.05s的帧数量,即2400帧        //因为复制数据、发送数据都是需要时间的,实际不一定每次都刚好是2400帧,可能会多一点点或者少一点点        //如果有数据,就立即socket发去客户端        if(Counter > (sizeof(WAVEFORMATEXTENSIBLE)>>2))            sendto(dlg->m_socket,(char*)DataToSend,Counter<<2,0,(SOCKADDR *)(&(dlg->m_ClientAddr)),sizeof(SOCKADDR));    }    //停止环回录音    hr = dlg->pAudioClient->Stop();      CoTaskMemFree(dlg->pwfx);    SAFE_RELEASE(dlg->pAudioClient)    SAFE_RELEASE(dlg->pCaptureClient)    return 0;}

五、最小化到系统托盘

   这一块内容就不说了,作者也是直接参考别人的代码稍作修改实现的,可以参考:

六、写在最后

  本作品发送的音频数据都是未经压缩的PCM原始数据,这种方法的好处就是发送端接收端没有压缩和解码的过程,效率高,实时性好。缺点就是传输的数据量大,占用网络带宽,以作者的48kHz、2通道、16位深的音频数据为例,网络带宽占用195KB/s。以下是发送端运行截图及windows资源管理器网络速度截图。

   

 

转载于:https://www.cnblogs.com/qzrzq1/p/9074132.html

你可能感兴趣的文章
SilverLight:基础控件使用(6)-Slider控件
查看>>
Android写的一个设置图片查看器,可以调整透明度
查看>>
第 5 章 File Share
查看>>
判断字符串解析是JsonObject或者JsonArray
查看>>
[LeetCode] Implement strStr()
查看>>
多模块Struts应用程序的几个问题(及部分解决方法)
查看>>
1.2. MariaDB
查看>>
SpringSide示例之HelloWorld
查看>>
LINQ-to-SQL那点事~LINQ-to-SQL中的并发冲突与应对
查看>>
日志不说谎--Asp.net的生命周期
查看>>
C#~异步编程续~.net4.5主推的await&async应用
查看>>
C#进行MapX二次开发之图层操作
查看>>
ASP.NET 运行机制详解
查看>>
C++ little errors , Big problem
查看>>
iOS - Phone 电话
查看>>
根据点提取栅格值
查看>>
在 ML2 中配置 OVS vlan network - 每天5分钟玩转 OpenStack(136)
查看>>
Selenium2+python自动化34-获取百度输入联想词
查看>>
【★★★★★】提高PHP代码质量的36个技巧
查看>>
如何解决/home/oracle: is a directory报警
查看>>