Music Visualizer

Jigoku - Game Designer & Unity Developer

Great gameplay makes use of every aspect of the game in ways sensible to the lore.

Stefan Alfredsson

+46 70 754 8228

LinkedIn Profile

stefan@jigoku.se

 

Title: Music Visualizer

Engine: Unity 4

Platform: PC

 

What it is:

A simple visualizer that analysis an audio file, such as a song, and scales 20 red pillars representing frequencies throughout the frequency range of the audio file. This gives a feeling for what frequencies the audio file consists of (lower frequencies to the left, higher to the right) but also produces an oddly satisfying visual representation of the sound.

 

How it came to be:

  • The goal of the project was to analyse sound in Unity and experiment with what could be done with real-time analysis of sound.
  • This was a short and simple one man project, I did everything on my own.
  • No major optimization was done. The project was completed in a short time and was meant as a proof of concept.

 

My responsibilities:

  • Design
  • Scripting (C#)
  • Scene Setup

 

Example Video

Song: FrostBurn - "Boss Fight"

Composer: Tobias Ekholm

Basic Function

 

The visualizer is made up of two scripts. One script that gets audio data from a specified audio source and scales a red "pillar" based on the strength of the audio. Another script that moves a grey "hat" based on it's corresponding pillar, pushing the hat up when the pillar reacts to stronger sounds. If the hat isn't continually pushed up by the pillar it will begin to fall.

 

Scripting

 

Pillars

The pillars each have a specified sample range. The script checks the strength of the audio within the range specified by these samples and scales the pillar according to the strongest sample. In order to account for the way the human ear registers sound the sample-strength is scaled logarithmically (just like human hearing). The scaling is based on the Mel Scale but has been slightly modified to give lower frequencies a bit more presence.

Exported from Notepad++
using UnityEngine; using System.Collections; using System.Collections.Generic; /* * This script analyses the strength of an AudioSource within a given range and * scales the transform based of the strongest sample within the specified range. * */ public class Script_Audio_BeatDetector : MonoBehaviour { public int lowerSampleLimit = 0; public int upperSampleLimit = 1024; public int totalSamples = 1024; public float transformScale = 40f; public float scaleLimit = 10f; private Transform m_Transform; private AudioSource m_AudioSource; private int m_TotalSamples; private float m_ClipLength; private float[] m_Samples; private float[] m_SpectrumData; void Start () { m_Transform = transform; m_AudioSource = m_Transform.root.GetComponent < AudioSource > (); //Basic error messages. if ( upperSampleLimit > totalSamples ) { Debug.Log ( "Upper Sample Limit must not be larger that Total Samples!" ); } if ( lowerSampleLimit < 0 ) { Debug.Log ( "Lower Sample Limit must not be smaller than 0 (zero)!" ); } if ( lowerSampleLimit > upperSampleLimit ) { Debug.Log ( "Lower Sample Limit must not be larger than Upper Sample Limit!" ); } m_Samples = new float [ totalSamples ]; } //Write spectrum data to m_Samples. //Scale transform. void Update () { m_AudioSource.GetSpectrumData ( m_Samples, 0, FFTWindow.BlackmanHarris ); ScaleByBinStrength (); } //Scales transform along the y-axis according to the strongest bin's strength (within the specified range). private void ScaleByBinStrength () { int _StrongestBin = GetStrongestBin ( m_Samples, lowerSampleLimit, upperSampleLimit ); float _BinStrength = GetStrongestBinStrength ( m_Samples, lowerSampleLimit, upperSampleLimit ); m_Transform.localScale = new Vector3 ( m_Transform.localScale.x, Mathf.Clamp( ( ( _BinStrength * MelScale ( _BinStrength, _StrongestBin ) ) * transformScale ), 0f, scaleLimit ), m_Transform.localScale.z ); } //Logarithmically scales strength based on the frequency of the strongest sample to immitate human hearing range. //Return scaled strength. private float MelScale ( float _Strength, int _StrongestBin ) { float _Frequency = _StrongestBin * ( 44100 / totalSamples ) + 2000f / ( _StrongestBin + 1 ); _Strength = 1127 * Mathf.Log ( 1 + ( _Frequency / 700f ) ); return _Strength; } //Get strongest bin (the frequency range with the strongest signal) between _Bottom and _Top and return it. private int GetStrongestBin ( float[] _Samples, int _Bottom, int _Top ) { float _Strength = 0f; int _StrongestIndex = 0; for ( int i = _Bottom; i < _Top; ++i ) { if ( _Strength < _Samples[ i ] ) { _Strength = _Samples[ i ]; _StrongestIndex = i; } } return _StrongestIndex; } //Get strength of strongest bin and return it. private float GetStrongestBinStrength ( float[] _Samples, int _Bottom, int _Top ) { float _Strength = 0f; int _StrongestIndex = 0; for ( int i = _Bottom; i < _Top; ++i ) { if ( _Strength < _Samples[ i ] ) { _Strength = _Samples[ i ]; _StrongestIndex = i; } } return _Strength; } }

Hats

Each hat has a specific pillar that it belongs to. Each frame this script checks the corresponding pillar's scale and compares it to the hat's position plus the offset. The offset is used to create a slight space between the hat and the pillar. If the pillar's scale is greater than the hat's position plus offset the hat is moved up to the top of the pillar. If the top of the pillar is further down than the hat the hat falls down towards the bottom of the screen.

Exported from Notepad++
using UnityEngine; using System.Collections; /* * This script moves a game object by having be "pushed up" by a different game object * and slowly fall to the ground if the other game object doesn't push it up again. * The "bouncing" objects will from here on be called "hats". * This script is placed on a gameObject which is a direct parent to all hats. * It also needs a reference to another gameObject which is a direct parent to all "pillars" in the visualizer. * The number of hats and pillars must match. * * This script could be optimized by using arrays for both hats and pillars, instead of getting new references each frame. * As the current application isn't very demanding this simpler apprach has been chosen to save time. * */ public class Script_Visualizer_Hat : MonoBehaviour { public Vector3 dropSpeed = new Vector3 ( 0f, 1f, 0f ); public Vector3 offset = new Vector3 ( 0f, .1f, 0f ); public Vector3 scaleModifier = new Vector3 ( 0f, 1f, 0f ); public GameObject pillarsGO; //Pillars-parent. private Transform m_Transform; private Transform m_ParentTrans; private Transform m_RootTrans; private Vector3 m_StartScale; private Vector3 m_CurrentScale; private Vector3 m_LastPos = Vector3.zero; private Vector3 m_NewPos = Vector3.zero; void Start () { m_Transform = transform; m_ParentTrans = m_Transform.parent; m_RootTrans = m_Transform.root; m_StartScale = m_Transform.localScale; } void Update () { IterateHats (); } //Iterate every hat and move it based on the hat's and it's corresponding pillar's position. private void IterateHats () { for ( int i = 0; i < m_Transform.childCount; ++i ) { MoveHat ( m_Transform.GetChild ( i ).transform, i ); } } //Get the transform of the current hat's corresponding pillar. //Get new height of the pillar. //If new height is greater than hat's current height: push hat up. //Else: move hat down with specified drop speed. private void MoveHat ( Transform _HatTrans, int _Index ) { Transform _PillarTrans = pillarsGO.transform.GetChild ( _Index ).transform; float _Height = _PillarTrans.localScale.y / 2 + offset.y; if ( _Height > _HatTrans.localPosition.y || _HatTrans.localPosition.y < _HatTrans.localScale.y + offset.y ) { _HatTrans.localPosition = new Vector3 ( _PillarTrans.localPosition.x + offset.x + _PillarTrans.localScale.x * scaleModifier.x / 2, _PillarTrans.localPosition.y + offset.y + _PillarTrans.localScale.y * scaleModifier.y / 2, _PillarTrans.localPosition.z + offset.z + _PillarTrans.localScale.z * scaleModifier.z / 2 ); } else { _HatTrans.localPosition = new Vector3 ( _HatTrans.localPosition.x - dropSpeed.x * Time.deltaTime, _HatTrans.localPosition.y - dropSpeed.y * Time.deltaTime, _HatTrans.localPosition.z - dropSpeed.z * Time.deltaTime ); } } }

Stefan Alfredsson - Designer & Unity Developer - E-mail: stefan@jigoku.se - Mobile: +46 70 754 8228