Durante meses, la herramienta principal de recortar audio a MP3 en convertiraudioamp3.com estaba pensada sobre todo para escritorio: vista de ondas, selección visual, teclado, ratón… En móvil funcionaba, pero la experiencia no era tan sencilla como quería, especialmente para algo tan cotidiano como recortar una nota de voz de WhatsApp.
En este artículo cuento cómo he adaptado el recortador para funcionar mejor en móvil creando un “modo simple”, apoyándonos en la arquitectura de jobs que ya usaba para el recortador avanzado y el mezclador de audio online.
Arquitectura: jobs asíncronos con FastAPI + FFmpeg
Tanto el recortador como el mezclador comparten la misma base:
Backend en FastAPI
Procesado de audio con FFmpeg
Un sistema de jobs que:
- crea un “trabajo” con un job_id
- procesa el audio en segundo plano
- expone un endpoint de estado para ver el progreso
- expone un endpoint de descarga cuando está listo
La función central que usamos en ambos casos es algo como:
def create_mix_job_from_files_and_spec(
files: List[UploadFile],
data: dict,
) -> tuple[str, Dict[str, Any]]:
calidad = str(data.get("calidad_mp3", "128k"))
items = data.get("items", [])
if not isinstance(items, list) or not items:
raise ValueError("items vacío")
# Guardar subidas en disco
uploaded_paths: List[str] = []
for f in files:
unique = uuid.uuid4().hex
safe_name = f.filename or "audio"
dst = UPLOAD_DIR / f"{unique}_{safe_name}"
with dst.open("wb") as buf:
shutil.copyfileobj(f.file, buf)
uploaded_paths.append(str(dst))
# Asegurar src_idx coherente
for i, it in enumerate(items):
if "src_idx" not in it:
it["src_idx"] = i if i < len(uploaded_paths) else (len(uploaded_paths) - 1)
job_id = uuid.uuid4().hex
job = {
"id": job_id,
"status": "queued",
"progress": 0.0,
"step": "queued",
"error": None,
"bitrate": calidad,
"items": items,
"uploaded_paths": uploaded_paths,
"result_path": None,
"created_at": datetime.utcnow().isoformat(),
}
JOBS[job_id] = job
_write_status_file(job) # Persistimos estado en disco
EXECUTOR.submit(_run_mix_job, job) # Lanzamos FFmpeg en background
return job_id, job
La clave es el campo items: una lista de segmentos a procesar (inicio, fin, crossfade, etc.). Para recortar un solo audio, simplemente mandamos un solo item con inicio y fin.
Modo simple de recorte: centrado en el móvil
La página principal de recorte tiene vista de ondas, JS, interacción más avanzada… Para móvil queríamos algo ultra directo:
- Subes un archivo de audio (WhatsApp, M4A, OGG, WAV, MP3…)
- Escribes segundo de inicio y de fin
- El servidor recorta y te devuelve un MP3
En FastAPI definimos un modo simple separado:
@router.get(
"/recortar-audio-mp3/simple",
name="recortar_audio_mp3_simple",
tags=["seo"],
)
def recortar_audio_mp3_simple(request: Request):
"""
Versión simple: 1 archivo + inicio/fin + calidad.
Canonical → /recortar-audio-mp3.
"""
ctx = build_ctx_simple_es(request, with_recaptcha=True)
resp = render_es(request, "recortar-audio-mp3-simple.html", ctx)
return _no_cache(resp)
El build_ctx_simple_es se encarga de montar el contexto para la plantilla (valores por defecto, hreflang, etc.) y _no_cache añade cabeceras para evitar caches “rebeldes” (ahora lo vemos).
En el POST hacemos:
- Validar que fin > inicio
- Verificar reCAPTCHA
- Construir el spec_data con un solo item
- Crear el job con create_mix_job_from_files_and_spec
- Redirigir a la página de estado
@router.post(
"/recortar-audio-mp3/simple",
name="recortar_audio_mp3_simple_post",
)
async def recortar_audio_mp3_simple_post(
request: Request,
file: UploadFile = File(...),
inicio: float = Form(...),
fin: float = Form(...),
calidad_mp3: str = Form("128k"),
recaptcha_token: str = Form(""),
):
if fin <= inicio:
# devolver error de validación al formulario
...
ok = await verificar_recaptcha(recaptcha_token, request.client.host)
if not ok:
# mostrar error de reCAPTCHA al usuario
...
spec_data = {
"calidad_mp3": calidad_mp3,
"items": [
{
"nombre": file.filename or "audio",
"inicio": float(inicio),
"fin": float(fin),
"crossfade_s": 0.0, # sin crossfade; es un simple recorte
}
],
}
job_id, _job = create_mix_job_from_files_and_spec([file], spec_data)
url = request.url_for("recortar_audio_mp3_simple_status", job_id=job_id)
return RedirectResponse(url=url, status_code=303)
Pantalla de estado: caches, móvil y progreso real
En escritorio todo parecía ir bien: la página de estado iba actualizando progreso y al final mostraba el botón de descarga.
En móvil (sobre todo en algunos navegadores), nos encontramos con dos problemas:
- Caché agresiva: al recargar, el navegador mostraba siempre el mismo HTML (por ejemplo, “20 % — Normalizando”), aunque el job ya hubiera terminado.
- Porcentaje calculado en cliente: el progreso estimado se basaba en un step textual ("normalize 1/3", "trim 2/5", etc.), lo que hacía más fácil que se quedara “atascado” visualmente si intermedia algo se cacheaba.
Solución 1: desactivar caché de forma explícita
En la capa Python, envolvemos la respuesta con un helper _no_cache que añade cabeceras:
def _no_cache(resp: Response) -> Response:
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0, private"
resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0"
return resp
Y lo usamos siempre en las vistas de estado:
resp = render_es(request, "recortar-audio-mp3-simple-status.html", ctx)
return _no_cache(resp)
En la plantilla también añadimos meta robots para no indexar las URLs de job:
{% block meta_extra %}
<meta name="robots" content="noindex,nofollow">
{% if auto_refresh %}
<meta http-equiv="refresh" content="15">
{% endif %}
{% endblock %}
Fíjate en que el refresh no lleva url=..., solo el número de segundos; así evitamos que el navegador “combine” cosas raras con la caché previa.
Solución 2: usar el progress del backend
El endpoint de estado (/mezclar-audio/status/{job_id}) ya devolvía un campo numérico progress (0–100). En lugar de recomputar el porcentaje en función de step, usamos directamente lo que diga el backend:
status = js.get("status", "unknown")
step = js.get("step") or ""
error_msg = js.get("error") or None
raw_progress = js.get("progress", 0.0)
try:
pct = float(raw_progress or 0.0)
except (TypeError, ValueError):
pct = 0.0
pct = max(0.0, min(100.0, pct))
if status == "done":
pct = 100.0
Para el texto “bonito” seguimos usando el step
_p, label = step_to_pct_label_es(step)
En la plantilla, el progreso se pinta con algo muy simple:
<div class="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div class="bg-indigo-600 h-3"
style="width: {{ (pct or 5.0)|round(0) }}%;"></div>
</div>
Con esto, el porcentaje ya no se queda “congelado” en móvil: cada petición GET devuelve un HTML fresco con el valor real de progress que va escribiendo el worker mientras FFmpeg avanza.
Mezclar varios audios en el móvil (también sin apps)
Además del recortador, he preparado un
mezclador simple de audios pensado para móvil
, también 100 % online, sin registros, y sin instalar aplicaciones.
En vez de trabajar con formas de onda avanzadas, este modo simple te deja:
- Elegir varios audios a la vez (notas de voz de WhatsApp, MP3, M4A, OGG, WAV…).
- Definir un crossfade global en segundos (por ejemplo, 2 s entre cada pista).
- Elegir la calidad MP3 de salida.
- Lanzar el proceso y recibir un solo MP3 mezclado, listo para compartir.
A nivel técnico, por debajo uso exactamente el mismo sistema de jobs asíncronos con FFmpeg que en el recortador:
- Cada archivo se normaliza y se convierte a un formato interno homogéneo (WAV 44.1 kHz, 16-bit).
- Generamos una lista de items con inicio, fin y crossfade_s para cada pista.
- El backend concatena los clips y aplica acrossfade entre uno y otro cuando procede, exportando el resultado en MP3 con el bitrate elegido.
Eso nos permite mantener el mezclador:
- Ligero en el navegador móvil (el trabajo pesado va al servidor).
- Robusto ante audios largos (lo mismo: todo se hace por job, no en el hilo principal).
- Cohesivo con el resto de la plataforma (misma API de mezcla/recorte, mismo sistema de progreso).
👉 Para el usuario final el flujo es:
subir varios audios → elegir segs de crossfade y calidad → ver la página de progreso → descargar el MP3 final.
Recortar audios a MP3 en el móvil — Preguntas frecuentes ❓
¿Puedo recortar notas de voz de WhatsApp sin instalar aplicaciones?
Sí. Solo tienes que:
Abrir el navegador de tu móvil (Chrome, Safari, etc.).
Guardar la nota de voz de WhatsApp como archivo de audio.
Subirla al recortador simple.
Indicar segundos de inicio y fin, y descargar el MP3 recortado.
Todo se hace online, sin instalar apps adicionales.
¿Funciona tanto en Android como en iPhone?
Sí. El recortador y el mezclador funcionan en cualquier dispositivo con un navegador moderno:
- Android (Chrome, Firefox, etc.).
- iPhone (Safari, Chrome).
Solo necesitas conexión a Internet y capacidad para seleccionar el archivo de audio desde el móvil.
¿Es gratis recortar audio a MP3?
Sí, el uso de la herramienta es gratis. No pedimos registro y no hay planes de pago ocultos para recortar o mezclar audios. Solo subes tu archivo, eliges el recorte y descargas tu MP3.
¿Puedo usarlo también para mezclar varios audios en el móvil?
Sí. Además del recortador, tienes un modo simple de mezcla de audios pensado para móvil:
Eliges varios archivos en el selector.
Configuras un crossfade en segundos (por ejemplo, 2 s entre pistas).
Descargas un único MP3 mezclado.
Es ideal para juntar varias notas de voz o clips cortos sin tener que instalar un editor de audio complejo.
¿Qué formatos de entrada soporta el recortador móvil?
Los formatos más habituales de audio en móvil, entre ellos:
OPUS (muy común en WhatsApp),
AMR, OGG, M4A,
WAV y MP3.
Independientemente del formato de entrada, la salida es siempre MP3, para máxima compatibilidad con reproductores, coches, apps de mensajería y redes sociales.
¿Qué calidad MP3 debería elegir para el recorte?
Depende del uso:
Para voz / notas de WhatsApp:
64k o 96k suelen ser suficientes y generan archivos muy ligeros.
Para música o mezclas:
128k–192k ofrecen buena calidad; 256k–320k son opciones “máxima calidad”.
En el modo simple dejamos 128k como valor por defecto porque equilibra calidad y peso del archivo.
Top comments (0)