---
title: 音乐律动功能
summary: 介绍幻彩串灯的音乐律动功能，支持本地音乐/App 音乐两种模式，根据音频实时调整灯光效果，包含实现详解、数据流与 API 调用。
---

## 音乐律动功能

> **项目简介**：本模块适用于 Wi-Fi + BLE 协议的幻彩串灯（灯串）设备，支持本地音乐律动和 App 音乐律动两种模式，能够根据音频信号实时调整灯光颜色和亮度，实现音乐与灯光的同步效果。

### 🖼️ 预览：
<Image src='/images/extended/lampSting/string-music.png' style={{ width: '120px', height: '224px' }} />

音乐律动功能通过监听设备麦克风或 App 音频信号，实时分析音频特征（如分贝值、频率等），并将音频特征转换为灯光效果。该功能支持多种音乐模式选择，可调节灵敏度，支持随机颜色和预设颜色两种模式，为用户提供沉浸式的音乐灯光体验。

### 功能列表

1. **本地音乐律动**：通过设备麦克风采集音频，实时分析并控制灯光
2. **App 音乐律动**：通过 App 音频接口获取音频数据，控制灯光效果
3. **音乐模式选择**：支持多种内置音乐模式（摇滚、爵士、古典等）
4. **灵敏度调节**：可调节音频灵敏度，控制灯光响应程度
5. **随机颜色模式**：根据音频随机选择颜色，实现动态效果
6. **预设颜色模式**：使用预设的颜色组合，实现固定配色效果
7. **模式切换**：支持本地音乐和云端音乐模式切换
8. **状态同步**：音乐律动状态与设备状态同步

### 功能实现详解

#### 实现概述

音乐律动功能采用两种不同的实现方式：

- **本地音乐律动**：使用 `dreamlightmic_music_data` DP 点，设备端通过麦克风采集音频并处理，面板通过 DP 点控制音乐模式和参数
- **App 音乐律动**：使用 `music_data` DP 点，通过 App 的 MediaKit 音频接口获取音频数据，面板端实时计算颜色并下发

核心设计思路：

- **数据分离**：本地和云端使用不同的 DP 点和数据结构
- **实时处理**：云端模式使用节流处理音频数据，避免频繁下发
- **状态管理**：使用云存储保存当前音乐模式和灵敏度设置
- **颜色策略**：支持随机颜色和预设颜色两种策略

#### 核心组件

- `src/components/music/index.tsx`: 音乐律动主组件，负责模式切换和整体布局
- `src/components/music/local/index.tsx`: 本地音乐律动组件
- `src/components/music/app/index.tsx`: App 音乐律动组件
- `src/hooks/useLocalMusicInit.ts`: 本地音乐初始化和管理 Hook
- `src/standModel/musicModel/LocalMusic/dpParser/localMusic__dreamlightmic_music_data.ts`: 本地音乐 DP 解析器
- `src/components/music/app/sdk.ts`: App 音乐音频处理 SDK
- `src/hooks/useStoreSensitivity.ts`: 灵敏度存储管理 Hook
- `src/components/random-colors/index.tsx`: 随机颜色选择组件

**组件职责说明**：

- **Music 组件**：作为容器组件，管理音乐模式切换（本地/云端），协调子组件
- **LocalMusic 组件**：处理本地音乐模式，显示音乐列表和灵敏度调节
- **AppMusic 组件**：处理云端音乐模式，注册音频监听和颜色计算
- **useLocalMusicInit Hook**：封装本地音乐的初始化、播放、灵敏度调节逻辑
- **音频 SDK**：封装 MediaKit 音频接口，提供音频数据回调

#### 数据流

```mermaid
sequenceDiagram
    participant User as 用户
    participant Music as 音乐组件
    participant Local as 本地音乐组件
    participant App as 云端音乐组件
    participant Hook as 初始化Hook
    participant SDK as 音频SDK
    participant DP as DP协议
    participant Device as 设备

    User->>Music: 选择音乐模式
    Music->>Local: 显示本地音乐
    User->>Local: 选择音乐类型
    Local->>Hook: 调用onPlay
    Hook->>DP: 编码dreamlightmic_music_data
    DP->>Device: 下发DP
    Device->>Device: 麦克风采集音频
    Device->>User: 显示灯光效果

    User->>Music: 切换到云端音乐
    Music->>App: 显示云端音乐
    User->>App: 选择音乐类型
    App->>SDK: 注册音频监听
    SDK->>SDK: 获取音频数据
    SDK->>App: 回调音频数据
    App->>App: 计算颜色和亮度
    App->>DP: 编码music_data
    DP->>Device: 下发DP
    Device->>User: 显示灯光效果
```

#### Mermaid 流程图

**功能主流程图**：

```mermaid
flowchart TD
    A[进入音乐律动页面] --> B{选择音乐模式}
    B -->|本地音乐| C[显示本地音乐列表]
    B -->|云端音乐| D[显示云端音乐列表]
    C --> E[用户选择音乐类型]
    D --> F[用户选择音乐类型]
    E --> G[设置音乐参数]
    F --> H[注册音频监听]
    G --> I[编码dreamlightmic_music_data]
    H --> J[获取音频数据]
    J --> K[计算颜色和亮度]
    K --> L[编码music_data]
    I --> M[下发DP到设备]
    L --> M
    M --> N[设备执行灯光效果]
    N --> O[用户观察效果]
    O --> P{继续调节?}
    P -->|是| Q[调节灵敏度/颜色]
    P -->|否| R[保持当前状态]
    Q --> G
    Q --> K
```

**数据流转图**：

```mermaid
sequenceDiagram
    participant User as 用户操作
    participant UI as UI组件
    participant Hook as 业务Hook
    participant Parser as DP解析器
    participant Device as 设备/音频
    participant Storage as 云存储

    User->>UI: 选择音乐模式
    UI->>Hook: 初始化音乐数据
    Hook->>Storage: 读取保存的音乐ID
    Storage-->>Hook: 返回音乐ID
    Hook->>Parser: 构建DP数据
    Parser->>Device: 下发DP指令

    Note over Device: 本地模式：设备处理音频<br/>云端模式：App提供音频

    Device->>Device: 分析音频特征
    Device->>User: 显示灯光效果

    User->>UI: 调节灵敏度
    UI->>Hook: 更新灵敏度
    Hook->>Parser: 更新DP数据
    Parser->>Device: 下发更新
    Hook->>Storage: 保存灵敏度
```

**组件交互图**：

```mermaid
graph TB
    A[Music组件] -->|切换模式| B[LocalMusic组件]
    A -->|切换模式| C[AppMusic组件]
    A -->|使用| D[RandomColors组件]
    B -->|调用| E[useLocalMusicInit]
    C -->|使用| F[音频SDK]
    E -->|使用| G[useStoreSensitivity]
    E -->|编码| H[LocalMusicParser]
    F -->|回调| C
    C -->|编码| I[MusicDataParser]
    H -->|下发| J[设备]
    I -->|下发| J
    G -->|保存| K[云存储]
```

**状态变化图**：

```mermaid
stateDiagram-v2
    [*] --> 初始化: 页面加载
    初始化 --> 选择模式: 用户操作
    选择模式 --> 本地模式: 选择本地
    选择模式 --> 云端模式: 选择云端
    本地模式 --> 选择音乐: 显示列表
    云端模式 --> 选择音乐: 显示列表
    选择音乐 --> 播放中: 开始播放
    播放中 --> 调节参数: 用户调节
    调节参数 --> 播放中: 更新参数
    播放中 --> 停止播放: 用户停止
    停止播放 --> 选择模式: 重新选择
    停止播放 --> [*]: 退出页面
```

#### 关键代码片段

**本地音乐播放**：

```tsx
// 文件: src/hooks/useLocalMusicInit.ts
  const onPlay = (
    active: boolean,
    item: { id: number; colorArr: any[] },
    sensitivity: number,
    isInitDefault = false
  ) => {
    const nextLocalMusicList = {
      ...DefaultLocalMusicData,
      ...(localMusicList || {}),
      brightness: 100,
      sensitivity,
    };
    // 设置律动颜色
    nextLocalMusicList.power = active;

    if (item) {
      if (active) {
        actions.work_mode.set('music', {
          ignoreDpDataResponse: isInitDefault ? false : true,
        });
      }

      nextLocalMusicList.id = item.id; // 音乐模式编号
      if (enableRandom) {
        nextLocalMusicList.colors = getArray(item.colorArr);
        nextLocalMusicList.a = 1;
      } else {
        nextLocalMusicList.colors = getArray(selectedRandomColors?.colors); // 关闭随机后，选择默认色盘
        nextLocalMusicList.a = 0;
      }
    }
    structuredActions.dreamlightmic_music_data.set(nextLocalMusicList, {
      ignoreDpDataResponse: isInitDefault ? false : true,
    });
  };
```

**云端音乐音频监听**：

```tsx
// 文件: src/components/music/app/index.tsx
// 注册监听
const onPlay = () => {
  console.log('注册监听');
  onMusic2RgbChange(
    data => {
      if (!data) return;

      // 这里面的状态是实时变化的
      // react state 特性会导致这里都是首次的状态, 所以都是从Ref里取

      // 获取当前的颜色组
      // ------------------------
      const currentId = currentIdRef.current;
      const appItem = getArray(dataSource).find(item => item.id === currentId);
      const mode = appItem?.mode ?? 1;
      let colorList: { hue: number; saturation: number; value: number }[];

      const enableRandom = enableRandomRef.current;
      const selectedRandomColors = selectedRandomItemRef.current;
      if (enableRandom) {
        // 开启随机色后，使用app律动回调给的hsv
        colorList = getArray(appItem?.colorArr) as any;
      } else {
        // 关闭随机色后，使用预设颜色
        colorList = getArray(selectedRandomColors?.colors).map(item => ({
          ...item,
          saturation: item.saturation * 10,
          value: item.value * 10,
        }));
      }

      const nextColorList = getArray(colorList).length > 0 ? getArray(colorList) : [];
      // ------------------------

      const musicData = {
        mode: mode as any,
        hue: data?.hue,
        saturation: data?.saturation,
        value: data?.value,
        brightness: data?.bright,
        temperature: data?.temperature,
      };

      const { db, dB } = data?.extra || {};
      const _db = dB || db || 0;

      if (nextColorList.length > 0) {
        // 随机获取colorList中的一个
        // ------------------------
        const randomColor = nextColorList[Math.floor(Math.random() * nextColorList.length)];

        musicData.hue = randomColor.hue;
        musicData.saturation = randomColor.saturation;
        let brightness = randomColor.value;

        const dBRange = [40, 80];
        const [minDB, maxDB] = dBRange;
        const [minBright, maxBright] = [0, 1000];

        if (_db <= minDB) {
          brightness = minBright;
        } else if (_db >= maxDB) {
          brightness = maxBright;
        } else {
          brightness = Math.round(calcPosition(_db, minDB, maxDB, minBright, maxBright));
        }
        musicData.value = brightness;
      }

      const SupportUtils = devices.common.model.abilities.support;
      if (SupportUtils.isSupportColour()) {
        // 支持彩光的时候，去掉白光
        musicData.brightness = 0;
        musicData.temperature = 0;
      }

      structuredActions.music_data.set(musicData, {
        throttle: 300,
      });
    },
    {
      mode: 1,
      colorList: defaultAppMusicList[0].colorArea,
    }
  );
};
```

**音频数据处理 SDK**：

```tsx
// 文件: src/components/music/app/sdk.ts
export const onMusic2RgbChange = (
  callback: (musicData: {
    mode: number;
    hue: number;
    saturation: number;
    value: number;
    bright: number;
    temperature: number;
    extra: any;
  }) => void,
  musicOption?: TMusicOption
) => {
  // 如果当前正在执行监听，直接返回
  if (isListening) {
    return;
  }
  if (!manager) {
    try {
      manager = (ty as any)?.media?.getRGBAudioManager?.();
    } catch (err) {
      console.warn('no getRGBAudioManager', err);
      return;
    }
  }
  if (!manager) {
    return;
  }
  const handleAudioRgbChange = throttle((data: string) => {
    const {
      R,
      G,
      B,
      C: temp,
      L: bright,
      db,
      dB,
    } = (JSON.parse(data) || {}) as {
      R: number;
      G: number;
      B: number;
      C: number;
      L: number;
      db: number;
      dB: number;
      index: number;
    };
    // 兼容不同版本字段
    const _db = dB || db || 0;
    let hue = 0;
    let saturation = 1000;
    let value = 1000;
    const { mode = 1, colorList, dBRange = [40, 80] } = musicOption || {};
    const [minDB, maxDB] = dBRange;
    const [minBright, maxBright] = [0, 1000];
    if (colorList) {
      // 随机获取colorList中的一个
      const randomColor = colorList[Math.floor(Math.random() * colorList.length)];
      if (!randomColor) {
        console.log('onMusic2RgbChange => 未获取到随机颜色');
        return;
      }
      hue = randomColor.hue;
      saturation = randomColor.saturation;
      let brightness = randomColor.value;
      if (_db <= minDB) {
        brightness = minBright;
      } else if (_db >= maxDB) {
        brightness = maxBright;
      } else {
        brightness = Math.round(calcPosition(_db, minDB, maxDB, minBright, maxBright));
      }
      value = brightness;
    } else {
      [hue, saturation, value] = rgb2hsb(R, G, B).map((v, i) => (i > 0 ? v * 10 : v));
    }

    const musicData = {
      mode,
      hue: Math.round(hue),
      saturation: Math.round(saturation),
      value: Math.round(value),
      bright: Math.round(bright * 10),
      temperature: Math.round(temp * 10),
      extra: {
        R,
        G,
        B,
        C: temp,
        L: bright,
        db,
        dB,
      },
    };

    callback && callback(musicData);
  }, 300);
  manager.onAudioRgbChange(({ body }) => {
    handleAudioRgbChange(body);
  });
  start();
};
```

**灵敏度调节**：

```tsx
// 文件: src/hooks/useLocalMusicInit.ts
const onSensitivityChange = (sensitivity: number) => {
  localMusicList.sensitivity = sensitivity;
  setIdSi(localMusicList?.id, String(sensitivity));
  structuredActions.dreamlightmic_music_data.set(
    {
      ...localMusicList,
      brightness: 100,
    },
    {
      throttle: 100,
    }
  );
};
```

#### 数据结构

**本地音乐 DP 数据结构**：

```typescript
type TMusic = {
  v: number; // 版本号
  power: boolean; // 是否开启
  id: number; // 音乐模式编号
  isLight: number; // 0: 无声时灯灭, 1: 无声时灯维持10%亮度
  mode: number; // 0: 跳变, 1: 渐变, 2: 呼吸, 3: 闪烁
  speed: number; // 速度 (0-100)
  sensitivity: number; // 灵敏度 (0-100)
  a: number; // 随机标志 (0: 预设颜色, 1: 随机颜色)
  b: number; // 预留字段
  c: number; // 预留字段
  brightness: number; // 亮度 (0-100)
  colors: Array<{
    hue: number; // 色相 (0-360)
    saturation: number; // 饱和度 (0-100)
  }>; // 颜色列表
};
```

**云端音乐 DP 数据结构**：

```typescript
type MusicData = {
  mode: number; // 音乐模式 (0: 跳变, 1: 渐变)
  hue: number; // 色相 (0-360)
  saturation: number; // 饱和度 (0-1000)
  value: number; // 亮度 (0-1000)
  brightness: number; // 白光亮度 (0-1000)
  temperature: number; // 色温 (0-1000)
};
```

**音频数据回调结构**：

```typescript
type AudioData = {
  R: number; // 红色分量 (0-255)
  G: number; // 绿色分量 (0-255)
  B: number; // 蓝色分量 (0-255)
  C: number; // 色温
  L: number; // 亮度
  db: number; // 分贝值
  dB: number; // 分贝值（兼容字段）
  index: number; // 索引
};
```

**随机颜色数据结构**：

```typescript
type RandomItem = {
  id: number; // 颜色组合ID
  colors: Array<{
    hue: number; // 色相
    saturation: number; // 饱和度 (0-100)
    value: number; // 亮度 (0-100)
  }>; // 颜色列表
};
```

#### API 调用

**本地音乐 DP 接口**：

- **DP 点**：`dreamlightmic_music_data`
- **调用方式**：`structuredActions.dreamlightmic_music_data.set(data, options)`
- **数据格式**：经过 `MicMusicFormater` 编码的十六进制字符串
- **调用时机**：
  - 选择音乐模式时
  - 调节灵敏度时
  - 切换随机/预设颜色时
  - 初始化时恢复上次状态

**云端音乐 DP 接口**：

- **DP 点**：`music_data`
- **调用方式**：`structuredActions.music_data.set(data, options)`
- **数据格式**：经过协议编码的数据对象
- **调用时机**：
  - 音频数据回调时（节流 300ms）
  - 选择音乐模式时
  - 切换随机/预设颜色时

**音频接口**：

- **接口**：`ty.media.getRGBAudioManager()`
- **方法**：
  - `startRGBRecord()`: 开始音频录制
  - `stopRGBRecord()`: 停止音频录制
  - `onAudioRgbChange(callback)`: 注册音频数据回调
  - `offAudioRgbChange()`: 取消音频数据回调
- **调用时机**：
  - 进入云端音乐模式时注册
  - 离开云端音乐模式时取消

**云存储接口**：

- **存储键**：
  - `CLOUD_KEY_LOCAL_MUSIC_ID`: 本地音乐 ID
  - `CLOUD_KEY_APP_MUSIC_ID`: 云端音乐 ID
  - `CLOUD_RANDOM_COLOR_ID`: 随机颜色 ID
- **调用方式**：`useCloudStorageKey(key, options)`
- **数据格式**：字符串（音乐 ID 或颜色 ID）

### 注意事项

#### 实现难点

1. **音频数据处理性能**：

   - **难点**：音频数据回调频率很高，频繁计算和下发可能导致性能问题
   - **解决方案**：使用节流（throttle）限制处理频率（300ms），减少计算和下发次数

2. **状态同步问题**：

   - **难点**：音频回调中使用闭包，React state 可能不是最新值
   - **解决方案**：使用 `useRef` 保存最新状态，在回调中从 ref 读取

3. **颜色计算逻辑**：

   - **难点**：需要根据分贝值动态计算亮度，同时支持随机颜色和预设颜色
   - **解决方案**：使用 `calcPosition` 函数计算分贝值到亮度的映射，根据模式选择颜色策略

4. **模式切换清理**：

   - **难点**：切换音乐模式时需要清理之前的监听和状态
   - **解决方案**：在组件卸载时调用 `offMusic2RgbChange`，清理音频监听和保持屏幕常亮状态

5. **灵敏度持久化**：
   - **难点**：不同音乐模式的灵敏度需要分别保存和恢复
   - **解决方案**：使用 `useStoreSensitivity` Hook，以音乐 ID 为键存储灵敏度值

#### 常见问题

1. **问题**：云端音乐模式无效果

   - **原因**：音频接口未初始化或权限未授予
   - **解决方法**：检查 `getRGBAudioManager` 是否可用，确保已授予麦克风权限

2. **问题**：本地音乐模式设备无响应

   - **原因**：DP 数据格式错误或设备不支持该音乐模式
   - **解决方法**：检查 DP 数据编码是否正确，确认设备支持的音乐模式列表

3. **问题**：颜色变化不流畅

   - **原因**：节流时间设置过长或 DP 下发失败
   - **解决方法**：调整节流时间（建议 300ms），检查网络连接和设备状态

4. **问题**：灵敏度调节无效

   - **原因**：灵敏度值未正确保存或 DP 下发失败
   - **解决方法**：检查云存储是否正常，确认 DP 下发成功回调

5. **问题**：切换模式后状态混乱
   - **原因**：未正确清理之前模式的状态和监听
   - **解决方法**：在模式切换时调用清理函数，重置相关状态

#### 性能优化

1. **节流处理**：

   - 音频数据回调使用节流（300ms），减少计算和下发频率
   - 灵敏度调节使用节流（100ms），避免频繁更新

2. **Ref 使用**：

   - 使用 `useRef` 保存最新状态，避免闭包问题
   - 使用 `useRef` 缓存音频管理器实例，避免重复创建

3. **条件渲染**：

   - 根据音乐模式条件渲染对应组件，减少不必要的渲染
   - 使用 `useMemo` 缓存音乐列表数据

4. **数据缓存**：

   - 灵敏度数据缓存到云存储，避免重复计算
   - 音乐列表数据使用 `useMemo` 缓存

5. **屏幕常亮管理**：
   - 仅在音频监听时保持屏幕常亮，退出时恢复
   - 使用 `setKeepScreenOn` 管理屏幕状态

#### 边界情况

1. **音频接口不可用**：

   - 处理：显示错误提示，禁用云端音乐模式
   - 代码：检查 `getRGBAudioManager` 返回值，提供降级方案

2. **设备不支持音乐模式**：

   - 处理：根据 DP schema 判断支持情况，隐藏不支持的功能
   - 代码：使用 `useSupport` 检查 DP 支持情况

3. **音频数据异常**：

   - 处理：校验数据格式，异常数据不处理
   - 代码：在回调中添加数据校验逻辑

4. **分贝值超出范围**：

   - 处理：限制分贝值范围，超出范围使用边界值
   - 代码：使用 `Math.max` 和 `Math.min` 限制范围

5. **颜色列表为空**：

   - 处理：使用默认颜色或从音频 RGB 值计算颜色
   - 代码：检查颜色列表长度，提供默认值

6. **组件卸载时清理**：
   - 处理：在 `useEffect` 清理函数中取消监听和恢复状态
   - 代码：返回清理函数，调用 `offMusic2RgbChange`

#### 依赖关系

1. **依赖注意事项**：

   - MediaKit 需要在 TTT 配置中启用
   - 音频权限需要在 App 中授予

2. **解耦建议**：
   - 将音频处理逻辑封装为独立 SDK，便于复用和测试
   - 将颜色计算逻辑抽离为工具函数，便于单元测试
   - 将音乐模式数据配置化，便于扩展和维护
