2019-06-20 20:20:46 +00:00
|
|
|
"""
|
|
|
|
Sends live audio analysis to the terminal.
|
|
|
|
|
|
|
|
Based on musicinformationretrieval.com/realtime_spectrogram.py
|
|
|
|
|
|
|
|
For more examples using PyAudio:
|
|
|
|
https://github.com/mwickert/scikit-dsp-comm/blob/master/sk_dsp_comm/pyaudio_helper.py
|
|
|
|
"""
|
|
|
|
|
2020-09-27 19:02:19 +00:00
|
|
|
from __future__ import print_function
|
2019-06-20 20:20:46 +00:00
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import librosa
|
2020-09-28 12:40:39 +00:00
|
|
|
import math
|
2019-06-20 20:20:46 +00:00
|
|
|
import numpy
|
|
|
|
import os
|
|
|
|
import pyaudio
|
|
|
|
import redis
|
2020-09-28 12:40:39 +00:00
|
|
|
import statistics
|
2020-09-27 19:02:19 +00:00
|
|
|
import sys
|
2019-06-20 20:20:46 +00:00
|
|
|
import time
|
|
|
|
|
2020-09-27 19:02:19 +00:00
|
|
|
def debug(*args, **kwargs):
|
|
|
|
if( verbose == False ):
|
|
|
|
return
|
|
|
|
print(*args, file=sys.stderr, **kwargs)
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Define default variables.
|
2020-09-28 12:40:39 +00:00
|
|
|
BAND_OCTAVES = 10 # 12 * 9 octaves
|
|
|
|
_BAND_TONES = BAND_OCTAVES * 12 # octaves * notes per octave
|
2019-06-20 20:20:46 +00:00
|
|
|
_CHANNELS = 1
|
|
|
|
_FRAMES_PER_BUFFER = 4410
|
|
|
|
_N_FFT = 4096
|
|
|
|
_RATE = 44100
|
|
|
|
_SAMPLING_FREQUENCY = 0.1
|
2020-09-28 22:56:54 +00:00
|
|
|
_BPM_MIN=10
|
|
|
|
_BPM_MAX=400
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
# Argument parsing
|
|
|
|
parser = argparse.ArgumentParser(prog='realtime_redis')
|
2020-09-29 13:50:00 +00:00
|
|
|
# Standard Args
|
|
|
|
parser.add_argument("-v","--verbose",action="store_true",help="Verbose")
|
|
|
|
# Redis Args
|
|
|
|
parser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str)
|
|
|
|
parser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str)
|
2020-09-28 22:56:54 +00:00
|
|
|
# Audio Capture Args
|
2019-06-20 20:20:46 +00:00
|
|
|
parser.add_argument('--list-devices','-L', action='store_true', help='Which devices are detected by pyaudio')
|
|
|
|
parser.add_argument('--mode','-m', required=False, default='spectrum', choices=['spectrum', 'bpm'], type=str, help='Which mode to use. Default=spectrum')
|
|
|
|
parser.add_argument('--device','-d', required=False, type=int, help='Which pyaudio device to use')
|
|
|
|
parser.add_argument('--sampling-frequency','-s', required=False, default=0.1, type=float, help='Which frequency, in seconds. Default={}f '.format(_SAMPLING_FREQUENCY))
|
|
|
|
parser.add_argument('--channels','-c', required=False, default=_CHANNELS, type=int, help='How many channels. Default={} '.format(_CHANNELS))
|
2020-09-28 22:56:54 +00:00
|
|
|
parser.add_argument('--rate','-r', required=False, default=44100, type=int, help='The audio capture rate in Hz. Default={} '.format(_RATE))
|
2020-09-29 13:50:00 +00:00
|
|
|
parser.add_argument('--frames','-f', required=False, default=4410, type=int, help='How many frames per buffer. Default={}'.format(_FRAMES_PER_BUFFER))
|
2020-09-28 22:56:54 +00:00
|
|
|
# BPM Mode Args
|
|
|
|
parser.add_argument('--bpm-min', required=False, default=_BPM_MIN, type=int, help='BPM mode only. The low BPM threshold. Default={} '.format(_BPM_MIN))
|
|
|
|
parser.add_argument('--bpm-max', required=False, default=_BPM_MAX, type=int, help='BPM mode only. The high BPM threshold. Default={} '.format(_BPM_MAX))
|
|
|
|
|
2019-06-20 20:20:46 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
2020-09-28 12:40:39 +00:00
|
|
|
# global
|
|
|
|
bpm = 120.0
|
2020-09-28 22:56:54 +00:00
|
|
|
start = 0
|
2020-09-28 12:40:39 +00:00
|
|
|
|
2019-06-20 20:20:46 +00:00
|
|
|
# Set real variables
|
2020-09-28 12:40:39 +00:00
|
|
|
F_LO = librosa.note_to_hz('C0')
|
|
|
|
F_HI = librosa.note_to_hz('C10')
|
2020-09-28 22:56:54 +00:00
|
|
|
BAND_TONES = _BAND_TONES
|
|
|
|
N_FFT = _N_FFT
|
|
|
|
CHANNELS = args.channels
|
|
|
|
DEVICE = args.device
|
|
|
|
FRAMES_PER_BUFFER = int(args.rate * args.sampling_frequency )
|
|
|
|
LIST_DEVICES = args.list_devices
|
|
|
|
MODE = args.mode
|
|
|
|
RATE = args.rate
|
|
|
|
SAMPLING_FREQUENCY = args.sampling_frequency
|
|
|
|
bpm_min = args.bpm_min
|
|
|
|
bpm_max = args.bpm_max
|
|
|
|
ip = args.ip
|
|
|
|
port = args.port
|
|
|
|
verbose = args.verbose
|
2020-09-27 19:02:19 +00:00
|
|
|
|
2020-09-28 12:40:39 +00:00
|
|
|
if( MODE == "bpm" and SAMPLING_FREQUENCY < 0.5 ):
|
|
|
|
debug( "You should use a --sampling_frequency superior to 0.5 in BPM mode...")
|
2020-09-27 19:02:19 +00:00
|
|
|
|
2019-06-20 20:20:46 +00:00
|
|
|
|
2020-09-28 12:40:39 +00:00
|
|
|
melFilter = librosa.filters.mel(RATE, N_FFT, BAND_TONES, fmin=F_LO, fmax=F_HI)
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
r = redis.Redis(
|
2020-09-27 19:02:19 +00:00
|
|
|
host=ip,
|
|
|
|
port=port)
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
# Early exit to list devices
|
2020-09-27 19:02:19 +00:00
|
|
|
# As it may crash later if not properly configured
|
|
|
|
#
|
|
|
|
def list_devices():
|
|
|
|
# List all audio input devices
|
|
|
|
p = pyaudio.PyAudio()
|
|
|
|
i = 0
|
|
|
|
n = p.get_device_count()
|
2020-09-28 12:40:39 +00:00
|
|
|
print("\nFound {} devices\n".format(n))
|
|
|
|
print(" {} {}".format('ID', 'Device name'))
|
2020-09-27 19:02:19 +00:00
|
|
|
while i < n:
|
|
|
|
dev = p.get_device_info_by_index(i)
|
|
|
|
if dev['maxInputChannels'] > 0:
|
2020-09-28 12:40:39 +00:00
|
|
|
print(" {} {}".format(i, dev['name']))
|
2020-09-27 19:02:19 +00:00
|
|
|
i += 1
|
2019-06-20 20:20:46 +00:00
|
|
|
if( LIST_DEVICES ):
|
|
|
|
list_devices()
|
|
|
|
os._exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def m_bpm(audio_data):
|
|
|
|
"""
|
|
|
|
This function saves slow analysis to redis
|
2020-09-27 19:02:19 +00:00
|
|
|
* bpm
|
2019-06-20 20:20:46 +00:00
|
|
|
* beat
|
|
|
|
"""
|
|
|
|
global bpm
|
2020-09-28 22:56:54 +00:00
|
|
|
global start
|
2019-06-20 20:20:46 +00:00
|
|
|
|
2020-09-28 22:56:54 +00:00
|
|
|
|
|
|
|
# Detect tempo / bpm
|
2020-09-27 19:02:19 +00:00
|
|
|
new_bpm, beats = librosa.beat.beat_track(
|
|
|
|
y = audio_data,
|
|
|
|
sr = RATE,
|
|
|
|
trim = False,
|
2020-09-28 22:56:54 +00:00
|
|
|
#start_bpm = bpm,
|
2020-09-27 19:02:19 +00:00
|
|
|
units = "time"
|
|
|
|
)
|
2020-09-28 22:56:54 +00:00
|
|
|
'''
|
|
|
|
new_bpm = librosa.beat.tempo(y = audio_data, sr=RATE)[0]
|
|
|
|
|
|
|
|
'''
|
|
|
|
# Correct the eventual octave error
|
|
|
|
if new_bpm < bpm_min or new_bpm > bpm_max:
|
2020-09-29 13:50:00 +00:00
|
|
|
found = False
|
2020-09-28 22:56:54 +00:00
|
|
|
octaveErrorList = [ 0.5, 2, 0.3333, 3 ]
|
|
|
|
for key,factor in enumerate(octaveErrorList):
|
|
|
|
correction = new_bpm * factor
|
|
|
|
if correction > bpm_min and correction < bpm_max:
|
2020-09-29 13:50:00 +00:00
|
|
|
debug( "Corrected high/low bpm:{} to:{}".format(new_bpm, correction))
|
2020-09-28 22:56:54 +00:00
|
|
|
new_bpm = correction
|
2020-09-29 13:50:00 +00:00
|
|
|
found = True
|
2020-09-28 22:56:54 +00:00
|
|
|
break
|
2020-09-29 13:50:00 +00:00
|
|
|
if found == False:
|
|
|
|
if new_bpm < bpm_min :
|
|
|
|
new_bpm = bpm_min
|
|
|
|
else :
|
|
|
|
new_bpm = bpm_max
|
2020-09-28 22:56:54 +00:00
|
|
|
|
2020-09-29 13:50:00 +00:00
|
|
|
debug("new_bpm:{}".format(new_bpm))
|
2020-09-28 22:56:54 +00:00
|
|
|
'''
|
2020-09-29 13:50:00 +00:00
|
|
|
How to guess the next beats based on the data sent to redis
|
|
|
|
~~ A Dirty Graph ~~
|
2020-09-28 22:56:54 +00:00
|
|
|
|
|
|
|
|start end|
|
|
|
|
Capture |........................|
|
|
|
|
BPM detect+Redis set ||
|
|
|
|
Client Redis get |
|
|
|
|
|
|
|
|
Time |........................||.............|
|
|
|
|
---SAMPLING_FREQUENCY----
|
|
|
|
- < TIME-START
|
|
|
|
Read Delay --------------- < 2*SAMPLING_FREQUENCY - PTTL
|
|
|
|
Delay -----------------------------------------
|
|
|
|
Beats |last beat
|
|
|
|
. known ...b....b....b....b....b.
|
|
|
|
. passed (...b....b....b.)
|
|
|
|
. guessed (..b....b....b....b...
|
|
|
|
Next Beat Calculation b....b....b....b.|..b
|
2020-09-29 13:50:00 +00:00
|
|
|
Beats |last beat
|
|
|
|
0 1 2 3 4
|
2020-09-28 22:56:54 +00:00
|
|
|
|
|
|
|
Redis:
|
2020-09-29 13:50:00 +00:00
|
|
|
|
|
|
|
key bpm_sample_interval
|
|
|
|
visual |........................|
|
|
|
|
|
|
|
|
key bpm_delay
|
|
|
|
visual |.........................|
|
2020-09-28 22:56:54 +00:00
|
|
|
|
|
|
|
'''
|
|
|
|
bpm = new_bpm
|
2020-09-29 13:50:00 +00:00
|
|
|
bpm_sample_interval = SAMPLING_FREQUENCY * 1000
|
|
|
|
bpm_delay = (SAMPLING_FREQUENCY + time.time() - start ) * 1000
|
|
|
|
pexpireat = int( 2 * bpm_sample_interval);
|
2020-09-27 19:02:19 +00:00
|
|
|
# Save to Redis
|
2020-09-29 13:50:00 +00:00
|
|
|
r.set( 'bpm', round(bpm,2), px = pexpireat )
|
|
|
|
r.set( 'bpm_sample_interval', bpm_sample_interval )
|
2020-09-28 22:56:54 +00:00
|
|
|
r.set( 'bpm_delay', bpm_delay )
|
2019-06-20 20:20:46 +00:00
|
|
|
r.set( 'beats', json.dumps( beats.tolist() ) )
|
2020-09-29 13:50:00 +00:00
|
|
|
#debug( "pexpireat:{}".format(pexpireat))
|
|
|
|
debug( "bpm:{} bpm_delay:{} bpm_sample_interval:{} beats:{}".format(bpm,bpm_delay,bpm_sample_interval,beats) )
|
2019-06-20 20:20:46 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
def m_spectrum(audio_data):
|
|
|
|
"""
|
|
|
|
This function saves fast analysis to redis
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Compute real FFT.
|
2020-09-27 19:02:19 +00:00
|
|
|
fft = numpy.fft.rfft(audio_data, n=N_FFT)
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
# Compute mel spectrum.
|
2020-09-27 19:02:19 +00:00
|
|
|
melspectrum = melFilter.dot(abs(fft))
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Initialize output characters to display.
|
2020-09-28 12:40:39 +00:00
|
|
|
spectrum_120 = [0]*BAND_TONES
|
|
|
|
spectrum_10 = [0]*BAND_OCTAVES
|
|
|
|
spectrum_oct = [[] for i in range(10)]
|
|
|
|
|
|
|
|
# Assign values
|
|
|
|
for i in range(BAND_TONES):
|
|
|
|
val = round(melspectrum[i],2)
|
|
|
|
spectrum_120[i] = val
|
|
|
|
key = int(math.floor( i / 12 ))
|
|
|
|
spectrum_oct[key].append(val)
|
|
|
|
|
|
|
|
for i in range(BAND_OCTAVES):
|
|
|
|
spectrum_10[i] = round(sum( spectrum_oct[i] ) / len( spectrum_oct[i]),2)
|
|
|
|
|
|
|
|
# Get RMS
|
|
|
|
#rms = librosa.feature.rms( S=melspectrum )
|
|
|
|
rms = librosa.feature.rms( y=audio_data ).tolist()[0]
|
|
|
|
rms_avg = round(sum(rms) / len(rms),2)
|
2019-06-20 20:20:46 +00:00
|
|
|
|
|
|
|
# Save to redis
|
2020-09-28 12:40:39 +00:00
|
|
|
#debug( 'spectrum_120:{} '.format(spectrum_120))
|
|
|
|
#debug( 'spectrum_10:{}'.format(spectrum_10))
|
|
|
|
#debug( 'rms:{}'.format(rms_avg))
|
|
|
|
r.set( 'spectrum_120', json.dumps( spectrum_120 ) )
|
|
|
|
r.set( 'spectrum_10', json.dumps( spectrum_10 ) )
|
|
|
|
r.set( 'rms', "{}".format(rms_avg) )
|
2019-06-20 20:20:46 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def callback(in_data, frame_count, time_info, status):
|
|
|
|
audio_data = numpy.fromstring(in_data, dtype=numpy.float32)
|
|
|
|
|
2020-09-28 22:56:54 +00:00
|
|
|
global start
|
2019-06-20 20:20:46 +00:00
|
|
|
start = time.time()
|
|
|
|
if MODE == 'spectrum':
|
|
|
|
m_spectrum(audio_data)
|
|
|
|
elif MODE == 'bpm':
|
|
|
|
m_bpm( audio_data)
|
|
|
|
else:
|
2020-09-27 19:02:19 +00:00
|
|
|
debug( "Unknown mode. Exiting")
|
2019-06-20 20:20:46 +00:00
|
|
|
os._exit(2)
|
|
|
|
end = time.time()
|
2020-09-28 12:40:39 +00:00
|
|
|
debug ("\rLoop took {:.2}s on {}s ".format(end - start, SAMPLING_FREQUENCY))
|
2019-06-20 20:20:46 +00:00
|
|
|
return (in_data, pyaudio.paContinue)
|
|
|
|
|
|
|
|
|
2020-09-27 19:02:19 +00:00
|
|
|
debug( "\n\nRunning! Using mode {}.\n\n".format(MODE))
|
|
|
|
if MODE == 'spectrum':
|
|
|
|
debug("In this mode, we will set keys: rms, spectrum, tuning")
|
|
|
|
elif MODE == 'bpm':
|
|
|
|
debug("In this mode, we will set keys: onset, bpm, beats")
|
2019-06-20 20:20:46 +00:00
|
|
|
|
2020-09-29 13:50:00 +00:00
|
|
|
p = pyaudio.PyAudio()
|
2019-06-20 20:20:46 +00:00
|
|
|
stream = p.open(format=pyaudio.paFloat32,
|
|
|
|
channels=CHANNELS,
|
|
|
|
rate=RATE,
|
|
|
|
input=True, # Do record input.
|
|
|
|
output=False, # Do not play back output.
|
|
|
|
frames_per_buffer=FRAMES_PER_BUFFER,
|
|
|
|
input_device_index = DEVICE,
|
|
|
|
stream_callback=callback)
|
|
|
|
|
|
|
|
stream.start_stream()
|
|
|
|
|
|
|
|
while stream.is_active():
|
|
|
|
time.sleep(SAMPLING_FREQUENCY)
|
|
|
|
|
|
|
|
stream.stop_stream()
|
|
|
|
stream.close()
|
|
|
|
|
|
|
|
p.terminate()
|