Sitemap

Visualize audio in terminal using python

5 min readJan 11, 2023

--

Ever wondered how do music players display audio visualizations ?

Recently I’ve been playing a lot with audio in python so I decided to create a quick guide on how to visualize audio in terminal using python (I’ve also created a audiovisualizer package that does just that).

Basic idea behind audio visualization is to take audio data and transform it in a way that is suitable for graphical representation.
Audio is represented digitally as an array of numbers where each value is the amplitude of a sound wave at the given moment. Our goal is to take this array and represent it in a terminal while keeping in sync with the audio. We will do this by drawing a bar graph in terminal where each bar is scaled amplitude at the given moment. So lets start with a function that draws a bar graph.

Drawing a bar graph in terminal

def print_frame(frame, height, print_char):
"""
Prints print_char bars of height frame[x] where x is a position of
the bar from left to right.

:param frame: list of heights
:param height: maximum height
:param print_char: char used to represent a bar
"""
for i in range(height, 0, -1):
for j in range(len(frame)):
if i <= frame[j]:
print(print_char, end='')
else:
print(' ', end='')
print()

Here is an example usage of print_frame function:

print_frame function allows us to draw a bar graph, but obviously we don’t want to draw entire audio data array at once, instead, like the first parameter name of this function suggest we will draw a frame at given intervals. This means that we need a way to clear a frame from screen so that we can draw next one (for now lets not worry about what frame exactly is, just that it is a list of integers of reasonable length).

Clearing a frame

def clear_frame(height):
for _ in range(height):
sys.stdout.write("\033[F\033[K")

This function will clear last height lines from terminal, “\033[F” will clear the line and “\033[K” will move the cursor.

Get tracksuitdev’s stories in your inbox

Join Medium for free to get updates from this writer.

Calling print_frame and clear_frame at exact interval allows us to create an effect of animation. Now we just need to get the frames we want to print to the screen. We will do this by transforming audio data to a form more suitable for graphical representation.

Transforming audio data

def graph_frames_from_audio(data, step, width, divisor):
"""
Interpolates values in data list by taking an average of step values.
After processing width * step values will yield a list of width
interpolated values.
Once the data array has been processed yields final list of the remaining
values if there are any.

:param data: array of numbers representing audio data
:param step: number of values to use for interpolation
:param width: width of frame, number of interpolated values
:param divisor: divide each value with this number
:return: generator yielding a current frame
"""
point = 0
current_sum = 0
current_frame = []
for x in data:
current_sum += int((abs(x) // divisor))
point += 1
if point == step:
current_frame.append(math.ceil(current_sum / point))
point = 0
current_sum = 0
if len(current_frame) == width:
yield current_frame
current_frame = []
if current_frame:
yield current_frame
elif current_sum:
yield [current_sum / point]

This generator will yield a graphical frame given audio data and width of the frame, we can then feed the resulting frame to a print_frame function.

Graphical frames are constructed by dividing each value form audio data array by divisor (this allows us to scale from audio to graph data), taking an average of step of scaled values and finally after we produced width of such averaged and scaled data points we yield our frame. If there is some data left over we yield that at the end as well.

Putting it all together

So far we have a function to transform audio data to graphical data, a function to print this data and a function to clear it. We will combine this functions to create a program that is capable of audio visualization. But first we are going to need to do some calculations:

def calc_data_for_visualization(data: List[float], frame_rate: int, max_amp: float, width: int, fps: int, height: int):
"""
Calculates all the data we need to visualize audio stream
"""
length = len(data)
point_interval = frame_rate // (width * fps) # interval used for interpolation
if point_interval == 0:
raise FrameLengthSmallerThanWidth(fps, width)
frame_mod = length % (point_interval * width)
last_frame_length = frame_rate / (length % (point_interval * width)) if frame_mod != 0 else 1
interval = 1.0 / fps
divisor = max_amp / height
return length, point_interval, last_frame_length, interval, divisor

This function returns length of audio data, ratio between audio frame rate and graphical frame rate multiplied by width of graphical frame (essentially how many audio data points constitute one graphical data point), ration between length of full frame and last frame (when transforming audio data to graphical data, last graphical frame can be only partially complete), interval between too graphical frames, ratio between maximal amplitude of audio data and maximal height of graphical data.

Now we have everything we need:

def visualize(data, frame_rate: int, max_amp: float, fps=30, width=30, height=15, print_char="#", debug=False,
sync=True):
"""
Visualizes the data array

:param data: array of numbers representing audio stream
:param frame_rate: audio frame rate, number of values in the data array representing one second of audio
:param max_amp: maximum amplitude of the audio data
:param fps: how many frames to render each second
:param width: width of rendered frames, 1 unit represents one char length in terminal
:param height: height of rendered frames, 1 unit represents one char length in terminal
:param print_char: char used to render frames
:param debug: if true will print number of skipped frames
:param sync: if true will keep the graphical representation in sync with audio, else will just print graphical
frames as they are calculated
:return: number of skipped frames
"""
length, point_interval, last_frame_length, interval, divisor = \
calc_data_for_visualization(data, frame_rate, max_amp, width, fps, height)
skipped_frames = 0
audio_start = time.time()
print_frame([0] * width, height, " ")
for i, f in enumerate(graph_frames_from_audio(data, point_interval, width, divisor)):
clear_frame(height)
print_frame(f, height, print_char)
frame_length = last_frame_length if i == length - 1 else 1 # 1 denotes full frame
end_time = audio_start + ((i + 1 * frame_length) * interval) # time at which we should print next frame
sleep_for = end_time - time.time()
if sync and sleep_for > 0:
time.sleep(sleep_for) # sleep till next frame
else:
skipped_frames += 1
if debug:
print(skipped_frames)
return skipped_frames

visualize function will print graphical frames to terminal while keeping in sync with audio. You can use pydub or similar to open audio file and extract the audio data that we need for visualization. In the example below we extract the first channel (audio usually comes in stereo form with 2 channels) from audio file and feed it to visualize function:

from pydub import AudioSegment

audio = AudioSegment.from_file("audio.mp3")
data = audio.get_array_of_samples()[0::2] # in stereo audio there are two channels, we only want one
visualize(data, audio.frame_rate, audio.max_possible_amplitude)
Press enter or click to view image in full size

Finally we can play the audio while visualizing it in terminal using the code segment below:

from pydub import AudioSegment
from pydub.playback import play

song = AudioSegment.from_mp3("audio.mp3")
data = song.get_array_of_samples()[0::2]
frame_rate = song.frame_rate
max_amp = song.max_possible_amplitude
thread = threading.Thread(target=visualize, args=[data, frame_rate, max_amp])
thread.start()
play(song)
thread.join()

--

--

tracksuitdev
tracksuitdev

Written by tracksuitdev

0 followers

A dev in a tracksuit