如何从实时流中播放音频

Posted

技术标签:

【中文标题】如何从实时流中播放音频【英文标题】:How to play audio from a real-time stream 【发布时间】:2010-11-21 02:43:53 【问题描述】:

我有一个程序可以产生应该同时播放的音频信号。为此,我在每 100 毫秒的周期内播放 100 毫秒的音频流。但是我在每个 100 ms 音频流的开始和结束时都有不想要的信号(因为 DC),所以即使信号值相同,输出声音也不平滑。我的代码附在下面。请帮助我如何获得正确的实时音频。

using System;
using System.Windows.Forms;
using Microsoft.DirectX.DirectSound;
using System.IO;

namespace TestSound

    class CSound : Form
    
        const int HEADER_SIZE = 44;
        const bool FLAG_STEREO = true;
        const short BITS_PER_SAMPLE = 16;
        const int SAMPLE_RATE = 44100;

        int numberOfSamples;
        MemoryStream stream;
        BinaryWriter writer;
        Device ApplicationDevice = null;
        SecondaryBuffer buffer = null;
        BufferDescription description;

        public CSound()
        
            try
            
                ApplicationDevice = new Device();
            
            catch
            
                MessageBox.Show("Unable to create sound device.");
                ApplicationDevice = null;
                return;
            
            ApplicationDevice.SetCooperativeLevel(this, CooperativeLevel.Priority);
            description = new BufferDescription();
            description.ControlEffects = false;
            stream = new MemoryStream();
            writer = new BinaryWriter(stream);
        

        private void AddHeader()
        
            stream.Position = 0;

            writer.Write(0x46464952); // "RIFF" in ASCII
            writer.Write((int)(HEADER_SIZE + (numberOfSamples * BITS_PER_SAMPLE * (FLAG_STEREO ? 2 : 1) / 8)) - 8);
            writer.Write(0x45564157); // "WAVE" in ASCII
            writer.Write(0x20746d66); // "fmt " in ASCII
            writer.Write(16);
            writer.Write((short)1);
            writer.Write((short)(FLAG_STEREO ? 2 : 1));
            writer.Write(SAMPLE_RATE);
            writer.Write(SAMPLE_RATE * (FLAG_STEREO ? 2 : 1) * BITS_PER_SAMPLE / 8);
            writer.Write((short)((FLAG_STEREO ? 2 : 1) * BITS_PER_SAMPLE / 8));
            writer.Write(BITS_PER_SAMPLE);
            writer.Write(0x61746164); // "data" in ASCII
            writer.Write((int)(numberOfSamples * BITS_PER_SAMPLE * (FLAG_STEREO ? 2 : 1) / 8));
        

        public void Play(short[] samples)
        
            if (ApplicationDevice == null)
                return;

            stream.Position = HEADER_SIZE;
            numberOfSamples = samples.Length;
            for (int i = 0; i < numberOfSamples; i++)
            
                writer.Write(samples[i]);
                if (FLAG_STEREO)
                    writer.Write(samples[i]);
            
            AddHeader();
            stream.Position = 0;

            try
            
                if (buffer != null)
                
                    buffer.Dispose();
                    buffer = null;
                
                buffer = new SecondaryBuffer(stream, description, ApplicationDevice);
                buffer.Play(0, BufferPlayFlags.Default);
            
            catch (Exception e)
            
                MessageBox.Show(e.Message);
            
        

        static short[] samples = new short[4410]; // 100 ms
        static CSound sound;

        static void Main()
        
            Form form = new Form();
            form.Show();

            sound = new CSound();
            Random random = new Random();
            for (int i = 0; i < samples.Length; i++)
                samples[i] = 1000; // constant value

            while (true)
            
                sound.Play(samples);
                System.Threading.Thread.Sleep(100); // 100 ms
            
        
     

【问题讨论】:

【参考方案1】:

如果您正在寻找一种通过定义的流播放音频的方法,您是否考虑过 NAudio http://naudio.codeplex.com/?

您可以从文件或其他位置(即内存)定义流,然后使用要播放的数据填充流。只要您能够在读取指针到达缓冲区末尾之前继续向流提供音频数据,您就不会在生成的音频中听到这些伪影。

顺便说一句 - 我假设您知道 .Net 的 Managed Direct X 库已不再开发,并且实际上是此类音频开发的死胡同?

【讨论】:

【参考方案2】:

这段代码有很多问题。我猜当您运行此代码时,您会每 100 毫秒听到一次咔嗒声或爆裂声。这是因为在 while(true) 循环中调用了 Thread.Sleep(100)。基本上,您的应用程序等待 100 毫秒(给予或花费一小段时间)然后调用 Play(),后者会进行一些处理,然后将数组排队等待播放。因此,每个 100 ms 数组的播放之间会有一点时间间隔,这会产生点击。

但是,如果您只是注释掉 Thread.Sleep(100) 行,您的应用将进入无限循环,在 100 ms 数组之后,它会继续排队 100 ms 数组,直到内存不足。但至少播放不会每 100 毫秒出现一次伪影。

如果您将行更改为 Thread.Sleep(80),它会工作得更好一些,因为您需要更长的时间才能耗尽内存,但这仍然会发生,因为您仍然会转储缓冲区进入音频播放系统的速度比系统播放它们的速度要快。

此外,即使您消除了每 100 毫秒的点击声,您仍然不会从扬声器中听到任何声音,因为您的代码将每个样本值设置为 1000。只有在以下情况下您才会听到任何声音您会随着时间的推移改变样本值。顺便说一句,您听到咔哒声的唯一原因是因为该样本值设置为 1000,并且在块之间的这些小时间间隔内,播放值会回到 0。如果将每个样本值设置为 0,您将永远不会什么都听。

我可以进一步帮助您,但我需要更好地了解您正在尝试做什么,确切地说。您是否尝试以特定频率播放连续音调?

【讨论】:

【参考方案3】:

如果您所说的“不良信号”是指两端有轻微的爆裂声,则可能是包络问题,在 Csound 中可以通过“亚麻”操作码或类似的东西来控制。这个想法是你需要增加前端的幅度,然后稍微降低后端的幅度,以避免扬声器的咔嗒声突然停止在中波中的输出,可以这么说。几毫秒就足够了——试验一下,直到你在没有注意到幅度调制的情况下摆脱爆音。

看这里:http://www.csounds.com/journal/issue11/csoundEnvelopes.html

如果您试图通过以固定间隔顺序连接相同波形来产生无缝信号,那么您将始终听到这种爆裂声,因为一个波形的结尾与下一个波形的开头不对齐。让波形精确排列非常困难,这不是一个好策略。更好的策略是使用包络(如上所述)并重叠波形(称为鸠尾拖尾),以便旧发音的衰减与新发音的上升同时发生。

但是,这种策略不会产生完全纯净的声音,因为两个稍微相同的波形异步重叠的存在会相互抵消一点,并导致幅度在波形的每个接合点降低。

【讨论】:

以上是关于如何从实时流中播放音频的主要内容,如果未能解决你的问题,请参考以下文章

如何播放 RTMP 流中的音频?

iOS Swift 从网络流中播放音频(aac)

在 UWP 中播放流中的音频

从 URL 实时流式传输音频的 RadioKit 替代方案

如何在三星智能电视上提取嵌入在 Icecast 音频(广播)流中的流式“正在播放”数据

NAudio - 从 RTP 数据包播放音频有效负载