Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audio player mode #48

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ DEVICE = 'pi'
USE_GUI = False
DISPLAY_FPS = False
N_PIXELS = 144
MIC_RATE = 48000
AUDIO_RATE = 48000
FPS = 50
```

Expand Down Expand Up @@ -274,7 +274,7 @@ The connections are:
7. In [config.py](python/config.py):
- Set `N_PIXELS` to the number of LEDs in your LED strip (must match `NUM_LEDS` in [ws2812_controller.ino](arduino/ws2812_controller/ws2812_controller.ino))
- Set `UDP_IP` to the IP address of your ESP8266 (must match `ip` in [ws2812_controller.ino](arduino/ws2812_controller/ws2812_controller.ino))
- If needed, set `MIC_RATE` to your microphone sampling rate in Hz. Most of the time you will not need to change this.
- If needed, set `AUDIO_RATE` to your microphone sampling rate in Hz. Most of the time you will not need to change this.

# Installation for Raspberry Pi
If you encounter any problems running the visualization on a Raspberry Pi, please [open a new issue](https://github.com/scottlawsonbc/audio-reactive-led-strip/issues). Also, please consider opening an issue if you have any questions or suggestions for improving the installation process.
Expand Down
85 changes: 85 additions & 0 deletions python/audio_player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import time
import numpy as np
import pyaudio
import config
import wave
import queue
import os

wave_file = None
loopback_audio_stream = queue.Queue()
audio_file_paths = []
audio_file_index = 0


def scan_audio_files():
for entry in os.scandir(config.AUDIO_FILE_SCAN_PATH):
if entry.is_file() and entry.path.endswith('.wav'):
audio_file_paths.append(entry.path)

# If only one file, add it twice so the list looping will still work
if len(audio_file_paths) == 1:
audio_file_paths.append(audio_file_paths[0])


def need_more_data_callback(in_data, frame_count, time_info, status):
global wave_file
global audio_file_index
ret_status = pyaudio.paComplete

data = None
if wave_file:
data = wave_file.readframes(frame_count)
if len(data) < frame_count:
wave_file = None
# Loop to next audio file
audio_file_index = (audio_file_index + 1) % len(audio_file_paths)
else:
# Pass the data chunks back to visualization via queue
loopback_audio_stream.put(data)
# More data to come
ret_status = pyaudio.paContinue
return data, ret_status


def start_stream(callback):
global wave_file

scan_audio_files()
p = pyaudio.PyAudio()

while True:
playing_audio_file_index = audio_file_index
audio_file_path = audio_file_paths[audio_file_index]
wave_file = wave.open(audio_file_path, 'rb')
assert (wave_file.getframerate() == config.AUDIO_RATE)

# Creates a Stream to which the wav file is written to.
# Setting output to "True" makes the sound be "played" rather than recorded
stream = p.open(format=p.get_format_from_width(wave_file.getsampwidth()),
channels=wave_file.getnchannels(),
rate=wave_file.getframerate(),
output=True,
stream_callback=need_more_data_callback)

# Visualize the latest played samples until file changes
while playing_audio_file_index == audio_file_index:
# Clear the queue to wait for newest samples
while not loopback_audio_stream.empty():
loopback_audio_stream.get(block=False)
try:
data = loopback_audio_stream.get(timeout=1)
except queue.Empty:
break
y = np.fromstring(data, dtype=np.int16)
y = y.astype(np.float32)
# Small delay before visualization to sync it better
time.sleep(0.01)
callback(y)

# Close the stream
stream.stop_stream()
stream.close()

p.terminate()

26 changes: 22 additions & 4 deletions python/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
USE_GUI = False
"""Whether or not to display a PyQtGraph GUI plot of visualization"""

AUDIO_PLAYER_MODE = False
"""False = Listen to microphone | True = Play music from file(s)"""

DISPLAY_FPS = True
"""Whether to display the FPS when running (can reduce performance)"""

Expand All @@ -55,11 +58,26 @@
GAMMA_TABLE_PATH = os.path.join(os.path.dirname(__file__), 'gamma_table.npy')
"""Location of the gamma correction table"""

MIC_RATE = 48000
"""Sampling frequency of the microphone in Hz"""
AUDIO_RATE = 48000
"""Sampling frequency of the microphone or audio file in Hz"""

AUDIO_FILE_SCAN_PATH = '/home/pi'
"""Path to scan audio files from (for player mode only)"""

if AUDIO_PLAYER_MODE:
AUDIO_FRAME_SIZE = 1024
"""How many audio samples are processed as a frame, defined by pyaudio/portaudio stream callback"""

FPS = int(AUDIO_RATE / AUDIO_FRAME_SIZE)
"""Target FPS is automatically calculated"""
else:
FPS = 50
"""Target FPS, read more info below"""

AUDIO_FRAME_SIZE = int(AUDIO_RATE / FPS)
"""Audio frame size is automatically calculated"""

FPS = 50
"""Desired refresh rate of the visualization (frames per second)
"""FPS = Desired refresh rate of the visualization (frames per second)

FPS indicates the desired refresh rate, or frames-per-second, of the audio
visualization. The actual refresh rate may be lower if the computer cannot keep
Expand Down
8 changes: 4 additions & 4 deletions python/dsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,25 @@ def update(self, value):
def rfft(data, window=None):
window = 1.0 if window is None else window(len(data))
ys = np.abs(np.fft.rfft(data * window))
xs = np.fft.rfftfreq(len(data), 1.0 / config.MIC_RATE)
xs = np.fft.rfftfreq(len(data), 1.0 / config.AUDIO_RATE)
return xs, ys


def fft(data, window=None):
window = 1.0 if window is None else window(len(data))
ys = np.fft.fft(data * window)
xs = np.fft.fftfreq(len(data), 1.0 / config.MIC_RATE)
xs = np.fft.fftfreq(len(data), 1.0 / config.AUDIO_RATE)
return xs, ys


def create_mel_bank():
global samples, mel_y, mel_x
samples = int(config.MIC_RATE * config.N_ROLLING_HISTORY / (2.0 * config.FPS))
samples = int(config.AUDIO_FRAME_SIZE * config.N_ROLLING_HISTORY / 2.0)
mel_y, (_, mel_x) = melbank.compute_melmat(num_mel_bands=config.N_FFT_BINS,
freq_min=config.MIN_FREQUENCY,
freq_max=config.MAX_FREQUENCY,
num_fft_bands=samples,
sample_rate=config.MIC_RATE)
sample_rate=config.AUDIO_RATE)
samples = None
mel_y = None
mel_x = None
Expand Down
4 changes: 2 additions & 2 deletions python/microphone.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

def start_stream(callback):
p = pyaudio.PyAudio()
frames_per_buffer = int(config.MIC_RATE / config.FPS)
frames_per_buffer = config.AUDIO_FRAME_SIZE
stream = p.open(format=pyaudio.paInt16,
channels=1,
rate=config.MIC_RATE,
rate=config.AUDIO_RATE,
input=True,
frames_per_buffer=frames_per_buffer)
overflows = 0
Expand Down
25 changes: 13 additions & 12 deletions python/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from scipy.ndimage.filters import gaussian_filter1d
import config
import microphone
import audio_player
import dsp
import led
import sys
Expand Down Expand Up @@ -187,11 +188,11 @@ def visualize_spectrum(y):
alpha_decay=0.5, alpha_rise=0.99)
volume = dsp.ExpFilter(config.MIN_VOLUME_THRESHOLD,
alpha_decay=0.02, alpha_rise=0.02)
fft_window = np.hamming(int(config.MIC_RATE / config.FPS) * config.N_ROLLING_HISTORY)
fft_window = np.hamming(config.AUDIO_FRAME_SIZE * config.N_ROLLING_HISTORY)
prev_fps_update = time.time()


def microphone_update(audio_samples):
def audio_update(audio_samples):
global y_roll, prev_rms, prev_exp, prev_fps_update
# Normalize samples between 0 and 1
y = audio_samples / 2.0**15
Expand Down Expand Up @@ -245,11 +246,8 @@ def microphone_update(audio_samples):
print('FPS {:.0f} / {:.0f}'.format(fps, config.FPS))


# Number of audio samples to read every time frame
samples_per_frame = int(config.MIC_RATE / config.FPS)

# Array containing the rolling audio sample window
y_roll = np.random.rand(config.N_ROLLING_HISTORY, samples_per_frame) / 1e16
y_roll = np.random.rand(config.N_ROLLING_HISTORY, config.AUDIO_FRAME_SIZE) / 1e16

if sys.argv[1] == "spectrum":
visualization_type = visualize_spectrum
Expand Down Expand Up @@ -310,16 +308,16 @@ def microphone_update(audio_samples):
freq_label = pg.LabelItem('')
# Frequency slider
def freq_slider_change(tick):
minf = freq_slider.tickValue(0)**2.0 * (config.MIC_RATE / 2.0)
maxf = freq_slider.tickValue(1)**2.0 * (config.MIC_RATE / 2.0)
minf = freq_slider.tickValue(0)**2.0 * (config.AUDIO_RATE / 2.0)
maxf = freq_slider.tickValue(1)**2.0 * (config.AUDIO_RATE / 2.0)
t = 'Frequency range: {:.0f} - {:.0f} Hz'.format(minf, maxf)
freq_label.setText(t)
config.MIN_FREQUENCY = minf
config.MAX_FREQUENCY = maxf
dsp.create_mel_bank()
freq_slider = pg.TickSliderItem(orientation='bottom', allowAdd=False)
freq_slider.addTick((config.MIN_FREQUENCY / (config.MIC_RATE / 2.0))**0.5)
freq_slider.addTick((config.MAX_FREQUENCY / (config.MIC_RATE / 2.0))**0.5)
freq_slider.addTick((config.MIN_FREQUENCY / (config.AUDIO_RATE / 2.0))**0.5)
freq_slider.addTick((config.MAX_FREQUENCY / (config.AUDIO_RATE / 2.0))**0.5)
freq_slider.tickMoveFinished = freq_slider_change
freq_label.setText('Frequency range: {} - {} Hz'.format(
config.MIN_FREQUENCY,
Expand Down Expand Up @@ -364,5 +362,8 @@ def spectrum_click(x):
layout.addItem(spectrum_label)
# Initialize LEDs
led.update()
# Start listening to live audio stream
microphone.start_stream(microphone_update)
# Start the audio stream
if config.AUDIO_PLAYER_MODE:
audio_player.start_stream(audio_update)
else:
microphone.start_stream(audio_update)