Python音频音轨分离调整

发布时间:2026/7/7 7:15:06

Python音频音轨分离调整 html!DOCTYPE htmlhtml langzh-CNheadmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title音频音轨分离与时间偏移工具/titlestyle* {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[typefile] {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[typerange] {width: 120px;height: 6px;-webkit-appearance: none;background: #d0dcee;border-radius: 12px;outline: none;}.track-control input[typerange]::-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/headbodydiv classcardh1 音轨分离 · 时间偏移/h1div classsubhead上传音频 (MP3/WAV) 自动识别音轨 (最多8轨) · 拖动滑块独立调整播放开始时间/div!-- 上传区域 --div classupload-area iduploadAreainput typefile idfileInput acceptaudio/* multiplediv classupload-labelspan/spanspanstrong点击上传 或 拖拽音频文件/strong/spanspan classhint支持 MP3, WAV, AAC, OGG 等 (每个文件视为一个音轨)/span/divdiv idfileStatus classfile-status未加载音轨/div/div!-- 音轨列表 --div idtrackListContainer classtrack-list!-- 动态生成 --/div!-- 全局控制 --div classglobal-controlsdiv classplayback-buttonsbutton classbtn-primary idplayBtn 播放/buttonbutton classbtn-secondary idpauseBtn 暂停/buttonbutton classbtn-secondary idstopBtn 停止/button/divdivbutton classbtn-danger idresetBtn↺ 重置偏移/button/divdiv idplayStatus classstatus-badgespan classdot inactive/span 就绪/div/divdiv classfooter-note每个音轨独立偏移 (单位: 秒) · 偏移量可正可负 · 所有音轨共享同一播放进度/div/divscript(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 []; // 存储每个音轨的 AudioBufferlet 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 classdot/span 播放中 ${msg ? · msg : };} else if (state paused) {statusDot.className dot inactive;playStatus.innerHTML span classdot inactive/span 已暂停 ${msg ? · msg : };} else {statusDot.className dot inactive;playStatus.innerHTML span classdot 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, 就绪);}// 构建音轨列表 UIfunction renderTrackList() {if (audioBuffers.length 0) {trackListContainer.innerHTML div styletext-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] || 音轨 ${i1};const offset trackOffsets[i] || 0;const duration audioBuffers[i] ? audioBuffers[i].duration.toFixed(2) : --;html div classtrack-item data-trackindex${i}div classtrack-index${i1}/divdiv classtrack-name title${name}${name}/divdiv classtrack-controllabel偏移 input typerange min-10 max10 step0.05 value${offset} data-track${i} classoffset-slider/labelspan classoffset-value idoffsetDisplay_${i}${formatOffset(offset)}/spanspan stylefont-size:0.75rem; color:#5e7490;(时长 ${duration}s)/span/divdiv classtrack-actionsbutton classbtn-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其他0const 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使用 clampconst 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;

相关新闻