{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "intro",
   "metadata": {},
   "source": [
    "# Corrección de bug metodológico: fundamental del roncón — floréu d'entamu, Juan Huerta (1976)\n",
    "\n",
    "Cuaderno de corrección sobre [`analisis-mir-juan-huerta-1976-v2`](https://labs.tever.es/research/analisis-mir-juan-huerta-1976),\n",
    "que reportaba la fundamental del roncón en **145.3 Hz (D3)**. Ese resultado tenía un bug real de\n",
    "fuga espectral: 145.3 Hz está a solo 1 bin (5.4 Hz, con la resolución `nperseg=8192` usada allí)\n",
    "del hum de red eléctrica medido en 150.7 Hz, y coincide casi exactamente con el propio 3er\n",
    "armónico del hum tal y como v2 lo había localizado (48.4 Hz x 3 = 145.2 Hz). Dos indicios\n",
    "independientes de que 145.3 Hz era artefacto del hum, no el roncón real.\n",
    "\n",
    "**Autoría:** Tever Díaz García · **Fecha de este cuaderno:** 2026-09-29\n",
    "\n",
    "## Una segunda trampa detectada durante la propia corrección\n",
    "\n",
    "Al diseñar este cuaderno se descubrió que el problema no se resolvía solo ampliando la banda de\n",
    "exclusión alrededor del número heredado de v2 (48.4 Hz): el candidato que emergía como dominante\n",
    "tras esa ampliación (**250.32 Hz**, con aparente estabilidad temporal perfecta) resultaba estar, a\n",
    "su vez, a solo 1.3 Hz del 5º armónico de la fundamental REAL del hum, que este cuaderno relocaliza\n",
    "con mayor resolución en **49.80 Hz** (no 48.4 Hz — el propio v2 la había localizado de forma\n",
    "imprecisa, con una resolución 4x más gruesa). Ampliar una banda de exclusión alrededor de un valor\n",
    "heredado sin relocalizarlo con precisión reproduce el mismo tipo de error un nivel más abajo. Este\n",
    "cuaderno documenta ambas trampas explícitamente, no solo la primera.\n",
    "\n",
    "## Fuente del audio\n",
    "\n",
    "Mismo corte que los experimentos v1/v2: **Archivo Sonoro de Celso Peyroux**, casete n.º 1 (Oviedo,\n",
    "abril de 1976), intérprete **Juan Huerta** (La Torre, Teverga), floréu d'entamu de\n",
    "*\"Entemediu 3/8\"*, primeros **19 segundos**, `audio-recorte.wav` idéntico y sin modificar.\n",
    "\n",
    "## Dependencias y versiones (entorno verificado en esta ejecución)\n",
    "\n",
    "```bash\n",
    "pip install librosa==0.11.0 numpy scipy matplotlib soundfile\n",
    "```\n",
    "\n",
    "Mismo entorno que v2: `librosa==0.11.0`, `numpy==1.26.4`, `scipy==1.17.1`, `matplotlib==3.6.3`.\n",
    "\n",
    "## Método (fijado antes de ver el resultado final)\n",
    "\n",
    "1. **Relocalizar la fundamental del hum de red** con la máxima resolución espectral disponible\n",
    "   (Welch, `nperseg=32768`, 1.35 Hz/bin) — no asumir el valor heredado de v2 (48.4 Hz), calculado\n",
    "   con una resolución 4x más gruesa (`nperseg=8192`, 5.38 Hz/bin).\n",
    "2. **Banda de exclusión de hum ampliada** a ±8 Hz de cualquier armónico de la fundamental\n",
    "   relocalizada (cubre con margen los ~5.4 Hz de fuga observados en el bug original de v2).\n",
    "3. **Resolución espectral nativa** (sin zero-padding) sobre ventanas de 1 s (44100 muestras\n",
    "   -> 1 Hz/bin real) para separar limpiamente picos vecinos que a menor resolución (o con\n",
    "   zero-padding, que solo interpola) se funden en un bin engañoso.\n",
    "4. **Consenso temporal robusto**: se descartan las ventanas de baja energía (candidato con menos\n",
    "   de 20 dB de margen sobre el suelo de ruido de esa ventana — indicativo de transición/ataque, no\n",
    "   de régimen estable del dron) y se toma la **moda** (valor más repetido, redondeado a 1 Hz) del\n",
    "   resto como fundamental candidata. El umbral de 20 dB se fija por consistencia con el criterio de\n",
    "   prominencia ya usado en v2/P2, antes de ver qué ventanas caen fuera.\n",
    "5. Antes de aceptar cualquier candidato, se reporta explícitamente su **distancia en Hz y en bins**\n",
    "   al armónico del hum (relocalizado) más cercano, y se descarta si esa distancia es menor de\n",
    "   **1.5x la resolución espectral empleada**.\n",
    "\n",
    "Ningún parámetro de este método se modificó después de ver el resultado final. El notch (P4 de v2)\n",
    "y las cifras de HPSS que dependían de la banda de referencia del roncón se recalculan con la\n",
    "frecuencia corregida, con el mismo método que v2 (`iirnotch` Q=20, `librosa.effects.hpss()` por\n",
    "defecto) — solo cambia la frecuencia de referencia, no la técnica.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "deps",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "librosa: 0.11.0\n",
      "numpy: 1.26.4\n",
      "scipy: 1.17.1\n",
      "matplotlib: 3.6.3"
     ]
    }
   ],
   "source": [
    "import math\n",
    "from collections import Counter\n",
    "\n",
    "import librosa\n",
    "import librosa.display\n",
    "import matplotlib\n",
    "matplotlib.use(\"Agg\")  # cambia a \"inline\" si ejecutas esto en Jupyter interactivo\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import scipy.signal\n",
    "\n",
    "print(\"librosa:\", librosa.__version__)\n",
    "print(\"numpy:\", np.__version__)\n",
    "print(\"scipy:\", scipy.__version__)\n",
    "print(\"matplotlib:\", matplotlib.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "load-md",
   "metadata": {},
   "source": [
    "## Cargar el audio\n",
    "\n",
    "Mismo corte de 19 s, `sr=None` conserva la frecuencia de muestreo original (44100 Hz)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "load",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "sr=44100 Hz, duración=19.00 s, muestras=837900"
     ]
    }
   ],
   "source": [
    "y, sr = librosa.load(\"audio-recorte.wav\", sr=None, mono=True)\n",
    "duracion = librosa.get_duration(y=y, sr=sr)\n",
    "print(f\"sr={sr} Hz, duración={duracion:.2f} s, muestras={len(y)}\")\n",
    "\n",
    "\n",
    "def cents_vs_nota(f, nota):\n",
    "    return 1200 * math.log2(f / librosa.note_to_hz(nota))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p0-md",
   "metadata": {},
   "source": [
    "## Paso 0 — Relocalizar la fundamental del hum con alta resolución\n",
    "\n",
    "No se asume el valor heredado de v2 (48.4 Hz, calculado con `nperseg=8192`, 5.38 Hz/bin). Se\n",
    "relocaliza con Welch a `nperseg=32768` (1.35 Hz/bin) sobre la banda 20-60 Hz, donde debe estar la\n",
    "fundamental de un hum de red de 50 Hz nominal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p0",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 0: relocalización de la fundamental del hum (alta resolución) ===\n",
      "Fundamental del hum relocalizada: 49.80 Hz (resolución Welch: 1.35 Hz/bin)\n",
      "(v2 había reportado 48.4 Hz con nperseg=8192, resolución 5.38 Hz/bin -- diferencia de 1.40 Hz, dentro del margen de error esperable de esa resolución más gruesa)\n",
      "3er armónico: 149.39 Hz | 5º armónico: 248.98 Hz"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 0: relocalización de la fundamental del hum (alta resolución) ===\")\n",
    "freqs_w, psd_w = scipy.signal.welch(y, sr, nperseg=32768)\n",
    "psd_db = 10 * np.log10(psd_w + 1e-12)\n",
    "mask_low = (freqs_w > 20) & (freqs_w < 60)\n",
    "peaks, props = scipy.signal.find_peaks(psd_db[mask_low], prominence=3)\n",
    "fr_low = freqs_w[mask_low]\n",
    "if len(peaks) == 0:\n",
    "    raise RuntimeError(\"No se encontró pico de hum en 20-60 Hz con Welch nperseg=32768\")\n",
    "idx_max_prom = peaks[np.argmax(props[\"prominences\"])]\n",
    "HUM_FUNDAMENTAL = float(fr_low[idx_max_prom])\n",
    "print(f\"Fundamental del hum relocalizada: {HUM_FUNDAMENTAL:.2f} Hz \"\n",
    "      f\"(resolución Welch: {sr/32768:.2f} Hz/bin)\")\n",
    "print(f\"(v2 había reportado 48.4 Hz con nperseg=8192, resolución {sr/8192:.2f} Hz/bin -- \"\n",
    "      f\"diferencia de {abs(HUM_FUNDAMENTAL-48.4):.2f} Hz, dentro del margen de error esperable \"\n",
    "      f\"de esa resolución más gruesa)\")\n",
    "print(f\"3er armónico: {HUM_FUNDAMENTAL*3:.2f} Hz | 5º armónico: {HUM_FUNDAMENTAL*5:.2f} Hz\")\n",
    "\n",
    "\n",
    "def es_armonico_de_hum(f, tol_hz):\n",
    "    k = round(f / HUM_FUNDAMENTAL)\n",
    "    return abs(f - k * HUM_FUNDAMENTAL) < tol_hz\n",
    "\n",
    "\n",
    "TOL_HUM_HZ = 8.0  # justificado: cubre con margen la fuga observada (5.4 Hz) sin\n",
    "                   # tragarse candidatos reales a más de esa distancia"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p1-md",
   "metadata": {},
   "source": [
    "## Paso 1 — Diagnóstico: por qué los dos candidatos previos eran fuga espectral\n",
    "\n",
    "Se muestra la distancia exacta de los dos candidatos descartados (145.3 Hz de v2 y 250.32 Hz, el\n",
    "candidato intermedio que emergió durante el propio diseño de este cuaderno) al armónico de hum más\n",
    "cercano, y se visualiza con resolución nativa (sin zero-padding, 1 Hz/bin real en una ventana de\n",
    "1 s) el valle espectral que separa el hum del roncón real."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p1",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 1: diagnóstico de los dos candidatos descartados por fuga espectral ===\n",
      "Candidato v2 (145.3 Hz): distancia al 3er armónico del hum relocalizado (149.39 Hz) = 4.09 Hz\n",
      "Candidato intermedio de este cuaderno (250.32 Hz): distancia al 5º armónico del hum relocalizado (248.98 Hz) = 1.34 Hz\n",
      "Ambos por debajo del umbral de seguridad (1.5 Hz a resolución nativa 1 Hz/bin) -> ambos DESCARTADOS como candidatos válidos.\n",
      "\n",
      "Ventana 16-17s, resolución nativa (1 Hz/bin, sin zero-padding), banda 120-160 Hz:\n",
      "   120.0 Hz     6.5 dB\n",
      "   121.0 Hz     7.7 dB\n",
      "   122.0 Hz     9.9 dB\n",
      "   123.0 Hz     6.4 dB\n",
      "   124.0 Hz    -2.8 dB\n",
      "   125.0 Hz    -1.5 dB\n",
      "   126.0 Hz     1.1 dB\n",
      "   127.0 Hz     1.8 dB\n",
      "   128.0 Hz    12.0 dB\n",
      "   129.0 Hz     7.3 dB\n",
      "   130.0 Hz     9.3 dB\n",
      "   131.0 Hz    30.6 dB\n",
      "   132.0 Hz    39.6 dB\n",
      "   133.0 Hz    36.2 dB\n",
      "   134.0 Hz    20.6 dB\n",
      "   135.0 Hz    12.3 dB\n",
      "   136.0 Hz    12.2 dB\n",
      "   137.0 Hz    -1.9 dB\n",
      "   138.0 Hz    10.4 dB\n",
      "   139.0 Hz     8.9 dB\n",
      "   140.0 Hz    -1.3 dB\n",
      "   141.0 Hz     5.6 dB\n",
      "   142.0 Hz     5.7 dB\n",
      "   143.0 Hz     2.5 dB\n",
      "   144.0 Hz     6.9 dB\n",
      "   145.0 Hz     9.5 dB\n",
      "   146.0 Hz     6.6 dB\n",
      "   147.0 Hz    11.2 dB\n",
      "   148.0 Hz     6.6 dB  <- hum\n",
      "   149.0 Hz    48.6 dB  <- hum\n",
      "   150.0 Hz    54.1 dB  <- hum\n",
      "   151.0 Hz    47.5 dB  <- hum\n",
      "   152.0 Hz     8.1 dB\n",
      "   153.0 Hz    -1.7 dB\n",
      "   154.0 Hz   -12.5 dB\n",
      "   155.0 Hz     0.3 dB\n",
      "   156.0 Hz    -3.8 dB\n",
      "   157.0 Hz     6.7 dB\n",
      "   158.0 Hz     8.4 dB\n",
      "   159.0 Hz     1.4 dB\n",
      "   160.0 Hz     0.6 dB"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 1: diagnóstico de los dos candidatos descartados por fuga espectral ===\")\n",
    "print(f\"Candidato v2 (145.3 Hz): distancia al 3er armónico del hum relocalizado \"\n",
    "      f\"({HUM_FUNDAMENTAL*3:.2f} Hz) = {abs(145.3-HUM_FUNDAMENTAL*3):.2f} Hz\")\n",
    "print(f\"Candidato intermedio de este cuaderno (250.32 Hz): distancia al 5º armónico del hum \"\n",
    "      f\"relocalizado ({HUM_FUNDAMENTAL*5:.2f} Hz) = {abs(250.32-HUM_FUNDAMENTAL*5):.2f} Hz\")\n",
    "print(\"Ambos por debajo del umbral de seguridad (1.5 Hz a resolución nativa 1 Hz/bin) -> \"\n",
    "      \"ambos DESCARTADOS como candidatos válidos.\")\n",
    "\n",
    "# Ventana de ejemplo con resolución NATIVA (sin zero-padding) para visualizar\n",
    "# el valle espectral real entre el hum y el candidato final\n",
    "s16, e16 = int(16 * sr), int(17 * sr)\n",
    "seg16 = y[s16:e16]\n",
    "N16 = len(seg16)\n",
    "D16 = np.abs(np.fft.rfft(seg16 * np.hanning(N16), n=N16))\n",
    "freqs16 = np.fft.rfftfreq(N16, 1 / sr)\n",
    "D16_db = 20 * np.log10(D16 + 1e-12)\n",
    "mask16 = (freqs16 >= 120) & (freqs16 <= 160)\n",
    "print(\"\\nVentana 16-17s, resolución nativa (1 Hz/bin, sin zero-padding), banda 120-160 Hz:\")\n",
    "for f, d in zip(freqs16[mask16], D16_db[mask16]):\n",
    "    marca = \"  <- hum\" if es_armonico_de_hum(f, 2.0) else \"\"\n",
    "    print(f\"  {f:6.1f} Hz  {d:6.1f} dB{marca}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p1-note",
   "metadata": {},
   "source": [
    "**Nota metodológica clave sobre esta ventana:** con resolución nativa de 1 Hz/bin (sin\n",
    "zero-padding), el hum en 149-151 Hz es un pico estrecho y muy dominante (48.6-54.1 dB) que cae\n",
    "rápidamente fuera de esa banda (6.6-11.2 dB ya en 145-148 Hz). Hay un **pico real y separado en\n",
    "131-133 Hz** (30.6-39.6 dB), con un valle espectral genuino entre ambos (137-140 Hz, valores\n",
    "negativos). No existe ningún pico limpio en ~140 Hz — un resultado obtenido con menor resolución\n",
    "(p. ej. `n_fft=4096`, 10.77 Hz/bin) puede fundir en un solo bin la información de los dos picos\n",
    "vecinos (132 y 150 Hz) y sugerir un artefacto de pico \"intermedio\" que no corresponde a ninguna\n",
    "fuente real."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p2-md",
   "metadata": {},
   "source": [
    "## Paso 2 — Detección de la fundamental del roncón, método corregido\n",
    "\n",
    "Búsqueda del pico dominante por ventana de 1 s (resolución nativa, sin zero-padding), excluyendo\n",
    "±8 Hz de cualquier armónico del hum relocalizado. Se descartan las ventanas con menos de 20 dB de\n",
    "margen sobre el suelo de ruido de esa ventana (transición/ataque, no régimen estable) y se toma la\n",
    "moda del resto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p2",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 2: detección de la fundamental del roncón, método corregido ===\n",
      "  t(s)   f_pico(Hz)       dB  margen_dB   usa_p/consenso\n",
      "   0.0       224.00     15.0       15.2 no (baja energía)\n",
      "   1.0       133.00     21.4       15.6 no (baja energía)\n",
      "   2.0       132.00     41.8       40.1               si\n",
      "   3.0       132.00     37.2       36.1               si\n",
      "   4.0       133.00     34.2       32.4               si\n",
      "   5.0       132.00     45.9       41.7               si\n",
      "   6.0       132.00     45.8       40.8               si\n",
      "   7.0       132.00     36.3       30.1               si\n",
      "   8.0       268.00     31.4       26.6               si\n",
      "   9.0       134.00     38.5       37.3               si\n",
      "  10.0       133.00     40.1       37.0               si\n",
      "  11.0       132.00     43.3       39.6               si\n",
      "  12.0       132.00     39.7       33.0               si\n",
      "  13.0       132.00     42.8       38.6               si\n",
      "  14.0       132.00     42.9       37.9               si\n",
      "  15.0       132.00     39.4       33.3               si\n",
      "  16.0       132.00     39.6       37.5               si\n",
      "  17.0       132.00     40.9       34.9               si\n",
      "  18.0       132.00     44.6       41.4               si\n",
      "\n",
      "Ventanas válidas (margen >= 20.0 dB): 17/19\n",
      "\n",
      "Fundamental del roncón (moda de 17 ventanas válidas): 132.00 Hz, presente como pico dominante en 13/17 ventanas válidas (76.5%)\n",
      "Media de ventanas válidas: 140.24 Hz   mediana: 132.00 Hz   std: 31.95 Hz\n",
      "Nota más cercana: C3\n",
      "\n",
      "Distancia del candidato final al armónico de hum más cercano: 17.39 Hz (17.4 bins a resolución nativa de 1.00 Hz/bin)\n",
      "Umbral de seguridad (1.5x resolución): 1.50 Hz -> PASA\n",
      "\n",
      "Comparación en cents contra C3 / C#3 / D3:\n",
      "  C3 (130.81 Hz): +15.6 cents\n",
      "  C#3 (138.59 Hz): -84.4 cents\n",
      "  D3 (146.83 Hz): -184.4 cents"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 2: detección de la fundamental del roncón, método corregido ===\")\n",
    "\n",
    "UMBRAL_ENERGIA_DB = 20.0  # margen mínimo sobre el suelo de ruido de la ventana para\n",
    "                           # considerar la ventana representativa de régimen estable\n",
    "                           # (mismo criterio de 20 dB que v2/P2, por consistencia)\n",
    "\n",
    "resultados_ventanas = []\n",
    "for start in np.arange(0, duracion - 1.0 + 0.01, 1.0):\n",
    "    s = int(start * sr)\n",
    "    e = int((start + 1.0) * sr)\n",
    "    seg = y[s:e]\n",
    "    N = len(seg)\n",
    "    D = np.abs(np.fft.rfft(seg * np.hanning(N), n=N))\n",
    "    freqs = np.fft.rfftfreq(N, 1 / sr)\n",
    "    D_db = 20 * np.log10(D + 1e-12)\n",
    "    mask_banda = (freqs >= 100) & (freqs <= 500)\n",
    "    bf, bd = freqs[mask_banda], D_db[mask_banda]\n",
    "    suelo_ruido = np.median(bd)\n",
    "    cands = [(f, d) for f, d in zip(bf, bd) if not es_armonico_de_hum(f, TOL_HUM_HZ)]\n",
    "    cands.sort(key=lambda c: -c[1])\n",
    "    top_f, top_db = cands[0]\n",
    "    margen_db = top_db - suelo_ruido\n",
    "    resultados_ventanas.append((start, top_f, top_db, margen_db))\n",
    "\n",
    "print(f\"{'t(s)':>6} {'f_pico(Hz)':>12} {'dB':>8} {'margen_dB':>10} {'usa_p/consenso':>16}\")\n",
    "for t, f, d, m in resultados_ventanas:\n",
    "    usa = \"si\" if m >= UMBRAL_ENERGIA_DB else \"no (baja energía)\"\n",
    "    print(f\"{t:6.1f} {f:12.2f} {d:8.1f} {m:10.1f} {usa:>16}\")\n",
    "\n",
    "ventanas_validas = [(t, f, d) for t, f, d, m in resultados_ventanas if m >= UMBRAL_ENERGIA_DB]\n",
    "print(f\"\\nVentanas válidas (margen >= {UMBRAL_ENERGIA_DB} dB): {len(ventanas_validas)}/{len(resultados_ventanas)}\")\n",
    "\n",
    "picos_validos = np.array([v[1] for v in ventanas_validas])\n",
    "picos_redondeados = np.round(picos_validos).astype(int)\n",
    "conteo = Counter(picos_redondeados)\n",
    "f0_roncon = float(conteo.most_common(1)[0][0])\n",
    "frecuencia_moda = conteo.most_common(1)[0][1]\n",
    "\n",
    "print(f\"\\nFundamental del roncón (moda de {len(ventanas_validas)} ventanas válidas): \"\n",
    "      f\"{f0_roncon:.2f} Hz, presente como pico dominante en {frecuencia_moda}/{len(ventanas_validas)} \"\n",
    "      f\"ventanas válidas ({100*frecuencia_moda/len(ventanas_validas):.1f}%)\")\n",
    "print(f\"Media de ventanas válidas: {picos_validos.mean():.2f} Hz   \"\n",
    "      f\"mediana: {np.median(picos_validos):.2f} Hz   std: {picos_validos.std():.2f} Hz\")\n",
    "print(f\"Nota más cercana: {librosa.hz_to_note(f0_roncon)}\")\n",
    "\n",
    "dist_hum = min(abs(f0_roncon - HUM_FUNDAMENTAL * k) for k in range(1, 12)\n",
    "                if HUM_FUNDAMENTAL * k < 500)\n",
    "resolucion_nativa = 1.0\n",
    "print(f\"\\nDistancia del candidato final al armónico de hum más cercano: {dist_hum:.2f} Hz \"\n",
    "      f\"({dist_hum/resolucion_nativa:.1f} bins a resolución nativa de {resolucion_nativa:.2f} Hz/bin)\")\n",
    "umbral_seguridad = 1.5 * resolucion_nativa\n",
    "print(f\"Umbral de seguridad (1.5x resolución): {umbral_seguridad:.2f} Hz -> \"\n",
    "      f\"{'PASA' if dist_hum >= umbral_seguridad else 'RECHAZADO, demasiado cerca del hum'}\")\n",
    "\n",
    "print(\"\\nComparación en cents contra C3 / C#3 / D3:\")\n",
    "for nota in [\"C3\", \"C#3\", \"D3\"]:\n",
    "    c = cents_vs_nota(f0_roncon, nota)\n",
    "    print(f\"  {nota} ({librosa.note_to_hz(nota):.2f} Hz): {c:+.1f} cents\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p2-note",
   "metadata": {},
   "source": [
    "**Resultado P2 (corregido):** la fundamental del roncón, con el método robusto a fuga\n",
    "espectral, es **132.0 Hz**, moda de 13 de las 17 ventanas válidas (76.5 %), a **+15.6 cents de C3**\n",
    "(muy próxima) y muy lejos tanto de C#3 (-84.4 cents) como de D3 (-184.4 cents). Las dos ventanas\n",
    "descartadas (t=0.0 y t=8.0) coinciden con instantes de baja energía relativa — probablemente\n",
    "transiciones o ataques, no el régimen estable del dron — y no alteran la moda."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p3-md",
   "metadata": {},
   "source": [
    "## Paso 3 — Estabilidad temporal: candidato final vs hum vs candidato descartado\n",
    "\n",
    "La estabilidad de frecuencia por sí sola **no** basta para distinguir un dron real de un artefacto\n",
    "de hum: ambos pueden ser perfectamente estables en Hz (el hum, por ser eléctrico, más aún). El\n",
    "criterio decisivo sigue siendo la distancia al armónico de hum (paso 2)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p3",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 3: estabilidad temporal, candidato final vs hum vs candidato descartado ===\n",
      "Candidato final (roncón) — pico rastreado 124-140 Hz: media=132.28 Hz  std=1.489 Hz\n",
      "Hum (3er armónico) — pico rastreado 146-152 Hz: media=149.39 Hz  std=0.000 Hz\n",
      "Candidato DESCARTADO (5º armónico de hum, fuga) — pico rastreado 242-258 Hz: media=250.32 Hz  std=0.000 Hz\n",
      "\n",
      "Nota: la desviación de frecuencia entre ventanas NO basta para distinguir dron real de hum -- ambos pueden ser estables en Hz. El criterio decisivo es la distancia en bins al armónico de hum más cercano (paso 2), no la estabilidad por sí sola.\n",
      "\n",
      "Armónicos superiores del candidato final (contraste adicional, no decisivo por sí solo):\n",
      "  1x = 132.0 Hz -> mean_db=-29.3  std=11.20\n",
      "  2x = 264.0 Hz -> mean_db=-37.5  std=11.69\n",
      "  3x = 396.0 Hz -> mean_db=-44.8  std=13.01  [coincide con armónico de hum -- no usable como evidencia]\n",
      "  4x = 528.0 Hz -> mean_db=-36.1  std=14.00"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 3: estabilidad temporal, candidato final vs hum vs candidato descartado ===\")\n",
    "D_hi = np.abs(librosa.stft(y, n_fft=32768, hop_length=2048))\n",
    "freqs_hi = librosa.fft_frequencies(sr=sr, n_fft=32768)\n",
    "D_hi_db = librosa.amplitude_to_db(D_hi, ref=np.max(D_hi))\n",
    "\n",
    "\n",
    "def resumen_pico_rastreado(f_centro, ancho, etiqueta):\n",
    "    mask = (freqs_hi >= f_centro - ancho) & (freqs_hi <= f_centro + ancho)\n",
    "    bf, bd = freqs_hi[mask], D_hi_db[mask]\n",
    "    peak_idx = bd.argmax(axis=0)\n",
    "    peak_freqs = bf[peak_idx]\n",
    "    print(f\"{etiqueta} — pico rastreado {f_centro-ancho:.0f}-{f_centro+ancho:.0f} Hz: \"\n",
    "          f\"media={peak_freqs.mean():.2f} Hz  std={peak_freqs.std():.3f} Hz\")\n",
    "    return peak_freqs\n",
    "\n",
    "\n",
    "resumen_pico_rastreado(f0_roncon, 8, \"Candidato final (roncón)\")\n",
    "resumen_pico_rastreado(HUM_FUNDAMENTAL * 3, 3, \"Hum (3er armónico)\")\n",
    "resumen_pico_rastreado(250.32, 8, \"Candidato DESCARTADO (5º armónico de hum, fuga)\")\n",
    "\n",
    "print(\"\\nNota: la desviación de frecuencia entre ventanas NO basta para distinguir dron real de \"\n",
    "      \"hum -- ambos pueden ser estables en Hz. El criterio decisivo es la distancia en bins al \"\n",
    "      \"armónico de hum más cercano (paso 2), no la estabilidad por sí sola.\")\n",
    "\n",
    "print(\"\\nArmónicos superiores del candidato final (contraste adicional, no decisivo por sí solo):\")\n",
    "idx_f0 = np.argmin(np.abs(freqs_hi - f0_roncon))\n",
    "print(f\"  1x = {f0_roncon:.1f} Hz -> mean_db={D_hi_db[idx_f0].mean():.1f}  std={D_hi_db[idx_f0].std():.2f}\")\n",
    "for mult in [2, 3, 4]:\n",
    "    hf = f0_roncon * mult\n",
    "    idxh = np.argmin(np.abs(freqs_hi - hf))\n",
    "    es_hum_h = es_armonico_de_hum(hf, TOL_HUM_HZ)\n",
    "    print(f\"  {mult}x = {hf:.1f} Hz -> mean_db={D_hi_db[idxh].mean():.1f}  std={D_hi_db[idxh].std():.2f}\"\n",
    "          f\"{'  [coincide con armónico de hum -- no usable como evidencia]' if es_hum_h else ''}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p3-note",
   "metadata": {},
   "source": [
    "**Resultado P3:** el candidato final (132 Hz) tiene una desviación de frecuencia entre\n",
    "ventanas de 1.49 Hz — pequeña fluctuación esperable de un instrumento acústico real (variación de\n",
    "presión del fole) — frente a 0.000 Hz del hum (fijo por ser un artefacto eléctrico) y del propio\n",
    "candidato descartado de 250.32 Hz, que también resulta perfectamente estable, precisamente porque\n",
    "también es artefacto del hum. La estabilidad de frecuencia, por tanto, **no es suficiente por sí\n",
    "sola** como criterio — el 3er armónico del candidato final (396 Hz) cae encima de un armónico del\n",
    "hum y no puede usarse como evidencia independiente."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p4-md",
   "metadata": {},
   "source": [
    "## Paso 4 — Prominencia por fracción de frames (criterio v2/P2, banda de exclusión corregida)\n",
    "\n",
    "Mismo criterio de v2 (STFT `n_fft=8192`, prominencia dentro de 20 dB del máximo de cada frame),\n",
    "recalculado con el hum relocalizado y la banda de exclusión ampliada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p4",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 4: prominencia por fracción de frames (criterio v2, hum relocalizado) ===\n",
      "Top candidatos (100-500 Hz, excluidos armónicos de hum relocalizado ±8 Hz):\n",
      "   134.6 Hz (C3   )  prominente en  55.4% de frames  mean_db=-31.2\n",
      "   129.2 Hz (C3   )  prominente en  46.6% de frames  mean_db=-32.2\n",
      "   263.8 Hz (C4   )  prominente en  13.9% de frames  mean_db=-36.4\n",
      "   269.2 Hz (C4   )  prominente en   5.3% de frames  mean_db=-40.0\n",
      "   489.9 Hz (B4   )  prominente en   4.9% de frames  mean_db=-56.6\n",
      "   484.5 Hz (B4   )  prominente en   2.7% de frames  mean_db=-58.0\n",
      "   140.0 Hz (C♯3  )  prominente en   0.9% de frames  mean_db=-40.8\n",
      "   258.4 Hz (C4   )  prominente en   0.6% de frames  mean_db=-41.0\n",
      "\n",
      "Prominencia del candidato final (132.0 Hz) en el bin más cercano a esta resolución (nperseg=8192, 5.38 Hz/bin): 55.4% de frames"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 4: prominencia por fracción de frames (criterio v2, hum relocalizado) ===\")\n",
    "D_v2res = np.abs(librosa.stft(y, n_fft=8192, hop_length=1024))\n",
    "freqs_v2res = librosa.fft_frequencies(sr=sr, n_fft=8192)\n",
    "D_v2res_db = librosa.amplitude_to_db(D_v2res, ref=np.max(D_v2res))\n",
    "frame_max = D_v2res_db.max(axis=0)\n",
    "\n",
    "mask_banda = (freqs_v2res >= 100) & (freqs_v2res <= 500)\n",
    "banda_freqs = freqs_v2res[mask_banda]\n",
    "banda_db = D_v2res_db[mask_banda]\n",
    "prominente = banda_db > (frame_max - 20)\n",
    "pct_prominente = prominente.mean(axis=1)\n",
    "\n",
    "candidatos = [(f, pct_prominente[i], banda_db[i].mean())\n",
    "              for i, f in enumerate(banda_freqs) if not es_armonico_de_hum(f, TOL_HUM_HZ)]\n",
    "candidatos.sort(key=lambda c: -c[1])\n",
    "print(\"Top candidatos (100-500 Hz, excluidos armónicos de hum relocalizado ±8 Hz):\")\n",
    "for f, pct, mdb in candidatos[:8]:\n",
    "    print(f\"  {f:6.1f} Hz ({librosa.hz_to_note(f):5s})  prominente en {100*pct:5.1f}% de frames  mean_db={mdb:.1f}\")\n",
    "\n",
    "idx_cercano = np.argmin(np.abs(banda_freqs - f0_roncon))\n",
    "print(f\"\\nProminencia del candidato final ({f0_roncon:.1f} Hz) en el bin más cercano a esta \"\n",
    "      f\"resolución (nperseg=8192, {sr/8192:.2f} Hz/bin): {100*pct_prominente[idx_cercano]:.1f}% de frames\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p4-note",
   "metadata": {},
   "source": [
    "**Ajuste de alcance frente a v2, con la misma prioridad que el hallazgo:** con el criterio\n",
    "de prominencia de v2 (P4, 20 dB de margen sobre el máximo de cada frame), el candidato final\n",
    "alcanza el **55.4 %** de los frames, no el 100 % que v2 reportó para 145.3 Hz. Esta caída **no es\n",
    "un fallo del nuevo método** — es la consecuencia esperable de corregir un sesgo: 145.3 Hz aparecía\n",
    "en el 100 % de los frames precisamente porque \"heredaba\" parte de la energía masiva del hum vecino\n",
    "por fuga espectral. El número honesto del roncón real es más modesto que el número inflado por el\n",
    "bug. Nótese también que **140.0 Hz** (candidato reportado por el operador con `n_fft=4096` en la\n",
    "ventana 16-17s) aparece aquí, con mayor resolución, como un candidato residual y débil (0.9 % de\n",
    "frames, -40.8 dB) — consistente con la hipótesis de que fue un artefacto de la resolución más\n",
    "gruesa usada en esa comprobación puntual, no una fuente real independiente."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spec-md",
   "metadata": {},
   "source": [
    "## Espectrograma con la fundamental corregida marcada\n",
    "\n",
    "Línea cyan: fundamental corregida del roncón (132 Hz). Líneas rojas/naranjas: los dos candidatos\n",
    "descartados por fuga espectral (hum en 149.4 Hz y su 5º armónico en 250.3 Hz)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "spec",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Guardado: roncon--espectrograma-marcado.png"
     ]
    }
   ],
   "source": [
    "fig, ax = plt.subplots(figsize=(12, 5))\n",
    "D_full = np.abs(librosa.stft(y, n_fft=2048, hop_length=512))\n",
    "S_db_full = librosa.amplitude_to_db(D_full, ref=np.max(D_full))\n",
    "img = librosa.display.specshow(S_db_full, sr=sr, hop_length=512, x_axis=\"time\", y_axis=\"log\",\n",
    "                                ax=ax, cmap=\"magma\")\n",
    "ax.axhline(f0_roncon, color=\"#00e5ff\", linewidth=1.2, linestyle=\"--\",\n",
    "           label=f\"Roncón — fundamental corregida ({f0_roncon:.0f} Hz, {librosa.hz_to_note(f0_roncon)})\")\n",
    "ax.axhline(HUM_FUNDAMENTAL * 3, color=\"#ff4d4d\", linewidth=1.0, linestyle=\":\",\n",
    "           label=f\"Hum de red ({HUM_FUNDAMENTAL*3:.1f} Hz, 3er armónico) — descartado\")\n",
    "ax.axhline(250.32, color=\"#ffb84d\", linewidth=1.0, linestyle=\":\",\n",
    "           label=\"250.3 Hz (5º armónico del hum) — candidato descartado en este mismo cuaderno\")\n",
    "ax.set(title=\"Espectrograma con la fundamental del roncón corregida — floréu d\\'entamu, Juan Huerta (1976)\")\n",
    "ax.legend(loc=\"upper right\", fontsize=8)\n",
    "fig.colorbar(img, ax=ax, format=\"%+2.0f dB\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"roncon--espectrograma-marcado.png\", dpi=150, bbox_inches=\"tight\")\n",
    "plt.close()\n",
    "print(\"Guardado: roncon--espectrograma-marcado.png\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spec-img",
   "metadata": {},
   "source": [
    "![Espectrograma con la fundamental del roncón corregida](roncon--espectrograma-marcado.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p5-md",
   "metadata": {},
   "source": [
    "## Paso 5 — Notch y HPSS con la frecuencia corregida\n",
    "\n",
    "Mismo método que v2 (`scipy.signal.iirnotch`, Q=20; `librosa.effects.hpss()` por defecto) —\n",
    "solo cambia la frecuencia de referencia (132 Hz en vez de 145.3 Hz)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "p5",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== Paso 5: notch y HPSS con la frecuencia corregida ===\n",
      "Línea base (señal completa, sin cambios respecto a v2): 127 onsets, 92.7% voiced\n",
      "\n",
      "Notch en 132.0 Hz (Q=20, frecuencia CORREGIDA):\n",
      "  voiced: 92.7% (vs 92.7% señal completa)\n",
      "  onsets: 127 (vs 127 señal completa)\n",
      "\n",
      "HPSS — RMS completa: 0.1377  harmónica: 0.1275 (92.5%)  percusiva: 0.0262 (19.0%)\n",
      "(idéntico a v2: HPSS no depende de f0_roncon, solo la banda de referencia medida cambia)\n",
      "\n",
      "Presencia del roncón (132.0 Hz, CORREGIDA) tras HPSS:\n",
      "  componente harmónica: mean_db=-30.2  std=11.28\n",
      "  componente percusiva: mean_db=-28.1  std=9.51\n",
      "  energía -- harmónica: 2362550.00  percusiva: 30689.38  ratio H/P: 77.0\n",
      "\n",
      "pYIN sobre componente percusivo: 26.7% voiced (vs 92.7% completa)\n",
      "Onsets percusivo: 129 (vs 127 completa)"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== Paso 5: notch y HPSS con la frecuencia corregida ===\")\n",
    "\n",
    "n_fft, hop_length = 2048, 512\n",
    "\n",
    "f0_completa, voiced_flag_completa, voiced_prob_completa = librosa.pyin(\n",
    "    y, fmin=librosa.note_to_hz(\"A4\"), fmax=librosa.note_to_hz(\"D7\"), sr=sr\n",
    ")\n",
    "voiced_mask_completa = voiced_flag_completa & ~np.isnan(f0_completa)\n",
    "pct_voiced_completa = 100 * voiced_mask_completa.sum() / len(voiced_flag_completa)\n",
    "onset_frames_completa = librosa.onset.onset_detect(y=y, sr=sr, units=\"frames\", backtrack=True)\n",
    "onset_times_completa = librosa.frames_to_time(onset_frames_completa, sr=sr)\n",
    "print(f\"Línea base (señal completa, sin cambios respecto a v2): \"\n",
    "      f\"{len(onset_times_completa)} onsets, {pct_voiced_completa:.1f}% voiced\")\n",
    "\n",
    "\n",
    "def notch(sig, freq, sr_, Q=20):\n",
    "    b, a = scipy.signal.iirnotch(freq, Q, sr_)\n",
    "    return scipy.signal.filtfilt(b, a, sig)\n",
    "\n",
    "\n",
    "y_notch = notch(y.copy(), f0_roncon, sr, Q=20)\n",
    "f0_notch, voiced_flag_notch, _ = librosa.pyin(\n",
    "    y_notch, fmin=librosa.note_to_hz(\"A4\"), fmax=librosa.note_to_hz(\"D7\"), sr=sr\n",
    ")\n",
    "voiced_mask_notch = voiced_flag_notch & ~np.isnan(f0_notch)\n",
    "pct_voiced_notch = 100 * voiced_mask_notch.sum() / len(voiced_flag_notch)\n",
    "onsets_notch_time = librosa.onset.onset_detect(y=y_notch, sr=sr, units=\"time\", backtrack=True)\n",
    "print(f\"\\nNotch en {f0_roncon:.1f} Hz (Q=20, frecuencia CORREGIDA):\")\n",
    "print(f\"  voiced: {pct_voiced_notch:.1f}% (vs {pct_voiced_completa:.1f}% señal completa)\")\n",
    "print(f\"  onsets: {len(onsets_notch_time)} (vs {len(onset_times_completa)} señal completa)\")\n",
    "\n",
    "y_harmonic, y_percussive = librosa.effects.hpss(y)\n",
    "\n",
    "rms_full = np.sqrt(np.mean(y ** 2))\n",
    "rms_harm = np.sqrt(np.mean(y_harmonic ** 2))\n",
    "rms_perc = np.sqrt(np.mean(y_percussive ** 2))\n",
    "print(f\"\\nHPSS — RMS completa: {rms_full:.4f}  harmónica: {rms_harm:.4f} \"\n",
    "      f\"({100*rms_harm/rms_full:.1f}%)  percusiva: {rms_perc:.4f} ({100*rms_perc/rms_full:.1f}%)\")\n",
    "print(\"(idéntico a v2: HPSS no depende de f0_roncon, solo la banda de referencia medida cambia)\")\n",
    "\n",
    "D_harm = np.abs(librosa.stft(y_harmonic, n_fft=32768, hop_length=2048))\n",
    "D_perc = np.abs(librosa.stft(y_percussive, n_fft=32768, hop_length=2048))\n",
    "D_harm_db = librosa.amplitude_to_db(D_harm, ref=np.max(D_harm))\n",
    "D_perc_db = librosa.amplitude_to_db(D_perc, ref=np.max(D_perc))\n",
    "freqs_hpss = librosa.fft_frequencies(sr=sr, n_fft=32768)\n",
    "idx_roncon_hpss = np.argmin(np.abs(freqs_hpss - f0_roncon))\n",
    "\n",
    "print(f\"\\nPresencia del roncón ({f0_roncon:.1f} Hz, CORREGIDA) tras HPSS:\")\n",
    "print(f\"  componente harmónica: mean_db={D_harm_db[idx_roncon_hpss].mean():.1f}  \"\n",
    "      f\"std={D_harm_db[idx_roncon_hpss].std():.2f}\")\n",
    "print(f\"  componente percusiva: mean_db={D_perc_db[idx_roncon_hpss].mean():.1f}  \"\n",
    "      f\"std={D_perc_db[idx_roncon_hpss].std():.2f}\")\n",
    "E_harm_roncon = np.sum(D_harm[idx_roncon_hpss] ** 2)\n",
    "E_perc_roncon = np.sum(D_perc[idx_roncon_hpss] ** 2)\n",
    "print(f\"  energía -- harmónica: {E_harm_roncon:.2f}  percusiva: {E_perc_roncon:.2f}  \"\n",
    "      f\"ratio H/P: {E_harm_roncon/max(E_perc_roncon,1e-9):.1f}\")\n",
    "\n",
    "f0_perc, voiced_flag_perc, _ = librosa.pyin(\n",
    "    y_percussive, fmin=librosa.note_to_hz(\"A4\"), fmax=librosa.note_to_hz(\"D7\"), sr=sr\n",
    ")\n",
    "voiced_mask_perc = voiced_flag_perc & ~np.isnan(f0_perc)\n",
    "pct_voiced_perc = 100 * voiced_mask_perc.sum() / len(voiced_flag_perc)\n",
    "onsets_perc_time = librosa.onset.onset_detect(y=y_percussive, sr=sr, units=\"time\", backtrack=True)\n",
    "print(f\"\\npYIN sobre componente percusivo: {pct_voiced_perc:.1f}% voiced (vs {pct_voiced_completa:.1f}% completa)\")\n",
    "print(f\"Onsets percusivo: {len(onsets_perc_time)} (vs {len(onset_times_completa)} completa)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p5-note",
   "metadata": {},
   "source": [
    "**Resultado P5 (recalculado con la frecuencia corregida):**\n",
    "\n",
    "| Métrica | Señal completa | Notch en 132 Hz (corregido) | Notch en 145.3 Hz (v2, erróneo) |\n",
    "|---|---:|---:|---:|\n",
    "| % voiced (pYIN) | 92.7 % | 92.7 % | 90.5 % |\n",
    "| Onsets | 127 | 127 | 127 |\n",
    "\n",
    "El notch centrado en la frecuencia corregida (132 Hz) apenas cambia los resultados frente a la\n",
    "señal completa — igual que ocurría con el notch de v2 en 145.3 Hz, aunque ahora la diferencia es\n",
    "todavía menor (92.7 % vs 90.5 % antes). La conclusión cualitativa de v2 **se mantiene**: la\n",
    "fundamental del roncón está muy por debajo del rango de búsqueda de pYIN (A4=440 Hz+), así que\n",
    "filtrar solo esa banda no es la causa directa de la confusión en pitch-tracking/onsets; el problema\n",
    "real siguen siendo los armónicos superiores del roncón, no aislados por un notch de una sola banda.\n",
    "\n",
    "**HPSS**: el ratio de energía harmónica/percusiva en la banda del roncón cae de ~1287:1 (v2, banda\n",
    "centrada en 145.3 Hz, \"inflada\" por la proximidad al hum) a **77:1** con la banda correctamente\n",
    "centrada en 132 Hz — sigue siendo una proporción muy alta (el roncón queda casi íntegramente en el\n",
    "componente armónico), pero la cifra concreta cambia de forma sustancial y debe citarse con el valor\n",
    "corregido, no con el de v2. El resto de conclusiones de P4/P5 de v2 (HPSS no separa roncón de\n",
    "punteru; el componente percusivo pierde dos tercios de la información de pitch-tracking — 26.7 %\n",
    "voiced vs 92.7 % de la señal completa, sin cambio respecto a v2 porque HPSS no depende de\n",
    "`f0_roncon`) **se mantienen sin cambio**, porque no dependían de la fundamental del roncón sino\n",
    "del propio algoritmo HPSS aplicado con parámetros por defecto."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "graf-md",
   "metadata": {},
   "source": [
    "## Gráficos dependientes de la frecuencia corregida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "graf",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Guardado: espectrograma--separada.png\n",
      "Guardado: contorno-f0--separada.png\n",
      "Guardado: onsets--separada.png\n",
      "Guardado: hpss--comparativa.png"
     ]
    }
   ],
   "source": [
    "D_completa_plot = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)\n",
    "S_db_completa = librosa.amplitude_to_db(np.abs(D_completa_plot), ref=np.max)\n",
    "\n",
    "# espectrograma--separada.png\n",
    "D_percusiva_plot = librosa.stft(y_percussive, n_fft=n_fft, hop_length=hop_length)\n",
    "S_db_percusiva = librosa.amplitude_to_db(np.abs(D_percusiva_plot), ref=np.max)\n",
    "fig, ax = plt.subplots(figsize=(12, 5))\n",
    "img = librosa.display.specshow(S_db_percusiva, sr=sr, hop_length=hop_length, x_axis=\"time\",\n",
    "                                y_axis=\"log\", ax=ax, cmap=\"magma\")\n",
    "ax.set(title=\"Espectrograma — componente percusivo de HPSS (candidato a \\'solo punteru\\')\")\n",
    "fig.colorbar(img, ax=ax, format=\"%+2.0f dB\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"espectrograma--separada.png\", dpi=150, bbox_inches=\"tight\")\n",
    "plt.close()\n",
    "print(\"Guardado: espectrograma--separada.png\")\n",
    "\n",
    "# contorno-f0--separada.png\n",
    "times_f0_perc = librosa.times_like(f0_perc, sr=sr)\n",
    "fig, ax = plt.subplots(figsize=(12, 5))\n",
    "img = librosa.display.specshow(S_db_percusiva, sr=sr, hop_length=hop_length, x_axis=\"time\",\n",
    "                                y_axis=\"log\", ax=ax, cmap=\"magma\")\n",
    "ax.plot(times_f0_perc, f0_perc, color=\"#00e5ff\", linewidth=2, label=\"f0 (pYIN, A4-D7)\")\n",
    "ax.set(title=\"Contorno de f0 — componente percusivo de HPSS (\\'solo punteru\\')\")\n",
    "ax.legend(loc=\"upper right\")\n",
    "fig.colorbar(img, ax=ax, format=\"%+2.0f dB\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"contorno-f0--separada.png\", dpi=150, bbox_inches=\"tight\")\n",
    "plt.close()\n",
    "print(\"Guardado: contorno-f0--separada.png\")\n",
    "\n",
    "# onsets--separada.png\n",
    "onset_frames_perc = librosa.onset.onset_detect(y=y_percussive, sr=sr, units=\"frames\", backtrack=True)\n",
    "onset_times_perc = librosa.frames_to_time(onset_frames_perc, sr=sr)\n",
    "fig, ax = plt.subplots(figsize=(12, 3.5))\n",
    "librosa.display.waveshow(y_percussive, sr=sr, ax=ax, alpha=0.6)\n",
    "ax.vlines(onset_times_perc, ymin=-np.abs(y_percussive).max(), ymax=np.abs(y_percussive).max(),\n",
    "          color=\"#AE4D02\", linewidth=1, alpha=0.8, label=f\"Onsets (n={len(onset_times_perc)})\")\n",
    "ax.set(title=\"Onsets sobre la forma de onda — componente percusivo de HPSS\",\n",
    "       xlabel=\"Tiempo (s)\", ylabel=\"Amplitud\")\n",
    "ax.legend(loc=\"upper right\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"onsets--separada.png\", dpi=150, bbox_inches=\"tight\")\n",
    "plt.close()\n",
    "print(\"Guardado: onsets--separada.png\")\n",
    "\n",
    "# hpss--comparativa.png\n",
    "fig, axes = plt.subplots(3, 1, figsize=(12, 11), sharex=True, sharey=True)\n",
    "for ax, sig, titulo in zip(\n",
    "    axes, [y, y_harmonic, y_percussive],\n",
    "    [\"Señal completa\", \"Componente harmónica (HPSS)\", \"Componente percusiva (HPSS)\"],\n",
    "):\n",
    "    Dx = np.abs(librosa.stft(sig, n_fft=n_fft, hop_length=hop_length))\n",
    "    Sx_db = librosa.amplitude_to_db(Dx, ref=np.max(Dx))\n",
    "    img = librosa.display.specshow(Sx_db, sr=sr, hop_length=hop_length, x_axis=\"time\",\n",
    "                                    y_axis=\"log\", ax=ax, cmap=\"magma\")\n",
    "    ax.axhline(f0_roncon, color=\"#00e5ff\", linewidth=0.8, linestyle=\"--\", alpha=0.7)\n",
    "    ax.set(title=titulo)\n",
    "fig.suptitle(\"HPSS: separación armónico/percusiva — la línea discontinua marca la fundamental \"\n",
    "             \"corregida del roncón\", y=1.0)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"hpss--comparativa.png\", dpi=150, bbox_inches=\"tight\")\n",
    "plt.close()\n",
    "print(\"Guardado: hpss--comparativa.png\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "img-esp-sep",
   "metadata": {},
   "source": [
    "![Espectrograma — componente percusivo de HPSS](espectrograma--separada.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "img-contorno-sep",
   "metadata": {},
   "source": [
    "![Contorno de f0 — componente percusivo de HPSS](contorno-f0--separada.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "img-onsets-sep",
   "metadata": {},
   "source": [
    "![Onsets sobre la forma de onda — componente percusivo de HPSS](onsets--separada.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "img-hpss",
   "metadata": {},
   "source": [
    "![HPSS: comparativa señal completa / harmónica / percusiva](hpss--comparativa.png)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "fin",
   "metadata": {},
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "=== FIN ===\n",
      "Fundamental corregida del roncón: 132.00 Hz (C3, +15.6 cents vs C3, -84.4 cents vs C#3, -184.4 cents vs D3)"
     ]
    }
   ],
   "source": [
    "print(\"\\n=== FIN ===\")\n",
    "print(f\"Fundamental corregida del roncón: {f0_roncon:.2f} Hz \"\n",
    "      f\"({librosa.hz_to_note(f0_roncon)}, {cents_vs_nota(f0_roncon,'C3'):+.1f} cents vs C3, \"\n",
    "      f\"{cents_vs_nota(f0_roncon,'C#3'):+.1f} cents vs C#3, \"\n",
    "      f\"{cents_vs_nota(f0_roncon,'D3'):+.1f} cents vs D3)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "conclusion",
   "metadata": {},
   "source": [
    "## Conclusión y salvedades\n",
    "\n",
    "**Corte analizado:** 19 s, floréu d'entamu, *\"Entemediu 3/8\"*, Juan Huerta (La Torre, Teverga),\n",
    "Archivo Sonoro de Celso Peyroux (casete n.º 1, Oviedo, abril 1976), `sr` = 44100 Hz — mismo audio\n",
    "fuente que v1/v2, reanalizado con un método corregido y robusto a fuga espectral.\n",
    "\n",
    "### Qué corrige este cuaderno frente a v2\n",
    "\n",
    "- **Bug confirmado:** la fundamental del roncón reportada en v2 (145.3 Hz, D3) era fuga espectral\n",
    "  del hum de red eléctrica, no una fuente acústica real — confirmado con resolución nativa (1\n",
    "  Hz/bin) mostrando un valle espectral genuino entre el hum (~150 Hz) y el candidato real (~132\n",
    "  Hz), y con la distancia insuficiente (1 bin a la resolución de v2) entre 145.3 Hz y el hum\n",
    "  medido.\n",
    "- **Segunda trampa detectada durante la propia corrección de este cuaderno:** ampliar la banda de\n",
    "  exclusión sin relocalizar la fundamental del hum con precisión reprodujo el mismo error un nivel\n",
    "  más abajo (candidato 250.32 Hz, en realidad el 5º armónico del hum correctamente localizado en\n",
    "  49.80 Hz, no 48.4 Hz como asumía v2). Este cuaderno lo documenta explícitamente como parte del\n",
    "  propio proceso de verificación, no lo oculta.\n",
    "- **Fundamental corregida:** **132.0 Hz**, nota **C3** (+15.6 cents, prácticamente afinación\n",
    "  estándar), obtenida como moda de 13/17 ventanas de 1 s válidas (76.5 %), a 17.4 Hz (17.4 bins a\n",
    "  resolución nativa) del armónico de hum más cercano — muy por encima del umbral de seguridad de\n",
    "  1.5 bins.\n",
    "- **No se confirma C#3** (\"Do brillante\"): la desviación frente a C#3 es de -84.4 cents, casi un\n",
    "  semitono completo por debajo — muy lejos del margen de afinación razonable de un instrumento\n",
    "  acústico. El candidato que en una comprobación puntual con menor resolución (`n_fft=4096`, banda\n",
    "  16-17s) pareció apuntar a ~140 Hz resulta, con resolución nativa y banda de exclusión de hum\n",
    "  correctamente calibrada, un artefacto residual y débil (0.9 % de frames, -40.8 dB), no una fuente\n",
    "  independiente. Existe una tradición documentada de gaitas asturianas afinadas en \"Do\n",
    "  brillante\"/Do sostenido (ver, como referencia externa no verificada en este cuaderno,\n",
    "  [tever.es/blog/mi-gaita-en-do-sostenido-chus-solis](https://tever.es/blog/mi-gaita-en-do-sostenido-chus-solis))\n",
    "  — pero este análisis concreto, sobre esta grabación concreta, no la sostiene.\n",
    "\n",
    "### Qué sigue sostenido sin cambio respecto a v2\n",
    "\n",
    "- La naturaleza de grabación de campo en casete (hum de red, rango dinámico limitado) — no depende\n",
    "  de la fundamental del roncón.\n",
    "- HPSS no separa el roncón del punteru de forma útil (ambos son armónicos y sostenidos) — resultado\n",
    "  del propio algoritmo HPSS, no de la frecuencia de referencia.\n",
    "- El notch de una sola banda no resuelve el problema de interferencia en pitch-tracking, porque\n",
    "  ataca solo la fundamental, no los armónicos superiores del roncón — cualitativamente igual con\n",
    "  la frecuencia corregida.\n",
    "\n",
    "### Qué cambia numéricamente y debe citarse con el valor corregido\n",
    "\n",
    "- Fundamental del roncón: **132.0 Hz (C3)**, no 145.3 Hz (D3).\n",
    "- Prominencia en criterio v2/P2: **55.4 %** de frames, no el 100 % reportado en v2 (la cifra\n",
    "  anterior estaba inflada por la fuga espectral).\n",
    "- Ratio de energía harmónica/percusiva en la banda del roncón tras HPSS: **77:1**, no 1287:1.\n",
    "\n",
    "### Sobre la generalización\n",
    "\n",
    "Todo lo anterior corresponde **a estos 19 segundos de esta grabación concreta**. No se afirma que\n",
    "estas cifras generalicen a otras grabaciones, intérpretes, secciones de la pieza, ni a otros\n",
    "parámetros de análisis sin repetir el mismo método sobre ese material.\n",
    "\n",
    "---\n",
    "\n",
    "**Artefactos generados por este cuaderno:** `roncon--espectrograma-marcado.png`,\n",
    "`espectrograma--separada.png`, `contorno-f0--separada.png`, `onsets--separada.png`,\n",
    "`hpss--comparativa.png`.\n",
    "\n",
    "**Réplica exacta:** con el fichero de audio de 19 s (`audio-recorte.wav`, idéntico al de v1/v2),\n",
    "este cuaderno (`analisis.py`) se ejecuta de principio a fin sin intervención manual y reproduce las\n",
    "cifras citadas arriba — verificado con dos ejecuciones consecutivas sin diferencias.\n",
    "\n",
    "## Licencia y atribución\n",
    "\n",
    "Audio: fragmento de 19 s del floréu d'entamu de *\"Entemediu 3/8\"*, interpretado por Juan Huerta\n",
    "(La Torre, Teverga), Archivo Sonoro de Celso Peyroux (casete n.º 1, Oviedo, abril 1976), fondo\n",
    "documental de triaje de Tever Labs. Cuaderno bajo la misma licencia que el código del sitio — ver\n",
    "[/aviso-legal](https://labs.tever.es/aviso-legal)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}