本帖最后由 bob666zxj 于 2024-7-23 13:42 编辑
[重发]
由于本人账号原因,重新注册了一个账号,为了防止误会,本贴选用转载的方式
请不要在本帖下回复,评分,谢谢。
之前文章的链接 已失效
MusicVisualizer 是一个基于 Python 的实时音乐可视化项目,旨在将音频数据转化为动态的视觉效果。通过结合 analyzer 库进行音乐分析和 pygame 库进行图形绘制,MusicVisualizer 能够实时显示音乐的声波和鼓点,使用户在听觉之外还能享受视觉上的盛宴。
项目原理
音乐分析:
- 使用 analyzer 库对输入的音频文件进行分析。
- 提取音频的关键特征,如声波形状和鼓点位置。
- 实时处理音频数据,确保可视化效果与音乐同步。
图形绘制:
- 利用 pygame 库创建一个图形窗口。
- 在窗口中动态绘制声波形状,根据音频频谱的变化显示不同的视觉效果。
- 通过鼓点检测,将鼓点转化为视觉上的高亮或其他特殊效果,增强音乐的节奏感。
项目特点
- 实时处理:MusicVisualizer 能够实时分析音频并动态显示声波和鼓点,保证视觉效果与音频同步。
- 直观界面:使用 pygame 提供简洁直观的图形界面,用户可以轻松观看音乐的可视化效果。
- 高扩展性:项目结构清晰,代码模块化设计,便于扩展和定制新的可视化效果。
解压密码 :52pojie
main.py
[Python] 纯文本查看 复制代码
# 导入所需的模块和类 from AudioAnalyzer import * # 导入AudioAnalyzer类,用于音频分析 import random # 导入random模块,用于生成随机数 import colorsys # 导入colorsys模块,用于颜色系统转换 # 定义一个函数,生成随机颜色 def rnd_color(): # 使用random模块生成介于0到1之间的随机值作为色相(h)、饱和度(s)和亮度(l) h, s, l = random.random(), 0.5 + random.random() / 2.0, 0.4 + random.random() / 5.0 # 将色相、亮度和饱和度从HLS色彩空间转换为RGB色彩空间,并将结果量化为整数,范围在0-255之间 return [int(256 * i) for i in colorsys.hls_to_rgb(h, l, s)] # 创建一个AudioAnalyzer对象 analyzer = AudioAnalyzer() filename="1.wav" # 加载指定的音频文件进行分析 analyzer.load(filename) # 使用load方法加载音频文件 # 初始化 pygame pygame.init() # 获取显示信息对象 infoObject = pygame.display.Info() # 设置屏幕宽度和高度 screen_w = int(infoObject.current_w/2) screen_h = int(infoObject.current_w/2) # 设置显示模式 screen = pygame.display.set_mode([screen_w, screen_h]) # 获取当前时间戳 t = pygame.time.get_ticks() getTicksLastFrame = t # 初始化时间计数器 timeCount = 0 # 初始化低音平均值和触发值 avg_bass = 0 bass_trigger = -30 bass_trigger_started = 0 # 设置最小和最大分贝值 min_decibel = -80 max_decibel = 80 # 设置圆的颜色 circle_color = (40, 40, 40) # 设置多边形的默认颜色 polygon_default_color = [255, 255, 255] polygon_bass_color = polygon_default_color.copy() polygon_color_vel = [0, 0, 0] # 初始化多边形列表和颜色 poly = [] poly_color = polygon_default_color.copy() # 设置圆的中心位置 circleX = int(screen_w / 2) circleY = int(screen_h/2) # 设置圆的最小和最大半径及初始半径和速度 min_radius = 100 max_radius = 150 radius = min_radius radius_vel = 0 # 定义不同频率组的范围和计数 bass = {"start": 50, "stop": 100, "count": 12} heavy_area = {"start": 120, "stop": 250, "count": 40} low_mids = {"start": 251, "stop": 2000, "count": 50} high_mids = {"start": 2001, "stop": 4000, "count": 20} # 频率组列表 freq_groups = [bass, heavy_area, low_mids, high_mids] # 初始化条形图列表 bars = [] # 临时条形图列表 tmp_bars = [] # 初始化长度变量 length = 0 for group in freq_groups: # 遍历频率组 g = [] # 初始化组列表 s = group["stop"] - group["start"] # 计算组的时间间隔 count = group["count"] # 获取组的计数 reminder = s % count # 计算时间间隔的余数 step = int(s / count) # 计算每步的时间间隔 rng = group["start"] # 获取组的起始时间 # 遍历 count 次 for i in range(count): # 初始化 arr 为 None arr = None # 如果 reminder 大于 0 if reminder > 0: # 减少 reminder reminder -= 1 # 生成一个数组,范围从 rng 到 rng + step + 2 arr = np.arange(start=rng, stop=rng + step + 2) # 更新 rng rng += step + 3 else: # 生成一个数组,范围从 rng 到 rng + step + 1 arr = np.arange(start=rng, stop=rng + step + 1) # 更新 rng rng += step + 2 # 将生成的数组添加到 g 中 g.append(arr) # 增加 length length += 1 # 将 g 添加到 tmp_bars 中 tmp_bars.append(g) # 计算每个条形图的角度间隔 angle_dt = 360/length # 初始化角度 ang = 0 # 遍历临时条形图列表 for g in tmp_bars: gr = [] # 遍历每个条形图中的元素 for c in g: # 添加一个旋转的音频条到图形列表中 gr.append( RotatedAverageAudioBar(circleX+radius*math.cos(math.radians(ang - 90)), circleY+radius*math.sin(math.radians(ang - 90)), c, (255, 0, 255), angle=ang, width=8, max_height=370)) # 更新角度 ang += angle_dt # 将生成的条形图列表添加到bars中 bars.append(gr) # 加载音乐文件并播放 pygame.mixer.music.load(filename) pygame.mixer.music.play(0) running = True while running: avg_bass = 0 poly = [] # 获取当前时间戳 t = pygame.time.get_ticks() deltaTime = (t - getTicksLastFrame) / 1000.0 getTicksLastFrame = t timeCount += deltaTime # 用指定的颜色填充屏幕 screen.fill(circle_color) # 遍历所有事件 for event in pygame.event.get(): # 如果事件类型是退出,则设置运行标志为False if event.type == pygame.QUIT: running = False # 遍历所有的 bars for b1 in bars: # 遍历 b1 中的每一个元素 for b in b1: # 更新 b 的所有属性,包括时间增量、音乐播放位置和分析器 b.update_all(deltaTime, pygame.mixer.music.get_pos() / 1000.0, analyzer) for b in bars[0]: avg_bass += b.avg avg_bass /= len(bars[0]) # 如果平均低音值大于触发阈值 if avg_bass > bass_trigger: # 如果低音触发尚未开始 if bass_trigger_started == 0: bass_trigger_started = pygame.time.get_ticks() # 如果低音触发持续时间超过2秒 if (pygame.time.get_ticks() - bass_trigger_started)/1000.0 > 2: polygon_bass_color = rnd_color() bass_trigger_started = 0 # 如果低音颜色尚未设置 if polygon_bass_color is None: polygon_bass_color = rnd_color() # 计算新的半径 newr = min_radius + int(avg_bass * ((max_radius - min_radius) / (max_decibel - min_decibel)) + (max_radius - min_radius)) radius_vel = (newr - radius) / 0.15 # 计算颜色变化速度 polygon_color_vel = [(polygon_bass_color[x] - poly_color[x])/0.15 for x in range(len(poly_color))] # 如果半径大于最小半径 elif radius > min_radius: bass_trigger_started = 0 # 初始化低音触发标志 polygon_bass_color = None # 初始化多边形低音颜色 radius_vel = (min_radius - radius) / 0.15 # 计算半径变化速度 polygon_color_vel = [(polygon_default_color[x] - poly_color[x])/0.15 for x in range(len(poly_color))] # 计算多边形颜色变化速度 # 如果半径等于最小半径 else: bass_trigger_started = 0 # 低音触发开始标志 poly_color = polygon_default_color.copy() # 复制默认的多边形颜色 polygon_bass_color = None # 多边形的低音颜色 polygon_color_vel = [0, 0, 0] # 多边形颜色的速度 radius_vel = 0 # 半径速度 radius = min_radius # 最小半径 radius += radius_vel * deltaTime # 更新多边形颜色的循环 for x in range(len(polygon_color_vel)): # 计算多边形颜色的新值,考虑颜色变化速度和时间增量 value = polygon_color_vel[x]*deltaTime + poly_color[x] # 更新多边形的颜色 poly_color[x] = value # 更新条形图位置和状态的循环 for b1 in bars: for b in b1: # 计算子弹的位置,基于圆心和半径以及子弹的角度 b.x, b.y = circleX+radius*math.cos(math.radians(b.angle - 90)), circleY+radius*math.sin(math.radians(b.angle - 90)) # 更新子弹的矩形区域 b.update_rect() # 将子弹矩形区域的特定点添加到多边形中 poly.append(b.rect.points[3]) poly.append(b.rect.points[2]) # 绘制多边形 pygame.draw.polygon(screen, poly_color, poly) # 绘制圆形 pygame.draw.circle(screen, circle_color, (circleX, circleY), int(radius)) pygame.display.flip() pygame.quit()
AudioAnalyzer.py
[Python] 纯文本查看 复制代码
import math import matplotlib.pyplot as plt import librosa.display import numpy as np import pygame def bin_search(arr, target): # 初始化中间索引、最小索引和最大索引 index = int(len(arr) / 2) min_index = 0 max_index = len(arr) - 1 found = False # 如果目标值小于数组最小值,返回0 if target < arr[0]: return 0 # 如果目标值大于数组最大值,返回数组长度减1 if target > arr[len(arr) - 1]: return len(arr) - 1 # 循环直到找到目标值 while not found: # 如果最小索引接近数组末尾,返回数组长度减1 if min_index == len(arr) - 2: return len(arr) - 1 # 如果目标值在当前索引值和下一个索引值之间或等于当前索引值,返回当前索引 if arr[index] < target < arr[index + 1] or arr[index] == target: return index # 如果当前索引值大于目标值,更新最大索引 if arr[index] > target: max_index = index else: # 否则更新最小索引 min_index = index # 更新中间索引 index = int((min_index + max_index) / 2) def rotate(xy, theta): # 通过旋转矩阵在二维平面上旋转点 cos_theta, sin_theta = math.cos(theta), math.sin(theta) # 返回旋转后的坐标 return ( xy[0] * cos_theta - xy[1] * sin_theta, xy[0] * sin_theta + xy[1] * cos_theta ) def translate(xy, offset): # 根据偏移量平移点 return xy[0] + offset[0], xy[1] + offset[1] def clamp(min_value, max_value, value): # 将值限制在最小值和最大值之间 if value < min_value: return min_value if value > max_value: return max_value return value class AudioAnalyzer: # 音频分析器类,用于加载音频文件并进行频谱分析 def __init__(self): # 初始化类实例的属性 self.frequencies_index_ratio = 0 # 频率索引比率 self.time_index_ratio = 0 # 时间索引比率 self.spectrogram = None # 频谱图,包含根据频率和时间索引的分贝值 def load(self, filename): # 加载音频文件并生成频谱图 time_series, sample_rate = librosa.load(filename) # 从文件中获取音频信息 # 获取包含根据频率和时间索引的幅度值的矩阵 stft = np.abs(librosa.stft(time_series, hop_length=512, n_fft=2048*4)) self.spectrogram = librosa.amplitude_to_db(stft, ref=np.max) # 将矩阵转换为分贝矩阵 frequencies = librosa.core.fft_frequencies(n_fft=2048*4) # 获取频率数组 # 获取时间周期数组 times = librosa.core.frames_to_time(np.arange(self.spectrogram.shape[1]), sr=sample_rate, hop_length=512, n_fft=2048*4) self.time_index_ratio = len(times)/times[len(times) - 1] # 计算时间索引比率 self.frequencies_index_ratio = len(frequencies)/frequencies[len(frequencies)-1] # 计算频率索引比率 def show(self): # 显示频谱图 librosa.display.specshow(self.spectrogram, y_axis='log', x_axis='time') plt.title('spectrogram') plt.colorbar(format='%+2.0f dB') plt.tight_layout() plt.show() def get_decibel(self, target_time, freq): # 根据给定的时间和频率获取分贝值 return self.spectrogram[int(freq*self.frequencies_index_ratio)][int(target_time*self.time_index_ratio)] def get_decibel_array(self, target_time, freq_arr): # 根据给定的时间和频率数组获取分贝值数组 arr = [] for f in freq_arr: arr.append(self.get_decibel(target_time,f)) return arr class AudioBar: # 初始化音频条的属性 def __init__(self, x, y, freq, color, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): # 初始化音频分析器的基本属性 self.x, self.y, self.freq = x, y, freq self.color = color self.width, self.min_height, self.max_height = width, min_height, max_height self.height = min_height self.min_decibel, self.max_decibel = min_decibel, max_decibel self.__decibel_height_ratio = (self.max_height - self.min_height)/(self.max_decibel - self.min_decibel) # 更新音频条的高度 def update(self, dt, decibel): # 计算期望的高度,基于当前的音量和高度比例 desired_height = decibel * self.__decibel_height_ratio + self.max_height # 计算速度,基于期望高度和当前高度的差异 speed = (desired_height - self.height)/0.1 # 更新当前高度,基于计算的速度和时间差 self.height += speed * dt # 确保当前高度在最小和最大高度之间 self.height = clamp(self.min_height, self.max_height, self.height) # 在屏幕上渲染音频条 def render(self, screen): pygame.draw.rect(screen, self.color, (self.x, self.y + self.max_height - self.height, self.width, self.height)) class AverageAudioBar(AudioBar): """ 继承自AudioBar类,用于表示音频条的平均值。 """ def __init__(self, x, y, rng, color, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): """ 初始化AverageAudioBar对象。 参数: x (int): 音频条的x坐标。 y (int): 音频条的y坐标。 rng (range): 用于计算平均值的范围。 color (str): 音频条的颜色。 width (int): 音频条的宽度,默认为50。 min_height (int): 音频条的最小高度,默认为10。 max_height (int): 音频条的最大高度,默认为100。 min_decibel (int): 最小分贝值,默认为-80。 max_decibel (int): 最大分贝值,默认为0。 """ # 调用父类的初始化方法,传递相关参数 super().__init__(x, y, 0, color, width, min_height, max_height, min_decibel, max_decibel) # 设置随机数生成器 self.rng = rng # 初始化平均值为0 self.avg = 0 def update_all(self, dt, time, analyzer): """ 更新音频条的平均值。 参数: dt (float): 时间间隔。 time (float): 当前时间。 analyzer (AudioAnalyzer): 音频分析器对象。 """ # 初始化平均值为0 self.avg = 0 # 遍历范围中的每个元素,累加分析器获取的分贝值 for i in self.rng: self.avg += analyzer.get_decibel(time, i) # 计算平均值 self.avg /= len(self.rng) # 更新数据 self.update(dt, self.avg) class RotatedAverageAudioBar(AverageAudioBar): # 继承自AverageAudioBar类,用于处理旋转的音频条 def __init__(self, x, y, rng, color, angle=0, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): # 初始化方法,设置旋转音频条的属性 super().__init__(x, y, 0, color, width, min_height, max_height, min_decibel, max_decibel) self.rng = rng self.rect = None self.angle = angle def render(self, screen): # 在屏幕上绘制旋转音频条 pygame.draw.polygon(screen, self.color, self.rect.points) def render_c(self, screen, color): # 在屏幕上绘制旋转音频条,使用指定的颜色 pygame.draw.polygon(screen, color, self.rect.points) def update_rect(self): # 更新旋转音频条的矩形区域并旋转 self.rect = Rect(self.x, self.y, self.width, self.height) self.rect.rotate(self.angle) class Rect: # 初始化矩形对象的属性 def __init__(self,x ,y, w, h): # 初始化矩形的坐标和尺寸 self.x, self.y, self.w, self.h = x,y, w, h # 初始化点的列表 self.points = [] # 计算原点位置 self.origin = [self.w/2,0] # 计算偏移量 self.offset = [self.origin[0] + x, self.origin[1] + y] # 初始旋转角度为0 self.rotate(0) # 旋转矩形对象 def rotate(self, angle): # 计算旋转前的模板点 template = [ (-self.origin[0], self.origin[1]), (-self.origin[0] + self.w, self.origin[1]), (-self.origin[0] + self.w, self.origin[1] - self.h), (-self.origin[0], self.origin[1] - self.h) ] # 旋转模板点并应用偏移量 self.points = [translate(rotate(xy, math.radians(angle)), self.offset) for xy in template] # 在屏幕上绘制矩形对象 def draw(self,screen): pygame.draw.polygon(screen, (255,255, 0), self.points)
-
解压密码 :52pojie