怀念经典,一步步开发一个ttplayer千千静听,频谱分析

我们先从千千静听(TTPlayer)频谱可视化在皮肤 XML 中的定义方式讲起,然后对 TtVisualizer 这个类的源码进行完整的深度剖析

image

image

image

image


第一部分:频谱可视化在皮肤 XML 中的定义

1. XML 标签与属性

在 TTPlayer 的 Skin.xml 中,频谱显示区域通常使用 <visualization>(或简写 <vis>)标签定义,放置在主窗口(<main_window>)或迷你窗口(<mini_window>)内部。其核心属性包括:

  • position:必填,格式为 "left,top,right,bottom",定义频谱面板的矩形区域。
  • mode:可选,指定默认的频谱模式(整数 0~7,对应 TtVisualizer.Mode 枚举顺序)。
  • colors:可选,自定义配色(如 "top,mid,bottom,peak,blur"),但 TTPlayer 经典皮肤通常使用内置默认配色。
  • transparent:可选,"true""false",但 TtVisualizer 背景默认不透明,此处可忽略。

示例 XML 片段(经典千千静听皮肤):

<main_window position="0,0,275,116" image="main.bmp">
    <!-- 频谱可视化区域 -->
    <visualization position="8,50,260,80" mode="0" />
    <!-- 其他控件... -->
</main_window>

TtSkin 解析器中,虽然未显式处理 <visualization> 标签(因为 TtSkin.getWindows() 只解析子元素并存入 Ctl,而 Ctl 没有专门为 vis 设计字段),但上层 UI 构建器(如 SkinWindow)会单独识别这个标签,创建 TtVisualizer 实例并设置其位置和模式。因此,XML 的 mode 属性会被读取并调用 setMode()


第二部分:TtVisualizer 源码完整深度剖析

TtVisualizer 是一个自定义 Swing 组件,它利用 FFT(快速傅里叶变换) 从 PCM 音频数据中提取频谱,并以 8 种不同的视觉效果实时绘制。代码结构清晰,移植自 C 语言的 c_ttplayer 实现。

1. 类结构总览

public class TtVisualizer extends JComponent {
    public enum Mode { BARS, WAVE, MIRROR_BARS, PARTICLES, LINE, AREA, RADAR, LED }
    
    private Mode mode = Mode.BARS;
    private TtRdft rdft = new TtRdft();           // FFT 引擎
    public Color backgroundColor = new Color(16, 22, 30);
    public Color skinColorTop, skinColorMid, skinColorBtm, skinColorPeak, skinColorBlur;
    
    private float[] spectrumBins;                  // 幅度谱(前 FFT_N/2 个 bin)
    private boolean hasSpectrum;
    private float[] pcmSamples;                   // 最近一帧 PCM 样本(供 WAVE/PARTICLES)
    private int pcmCount;
    private final float[] smoothLevels = new float[128]; // 平滑后的柱状值(C g_smooth_level)
    // ... 
}
  • Mode 枚举:定义了 8 种频谱样式,对应 C 语言的 render_visualization 的 mode 0~7。
  • TtRdft:一个自定义 RDFT(实数离散傅里叶变换)类,数学等价于 FFmpeg 的 AV_TX_FLOAT_RDFT,将 PCM 时域信号转换为频域幅度谱。
  • 配色字段:允许外部设置皮肤配色,若未设置则使用默认的亮绿色/黄色/蓝色。
  • spectrumBins:存储 FFT 计算出的幅度值(归一化到 0~1 左右)。
  • pcmSamples:缓存原始 PCM 样本(归一化到 [-1, 1]),用于波形和粒子模式。
  • smoothLevels:用于柱状/折线/面积/雷达模式,对原始 spectrumLevel 进行平滑(attack/decay),减少闪烁。

2. 公共接口

2.1 构造方法与初始化

public TtVisualizer() {
    setOpaque(true);
}
  • 设置 opaquetrue,确保背景完全绘制,避免透出下层窗口内容(防止“白闪”或透明感)。

2.2 模式控制

public void setMode(Mode mode) { this.mode = mode; repaint(); }
public Mode getMode() { return mode; }
public void cycleMode() {
    Mode[] modes = Mode.values();
    mode = modes[(mode.ordinal() + 1) % modes.length];
    repaint();
}
  • 支持外部切换模式,cycleMode 循环切换(可用于按键或菜单)。

2.3 数据喂入(updateData

public void updateData(byte[] pcmData) {
    if (pcmData != null && pcmData.length > 8) {
        spectrumBins = rdft.spectrum(pcmData, pcmData.length);
        hasSpectrum = true;

        int n = pcmData.length / 2;
        if (pcmSamples.length < n) pcmSamples = new float[n];
        for (int i = 0; i < n; i++) {
            short s = (short) ((pcmData[i*2+1] << 8) | (pcmData[i*2] & 0xFF));
            pcmSamples[i] = s / 32768.0f;
        }
        pcmCount = n;
    } else {
        hasSpectrum = false;
        Arrays.fill(spectrumBins, 0f);
        pcmSamples = new float[0];
        pcmCount = 0;
    }
    repaint();
}
  • 输入为 16-bit 小端 PCM 数据byte[]),长度可变。
  • 调用 rdft.spectrum() 计算幅度谱,返回长度 FFT_N/2float[](剔除直流和镜像)。
  • 同时将 PCM 样本转换为归一化浮点数(-1~1),缓存供 WAVEPARTICLES 使用。
  • 若数据无效,清零并 hasSpectrum=false,最终触发 repaint()

3. 频谱数学辅助方法

3.1 spectrumLevel(int binStart, int binEnd)

private float spectrumLevel(int binStart, int binEnd) {
    float mag = 0;
    int count = 0;
    int half = TtRdft.FFT_N / 2;
    int start = Math.max(1, binStart);  // 跳过 DC bin(0)
    for (int b = start; b < binEnd && b < half; b++) {
        mag += spectrumBins[b];
        count++;
    }
    if (count > 0) mag /= (float) count;
    float level = 0;
    if (mag > 0) {
        float scaled = mag * 100.0f;
        level = (float) (Math.log1p(scaled) / Math.log1p(100.0));
    }
    if (level > 1.0f) level = 1.0f;
    if (level < 0.02f) level = 0.0f;
    return level;
}
  • 功能:计算指定频率区间(binStartbinEnd)的平均幅度,并应用对数压缩log1p)以模拟人耳感知。
  • 跳过 DCstart = Math.max(1, binStart) 忽略直流分量(bin 0),避免柱状图左侧过高。
  • 压缩公式log1p(mag*100) / log1p(100),将幅度映射到 [0,1],使得微弱信号也能有所显示。

3.2 smoothLevel(int idx, float rawLevel)

private float smoothLevel(int idx, float rawLevel) {
    float prev = smoothLevels[idx];
    float v = (rawLevel >= prev) ? prev + (rawLevel - prev) * 0.5f : prev * 0.9f;
    if (v < 0) v = 0;
    if (v > 1) v = 1;
    smoothLevels[idx] = v;
    return v;
}
  • attack:当 rawLevel >= prev 时,以 0.5 的系数快速上升(响应快)。
  • decay:当 rawLevel < prev 时,乘以 0.9 缓慢下降(余晖效应)。
  • 模拟了 C 语言中的平滑滤波器,使柱状/折线变化更自然。

3.3 themeColor(int index, int total, Color[] palette)

private Color themeColor(int index, int total, Color[] palette) {
    float t = (total > 1) ? (float) index / (total - 1) : 0f;
    float pos = t * (palette.length - 1);
    int lo = (int) pos;
    int hi = lo + 1;
    if (hi >= palette.length) hi = palette.length - 1;
    float k = pos - lo;
    Color a = palette[lo], b = palette[hi];
    return new Color(
        (int) (a.getRed() + (b.getRed() - a.getRed()) * k),
        (int) (a.getGreen() + (b.getGreen() - a.getGreen()) * k),
        (int) (a.getBlue() + (b.getBlue() - a.getBlue()) * k));
}
  • 在调色板中线性插值,根据当前柱/点的索引和总数返回渐变颜色。
  • 例如 palette = {btm, mid, top, peak},则从底到顶颜色渐变。

4. 双缓存与绘制调度

4.1 双缓存 backBuffer

private BufferedImage backBuffer;
private void ensureBuffer(int w, int h) {
    if (backBuffer == null || backBuffer.getWidth() != w || backBuffer.getHeight() != h) {
        backBuffer = new BufferedImage(Math.max(1, w), Math.max(1, h), BufferedImage.TYPE_INT_ARGB);
    }
}
  • 使用离屏缓冲,整帧渲染完成后一次性 blit 到屏幕,避免逐元素绘制时的闪烁。

4.2 paintComponent

protected void paintComponent(Graphics g) {
    int w = getWidth(), h = getHeight();
    if (w <= 0 || h <= 0) return;
    ensureBuffer(w, h);
    Graphics2D bg = backBuffer.createGraphics();
    try {
        bg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        bg.setColor(backgroundColor);
        bg.fillRect(0, 0, w, h);
        switch (mode) {
            case BARS:        drawBars(bg); break;
            case WAVE:        drawWave(bg); break;
            case MIRROR_BARS: drawMirrorBars(bg); break;
            case PARTICLES:   drawParticles(bg); break;
            case LINE:        drawLine(bg); break;
            case AREA:        drawArea(bg); break;
            case RADAR:       drawRadar(bg); break;
            case LED:         drawLed(bg); break;
        }
    } finally { bg.dispose(); }
    g.drawImage(backBuffer, 0, 0, null);
}
  • 先填充背景色(不透明),然后根据当前模式调用对应的绘制函数。

5. 八种频谱模式的绘制算法详解

5.1 BARS(柱状频谱)

  • 根据控件宽度计算柱数(4~32 根),每根柱高度由对应频段的 smoothLevel 决定。
  • 每根柱使用垂直渐变(6 段),从底部实色到顶部半透明,顶部有白色高亮线。
  • 颜色从左到右在 {btm, mid, top, peak} 调色板中渐变。

5.2 WAVE(多层波形 + 渐变填充)

  • 使用缓存的 pcmSamples 绘制时域波形。
  • 三层叠加:主波形(不透明) + 两层偏移波形(半透明),增强立体感。
  • 主波形下方绘制渐变填充(从波形底部到下方区域),颜色取自 btm,透明度逐渐降低。

5.3 MIRROR_BARS(镜像柱)

  • 以控件垂直中线为轴,上下对称绘制柱状频谱。
  • 上方实色(透明度 220),下方半透明(透明度 140),形成镜像效果。

5.4 PARTICLES(粒子系统)

  • 结合低音能量(时域前 8 个采样均值)和中频能量(频谱 bin 4~31)。
  • 低音爆发:当低音 > 0.4 时,从中心向四周喷射粒子,粒子速度与低音强度正相关。
  • 中频吸引:当 > 0.15 时,从边缘向中心发射粒子。
  • 粒子具有重力、衰减、大小变化,颜色在 {btm, top} 调色板中随机。
  • 最多维护 400 个粒子,旧粒子消亡后新粒子替换。

5.5 LINE(频谱折线)

  • 将频谱分为 points 个点(约 8~64),依次连线,最后连回右下角,形成闭合折线(但只绘制线,不填充)。

5.6 AREA(平滑面积图)

  • 对每个像素列插值计算频谱值,绘制由曲线和底部围成的填充区域,使用渐变填充(顶部 topbtm)。
  • 绘制顶部曲线,并叠加一层半透明光晕(峰值色),增强视觉层次。

5.7 RADAR(环形雷达)

  • 将频谱映射到 12 个轴向的半径,绘制一个多边形。
  • 背景绘制同心圆环径向辐条(低透明度),多边形填充为 top 色半透明,边缘使用 peak 色双层描边(光晕+主线)。

5.8 LED(LED 点阵)

  • 将频谱绘制为点阵矩阵,每个点为一个矩形单元格。
  • 每列高度根据频谱值填充,未填充的单元格显示暗灰色,填充的单元格颜色在 {btm, mid, top} 中渐变,顶部点亮格额外用白色高亮。

6. 粒子系统的实现细节

private static class VizParticle {
    float x, y, vx, vy;
    int r, g, b;
    float life, size;
}
private final VizParticle[] particles = new VizParticle[MAX_PARTICLES];
private int particleCount = 0;
  • 使用固定数组池,避免频繁对象创建。
  • drawParticles 中,每帧根据低音/中频能量生成新粒子,然后更新所有粒子位置、速度、生命值,并绘制存活粒子。
  • 粒子运动受重力vy += 0.02)和阻尼vx *= 0.99)影响。

7. 颜色工具与默认配色

  • getSkinTop() 等方法提供默认配色(亮绿、黄、蓝、白),若外部通过 skinColor* 字段设置了自定义颜色则使用自定义值。
  • lerp 方法用于颜色插值。

总结

TtVisualizer 是一个高度集成的频谱可视化组件,它从 PCM 数据出发,经过 FFT、对数压缩、平滑滤波,最终绘制出 8 种各具特色的动态图形。代码忠实还原了千千静听经典的视觉体验,同时保持了良好的可扩展性和可维护性。无论是用于音乐播放器项目,还是作为学习数字信号处理与图形渲染的范例,都具有很高的参考价值。

License

本项目仅供学习和个人使用。皮肤资源版权归原作者所有。

「获取方式」:后台回复 ttplayer

image

posted @ 2026-08-28 11:19  tomj2ee  阅读(2)  评论(0)    收藏  举报