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




第一部分:频谱可视化在皮肤 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);
}
- 设置
opaque为true,确保背景完全绘制,避免透出下层窗口内容(防止“白闪”或透明感)。
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/2的float[](剔除直流和镜像)。 - 同时将 PCM 样本转换为归一化浮点数(-1~1),缓存供
WAVE和PARTICLES使用。 - 若数据无效,清零并
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;
}
- 功能:计算指定频率区间(
binStart到binEnd)的平均幅度,并应用对数压缩(log1p)以模拟人耳感知。 - 跳过 DC:
start = 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(平滑面积图)
- 对每个像素列插值计算频谱值,绘制由曲线和底部围成的填充区域,使用渐变填充(顶部
top到btm)。 - 绘制顶部曲线,并叠加一层半透明光晕(峰值色),增强视觉层次。
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


浙公网安备 33010602011771号