Making an Audio Spectrum analyzer with Bass.dll, C# and Arduino






4.83/5 (44 votes)
A mini howto on using bass.dll & bass.net wrapper.
Introduction
A few years ago I wanted to create an universal audio spectrum analyzer for windows. A lot of audio players have this kind of functionality, but I coudn't find a program that's independent of the player and displays the data based on the default sound output. A week ago I thought about the app again, and I had some free time, so I made it using Bass.dll
Background
Bass is is an audio library for use in software on several platforms. Its purpose is to provide developers with powerful and efficient sample, stream (MP3, MP2, MP1, OGG, WAV, AIFF, custom generated, and more via OS codecs and add-ons), MOD music (XM, IT, S3M, MOD, MTM, UMX), MO3 music (MP3/OGG compressed MODs), and recording functions. All in a compact DLL that won't bloat your distribution.
from the bass.dll website: http://www.un4seen.com/
The Bass library can be used from any programing language that supports function calls from dll files. For the .net platform the best wrapper API is called bass.net. It's very powerful, because it has support for all the released bass.dll add-ons, and it comes with detailed help. The API can be found at http://bass.radio42.com/
Unfortunately the bass library and the bass.net wrapper isn't free. It can be used for free, if you develop freeware programs, but if you want to make money with your program, you must buy a developer license.
WASAPI
Since Windows 7 the default audio system is the Windows Audio Session API or WASAPI for short. It provides a mixer API that talks directly to your sound card. It handles sample rate conversion, recording, audio effects and everything that's Audio related.
Before WASAPI the sound playback was handled through Direct Sound, that hasn't had these advanced functions, but using Direct Sound the application was closer to the actual hardware. In Windows 7 and 8 Direct Sound calls are dispatched & emulated though WASAPI. This works in most cases, but unfortunately you simply can't record the main output of the PC using Direct Sound.
Bass.dll is built over the Direct Sound API, but it has an add-on called bass-wasapi.dll, that makes it possible to use WASAPI with bass.dll. This is required, because the program records samples from the output for processing.
Finding the correct audio output is a bit tricky, because the API separates devices based on their capabilities. If you have a single sound card in your system you will see at least three devices through the API. An output device and an input device and an additional output device with loopback mode.
In loopback mode, a client of WASAPI can capture the audio stream that is being played by a rendering endpoint device. In other words this is what we need.
How it Works
The main Spectrum analyzer code is placed in the Analyzer.cs file, which contains the Analyzer
class, which is far from production ready, it's more like a proof of concept example.
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();
}
}
}
The main GUI is constructed using WPF, and it has a custom control called Spectrum, which is constructed from 16 progress bars and a Set method, which sets's all the progress bar's value from a byte list.
The devicelist
ComboBox, used in the analyzer constructor holds a list of devices which are capable of loopback mode. Then the user can select which output he/she want's to monitor.
In the code the WASAPIPROC
delegate is created in a variable, instead of just passing a method to the code. That's because otherwise the .net garbage collector sees the delegate as unreferenced and removes it from memory, which crashes the app.
Arduino Code
A few months ago I picked up a pair of LED matrices with MAX7221 driver chips from ebay, so I decided to make the program a little bit cooler, adding some hardware displays.
The MAX7221 is a constant current 7 segment LED driver chip with serial input and output and can drive 8 displays or a 8x8 LED Matrix. On the Arduino playground detailed programing and hardware documentation is available. You can find them at http://playground.arduino.cc/Main/MAX72XXHardware and http://playground.arduino.cc//Main/LedControl
The Arduino code waits for 16 bytes of data, then send's the 16 bytes to the display. Any kind of Arduino can be used, but If you wan't to use a Leonardo based model, you may have to modify the serial port initialization part.
//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;
}
}
Video Demonstration
I uploaded a demo video to my youtube, so you can see the program in action. The sound and picture quality isn't the best, but you can see that the program works, and the display isn't slow. In the future I will definately update the display to 32x32 pixels, so the spectrogram can look more cooler :)
The video can be found at: http://youtu.be/A96HRXQql0Y
History
2014-07-17 - First release