Ir al contenido

Multicanal y rendimiento

Normas aplicables: IEC 61260IEC 61672Referencias: Bendat y Piersol 2010

La mayoría de las sesiones de medición reales producen más de un canal: los dos oídos de una cabeza artificial, la pareja de micrófonos de una sonda de intensidad, las varias posiciones de un muestreo de sala, las cápsulas de un array de conformación de haz. La convención para todos ellos es un único array con forma (canales, muestras) procesado en una sola llamada: cada función actúa sobre el último eje (el tiempo) y conserva el eje inicial de canales, de modo que cada fila se analiza exactamente como si fuera la única.

Esa garantía por canal es normativa, no solo cómoda. Los filtros de banda aplicados a cada fila son los diseños de IEC 61260-1 y las ponderaciones y balísticas del detector son las de IEC 61672-1, sin cambios respecto a la ruta monocanal; la vectorización agrupa la aritmética entre filas (un diseño de filtro, una llamada a SciPy) y nunca las mezcla. Es el procedimiento de libro de texto para múltiples registros de datos (Bendat y Piersol 2010, §10.4.2): analizar primero cada registro por separado y calcular lo conjunto como un paso aparte y explícito.

Usa esta página cuando tus canales sean grabaciones paralelas con el mismo reloj y quieras espectros o niveles por canal. Cuando la pregunta sea entre canales (cuál es el retardo de A a B, cuánta parte de B se explica por A), eso es análisis cruzado: consulta Correlación y retardo y Coherencia múltiple y parcial, las implementaciones de la correlación cruzada y de los modelos de múltiples entradas de Bendat y Piersol.

Análisis estéreo: ruido rosa y barrido logarítmico resueltos por canal en tercios de octavaAnálisis estéreo: ruido rosa y barrido logarítmico resueltos por canal en tercios de octava

Una sola llamada a octave_filter, dos canales: ruido rosa a la izquierda, un barrido logarítmico a la derecha. Los dos vuelven planos en tercios de octava — el ruido rosa porque su espectro tiene energía constante por banda de fracción de octava, el barrido porque pasa un tiempo proporcionalmente igual en cada una — y ninguna de las dos filas se ve afectada por la otra.

Mostrar el código de esta figura
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import chirp
from phonometry import filters
# Señal estéreo de prueba: ruido rosa a la izquierda, barrido logarítmico a la derecha
fs, duration = 48000, 5
t = np.linspace(0, duration, fs * duration, endpoint=False)
rng = np.random.default_rng(42)
spec = np.fft.rfft(rng.standard_normal(t.size))
spec[1:] /= np.sqrt(np.arange(1, spec.size)) # conformado 1/f: ruido rosa
left = np.fft.irfft(spec, t.size)
right = chirp(t, f0=50, t1=duration, f1=10000, method="logarithmic")
x = np.stack([left, right]) # (2, n_samples)
spl, freq = filters.octave_filter(x, fs, fraction=3, limits=[20, 20000])
fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True)
for ax, levels, name in zip(axes, spl, ["Izquierdo: ruido rosa", "Derecho: barrido logarítmico"]):
ax.semilogx(freq, levels, marker="o", label=name)
ax.set_ylabel("Nivel [dB]")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
axes[-1].set_xlabel("Frecuencia [Hz]")
plt.show()

Lee la figura fila a fila antes de seguir. Las caídas en los dos extremos de la fila derecha son los límites propios del barrido, 50 Hz y 10 kHz, no un efecto del filtro. Y de esa planitud se siguen dos conclusiones. Un espectro plano en fracciones de octava corresponde a una densidad espectral de potencia , así que el ruido blanco no se ve plano en esta representación: sube 3 dB por octava, porque cada banda es un 26 % más ancha que la de debajo. Y dos señales con estructuras temporales completamente distintas — un ruido estacionario y un barrido que en un instante está en 50 Hz y cinco segundos después en 10 kHz — dan el mismo espectro, y por eso los niveles de banda de esta página responden a «cuánta energía hay por banda» y nunca a «cuándo». El espectrograma de octava de Niveles integrados y estadísticos es la herramienta que devuelve el eje del tiempo.

La convención es uniforme en toda la biblioteca: el tiempo siempre es el último eje. Aplica a octave_filter, OctaveFilterBank, weighting_filter, time_weighting, leq, laeq, ln_levels y spectrogram.

import numpy as np
from phonometry import filters
# Dos canales calibrados en Pa para que la guía funcione por sí sola
fs = 48000
t = np.arange(fs) / fs
left = 0.2 * np.sin(2 * np.pi * 1000 * t)
right = 0.1 * np.sin(2 * np.pi * 500 * t)
stereo = np.stack([left, right]) # (2, n_samples)
spl, freq = filters.octave_filter(stereo, fs, fraction=3)
# spl tiene forma (2, n_bands): una fila por canal
Flujo de formas de array: una entrada 1-D (muestras,) se reduce a un escalar y una entrada 2-D (canales, muestras) se reduce a (canales,), porque la operación actúa sobre el último eje mientras el eje de canal se conservaFlujo de formas de array: una entrada 1-D (muestras,) se reduce a un escalar y una entrada 2-D (canales, muestras) se reduce a (canales,), porque la operación actúa sobre el último eje mientras el eje de canal se conserva
EntradaSe interpreta comoSalida típica
Array 1D (n,)un canalnivel escalar / (bands,)
Array 2D (ch, n)ch canales de n muestras cada unoniveles (ch,) / (ch, bands)
lista de floatsun canal (se convierte)como 1D
(ch, n) en spectrogramSTFT multicanal(ch, bands, frames)

Todo se vectoriza a lo largo del eje inicial de canales: un único diseño de filtro se aplica a todos los canales en una sola llamada a SciPy. Convención: canales primero, como la mayoría del código DSP (soundfile devuelve (n, ch): transpón con x.T). Qué se gana con eso, y qué no, es el asunto de Rendimiento, más abajo.

El procesado multicanal es estrictamente por canal: nunca se mezcla, suma ni promedia nada a través del eje de canales. Tres consecuencias que conviene explicitar:

  • Combinar canales es decisión tuya. Los niveles vuelven uno por canal. Si necesitas un nivel promedio del array (por ejemplo los niveles promediados por posición de las normas de acústica de salas), combina energías tú mismo: 10 * np.log10(np.mean(10 ** (spl / 10), axis=0)), nunca la media aritmética de los valores en dB (ver Niveles para el porqué).
  • Un factor de calibración significa una sensibilidad. El factor escalar (calibration=LevelCalibration(factor=...) en un banco de filtros, calibration_factor= en leq, laeq, ln_levels, lc_peak y sel) multiplica todos los canales, lo que solo es correcto si todos comparten la misma sensibilidad. Para un array de micrófonos con calibraciones individuales, escala primero las filas, x * factors[:, None], y deja el factor en su valor por defecto, 1.0.
  • Las clases con estado guardan un estado por canal. En el procesado por bloques el array de estado se ajusta al número de canales y un cambio del número de canales lo reinicia; ver Procesado por bloques.
Panel superior: cinco curvas finas grises de tercios de octava correspondientes a cinco posiciones de una misma sala, junto con un promedio energético azul grueso y un promedio aritmético de los valores en decibelios en rojo discontinuo. Por encima de unos 400 Hz las cinco posiciones coinciden dentro de un decibelio y los dos promedios se superponen; por debajo, en la región modal, las posiciones se dispersan hasta quince decibelios y el promedio rojo discontinuo queda por debajo del azul. Panel inferior: la diferencia entre ambos banda a banda, nula por encima de 400 Hz y creciente hasta una subestimación máxima de 3,8 dB a 40 HzPanel superior: cinco curvas finas grises de tercios de octava correspondientes a cinco posiciones de una misma sala, junto con un promedio energético azul grueso y un promedio aritmético de los valores en decibelios en rojo discontinuo. Por encima de unos 400 Hz las cinco posiciones coinciden dentro de un decibelio y los dos promedios se superponen; por debajo, en la región modal, las posiciones se dispersan hasta quince decibelios y el promedio rojo discontinuo queda por debajo del azul. Panel inferior: la diferencia entre ambos banda a banda, nula por encima de 400 Hz y creciente hasta una subestimación máxima de 3,8 dB a 40 Hz

Las dos líneas de código, sobre un muestreo realista. Donde las posiciones coinciden, los dos promedios son el mismo número; donde discrepan — la región modal, que es justo el motivo por el que un muestreo de sala tiene varias posiciones — la media aritmética de los decibelios subestima hasta en 3,8 dB. El error nunca es negativo, así que no se compensa al añadir posiciones.

Mostrar el código de esta figura
# Cinco posiciones de una sala, niveles de banda como array (5, bandas). Aquí
# las posiciones se sintetizan; en campo son cinco filas de una captura.
rng = np.random.default_rng(7)
survey = np.stack([0.02 * (1 + 0.3 * rng.standard_normal())
* rng.standard_normal(2 * fs) for _ in range(5)])
survey_levels, _ = filters.octave_filter(survey, fs, fraction=3)
energetic = 10 * np.log10(np.mean(10 ** (survey_levels / 10), axis=0))
arithmetic = np.mean(survey_levels, axis=0)
print(f"subestimación máxima {np.max(energetic - arithmetic):.2f} dB")
# subestimación máxima 2.12 dB

El vector de sensibilidades del punto anterior no es aritmética, es un procedimiento, y hay tres cosas que deciden si el resultado es correcto.

# Una sensibilidad por cápsula, en Pa por unidad digital. Cada elemento sale de
# metrology.sensitivity(cal_tone[i], target_spl=94.0, fs=fs) sobre una captura
# de calibración en la que el calibrador estaba acoplado a la cápsula i y no se
# tocó nada más.
factors = np.array([0.01021, 0.00984, 0.01007, 0.00998])
survey = np.stack([left, right, left, right]) # tu captura (4, n)
calibrated = survey * factors[:, None]
spl, freq = filters.octave_filter(calibrated, fs, fraction=3)

Fija las ganancias antes del primer tono y no vuelvas a tocarlas, porque cada factor solo vale para la ganancia con la que se midió. Acopla el calibrador a cada cápsula, una a una, en lugar de reutilizar un factor para todo el array: cápsulas del mismo modelo difieren en algunas décimas de decibelio, y esa diferencia se traslada directamente a un nivel promediado por posición. Repite la pasada al final de la sesión y toma la mayor diferencia por canal como cota de deriva del array. Y luego dos invariantes que ninguna comprobación posterior puede recuperar si están mal: todos los canales tienen que venir de un único reloj de conversión (dos interfaces son dos relojes, y una deriva relativa lenta arruina cualquier análisis entre canales), y el mapa de fila a posición hay que anotarlo en el momento de la captura, porque un par intercambiado produce niveles perfectamente válidos atribuidos a las posiciones equivocadas.

Captura con un array de cuatro micrófonos. A la izquierda, cuatro micrófonos de medida sobre trípodes con la separación de un muestreo de sala, etiquetados de P1 a P4, con las cápsulas a 1,2 m del suelo y cada una rotulada con su propia sensibilidad en milivoltios por pascal; un calibrador acústico aparece acoplado a la cápsula P2 con una flecha discontinua que indica que va pasando de cápsula en cápsula. En el centro, un preamplificador de cuatro canales y una interfaz con una barra resaltada que abarca las cuatro entradas y dice reloj de muestreo único, fs igual a 48 kHz, y a su lado una segunda interfaz tachada con la leyenda dos interfaces son dos relojes, no un array. A la derecha, el array x dibujado como cuatro filas apiladas etiquetadas de ch0 igual a P1 hasta ch3 igual a P4, con la forma (4, N). Un pie repite los tres invariantes: un reloj, ganancias fijadas, mapa de fila a posición anotadoCaptura con un array de cuatro micrófonos. A la izquierda, cuatro micrófonos de medida sobre trípodes con la separación de un muestreo de sala, etiquetados de P1 a P4, con las cápsulas a 1,2 m del suelo y cada una rotulada con su propia sensibilidad en milivoltios por pascal; un calibrador acústico aparece acoplado a la cápsula P2 con una flecha discontinua que indica que va pasando de cápsula en cápsula. En el centro, un preamplificador de cuatro canales y una interfaz con una barra resaltada que abarca las cuatro entradas y dice reloj de muestreo único, fs igual a 48 kHz, y a su lado una segunda interfaz tachada con la leyenda dos interfaces son dos relojes, no un array. A la derecha, el array x dibujado como cuatro filas apiladas etiquetadas de ch0 igual a P1 hasta ch3 igual a P4, con la forma (4, N). Un pie repite los tres invariantes: un reloj, ganancias fijadas, mapa de fila a posición anotado

La tabla de formas de más arriba es la abstracción de ese dibujo: cada fila de x es un micrófono, y la correspondencia entre un índice de fila y una posición sobre el suelo solo existe en tus notas.

Empieza por lo que agrupar en lotes no te da. La aritmética escala linealmente con los canales, porque cada canal hay que filtrarlo con cada banda, así que ocho canales cuestan ocho canales se escriba como se escriba la llamada. Medido a 48 kHz sobre 5 s por canal con un mismo banco de tercios de octava reutilizado:

CanalesUna llamada [ms]Bucle por canal [ms]Cociente
11931730,90
23373521,04
8139417211,23
32564652080,92

El cociente ronda la unidad, que es la respuesta honesta: lo que quita el lote es la sobrecarga de Python por llamada y la tentación de rediseñar el banco, y es el rediseño lo que de verdad cuesta: construir un banco de tercios de octava nuevo lleva unos 33 ms, un orden de magnitud más que una sola pasada de filtrado por banda sobre una trama corta.

Así que el modelo de coste es sencillo. Domina una pasada de filtrado SOS por banda y por canal, lo que significa que el tiempo escala con el número de bandas (un banco de tercios de octava es el triple que uno de octava), con la duración del registro y con el número de canales, y con nada más. Las reglas prácticas que se siguen: construye un OctaveFilterBank y reutilízalo; pasa todos los canales en un array por claridad, no por velocidad; y cuando una ejecución sea demasiado lenta, tira de fraction, limits y la duración del registro, que son los tres términos del modelo.

OctaveFilterBank es la herramienta para el análisis repetido o en streaming: un banco diseña sus filtros una vez y los aplica a cada trama, y el broadcasting de NumPy cubre todos los canales de una trama en una sola llamada de filtrado por banda, sin bucle Python sobre canales.

import numpy as np
from phonometry import filters
# Dos canales calibrados en Pa para que la guía funcione por sí sola
fs = 48000
t = np.arange(fs) / fs
left = 0.2 * np.sin(2 * np.pi * 1000 * t)
right = 0.1 * np.sin(2 * np.pi * 500 * t)
stereo = np.stack([left, right]) # (2, n_samples)
bank = filters.OctaveFilterBank(
fs=48000, fraction=3, design=filters.FilterDesign(filter_type='butter'))
# Propiedades calculadas
# bank.freq (centros), bank.freq_d (bordes inferiores), bank.freq_u (superiores), bank.sos
# Procesar múltiples señales de forma eficiente
stream = [stereo] # tu secuencia de tramas multicanal
for frame in stream:
# detrend=True (por defecto) elimina el offset DC y mejora la precisión en graves
spl, freq = bank.filter(frame, detrend=True)

Notas adicionales de rendimiento:

  • Caché de diseño: octave_filter() reutiliza los diseños del banco entre llamadas con parámetros idénticos (caché LRU de 32 entradas), así que llamarla en bucle no rediseña el banco cada vez. OctaveFilterBank te da control explícito sobre el ciclo de vida del diseño.
  • Diezmado multitasa: las bandas graves se filtran a una frecuencia diezmada, lo que es a la vez más rápido y numéricamente más estable (consulta Teoría).
  • numba opcional: el kernel del modo impulse de la ponderación temporal se compila JIT cuando numba está instalado (pip install phonometry[perf]).
  • Cubierto

    La IEC 61260-1:2014 y la IEC 61672-1:2013 en lo que condicionan cada fila de un array (canales, muestras): los filtros de banda, las ponderaciones y la balística del detector aplicados por canal son los mismos diseños monocanal sin cambios, y el soporte multicanal no añade contenido normativo propio. La convención misma sigue el procedimiento de Bendat y Piersol para registros de datos múltiples (apartado 10.4.2): analizar cada registro individualmente primero, y tratar lo conjunto como un paso aparte y explícito.

  • No cubierto

    Las preguntas entre canales, como el retardo entre dos canales o cuánto de uno explica el otro, quedan deliberadamente fuera: octave_filter, weighting_filter y time_weighting nunca mezclan ni combinan filas. Consulta Correlación y retardo y Coherencia múltiple y parcial para las herramientas entre canales construidas sobre la correlación cruzada y los modelos de entrada múltiple de Bendat y Piersol.

  • Correlación y retardo: las preguntas cruzadas en el dominio del tiempo (retardo, alineación) que la ruta por canal deja deliberadamente en tus manos.
  • Coherencia múltiple y parcial: cuál de varios canales correlacionados excita realmente una respuesta (Bendat y Piersol, cap. 7).
  • Procesado por bloques: la contrapartida en streaming, con un estado de filtro por canal.
  • Niveles: las métricas de nivel por canal, y por qué los dB se combinan energéticamente.
  • Referencia de la API: phonometry y filters.core.
  • Bendat, J. S. y Piersol, A. G. (2010). Random data: Analysis and measurement procedures (4.ª ed.). Wiley. https://doi.org/10.1002/9781118032428La sección 10.4.2 (el procedimiento para analizar múltiples registros de datos: primero el análisis individual por registro y, como paso aparte y deliberado, el análisis cruzado conjunto) y el capítulo 7 (los modelos de múltiples entradas/salidas a los que conduce ese paso conjunto). ISBN 978-0-470-24877-5.
  • International Electrotechnical Commission. (2013). Electroacoustics — Sound level meters — Part 1: Specifications (IEC 61672-1:2013). La semántica de ponderación e integración temporal aplicada por canal, exactamente como prescribe la norma monocanal.
  • International Electrotechnical Commission. (2014). Electroacoustics — Octave-band and fractional-octave-band filters — Part 1: Specifications (IEC 61260-1:2014). Las definiciones de banda con las que se filtra cada canal; la ruta multicanal las procesa por lotes sin cambios. El soporte multicanal no añade contenido normativo propio: cada canal se filtra exactamente como prescribe la norma monocanal, y la vectorización solo agrupa el cálculo a lo largo del eje de canales.