65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Bass.dll、C# 和 Arduino 制作音频频谱分析器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (44投票s)

2014年7月17日

CPOL

4分钟阅读

viewsIcon

147698

downloadIcon

11291

一个关于使用 bass.dll 和 bass.net 包装器的小教程。

下载 AudioSpectrum.zip (源代码和演示程序)
 

引言

几年前,我想为 Windows 创建一个通用的音频频谱分析器。很多音频播放器都有这种功能,但我找不到一个独立于播放器、并且能根据默认声音输出来显示数据的程序。一周前,我再次想到了这个应用,并且有一些空闲时间,于是我使用 Bass.dll 完成了它。

背景

Bass 是一个用于多个平台软件的音频库。它的目的是为开发人员提供强大而高效的采样、流(MP3、MP2、MP1、OGG、WAV、AIFF、自定义生成,以及通过 OS 编解码器和附加组件获取更多)、MOD 音乐(XM、IT、S3M、MOD、MTM、UMX)、MO3 音乐(MP3/OGG 压缩的 MOD)、以及录音功能。所有这些都包含在一个紧凑的 DLL 中,不会增加分发的体积。

来自 bass.dll 网站:http://www.un4seen.com/

Bass 库可以从任何支持从 dll 文件调用函数的编程语言中使用。对于 .net 平台,最佳的包装器 API 称为 bass.net。它非常强大,因为它支持所有发布的 bass.dll 附加组件,并且附带了详细的帮助文档。API 可以在 http://bass.radio42.com/ 找到。

不幸的是,bass 库和 bass.net 包装器不是免费的。如果您开发免费软件程序,可以免费使用,但如果您想通过您的程序赚钱,则必须购买开发者许可证。

WASAPI

自 Windows 7 起,默认音频系统是 Windows Audio Session API,简称 WASAPI。它提供了一个直接与声卡通信的混音器 API。它处理采样率转换、录音、音频效果以及所有与音频相关的内容。

在 WASAPI 之前,声音播放是通过 Direct Sound 处理的,它没有这些高级功能,但使用 Direct Sound 时,应用程序离实际硬件更近。在 Windows 7 和 8 中,Direct Sound 调用是通过 WASAPI 分派和模拟的。这在大多数情况下都能正常工作,但不幸的是,您根本无法通过 Direct Sound 录制 PC 的主输出。

Bass.dll 是建立在 Direct Sound API 之上的,但它有一个名为 bass-wasapi.dll 的附加组件,使得 bass.dll 可以使用 WASAPI。这是必需的,因为程序从输出录制样本以进行处理。

找到正确的音频输出有点棘手,因为 API 根据其功能区分设备。如果您的系统只有一个声卡,您将通过 API 看到至少三个设备。一个输出设备、一个输入设备和一个具有回路模式的附加输出设备。

在回路模式下,WASAPI 的客户端可以捕获由渲染端点设备播放的音频流。换句话说,这就是我们需要的。

工作原理

主要的频谱分析器代码位于 Analyzer.cs 文件中,该文件包含 Analyzer 类,它远非生产就绪,更像是一个概念验证示例。

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using Un4seen.Bass;
using Un4seen.BassWasapi;

namespace AudioSpectrum
{

    internal class Analyzer
    {
        private bool _enable;               //enabled status
        private DispatcherTimer _t;         //timer that refreshes the display
        private float[] _fft;               //buffer for fft data
        private ProgressBar _l, _r;         //progressbars for left and right channel intensity
        private WASAPIPROC _process;        //callback function to obtain data
        private int _lastlevel;             //last output level
        private int _hanctr;                //last output level counter
        private List<byte> _spectrumdata;   //spectrum data buffer
        private Spectrum _spectrum;         //spectrum dispay control
        private ComboBox _devicelist;       //device list
        private bool _initialized;          //initialized flag
        private int devindex;               //used device index

        private int _lines = 16;            // number of spectrum lines

        //ctor
        public Analyzer(ProgressBar left, ProgressBar right, Spectrum spectrum, ComboBox devicelist)
        {
            _fft = new float[1024];
            _lastlevel = 0;
            _hanctr = 0;
            _t = new DispatcherTimer();
            _t.Tick += _t_Tick;
            _t.Interval = TimeSpan.FromMilliseconds(25); //40hz refresh rate
            _t.IsEnabled = false;
            _l = left;
            _r = right;
            _l.Minimum = 0;
            _r.Minimum = 0;
            _r.Maximum = ushort.MaxValue;
            _l.Maximum = ushort.MaxValue;
            _process = new WASAPIPROC(Process);
            _spectrumdata = new List<byte>();
            _spectrum = spectrum;
            _devicelist = devicelist;
            _initialized = false;
            Init();
        }

        // Serial port for arduino output
        public SerialPort Serial { get; set; }

        // flag for display enable
        public bool DisplayEnable { get; set; }

        //flag for enabling and disabling program functionality
        public bool Enable
        {
            get { return _enable; }
            set
            {
                _enable = value;
                if (value)
                {
                    if (!_initialized)
                    {
                        var str = (_devicelist.Items[_devicelist.SelectedIndex] as string)
                        var array = str.Split(' ');
                        devindex = Convert.ToInt32(array[0]);
                        bool result = BassWasapi.BASS_WASAPI_Init(devindex, 0, 0, 
                                                                  BASSWASAPIInit.BASS_WASAPI_BUFFER,
                                                                  1f, 0.05f,
                                                                  _process, IntPtr.Zero);
                        if (!result)
                        {
                            var error = Bass.BASS_ErrorGetCode();
                            MessageBox.Show(error.ToString());
                        }
                        else
                        {
                            _initialized = true;
                            _devicelist.IsEnabled = false;
                        }
                    }
                    BassWasapi.BASS_WASAPI_Start();
                }
                else BassWasapi.BASS_WASAPI_Stop(true);
                System.Threading.Thread.Sleep(500);
                _t.IsEnabled = value;
            }
        }

        // initialization
        private void Init()
        {
            bool result = false;
            for (int i = 0; i < BassWasapi.BASS_WASAPI_GetDeviceCount(); i++)
            {
                var device = BassWasapi.BASS_WASAPI_GetDeviceInfo(i);
                if (device.IsEnabled && device.IsLoopback)
                {
                    _devicelist.Items.Add(string.Format("{0} - {1}", i, device.name));
                }
            }
            _devicelist.SelectedIndex = 0;
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_UPDATETHREADS, false);
            result = Bass.BASS_Init(0, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero);
            if (!result) throw new Exception("Init Error");
        }

        //timer 
        private void _t_Tick(object sender, EventArgs e)
        {
            // get fft data. Return value is -1 on error
            int ret = BassWasapi.BASS_WASAPI_GetData(_fft, (int)BASSData.BASS_DATA_FFT2048);
            if (ret < 0) return;
            int x, y;
            int b0 = 0;

            //computes the spectrum data, the code is taken from a bass_wasapi sample.
            for (x=0; x<_lines; x++)
            {
                float peak = 0;
                int b1 = (int)Math.Pow(2, x * 10.0 / (_lines - 1));
                if (b1 > 1023) b1 = 1023;
                if (b1 <= b0) b1 = b0 + 1;
                for (;b0<b1;b0++)
                {
                    if (peak < _fft[1 + b0]) peak = _fft[1 + b0];
                }
                y = (int)(Math.Sqrt(peak) * 3 * 255 - 4);
                if (y > 255) y = 255;
                if (y < 0) y = 0;
                _spectrumdata.Add((byte)y);
            }

            if (DisplayEnable) _spectrum.Set(_spectrumdata);
            if (Serial != null)
            {
                Serial.Write(_spectrumdata.ToArray(), 0, _spectrumdata.Count);
            }
            _spectrumdata.Clear();


            int level = BassWasapi.BASS_WASAPI_GetLevel();
            _l.Value = Utils.LowWord32(level);
            _r.Value = Utils.HighWord32(level);
            if (level == _lastlevel && level != 0) _hanctr++;
            _lastlevel = level;

            //Required, because some programs hang the output. If the output hangs for a 75ms
            //this piece of code re initializes the output
            //so it doesn't make a gliched sound for long.
            if (_hanctr > 3)
            {
                _hanctr = 0;
                _l.Value = 0;
                _r.Value = 0;
                Free();
                Bass.BASS_Init(0, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero);
                _initialized = false;
                Enable = true;
            }
        }

        // WASAPI callback, required for continuous recording
        private int Process(IntPtr buffer, int length, IntPtr user)
        {
            return length;
        }

        //cleanup
        public void Free()
        {
            BassWasapi.BASS_WASAPI_Free();
            Bass.BASS_Free();
        }
    }
}

主 GUI 是使用 WPF 构建的,它有一个名为 Spectrum 的自定义控件,该控件由 16 个进度条和一个 Set 方法构成,该方法将所有进度条的值设置为从一个字节列表中获取。

在分析器构造函数中使用的 devicelist ComboBox 包含一个支持回路模式的设备列表。然后用户可以选择他们想要监控的输出。

在代码中,WASAPIPROC 委托被创建在一个变量中,而不是简单地将一个方法传递给代码。这是因为否则 .net 垃圾回收器会认为委托未被引用并将其从内存中删除,这将导致应用程序崩溃。

Arduino 代码

几个月前,我从 eBay 上买了一对带有 MAX7221 驱动芯片的 LED 矩阵,所以我决定让程序更酷一些,添加一些硬件显示。

MAX7221 是一个恒流 7 段 LED 驱动芯片,具有串行输入和输出,可以驱动 8 个显示器或一个 8x8 LED 矩阵。在 Arduino Playground 上可以找到详细的编程和硬件文档。您可以在 http://playground.arduino.cc/Main/MAX72XXHardware http://playground.arduino.cc//Main/LedControl 找到它们。

Arduino 代码等待 16 字节数据,然后将这 16 字节发送到显示器。可以使用任何类型的 Arduino,但如果您想使用基于 Leonardo 的型号,可能需要修改串口初始化部分。

//We always have to include the library
#include "LedControl.h"

/*
 Now we need a LedControl to work with.
 ***** These pin numbers will probably not work with your hardware *****
 pin 12 is connected to the DataIn 
 pin 11 is connected to the CLK 
 pin 10 is connected to LOAD
 We are using two displays
 */
LedControl lc=LedControl(12,11,10,2);
int counter = 0;
int value = 0;
byte buffer[16] = { 
  0 };
int lastvalue = 0;

void setup() {
  /*
   The MAX72XX is in power-saving mode on startup,
   we have to do a wakeup call
   */
  lc.shutdown(0,false);
  lc.shutdown(1,false);
  /* Set the brightness to a medium values */
  lc.setIntensity(0,4);
  lc.setIntensity(1,4);
  /* and clear the display */
  lc.clearDisplay(0);
  lc.clearDisplay(1);
  Serial.begin(115200);
}

//Set's a single column value
//In my case the displays are rotated 90 degrees
//so in the code I'm setting rows instead of colums actualy
void Set(int index, int value)
{
  int device = index / 8; //calculate device
  int row = index - (device * 8); //calculate row
  int leds = map(value, 0, 255, 0, 9); //map value to number of leds.
  //display data
  switch (leds)
  {
  case 0:
    lc.setRow(device,row, 0x00);
    return;
  case 1:
    lc.setRow(device,row, 0x80);
    return;
  case 2:
    lc.setRow(device,row, 0xc0);
    return;
  case 3:
    lc.setRow(device,row, 0xe0);
    return;
  case 4:
    lc.setRow(device,row, 0xf0);
    return;
  case 5:
    lc.setRow(device,row, 0xf8);
    return;
  case 6:
    lc.setRow(device,row, 0xfc);
    return;
  case 7:
    lc.setRow(device,row, 0xfe);
    return;
  case 8:
    lc.setRow(device,row, 0xff);
    return;
  }
}

void loop()
{
  if (Serial.available() >= 15)
  {
    value = Serial.read();
    Set(counter, value);
    counter++;
    if (counter > 15) counter = 0;
  }
}

视频演示

我将一个演示视频上传到了我的 YouTube 频道,您可以在其中观看该程序。音频和图像质量不是最好,但您可以看到程序有效,并且显示屏不慢。将来我一定会将显示屏更新到 32x32 像素,这样频谱图就可以看起来更酷了 :)

视频可以在这里找到:http://youtu.be/A96HRXQql0Y

历史

2014-07-17 - 首次发布

© . All rights reserved.