Files
audio-cutter-pwa/index.html

164 lines
6.8 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Cutter Pro 2026</title>
<script src="wavesurfer.min.js"></script>
<script src="plugins/regions.min.js"></script>
<style>
:root { --bg: #0f172a; --card: #1e293b; --accent: #3b82f6; }
body { font-family: sans-serif; background: var(--bg); color: white; display: flex; flex-direction: column; align-items: center; padding: 20px; }
.card { background: var(--card); padding: 20px; border-radius: 15px; width: 100%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
#waveform { background: #000; border-radius: 10px; margin: 20px 0; border: 1px solid #334155; }
.info { display: flex; justify-content: space-between; font-family: monospace; color: #94a3b8; margin-bottom: 10px; }
.controls { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
button { padding: 10px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: 0.2s; }
.btn-play { background: #10b981; color: white; }
.btn-stop { background: #ef4444; color: white; }
.btn-export { background: var(--accent); color: white; }
.btn-zoom { background: #475569; color: white; }
.btn-share { background: #f59e0b; color: white; display: none; }
#status { font-size: 0.8rem; margin-top: 10px; text-align: center; color: #64748b; }
</style>
</head>
<body>
<div class="card">
<h3>Cortador Pro (Misma Calidad)</h3>
<input type="file" id="audioInput" accept="audio/*">
<div id="waveform"></div>
<div class="info">
<span id="totalTime">Total: 00:00</span>
<span id="selectionTime">Selección: 00:00 - 00:00</span>
</div>
<div class="controls">
<button class="btn-zoom" onclick="ws.zoom(ws.options.minPxPerSec + 30)">Zoom +</button>
<button class="btn-zoom" onclick="ws.zoom(ws.options.minPxPerSec - 30)">Zoom -</button>
<button class="btn-play" id="btnPlay">Probar</button>
<button class="btn-stop" id="btnStop">Parar</button>
<button class="btn-export" id="btnExport">Exportar</button>
<button class="btn-share" id="btnShare">Compartir</button>
</div>
<p id="status">Listo.</p>
</div>
<script>
let ws, wsRegions, activeRegion, lastBlob, originalFileType;
const audioInput = document.getElementById('audioInput');
const status = document.getElementById('status');
ws = WaveSurfer.create({
container: '#waveform',
waveColor: '#4f46e5',
progressColor: '#818cf8',
height: 120
});
wsRegions = ws.registerPlugin(WaveSurfer.Regions.create());
audioInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
originalFileType = file.type; // Guardamos el formato original
ws.load(URL.createObjectURL(file));
document.getElementById('btnShare').style.display = 'none';
}
};
ws.on('ready', () => {
const duration = ws.getDuration();
document.getElementById('totalTime').innerText = `Total: ${formatTime(duration)}`;
wsRegions.clearRegions();
activeRegion = wsRegions.addRegion({
start: 0,
end: Math.min(duration, 10),
color: 'rgba(59, 130, 246, 0.3)',
drag: true, resize: true
});
updateSelectionLabel();
});
wsRegions.on('region-updated', updateSelectionLabel);
function updateSelectionLabel() {
if (activeRegion) {
document.getElementById('selectionTime').innerText =
`Selección: ${formatTime(activeRegion.start)} - ${formatTime(activeRegion.end)}`;
}
}
function formatTime(s) {
return new Date(s * 1000).toISOString().substr(14, 5);
}
document.getElementById('btnPlay').onclick = () => activeRegion && activeRegion.play();
document.getElementById('btnStop').onclick = () => ws.stop();
// EXPORTACIÓN CON MEDIARECORDER (Mantiene formato de origen/comprimido)
document.getElementById('btnExport').onclick = async () => {
if (!activeRegion) return;
status.innerText = "Procesando...";
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const sourceBuffer = ws.getDecodedData();
const duration = activeRegion.end - activeRegion.start;
const offlineCtx = new OfflineAudioContext(
sourceBuffer.numberOfChannels,
duration * sourceBuffer.sampleRate,
sourceBuffer.sampleRate
);
const source = offlineCtx.createBufferSource();
source.buffer = sourceBuffer;
source.connect(offlineCtx.destination);
source.start(0, activeRegion.start, duration);
const renderedBuffer = await offlineCtx.startRendering();
// Grabación del Stream para comprimir
const destination = audioCtx.createMediaStreamDestination();
const recorder = new MediaRecorder(destination.stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
lastBlob = new Blob(chunks, { type: originalFileType || 'audio/mp4' });
const url = URL.createObjectURL(lastBlob);
const a = document.createElement('a');
a.href = url;
a.download = `cut_${audioInput.files[0].name}`;
a.click();
status.innerText = "¡Exportado!";
document.getElementById('btnShare').style.display = 'inline-block';
};
const playSource = audioCtx.createBufferSource();
playSource.buffer = renderedBuffer;
playSource.connect(destination);
recorder.start();
playSource.start();
playSource.onended = () => recorder.stop();
};
// COMPARTIR (Botón 3 bolitas nativo Android)
document.getElementById('btnShare').onclick = async () => {
if (!lastBlob) return;
const file = new File([lastBlob], `cut_${audioInput.files[0].name}`, { type: lastBlob.type });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
files: [file],
title: 'Audio Cortado',
text: 'Compartido desde Audio Cutter PWA'
});
}
};
</script>
</body>
</html>