Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

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

Rate me:
Please Sign up or sign in to vote.
4.83/5 (46 votes)
24 Jul 2015CPOL4 min read 139.8K   11.2K   74   43
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)


Written By
Architect
Hungary Hungary
Been interested in computers & electronics since I got my NES, eventually became a Computer engineer, now a Software Architect

Comments and Discussions

 
AnswerRe: How to get this to work on HT1632 display? Pin
webmaster44223-Mar-16 22:40
webmaster44223-Mar-16 22:40 
QuestionIs There any way of getting this thing worked on Windows Xp Pin
nikhilbhanda7-Dec-15 18:13
nikhilbhanda7-Dec-15 18:13 
AnswerRe: Is There any way of getting this thing worked on Windows Xp Pin
webmaster4428-Dec-15 1:33
webmaster4428-Dec-15 1:33 
GeneralMy vote of 5 Pin
Member 1061728923-Oct-15 12:35
Member 1061728923-Oct-15 12:35 
Questionhelp Pin
Member 118836824-Aug-15 19:19
Member 118836824-Aug-15 19:19 
AnswerRe: help Pin
webmaster4424-Aug-15 19:23
webmaster4424-Aug-15 19:23 
GeneralRe: help Pin
Member 118836824-Aug-15 20:07
Member 118836824-Aug-15 20:07 
GeneralRe: help Pin
webmaster4425-Aug-15 23:37
webmaster4425-Aug-15 23:37 
The Simple answer is use setColumn() methood on the LedControl object, but it has some issues.

First of all, the MAX7219 driver chips allow fast row updates, because of their wiring & the internal layout. You can look into the internals of it's working by reading the max7219 datasheet.

Basicly updating a collumn requires 8 row updates, which can be slow. The displays I used were rotated 90 degrees, so when the code calls the setRow() function it updates a column.
GeneralCool! Pin
NeverForever21-Jul-15 23:14
NeverForever21-Jul-15 23:14 
GeneralRe: Cool! Pin
Member 1309207829-Mar-17 1:34
Member 1309207829-Mar-17 1:34 
QuestionIs there any way to get the sound data from a sound file (not speaker!)? Pin
rafei24-Mar-15 6:33
rafei24-Mar-15 6:33 
AnswerRe: Is there any way to get the sound data from a sound file (not speaker!)? Pin
webmaster44226-Mar-15 20:55
webmaster44226-Mar-15 20:55 
QuestionIs there a way to make the transitions of the bars more smooth? Pin
Robert Iagar3-Feb-15 7:39
Robert Iagar3-Feb-15 7:39 
AnswerRe: Is there a way to make the transitions of the bars more smooth? Pin
webmaster4424-Feb-15 6:12
webmaster4424-Feb-15 6:12 
QuestionGet Licensed For commercial Pin
Member 112849265-Jan-15 15:09
Member 112849265-Jan-15 15:09 
AnswerRe: Get Licensed For commercial Pin
webmaster44210-Jan-15 10:53
webmaster44210-Jan-15 10:53 
GeneralMy vote of 4 Pin
Member 1108969517-Sep-14 5:43
Member 1108969517-Sep-14 5:43 
GeneralRe: My vote of 4 Pin
webmaster4425-Aug-15 23:38
webmaster4425-Aug-15 23:38 
QuestionThank for sharing Pin
Lilanga28-Aug-14 23:04
professionalLilanga28-Aug-14 23:04 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA13-Aug-14 3:14
professionalȘtefan-Mihai MOGA13-Aug-14 3:14 
GeneralMy vote of 5 Pin
sjelen11-Aug-14 3:59
professionalsjelen11-Aug-14 3:59 
QuestionMy Vote of 5 Pin
newton.saber17-Jul-14 10:23
newton.saber17-Jul-14 10:23 
GeneralMy vote of 5 Pin
Snorri Kristjansson17-Jul-14 3:11
professionalSnorri Kristjansson17-Jul-14 3:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.