尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Python音频音轨分离调整

Python音频音轨分离调整
📅 发布时间:2026/7/7 7:25:08

```html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>音频音轨分离与时间偏移工具</title>

<style>

* {

box-sizing: border-box;

font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

}

body {

background: #f5f7fc;

min-height: 100vh;

display: flex;

justify-content: center;

align-items: center;

margin: 0;

padding: 20px;

}

.card {

max-width: 900px;

width: 100%;

background: #ffffff;

border-radius: 32px;

box-shadow: 0 20px 40px rgba(0, 10, 30, 0.12), 0 8px 20px rgba(0, 0, 0, 0.05);

padding: 30px 30px 40px;

transition: all 0.2s ease;

}

h1 {

font-size: 1.9rem;

font-weight: 600;

color: #0b1a33;

margin-top: 0;

margin-bottom: 8px;

letter-spacing: -0.3px;

display: flex;

align-items: center;

gap: 10px;

}

.subhead {

color: #4a5a72;

margin-top: -4px;

margin-bottom: 28px;

font-size: 0.95rem;

border-left: 4px solid #3b7cff;

padding-left: 16px;

background: #f0f4fe;

border-radius: 0 12px 12px 0;

line-height: 1.5;

}

.upload-area {

background: #f8faff;

border: 2px dashed #cbd5e1;

border-radius: 24px;

padding: 28px 20px;

text-align: center;

transition: 0.2s;

cursor: pointer;

margin-bottom: 28px;

}

.upload-area:hover {

background: #f0f6ff;

border-color: #3b7cff;

}

.upload-area input[type="file"] {

display: none;

}

.upload-label {

display: flex;

flex-direction: column;

align-items: center;

gap: 8px;

font-weight: 500;

color: #1e2b45;

}

.upload-label span:first-child {

font-size: 2.4rem;

}

.upload-label .hint {

font-size: 0.9rem;

color: #5f6f88;

font-weight: 400;

}

.file-status {

margin-top: 12px;

font-size: 0.95rem;

color: #0b1a33;

background: #e9effa;

padding: 8px 16px;

border-radius: 40px;

display: inline-block;

}

.track-list {

display: flex;

flex-direction: column;

gap: 16px;

margin: 24px 0 30px;

max-height: 420px;

overflow-y: auto;

padding-right: 4px;

}

.track-item {

background: #f9fbfe;

border-radius: 20px;

padding: 16px 20px;

display: flex;

flex-wrap: wrap;

align-items: center;

gap: 12px 18px;

border: 1px solid #e6edf6;

transition: 0.15s;

}

.track-item:hover {

background: #f2f7ff;

border-color: #b6cef0;

}

.track-index {

background: #d2def5;

color: #1d2d4a;

width: 32px;

height: 32px;

border-radius: 40px;

display: flex;

align-items: center;

justify-content: center;

font-weight: 600;

font-size: 0.9rem;

}

.track-name {

flex: 2 1 140px;

font-weight: 500;

color: #14273e;

white-space: nowrap;

overflow: hidden;

text-overflow: ellipsis;

}

.track-control {

display: flex;

align-items: center;

gap: 12px;

flex-wrap: wrap;

flex: 3 1 260px;

}

.track-control label {

display: flex;

align-items: center;

gap: 6px;

font-size: 0.9rem;

color: #2f405b;

white-space: nowrap;

}

.track-control input[type="range"] {

width: 120px;

height: 6px;

-webkit-appearance: none;

background: #d0dcee;

border-radius: 12px;

outline: none;

}

.track-control input[type="range"]::-webkit-slider-thumb {

-webkit-appearance: none;

width: 16px;

height: 16px;

border-radius: 20px;

background: #1e5eff;

cursor: pointer;

border: 2px solid white;

box-shadow: 0 2px 8px rgba(0, 60, 200, 0.3);

}

.offset-value {

min-width: 58px;

font-weight: 500;

color: #0a1b36;

background: #eaf0fa;

padding: 4px 10px;

border-radius: 30px;

text-align: center;

font-size: 0.9rem;

}

.track-actions {

display: flex;

gap: 8px;

margin-left: auto;

}

.btn-icon {

background: transparent;

border: 1px solid #ccd9ed;

border-radius: 30px;

padding: 6px 14px;

font-size: 0.8rem;

font-weight: 500;

color: #1f3b60;

cursor: pointer;

transition: 0.15s;

display: inline-flex;

align-items: center;

gap: 6px;

}

.btn-icon:hover {

background: #e2ecfe;

border-color: #3b7cff;

}

.btn-icon.solo {

background: #d9e6ff;

border-color: #3b7cff;

}

.btn-icon.solo:hover {

background: #c0d6ff;

}

.global-controls {

display: flex;

flex-wrap: wrap;

align-items: center;

justify-content: space-between;

gap: 18px;

padding-top: 18px;

border-top: 1px solid #e2eaf5;

margin-top: 6px;

}

.playback-buttons {

display: flex;

gap: 12px;

}

.btn-primary {

background: #1e5eff;

border: none;

color: white;

padding: 10px 28px;

border-radius: 60px;

font-weight: 600;

font-size: 1rem;

display: inline-flex;

align-items: center;

gap: 8px;

cursor: pointer;

transition: 0.15s;

box-shadow: 0 6px 14px rgba(30, 94, 255, 0.25);

border: 1px solid #1e5eff;

}

.btn-primary:hover {

background: #1048e0;

transform: scale(0.98);

box-shadow: 0 4px 10px rgba(30, 94, 255, 0.3);

}

.btn-secondary {

background: transparent;

border: 1px solid #b8cae3;

color: #1f3b60;

padding: 10px 22px;

border-radius: 60px;

font-weight: 500;

font-size: 1rem;

cursor: pointer;

transition: 0.15s;

}

.btn-secondary:hover {

background: #eaf1fd;

border-color: #3b7cff;

}

.btn-danger {

background: #f2f2f2;

border: 1px solid #d6d6d6;

color: #5f5f5f;

padding: 10px 22px;

border-radius: 60px;

font-weight: 500;

font-size: 1rem;

cursor: pointer;

transition: 0.15s;

}

.btn-danger:hover {

background: #ffe5e5;

border-color: #ff7a7a;

color: #a10000;

}

.status-badge {

font-size: 0.9rem;

background: #eef3f9;

padding: 6px 18px;

border-radius: 60px;

color: #1d3557;

display: inline-flex;

align-items: center;

gap: 8px;

}

.status-badge .dot {

width: 10px;

height: 10px;

border-radius: 10px;

background: #4caf50;

display: inline-block;

}

.status-badge .dot.inactive {

background: #a0aec0;

}

.footer-note {

margin-top: 20px;

font-size: 0.8rem;

color: #6b7d99;

text-align: center;

border-top: 1px solid #e5ecf5;

padding-top: 18px;

}

@media (max-width: 650px) {

.card { padding: 20px; }

.track-item { flex-direction: column; align-items: stretch; }

.track-control { flex-wrap: wrap; }

.track-actions { margin-left: 0; justify-content: flex-end; }

}

</style>

</head>

<body>

<div class="card">

<h1> 音轨分离 · 时间偏移</h1>

<div class="subhead">

上传音频 (MP3/WAV) 自动识别音轨 (最多8轨) · 拖动滑块独立调整播放开始时间

</div>

<!-- 上传区域 -->

<div class="upload-area" id="uploadArea">

<input type="file" id="fileInput" accept="audio/*" multiple>

<div class="upload-label">

<span></span>

<span><strong>点击上传 或 拖拽音频文件</strong></span>

<span class="hint">支持 MP3, WAV, AAC, OGG 等 (每个文件视为一个音轨)</span>

</div>

<div id="fileStatus" class="file-status">未加载音轨</div>

</div>

<!-- 音轨列表 -->

<div id="trackListContainer" class="track-list">

<!-- 动态生成 -->

</div>

<!-- 全局控制 -->

<div class="global-controls">

<div class="playback-buttons">

<button class="btn-primary" id="playBtn"> 播放</button>

<button class="btn-secondary" id="pauseBtn"> 暂停</button>

<button class="btn-secondary" id="stopBtn"> 停止</button>

</div>

<div>

<button class="btn-danger" id="resetBtn">↺ 重置偏移</button>

</div>

<div id="playStatus" class="status-badge"><span class="dot inactive"></span> 就绪</div>

</div>

<div class="footer-note">

每个音轨独立偏移 (单位: 秒) · 偏移量可正可负 · 所有音轨共享同一播放进度

</div>

</div>

<script>

(function() {

// ---------- DOM 引用 ----------

const fileInput = document.getElementById('fileInput');

const uploadArea = document.getElementById('uploadArea');

const fileStatus = document.getElementById('fileStatus');

const trackListContainer = document.getElementById('trackListContainer');

const playBtn = document.getElementById('playBtn');

const pauseBtn = document.getElementById('pauseBtn');

const stopBtn = document.getElementById('stopBtn');

const resetBtn = document.getElementById('resetBtn');

const playStatus = document.getElementById('playStatus');

const statusDot = playStatus.querySelector('.dot');

// ---------- 状态 ----------

let audioBuffers = []; // 存储每个音轨的 AudioBuffer

let trackOffsets = []; // 每个音轨的偏移量 (秒)

let trackNames = [];

let trackElements = []; // 用于 UI 更新

let audioContext = null;

let currentSources = []; // 当前播放的 source 节点

let gainNodes = []; // 每个音轨的增益节点 (用于独立控制)

let startTime = 0; // context.currentTime 开始播放的基准

let pausedAt = 0; // 暂停时的进度 (秒)

let isPlaying = false;

// 播放状态

const STATUS_IDLE = 'idle';

const STATUS_PLAYING = 'playing';

const STATUS_PAUSED = 'paused';

let playState = STATUS_IDLE;

// UI 更新防抖

let updateTimeout = null;

// ---------- 辅助函数 ----------

function formatOffset(sec) {

return sec >= 0 ? `+${sec.toFixed(2)}s` : `${sec.toFixed(2)}s`;

}

// 更新状态显示

function updatePlayStatus(state, msg) {

if (state === 'playing') {

statusDot.className = 'dot';

playStatus.innerHTML = `<span class="dot"></span> 播放中 ${msg ? '· ' + msg : ''}`;

} else if (state === 'paused') {

statusDot.className = 'dot inactive';

playStatus.innerHTML = `<span class="dot inactive"></span> 已暂停 ${msg ? '· ' + msg : ''}`;

} else {

statusDot.className = 'dot inactive';

playStatus.innerHTML = `<span class="dot inactive"></span> ${msg || '就绪'}`;

}

}

// 停止所有音轨 (释放资源)

function stopAllSources() {

currentSources.forEach(src => {

try { src.stop(0); } catch (e) {}

try { src.disconnect(); } catch (e) {}

});

currentSources = [];

gainNodes = [];

isPlaying = false;

playState = STATUS_IDLE;

updatePlayStatus('idle', '已停止');

}

// 重置播放状态 (不清理 buffer)

function resetPlaybackState() {

stopAllSources();

pausedAt = 0;

startTime = 0;

if (audioContext && audioContext.state !== 'closed') {

// 不关闭 context,避免重新创建

}

// 重置所有轨道 solo/静音状态视觉 (仅UI)

document.querySelectorAll('.solo').forEach(el => el.classList.remove('solo'));

updatePlayStatus('idle', '就绪');

}

// 构建音轨列表 UI

function renderTrackList() {

if (audioBuffers.length === 0) {

trackListContainer.innerHTML = `<div style="text-align:center; color:#7a8aa3; padding: 20px; background: #f7faff; border-radius: 40px;"> 上传音频文件后,音轨将在此显示</div>`;

return;

}

let html = '';

for (let i = 0; i < audioBuffers.length; i++) {

const name = trackNames[i] || `音轨 ${i+1}`;

const offset = trackOffsets[i] || 0;

const duration = audioBuffers[i] ? audioBuffers[i].duration.toFixed(2) : '--';

html += `

<div class="track-item" data-trackindex="${i}">

<div class="track-index">${i+1}</div>

<div class="track-name" title="${name}">${name}</div>

<div class="track-control">

<label>偏移 <input type="range" min="-10" max="10" step="0.05" value="${offset}" data-track="${i}" class="offset-slider"></label>

<span class="offset-value" id="offsetDisplay_${i}">${formatOffset(offset)}</span>

<span style="font-size:0.75rem; color:#5e7490;">(时长 ${duration}s)</span>

</div>

<div class="track-actions">

<button class="btn-icon solo-track" data-track="${i}"> 独奏</button>

</div>

</div>

`;

}

trackListContainer.innerHTML = html;

// 绑定滑块事件

document.querySelectorAll('.offset-slider').forEach(slider => {

slider.addEventListener('input', function(e) {

const idx = parseInt(this.dataset.track, 10);

const val = parseFloat(this.value);

trackOffsets[idx] = val;

const display = document.getElementById(`offsetDisplay_${idx}`);

if (display) display.textContent = formatOffset(val);

// 如果正在播放,需要重新调度 (简单起见,停止并重置)

if (isPlaying || playState === 'playing' || playState === 'paused') {

// 如果正在播放,停止并变为暂停状态

if (isPlaying) {

stopAllSources();

// 保存当前进度

if (audioContext) {

const elapsed = audioContext.currentTime - startTime;

pausedAt = Math.max(0, elapsed);

} else {

pausedAt = 0;

}

isPlaying = false;

playState = STATUS_PAUSED;

updatePlayStatus('paused', `偏移已调整, 进度 ${pausedAt.toFixed(2)}s`);

} else {

// 暂停状态下只更新显示

}

}

});

});

// 绑定独奏按钮

document.querySelectorAll('.solo-track').forEach(btn => {

btn.addEventListener('click', function(e) {

const idx = parseInt(this.dataset.track, 10);

// 切换独奏样式 (视觉反馈)

this.classList.toggle('solo');

// 实际独奏逻辑: 如果正在播放,重新播放只播放此轨 (或者静音其他)

if (audioBuffers.length === 0) return;

if (isPlaying || playState === 'playing' || playState === 'paused') {

// 简单处理:停止,然后以独奏方式播放

const wasPlaying = isPlaying;

let currentProgress = 0;

if (audioContext) {

if (wasPlaying) {

currentProgress = audioContext.currentTime - startTime;

} else {

currentProgress = pausedAt;

}

}

stopAllSources();

// 如果独奏按钮被激活 (class包含solo) 则只播放该轨, 否则全部播放

const isSolo = this.classList.contains('solo');

// 重新播放

if (audioBuffers.length > 0) {

// 播放所有轨道,但是增益控制:独奏轨增益1,其他0

const gains = audioBuffers.map((_, i) => {

if (isSolo) {

return i === idx ? 1 : 0;

} else {

return 1; // 取消独奏,全部播放

}

});

// 如果所有轨道都被独奏? 只播放当前轨道

playAllTracks(gains, currentProgress);

}

} else {

// 未播放,仅UI反馈

}

});

});

}

// 播放所有音轨 (支持增益数组)

function playAllTracks(gainArray, seekTime = 0) {

if (!audioContext || audioContext.state === 'closed') {

audioContext = new (window.AudioContext || window.webkitAudioContext)();

}

if (audioContext.state === 'suspended') {

audioContext.resume();

}

// 停止之前的播放

stopAllSources();

if (audioBuffers.length === 0) return;

const currentTime = audioContext.currentTime;

// 如果 seekTime 大于0, 从指定位置开始

const offset = Math.max(0, seekTime);

// 计算每个音轨的实际起始时间 (考虑偏移)

// 偏移可以为负,但起始时间不能小于0,使用 clamp

const baseStart = currentTime;

// 保存 source 和 gain 节点

const sources = [];

const gains = [];

for (let i = 0; i < audioBuffers.length; i++) {

const buffer = audioBuffers[i];

if (!buffer) continue;

const gainVal = (gainArray && gainArray[i] !== undefined) ? gainArray[i] : 1;

if (gainVal === 0) continue; // 跳过静音轨

const offsetSec = trackOffsets[i] || 0;

// 实际播放起始位置: 从 offset 开始播放

let startOffset = Math.max(0, offsetSec); // 如果偏移为正, 从偏移位置开始

let delay = 0;

if (offsetSec < 0) {

// 负偏移: 延迟播放

delay = Math.abs(offsetSec);

startOffset = 0;

相关新闻

  • MD5哈希查询工具原理与应用:从算法特性到安全实践
  • BOM双sheet分区表:代理商压货场景下 passive 与 IC 怎么拆、字段怎么填
  • 基础使用2.1 基础模型

最新新闻

  • LyricsX:重构macOS音乐体验的歌词引擎
  • 计算机毕业设计之jsp母婴用品购物网站
  • 造形家:轻量化在线城市建筑三维AI建模工具测评
  • 5个维度深度解析:如何用G-Helper重构你的华硕硬件控制体验
  • 密码签名与加密方案安全性分析:从原理到实战选型与运维
  • 碧蓝航线自动化脚本终极指南:24小时智能管家解放你的双手

日新闻

  • Android逆向分析全能助手:集成化工具链与自动化工作流设计
  • 面搜索(Faceted Search)原理与工程实践指南
  • 神经网络调参避坑指南:从5个常见Loss曲线形态定位超参数问题

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号