Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

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

4.83/5 (46 votes)
24 Jul 2015CPOL4 min read 146.4K   11.2K  
A mini howto on using bass.dll & bass.net wrapper.
Download AudioSpectrum.zip (source and demp program)
 

Introduction

Image 1

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.

C#
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.

Image 2

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.

C++
//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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)