使用 python 和 librosa 的音乐可视化工具

Music visualizer using python and librosa

我正在尝试编写一个小脚本来生成 python 中音频文件的一些可视化效果。 我的最终目标是生成一个 30fps 视频,该视频由 python 中生成的整理图像组成,这些图像组装了一些图像资产。但我对声音分析部分有点卡,因为我对声音及其背后的物理和数学几乎一无所知。 无论如何。为了在 python 中处理导入声音,我使用了 librosa,它看起来非常强大。

import librosa
import numpy as np

#load an example audio
filename = librosa.ex('trumpet')
y, sr = librosa.load(filename, sr=44100)
#apply short-time Fourier transform
stft = np.abs(librosa.stft(y, n_fft=1024, hop_length=None, win_length=None, window='hann', center=True, dtype=None, pad_mode='reflect'))
# converting the matrix to decibel matrix
D = librosa.amplitude_to_db(stft, ref=np.max)

通过这种方式,我获得了形状为 (513,480) 的矩阵 D,这意味着频率范围内有 513 个“步长”和 480 个数据点或帧。鉴于样本的持续时间约为 5.3 秒,这使其约为每秒 172.3 帧。 因为我想要 30fps,所以我决定对采样率进行一些试验和错误,直到达到 23038。加载文件时应用它:

y, sr = librosa.load(filename, sr=23038)

我获得了 89.998 fps 的帧速率,这似乎更适合我的目的。 所以我继续降低了数据的分辨率,平均了我在 9 个“桶”中的频率读数:

#Initialize new empty array of target size
nD = np.empty(shape=(9,D.shape[1]))
#populate new array
for i in range(D.shape[1]):
    nD[:,i] = D[:,i].reshape(-1, 57).mean(axis=1)

这里的 9 是我可以除以 57 得到的一个数,57 是一个因数 513。 因此,我将 3 帧聚合在一起以达到 30fps:

count = 0
for i in range(0, nD.shape[1],3):
    try:
        nnD[:,count] = nD[:,i:i+3].reshape(-1, 3).mean(axis=1)
        count+=1
    except Exception:
        pass

这看起来完全是 hacky,try except 部分在那里,因为有时我有索引错误,并且计算递增计数器似乎很愚蠢。 但令人难以置信的是,它似乎以某种方式起作用。 出于测试目的,我制作了一个可视化例程

from time import sleep
from os import system
nnD = ((nnD+80)/10).astype(int)
for i in range(nnD.shape[1]):
    for j in range(nnD.shape[0]):
        print ("#"*nnD[j,i])
    sleep(1/30)
    system('clear')

在终端中显示由 # 组成的行。

现在,我的问题是:

我怎样才能使它成为正确的方法?

更具体地说:

1_is有没有办法在不破坏采样率的情况下匹配傅立叶数据的帧率?

2_is 有更合适的方法来聚合我的数据,可能是任意数字,而不是必须在 513 之间进行选择?

这个技巧做得很好。正如我所怀疑的,您需要调整 hop_length.

import time

import librosa
import numpy as np
import scipy.signal

# Tunable parameters
hop_length_secs = 1 / 30
bands = 10  # How many frequency bands?
characters = " ..::##@"  # Characters to print things with

filename = librosa.ex("trumpet")
y, sr = librosa.load(filename, sr=22050)
sound_length = y.shape[0] / sr
print(f"{sound_length = }")
hop_length_samples = int(hop_length_secs * sr)
print(f"{hop_length_secs = }")
print(f"{hop_length_samples = }")

stft = np.abs(librosa.stft(y, n_fft=1024, hop_length=hop_length_samples))
num_bins, num_samples = stft.shape

# This should be approximately `sound_length` now
print(f"{num_samples * hop_length_secs = }")

# Resample to the desired number of frequency bins
stft2 = np.abs(scipy.signal.resample(stft, bands, axis=0))
stft2 = stft2 / np.max(stft2)  # Normalize to 0..1


# Remap the 0..1 signal to integer indices
# -- the square root boosts the otherwise lower signals for better visibility.
char_indices = (np.sqrt(stft2) * (len(characters) - 1)).astype(np.uint8)

# Print out the signal "in time".
for y in range(num_samples):
    print("".join(characters[i] for i in char_indices[:, y]))
    time.sleep(hop_length_secs)

输出为

sound_length = 5.333378684807256
hop_length_secs = 0.03333333333333333
hop_length_samples = 735
num_samples * hop_length_secs = 5.366666666666666

.:..
.##:.. .
.#:: . .
.#:. .
.#:.
.#:. . .
....
.@#:.. ...
.##:.. .
.#:. .
.#:.
.::.
.#:.
.::.
.::.
.::.
....
.::.
.##:.. .
.##:.. .
.#:. .
.#:.
.::.
.:..
.:..
.:..
.:..
.##:..
.##:.. .
.##:.. .
.##:..
.:..

(etc...)

如果你眯着眼睛,它看起来确实像一个可视化...

如果您希望以秒为单位的时间分辨率约为 N FPS,您可以执行类似以下代码的操作。但请注意,由于跃点需要是整数,这会导致漂移,请参阅打印输出。因此有必要定期重置同步,例如每 1 分钟一次。 或者也许每首歌一次就可以逃脱。

import math

def next_power_of_2(x):
    return 2**(math.ceil(math.log(x, 2)))

def params_for_fps(fps=30, sr=16000):
    frame_seconds=1.0/fps
    frame_hop = round(frame_seconds*sr) # in samples
    frame_fft = next_power_of_2(2*frame_hop)
    rel_error = (frame_hop-(frame_seconds*sr))/frame_hop
    
    return frame_hop, frame_fft, rel_error


seconds = 10*60
fps = 15
sr = 16000
frame_hop, frame_fft, frame_err = params_for_fps(fps=fps, sr=sr)
print(f"Frame timestep error {frame_err*100:.2f} %")
drift = frame_err * seconds
print(f"Drift over {seconds} seconds: {drift:.2f} seconds. {drift*fps:.2f} frames")

# Then variables can be used with
# librosa.stft(...hop_length=frame_hop, n_fft=frame_fft)

如果这种方法不够好,需要根据(视频)帧计数器对音频特征进行插值。线性插值会很好。这允许补偿相关的漂移。 这可以为每一帧动态完成,或者可以重新采样音频时间序列以与 FPS 帧对齐。