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






4.83/5 (44投票s)
一个关于使用 bass.dll 和 bass.net 包装器的小教程。
引言
几年前,我想为 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 - 首次发布