> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunoapi.org/llms.txt
> Use this file to discover all available pages before exploring further.

# MIDI 生成回调

> 当从分离音频生成 MIDI 完成时,系统将调用此回调。

当您向 Suno API 提交 MIDI 生成任务时,可以使用 `callBackUrl` 参数设置回调 URL。系统将在任务完成时自动将结果推送到您指定的地址。

## 回调机制概述

<Info>
  回调机制无需轮询 API 查询任务状态。系统将主动向您的服务器推送任务完成结果。
</Info>

### 回调时机

系统将在以下情况发送回调通知:

* MIDI 生成任务成功完成
* MIDI 生成任务失败
* 任务处理过程中发生错误

### 回调方法

* **HTTP 方法**: POST
* **Content Type**: application/json
* **超时设置**: 15 秒

## 回调请求格式

当任务完成时,系统将向您的 `callBackUrl` 发送 POST 请求:

<CodeGroup>
  ```json 成功回调 theme={null}
  {
    "task_id": "5c79****be8e",
    "code": 200,
    "msg": "success",
    "data": {
      "state": "complete",
      "instruments": [
        {
          "name": "Drums",
          "notes": [
            {
              "pitch": 73,
              "start": "0.036458333333333336",
              "end": "0.18229166666666666",
              "velocity": 1
            },
            {
              "pitch": 61,
              "start": 0.046875,
              "end": "0.19270833333333334",
              "velocity": 1
            },
            {
              "pitch": 73,
              "start": 0.1875,
              "end": "0.4895833333333333",
              "velocity": 1
            }
          ]
        },
        {
          "name": "Electric Bass (finger)",
          "notes": [
            {
              "pitch": 44,
              "start": 7.6875,
              "end": "7.911458333333333",
              "velocity": 1
            },
            {
              "pitch": 56,
              "start": 7.6875,
              "end": "7.911458333333333",
              "velocity": 1
            },
            {
              "pitch": 51,
              "start": 7.6875,
              "end": "7.911458333333333",
              "velocity": 1
            }
          ]
        }
      ]
    }
  }
  ```

  ```json 失败回调 theme={null}
  {
    "task_id": "5c79****be8e",
    "code": 500,
    "msg": "MIDI generation failed",
    "data": null
  }
  ```
</CodeGroup>

## 状态码说明

<ParamField path="code" type="integer" required>
  回调状态码,指示任务处理结果:

  | 状态码 | 说明                |
  | --- | ----------------- |
  | 200 | 成功 - MIDI 生成已成功完成 |
  | 500 | 内部错误 - 请重试或联系技术支持 |
</ParamField>

<ParamField path="msg" type="string" required>
  状态消息,提供详细的状态描述
</ParamField>

<ParamField path="task_id" type="string" required>
  任务 ID,与您提交任务时返回的 task\_id 一致
</ParamField>

<ParamField path="data" type="object">
  MIDI 生成结果信息,成功时返回
</ParamField>

## 成功响应字段

<ParamField path="data.state" type="string">
  处理状态。成功时值为 `complete`
</ParamField>

<ParamField path="data.instruments" type="array">
  检测到的乐器数组及其 MIDI 音符数据

  <Expandable title="乐器对象属性">
    <ParamField path="name" type="string">
      乐器名称(例如:"Drums"、"Electric Bass (finger)"、"Acoustic Grand Piano")
    </ParamField>

    <ParamField path="notes" type="array">
      该乐器的 MIDI 音符数组

      <Expandable title="音符对象属性">
        <ParamField path="pitch" type="integer">
          MIDI 音符编号(0-127)。中央 C = 60。[MIDI 音符参考](https://inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies)
        </ParamField>

        <ParamField path="start" type="number | string">
          音符起始时间,从音频开头算起的秒数
        </ParamField>

        <ParamField path="end" type="number | string">
          音符结束时间,从音频开头算起的秒数
        </ParamField>

        <ParamField path="velocity" type="number">
          音符力度/强度(0-1 范围)。1 = 最大力度
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

## 回调接收示例

以下是使用主流编程语言接收回调的示例代码:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const express = require('express');
    const app = express();

    app.use(express.json());

    app.post('/suno-midi-callback', (req, res) => {
      const { code, msg, task_id, data } = req.body;
      
      console.log('收到 MIDI 生成回调:', {
        taskId: task_id,
        status: code,
        message: msg
      });
      
      if (code === 200) {
        // 任务成功完成
        console.log('MIDI 生成完成');
        
        if (data && data.instruments) {
          console.log(`检测到 ${data.instruments.length} 个乐器`);
          
          data.instruments.forEach(instrument => {
            console.log(`\n乐器: ${instrument.name}`);
            console.log(`  音符数量: ${instrument.notes.length}`);
            
            // 处理每个音符
            instrument.notes.forEach((note, idx) => {
              if (idx < 3) { // 显示前3个音符作为示例
                console.log(`  音符 ${idx + 1}: 音高 ${note.pitch}, ` +
                           `起始 ${note.start}秒, 结束 ${note.end}秒, ` +
                           `力度 ${note.velocity}`);
              }
            });
          });
          
          // 保存 MIDI 数据到数据库或文件
          // processMidiData(task_id, data);
        }
        
      } else {
        // 任务失败
        console.log('MIDI 生成失败:', msg);
        
        // 处理失败场景...
      }
      
      // 返回 200 状态码确认收到回调
      res.status(200).json({ status: 'received' });
    });

    app.listen(3000, () => {
      console.log('回调服务器运行在 3000 端口');
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from flask import Flask, request, jsonify
    import json

    app = Flask(__name__)

    @app.route('/suno-midi-callback', methods=['POST'])
    def handle_callback():
        data = request.json
        
        code = data.get('code')
        msg = data.get('msg')
        task_id = data.get('task_id')
        callback_data = data.get('data', {})
        
        print(f"收到 MIDI 生成回调: {task_id}, 状态: {code}, 消息: {msg}")
        
        if code == 200:
            # 任务成功完成
            print("MIDI 生成完成")
            
            if callback_data and 'instruments' in callback_data:
                instruments = callback_data['instruments']
                print(f"检测到 {len(instruments)} 个乐器")
                
                for instrument in instruments:
                    name = instrument.get('name')
                    notes = instrument.get('notes', [])
                    print(f"\n乐器: {name}")
                    print(f"  音符数量: {len(notes)}")
                    
                    # 处理每个音符
                    for idx, note in enumerate(notes[:3]):  # 显示前3个音符
                        print(f"  音符 {idx + 1}: 音高 {note['pitch']}, "
                              f"起始 {note['start']}秒, 结束 {note['end']}秒, "
                              f"力度 {note['velocity']}")
                
                # 保存 MIDI 数据到文件
                with open(f"midi_{task_id}.json", "w") as f:
                    json.dump(callback_data, f, indent=2)
                print(f"MIDI 数据已保存到 midi_{task_id}.json")
                
        else:
            # 任务失败
            print(f"MIDI 生成失败: {msg}")
            
            # 处理失败场景...
        
        # 返回 200 状态码确认收到回调
        return jsonify({'status': 'received'}), 200

    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=3000)
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php
    header('Content-Type: application/json');

    // 获取 POST 数据
    $input = file_get_contents('php://input');
    $data = json_decode($input, true);

    $code = $data['code'] ?? null;
    $msg = $data['msg'] ?? '';
    $taskId = $data['task_id'] ?? '';
    $callbackData = $data['data'] ?? null;

    error_log("收到 MIDI 生成回调: $taskId, 状态: $code, 消息: $msg");

    if ($code === 200) {
        // 任务成功完成
        error_log("MIDI 生成完成");
        
        if ($callbackData && isset($callbackData['instruments'])) {
            $instruments = $callbackData['instruments'];
            error_log("检测到 " . count($instruments) . " 个乐器");
            
            foreach ($instruments as $instrument) {
                $name = $instrument['name'] ?? '';
                $notes = $instrument['notes'] ?? [];
                error_log("乐器: $name");
                error_log("  音符数量: " . count($notes));
                
                // 处理前3个音符作为示例
                foreach (array_slice($notes, 0, 3) as $idx => $note) {
                    error_log(sprintf(
                        "  音符 %d: 音高 %d, 起始 %s秒, 结束 %s秒, 力度 %s",
                        $idx + 1,
                        $note['pitch'],
                        $note['start'],
                        $note['end'],
                        $note['velocity']
                    ));
                }
            }
            
            // 保存 MIDI 数据到文件
            $filename = "midi_$taskId.json";
            file_put_contents($filename, json_encode($callbackData, JSON_PRETTY_PRINT));
            error_log("MIDI 数据已保存到 $filename");
        }
        
    } else {
        // 任务失败
        error_log("MIDI 生成失败: $msg");
        
        // 处理失败场景...
    }

    // 返回 200 状态码确认收到回调
    http_response_code(200);
    echo json_encode(['status' => 'received']);
    ?>
    ```
  </Tab>
</Tabs>

## 最佳实践

<Tip>
  ### 回调 URL 配置建议

  1. **使用 HTTPS**: 确保回调 URL 使用 HTTPS 协议以保证数据传输安全
  2. **验证来源**: 在回调处理中验证请求来源的合法性
  3. **幂等处理**: 同一 task\_id 可能收到多次回调,确保处理逻辑是幂等的
  4. **快速响应**: 回调处理应快速返回 200 状态码以避免超时
  5. **异步处理**: 复杂的业务逻辑(如 MIDI 文件转换)应异步处理
  6. **处理缺失乐器**: 不是所有乐器都会被检测到 - 优雅地处理空的或缺失的乐器数组
  7. **存储原始数据**: 保存完整的 JSON 响应以供未来参考和重新处理
</Tip>

<Warning>
  ### 重要提醒

  * 回调 URL 必须可从公网访问
  * 服务器必须在 15 秒内响应,否则将被视为超时
  * 如果连续 3 次重试失败,系统将停止发送回调
  * 请确保回调处理逻辑的稳定性,避免因异常导致回调失败
  * MIDI 数据保留 14 天 - 如需长期保存请及时下载保存
  * 检测到的乐器数量和类型取决于音频内容
  * 音符时间(start/end)可能是字符串或数字 - 需要处理这两种类型
</Warning>

## 故障排查

如果您没有收到回调通知,请检查以下内容:

<AccordionGroup>
  <Accordion title="网络连接问题">
    * 确认回调 URL 可从公网访问
    * 检查防火墙设置,确保入站请求未被阻止
    * 验证域名解析正确
  </Accordion>

  <Accordion title="服务器响应问题">
    * 确保服务器在 15 秒内返回 HTTP 200 状态码
    * 检查服务器日志是否有错误消息
    * 验证端点路径和 HTTP 方法正确
  </Accordion>

  <Accordion title="内容格式问题">
    * 确认收到的 POST 请求体为 JSON 格式
    * 检查 Content-Type 是否为 application/json
    * 验证 JSON 解析正确
    * 处理时间值的字符串和数字两种类型
  </Accordion>

  <Accordion title="数据处理问题">
    * 某些乐器可能有空的音符数组
    * 并非所有音频都会检测到所有乐器类型
    * 验证原始人声分离使用的是 `split_stem` 类型(而非 `separate_vocal`)
    * 检查源 taskId 是否来自成功完成的分离任务
  </Accordion>
</AccordionGroup>

## 替代方案

如果您无法使用回调机制,也可以使用轮询方式:

<Card title="轮询查询结果" icon="radar" href="/cn/suno-api/get-midi-details">
  使用获取 MIDI 生成详情接口定期查询任务状态。我们建议每 10-30 秒查询一次。
</Card>
