从 C++ ASIO 库用 C# 录制音频流

Posted

技术标签:

【中文标题】从 C++ ASIO 库用 C# 录制音频流【英文标题】:Recording an audio stream in C# from C++ ASIO library 【发布时间】:2011-09-26 21:00:44 【问题描述】:

我需要找到录制音频流的最佳方式。我已经用 C++ 构建了低级代码,并将其中的一部分与 C# 接口。

所以我有一个 C++ 回调,它给了我一个浮点数组 - 音频信号。 目前,我的 C++ lib 正在以 wav 格式将数据直接记录到文件中,并且它只会在记录结束时通知我的 C# 应用程序。

但是,我希望在 UI 方面有更多的交互性,比如“无限”进度条、记录的数据量、取消按钮等,因为最坏的情况是一分钟,也许最好保留它在记忆中。我对 .NET 和 C# 内存管理知之甚少,所以我不知道如何使其高效。

在 C# 中是否有任何可快速调整大小的容器,我可以将数据放入其中,然后像数组一样访问它?

我也想用它构建一个波形图像。我已经用 C++ 完成了这些事情,但不知何故我不喜欢编写太多消息传递、传输对象等的想法。

所以把事情放在一起:

    我有 C++ 非托管回调,它从内部做一些事情 我想在处理数据后调用 C# 方法,C 原型是:

    void 进程(float **signal, int n); (通常是 [2][n] - 用于立体声)

    什么是 C# 等价物,我如何从那个 C++ 中调用它 回调?

    写入连续流的最佳类是什么(例如 mem.put(float[][] data, int size) ) 然后将其作为数组读取或 用其他简单的方法(例如从中保存一个 wav 文件或制作一个 波形位图)

    如果我在 C# 中执行此操作,会不会有显着的性能损失? (托管 c++ 包装器调用 c# 函数等......可能还有一些 DSP 的东西)或者我只是偏执狂? :)

干杯,

巴布罗克斯

我的解决方案

好的,我就是这样解决的:

在我的 C++ 头文件中,我得到了传输结构:

public ref struct CVAudio 
   public:
    float *left;
    float *right;
    int length;
   ;

然后在托管 C++ 类中我声明:

delegate void       GetAudioData([In, Out] CVAudio^ audio);

然后我可以将它用作初始化音频的方法的参数:

void                initializeSoundSystem(void *HWnd, GetAudioData ^audio);

该委托也有一个

的 C 原型
typedef void (CALLBACK *GETAUDIODATA)(CVAudio ^a);

在内部C++类中使用如下:

void                initializeSoundSystem(HWND HWnd, GETAUDIODATA audio);

那么第一个方法的主体是:

void VDAudio::initializeSoundSystem(void *HWnd, GetAudioData ^audio) 

    HWND h = (HWND) HWnd;

    acb = audio;
    pin_ptr<GetAudioData ^> tmp = &audio;

    IntPtr ip = Marshal::GetFunctionPointerForDelegate(audio);
    GETAUDIODATA cb = static_cast<GETAUDIODATA>(ip.ToPointer());

    audioObserver->initializeSoundSystem(h, cb);

audioObserver 的主体只是将回调存储在对象中并做一些与音频相关的事情。

然后像这样在处理方法中调用回调:

            VDAudio^ a = gcnew VDAudio();
            a->left = VHOST->Master->getSample()[0]; //returns left channel float*
            a->right = VHOST->Master->getSample()[1];
            a->length = length;
            (*callback)(a);

还有 C# 委托的主体:

public void GetSamples(CVAudio audio)

    unsafe
    

        float* l = (float*)audio.left;
        float* r = (float*)audio.right;

        if (l != null)
        

            SamplePack sample = new SamplePack();

            sample.left = new float[audio.length];
            sample.right = new float[audio.length];

            IntPtr lptr = new IntPtr((void*)l);
            IntPtr rptr = new IntPtr((void*)r);

            Marshal.Copy(lptr, sample.left, 0, audio.length);
            Marshal.Copy(rptr, sample.right, 0, audio.length);

            this.Dispatcher.Invoke(new Action(delegate()
            
                GetSamples(sample);
            ));
        
    

所以这可能不是最好的代码 - 我不知道。只能说它有效,似乎没有泄漏等:)

【问题讨论】:

一项令人钦佩的努力,但您考虑过code.google.com/p/sharpdx 吗? 我使用的不是directx,而是ASIO。我把所有东西都重新编码了,我只关心录音。我不想使用第 3 方库。还是谢谢你的回复:) @pablox,出于好奇,您使用什么与 ASIO 驱动程序通信?你自己的DLL?据我所知,唯一可与 .NET for ASIO 一起使用的库是 BASS.net (un4seen.com)。 我正在开发我几年前编写的音频应用程序的新版本,但我很难使用 UI(它是纯 c++ win32 应用程序和我的自定义控件等) .就像一个月前我尝试了 WPF 并且非常喜欢它,所以我将我写的所有音频内容提取到 C++/cli dll 中,以便它现在可以与 .NET 一起使用。我已经找到了我的问题的答案,所以我继续努力。在 ASIO 通信方面,我使用由我自己的“驱动程序”架构包装的 Steinberg 提供的代码,所以我可以以同样的方式使用 DirectX(但我对此不感兴趣)。 【参考方案1】:

问题 1:您希望将与非托管 C 签名匹配的 C# 委托传递给您的 C++。然后,您可以将其作为 C 函数指针进行回调。例如,

delegate void ASIOCallback(IntPtr signal, int n);

然后您需要手动将信号内存编组到托管缓冲区中,或者将其复制到非托管缓冲区中。两者都可以使用Marshal class 上的方法完成。这就引出了问题 2。

问题 2:您有几个选择,主要取决于您是将数据存储在托管内存还是非托管内存中。将其存储在非托管内存中可能会使与非托管代码的交互变得容易,并且理论上可以通过减少数据副本的数量来提高效率。但是,将其存储在托管内存中(通过数组、generic collection、MemoryStream 等)会更容易从托管代码中访问。

问题 3:关于性能,您需要考虑的两个因素是原始数字处理和复制数据。通常,C# 中的数值运算并不比 C/C++ 慢多少。网络上有多个很好的资源来比较这种类型的性能。至于复制数据,我认为这可能会给您带来更多问题。两者都是由于复制数据所花费的时间和应用程序将使用的额外内存。您需要考虑是否需要在托管和非托管内存中复制数据,我猜这将取决于您希望在 C# 和/或 C++ 中访问它的程度。

【讨论】:

以上是关于从 C++ ASIO 库用 C# 录制音频流的主要内容,如果未能解决你的问题,请参考以下文章

使用 C++ 教程和示例代码进行音频流传输 [关闭]

NAudio:使用 ASIO 录制音频并用吉他输出

录制和播放从麦克风录制的音频流

播放流后音频队列无法录制音频

NAudio Asio 录制播放和保存(仅限噪音)

从 URL React Native 录制音频(广播流)