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;
private DispatcherTimer _t;
private float[] _fft;
private ProgressBar _l, _r;
private WASAPIPROC _process;
private int _lastlevel;
private int _hanctr;
private List<byte> _spectrumdata;
private Spectrum _spectrum;
private ComboBox _devicelist;
private bool _initialized;
private int devindex;
private int _lines = 16;
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);
_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();
}
public SerialPort Serial { get; set; }
public bool DisplayEnable { get; set; }
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;
}
}
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");
}
private void _t_Tick(object sender, EventArgs e)
{
int ret = BassWasapi.BASS_WASAPI_GetData(_fft, (int)BASSData.BASS_DATA_FFT2048);
if (ret < 0) return;
int x, y;
int b0 = 0;
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;
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;
}
}
private int Process(IntPtr buffer, int length, IntPtr user)
{
return length;
}
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.
#include "LedControl.h"
LedControl lc=LedControl(12,11,10,2);
int counter = 0;
int value = 0;
byte buffer[16] = {
0 };
int lastvalue = 0;
void setup() {
lc.shutdown(0,false);
lc.shutdown(1,false);
lc.setIntensity(0,4);
lc.setIntensity(1,4);
lc.clearDisplay(0);
lc.clearDisplay(1);
Serial.begin(115200);
}
void Set(int index, int value)
{
int device = index / 8; int row = index - (device * 8); int leds = map(value, 0, 255, 0, 9); 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