[重发] python音乐可视化


本帖最后由 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

 

[油猴脚本]B站专栏复制删掉小尾巴

社区工作者易题库免登陆解锁会员

获取更多资讯请加入交流群


    协助本站SEO优化一下,谢谢!
    关键词不能为空
评 论
更换验证码