WasuratS's picture
Initial commit app.py
d457caf verified
Raw
History Blame
26.9 kB
"""
Marine Soundscape Analyzer β€” Gradio App
Designed for coral reef hydrophone recordings.
Analyses provided:
β€’ Waveform
β€’ Linear + Mel Spectrogram
β€’ Log-Frequency Spectrogram
β€’ Power Spectral Density (Welch)
β€’ Spectral Centroid over time
β€’ MFCC heatmap
β€’ Acoustic Complexity Index (ACI) – Pieretti et al. 2011
β€’ Bioacoustic Index (BI) – Boelman et al. 2007
β€’ Normalized Difference Soundscape Index (NDSI) – Kasten et al. 2012
β€’ Acoustic Diversity Index (ADI) – Villanueva-Rivera et al. 2011
β€’ Spectral Entropy (Hf) + Temporal Entropy (Ht) – Sueur et al. 2008
β€’ Summary report table
"""
import warnings
warnings.filterwarnings("ignore")
import os
import numpy as np
import librosa
import librosa.display
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from scipy import signal
import gradio as gr
# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────
BG_COLOR = "#0b1e35"
GRID_COLOR = "#1e3a5f"
TEXT_COLOR = "#cfe8ff"
ACCENT = "#00d4ff"
N_FFT = 2048
HOP = 512
MAX_DUR_S = 300 # clip to 5 min for HuggingFace timeout safety
# ──────────────────────────────────────────────────────────────────────────────
# Acoustic Index Implementations
# ──────────────────────────────────────────────────────────────────────────────
def aci(Sxx: np.ndarray, j_bin: int = 5) -> float:
"""Acoustic Complexity Index (Pieretti et al. 2011)."""
total = 0.0
for j in range(0, Sxx.shape[1] - j_bin, j_bin):
sl = Sxx[:, j : j + j_bin]
denom = sl.sum()
if denom > 0:
total += np.abs(np.diff(sl, axis=1)).sum() / denom
return float(total)
def bioacoustic_index(Sxx: np.ndarray, freqs: np.ndarray,
f_min: float = 2000, f_max: float = 8000) -> float:
"""Bioacoustic Index (Boelman et al. 2007)."""
mask = (freqs >= f_min) & (freqs <= f_max)
if not mask.any():
return 0.0
sl = Sxx[mask, :]
db = librosa.amplitude_to_db(sl + 1e-10, ref=np.max)
mu = db.mean(axis=1)
shifted = mu - mu.min()
return float(shifted.mean())
def ndsi(Sxx: np.ndarray, freqs: np.ndarray) -> float:
"""Normalized Difference Soundscape Index (Kasten et al. 2012).
Anthropogenic band: 1–2 kHz; Biotic band: 2–11 kHz."""
anthro = Sxx[(freqs >= 1000) & (freqs <= 2000), :].sum()
bio = Sxx[(freqs >= 2000) & (freqs <= 11000), :].sum()
denom = anthro + bio
return float((bio - anthro) / denom) if denom > 0 else 0.0
def adi(Sxx: np.ndarray, freqs: np.ndarray,
f_max: float = 10000, db_thresh: float = -50, n_bands: int = 10) -> float:
"""Acoustic Diversity Index (Villanueva-Rivera et al. 2011)."""
mask = freqs <= f_max
db = librosa.amplitude_to_db(Sxx[mask, :] + 1e-10, ref=np.max)
mu = db.mean(axis=1)
band_sz = len(mu) // n_bands
if band_sz == 0:
return 0.0
counts = np.array([
(mu[i * band_sz : (i + 1) * band_sz] > db_thresh).sum()
for i in range(n_bands)
], dtype=float)
total = counts.sum()
if total == 0:
return 0.0
p = counts / total
p = p[p > 0]
return float(-(p * np.log(p)).sum())
def spectral_entropy(Sxx: np.ndarray) -> float:
"""Normalized spectral entropy Hf (Sueur et al. 2008)."""
power = (Sxx ** 2).mean(axis=1)
total = power.sum()
if total == 0:
return 0.0
p = power / total
p = p[p > 0]
return float(-(p * np.log(p)).sum() / np.log(len(power)))
def temporal_entropy(y: np.ndarray, n_env: int = 1000) -> float:
"""Normalized temporal entropy Ht (Sueur et al. 2008)."""
frame = max(1, len(y) // n_env)
env = np.array([
np.sqrt((y[i : i + frame] ** 2).mean())
for i in range(0, len(y) - frame, frame)
])
total = env.sum()
if total == 0:
return 0.0
p = env / total
p = p[p > 0]
return float(-(p * np.log(p)).sum() / np.log(len(env)))
# ──────────────────────────────────────────────────────────────────────────────
# Plot helpers
# ──────────────────────────────────────────────────────────────────────────────
def _make_fig(nrows=1, ncols=1, figsize=(12, 4)):
fig, axes = plt.subplots(nrows, ncols, figsize=figsize, facecolor=BG_COLOR)
return fig, axes
def _style(ax, title="", xlabel="", ylabel=""):
ax.set_facecolor(BG_COLOR)
ax.set_title(title, color=ACCENT, fontsize=12, fontweight="bold", pad=8)
ax.set_xlabel(xlabel, color=TEXT_COLOR, fontsize=9)
ax.set_ylabel(ylabel, color=TEXT_COLOR, fontsize=9)
ax.tick_params(colors=TEXT_COLOR, labelsize=8)
for sp in ax.spines.values():
sp.set_edgecolor(GRID_COLOR)
ax.grid(True, alpha=0.18, color=GRID_COLOR)
def _colorbar(fig, im, ax, label="dB"):
cb = fig.colorbar(im, ax=ax, pad=0.02, aspect=25)
cb.set_label(label, color=TEXT_COLOR, fontsize=8)
cb.ax.yaxis.set_tick_params(color=TEXT_COLOR, labelsize=7)
plt.setp(cb.ax.yaxis.get_ticklabels(), color=TEXT_COLOR)
# ──────────────────────────────────────────────────────────────────────────────
# Core Analysis
# ──────────────────────────────────────────────────────────────────────────────
def analyze(file_path):
if file_path is None:
return (None,) * 5 + ("⚠️ Please upload an audio file.",)
# ── Load ──────────────────────────────────────────────────────────────────
try:
y, sr = librosa.load(file_path, sr=None, mono=True, duration=MAX_DUR_S)
except Exception as exc:
return (None,) * 5 + (f"❌ Could not load file: {exc}",)
duration = len(y) / sr
clipped = duration >= MAX_DUR_S
n_fft_use = min(N_FFT, 2 ** int(np.log2(len(y) / 4))) # safe for short files
# ── STFT ──────────────────────────────────────────────────────────────────
D = librosa.stft(y, n_fft=n_fft_use, hop_length=HOP)
Sxx = np.abs(D)
D_db = librosa.amplitude_to_db(Sxx, ref=np.max)
freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft_use)
frame_t = librosa.frames_to_time(np.arange(Sxx.shape[1]), sr=sr, hop_length=HOP)
# ── Spectral features (used in multiple plots) ────────────────────────────
centroid = librosa.feature.spectral_centroid(y=y, sr=sr, hop_length=HOP)[0]
c_times = librosa.frames_to_time(np.arange(len(centroid)), sr=sr, hop_length=HOP)
# ══════════════════════════════════════════════════════════════════════════
# FIGURE 1 β€” Waveform
# ══════════════════════════════════════════════════════════════════════════
t = np.linspace(0, duration, len(y))
fig1, ax1 = _make_fig(figsize=(13, 3))
ax1.plot(t, y, color=ACCENT, linewidth=0.45, alpha=0.85)
ax1.fill_between(t, y, 0, alpha=0.15, color=ACCENT)
_style(ax1, "Waveform", "Time (s)", "Amplitude")
ax1.set_xlim(0, duration)
if clipped:
ax1.set_title(f"Waveform (showing first {MAX_DUR_S}s)", color=ACCENT,
fontsize=12, fontweight="bold")
fig1.tight_layout(pad=0.8)
# ══════════════════════════════════════════════════════════════════════════
# FIGURE 2 β€” Spectrograms (linear + mel + log)
# ══════════════════════════════════════════════════════════════════════════
fmax_mel = min(sr // 2, 20000)
mel_spec = librosa.feature.melspectrogram(
y=y, sr=sr, n_fft=n_fft_use, hop_length=HOP, n_mels=128, fmax=fmax_mel
)
mel_db = librosa.power_to_db(mel_spec, ref=np.max)
fig2, axes2 = _make_fig(3, 1, figsize=(13, 11))
CMAP = "magma"
VRANGE = dict(vmin=-80, vmax=0)
# Linear
im1 = librosa.display.specshow(
D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="hz",
ax=axes2[0], cmap=CMAP, **VRANGE
)
_style(axes2[0], "Spectrogram β€” Linear Frequency", "Time (s)", "Frequency (Hz)")
_colorbar(fig2, im1, axes2[0])
# Mel
im2 = librosa.display.specshow(
mel_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="mel",
ax=axes2[1], cmap=CMAP, fmax=fmax_mel, **VRANGE
)
_style(axes2[1], "Spectrogram β€” Mel Scale", "Time (s)", "Mel Frequency")
_colorbar(fig2, im2, axes2[1])
# Log
im3 = librosa.display.specshow(
D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="log",
ax=axes2[2], cmap=CMAP, **VRANGE
)
_style(axes2[2], "Spectrogram β€” Log Frequency", "Time (s)", "Frequency (Hz, log)")
_colorbar(fig2, im3, axes2[2])
fig2.tight_layout(pad=1.2)
# ══════════════════════════════════════════════════════════════════════════
# FIGURE 3 β€” PSD Β· Spectral Centroid Β· MFCC
# ══════════════════════════════════════════════════════════════════════════
f_psd, psd = signal.welch(y, sr, nperseg=min(4096, len(y) // 2))
psd_db = 10 * np.log10(psd + 1e-20)
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20, hop_length=HOP)
mfcc_delta = librosa.feature.delta(mfcc)
fig3, axes3 = _make_fig(3, 1, figsize=(13, 12))
# PSD
axes3[0].plot(f_psd[1:], psd_db[1:], color="#ff7f0e", linewidth=1.3)
marine_bands = [
(20, 1000, "#2ca02c", "Fish & low-freq (20–1 kHz)"),
(1000, 5000, "#ff7f0e", "Snapping shrimp (1–5 kHz)"),
(5000, min(sr / 2, 20000), "#d62728", "High-freq biotic (5–20 kHz)"),
]
for flo, fhi, color, label in marine_bands:
if fhi <= sr / 2 and flo < sr / 2:
axes3[0].axvspan(flo, min(fhi, sr / 2), alpha=0.12, color=color, label=label)
axes3[0].set_xscale("log")
axes3[0].set_xlim(max(20, f_psd[1]), sr / 2)
_style(axes3[0], "Power Spectral Density (Welch)", "Frequency (Hz)", "PSD (dB/Hz)")
axes3[0].legend(fontsize=8, loc="lower left",
facecolor=BG_COLOR, edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
# Spectral centroid
axes3[1].plot(c_times, centroid, color="#9467bd", linewidth=1.1, alpha=0.9)
axes3[1].fill_between(c_times, centroid, alpha=0.12, color="#9467bd")
axes3[1].set_xlim(0, duration)
_style(axes3[1], "Spectral Centroid Over Time", "Time (s)", "Frequency (Hz)")
# MFCC
im_mfcc = librosa.display.specshow(
mfcc, sr=sr, hop_length=HOP, x_axis="time",
ax=axes3[2], cmap="coolwarm"
)
_style(axes3[2], "MFCCs (20 coefficients)", "Time (s)", "MFCC Coefficient")
axes3[2].set_facecolor(BG_COLOR)
_colorbar(fig3, im_mfcc, axes3[2], label="Amplitude")
fig3.tight_layout(pad=1.2)
# ══════════════════════════════════════════════════════════════════════════
# FIGURE 4 β€” Acoustic Indices over time
# ══════════════════════════════════════════════════════════════════════════
# Adaptive window: aim for β‰₯8 windows; each window 5–60 s
win_s = max(5.0, min(60.0, duration / 8))
win_len = int(sr * win_s)
n_win = max(3, len(y) // win_len)
win_len = len(y) // n_win # recompute for even coverage
aci_v, bi_v, ndsi_v, adi_v, rms_v, centers = [], [], [], [], [], []
for i in range(n_win):
seg = y[i * win_len : (i + 1) * win_len]
centers.append((i + 0.5) * win_len / sr)
D_s = librosa.stft(seg, n_fft=n_fft_use, hop_length=HOP)
Sxx_s = np.abs(D_s)
aci_v.append(aci(Sxx_s))
bi_v.append(bioacoustic_index(Sxx_s, freqs))
ndsi_v.append(ndsi(Sxx_s, freqs))
adi_v.append(adi(Sxx_s, freqs))
rms_v.append(float(np.sqrt((seg ** 2).mean())))
centers = np.array(centers)
bw = win_len / sr * 0.72
fig4, axes4 = _make_fig(3, 2, figsize=(14, 13))
def bar_plot(ax, vals, color, title, ylabel):
ax.bar(centers, vals, width=bw, color=color, alpha=0.82)
_style(ax, title, "Time (s)", ylabel)
ax.set_xlim(0, duration)
bar_plot(axes4[0, 0], aci_v, "#1f77b4", "Acoustic Complexity Index (ACI)", "ACI")
bar_plot(axes4[0, 1], bi_v, "#2ca02c", "Bioacoustic Index (BI)", "BI")
# NDSI β€” colour by sign
ndsi_colors = ["#2ca02c" if v >= 0 else "#d62728" for v in ndsi_v]
axes4[1, 0].bar(centers, ndsi_v, width=bw, color=ndsi_colors, alpha=0.82)
axes4[1, 0].axhline(0, color=TEXT_COLOR, linewidth=0.8, linestyle="--", alpha=0.6)
_style(axes4[1, 0], "NDSI (green > 0 = biotic dominated)", "Time (s)", "NDSI")
axes4[1, 0].set_xlim(0, duration)
bar_plot(axes4[1, 1], adi_v, "#ff7f0e", "Acoustic Diversity Index (ADI)", "ADI")
# RMS energy
axes4[2, 0].plot(centers, rms_v, "o-", color="#e377c2", linewidth=1.6,
markersize=5, alpha=0.9)
axes4[2, 0].fill_between(centers, rms_v, alpha=0.15, color="#e377c2")
_style(axes4[2, 0], "RMS Energy Over Time", "Time (s)", "RMS Amplitude")
axes4[2, 0].set_xlim(0, duration)
# Short-time RMS spectrogram (energy heatmap)
rms_frame = librosa.feature.rms(y=y, frame_length=n_fft_use, hop_length=HOP)
rms_db_frame = librosa.amplitude_to_db(rms_frame, ref=np.max)
axes4[2, 1].plot(
librosa.frames_to_time(np.arange(rms_frame.shape[1]), sr=sr, hop_length=HOP),
rms_db_frame[0], color=ACCENT, linewidth=0.8, alpha=0.9
)
_style(axes4[2, 1], "Short-time RMS Energy (dBFS)", "Time (s)", "RMS (dB)")
axes4[2, 1].set_xlim(0, duration)
fig4.tight_layout(pad=1.2)
# ══════════════════════════════════════════════════════════════════════════
# FIGURE 5 β€” Onset + Bandwidth over time
# ══════════════════════════════════════════════════════════════════════════
onset_frames = librosa.onset.onset_detect(y=y, sr=sr, hop_length=HOP)
onset_times = librosa.frames_to_time(onset_frames, sr=sr, hop_length=HOP)
bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr, hop_length=HOP)[0]
rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, hop_length=HOP, roll_percent=0.85)[0]
flatness = librosa.feature.spectral_flatness(y=y, hop_length=HOP)[0]
zcr_frame = librosa.feature.zero_crossing_rate(y, hop_length=HOP)[0]
fig5, axes5 = _make_fig(3, 2, figsize=(14, 11))
axes5[0, 0].plot(c_times, bandwidth, color="#17becf", linewidth=0.9, alpha=0.9)
_style(axes5[0, 0], "Spectral Bandwidth Over Time", "Time (s)", "Bandwidth (Hz)")
axes5[0, 0].set_xlim(0, duration)
axes5[0, 1].plot(c_times, rolloff, color="#bcbd22", linewidth=0.9, alpha=0.9)
_style(axes5[0, 1], "Spectral Rolloff (85%) Over Time", "Time (s)", "Frequency (Hz)")
axes5[0, 1].set_xlim(0, duration)
axes5[1, 0].plot(c_times, flatness, color="#7f7f7f", linewidth=0.9, alpha=0.9)
_style(axes5[1, 0], "Spectral Flatness Over Time", "Time (s)", "Flatness [0–1]")
axes5[1, 0].set_xlim(0, duration)
axes5[1, 1].plot(c_times, zcr_frame, color="#8c564b", linewidth=0.7, alpha=0.9)
_style(axes5[1, 1], "Zero Crossing Rate Over Time", "Time (s)", "ZCR")
axes5[1, 1].set_xlim(0, duration)
# Onset plot
axes5[2, 0].plot(c_times, rms_db_frame[0], color=ACCENT, linewidth=0.7, alpha=0.7,
label="RMS (dBFS)")
for ot in onset_times:
axes5[2, 0].axvline(ot, color="#ff4444", linewidth=0.6, alpha=0.6)
axes5[2, 0].set_xlim(0, duration)
_style(axes5[2, 0], f"Onset Detection ({len(onset_times)} events)", "Time (s)", "RMS (dB)")
axes5[2, 0].text(0.01, 0.96, f"{len(onset_times)} onsets detected",
transform=axes5[2, 0].transAxes,
color="#ff4444", fontsize=9, va="top")
# MFCC delta (showing change)
im_delta = librosa.display.specshow(
mfcc_delta, sr=sr, hop_length=HOP, x_axis="time",
ax=axes5[2, 1], cmap="RdBu_r"
)
axes5[2, 1].set_facecolor(BG_COLOR)
_style(axes5[2, 1], "MFCC Delta (Rate of Change)", "Time (s)", "MFCC Coefficient")
_colorbar(fig5, im_delta, axes5[2, 1], label="Ξ” Amplitude")
fig5.tight_layout(pad=1.2)
# ══════════════════════════════════════════════════════════════════════════
# Summary Report
# ══════════════════════════════════════════════════════════════════════════
rms_overall = float(np.sqrt((y ** 2).mean()))
peak = float(np.abs(y).max())
rms_db_val = 20 * np.log10(rms_overall + 1e-12)
peak_db_val = 20 * np.log10(peak + 1e-12)
dyn_range = 20 * np.log10(peak / (rms_overall + 1e-12))
zcr_mean = float(librosa.feature.zero_crossing_rate(y).mean())
sp_c_mean = float(centroid.mean())
sp_bw_mean = float(bandwidth.mean())
sp_ro_mean = float(rolloff.mean())
sp_fl_mean = float(flatness.mean())
# Top 5 dominant frequencies (mean spectrum)
top5 = freqs[np.argsort((Sxx ** 2).mean(axis=1))[-5:][::-1]]
# Global indices
aci_g = aci(Sxx)
bi_g = bioacoustic_index(Sxx, freqs)
ndsi_g = ndsi(Sxx, freqs)
adi_g = adi(Sxx, freqs)
Hf = spectral_entropy(Sxx)
Ht = temporal_entropy(y)
H_total = Hf * Ht
ndsi_label = (
"Strong biotic dominance" if ndsi_g > 0.5 else
"Moderate biotic dominance" if ndsi_g > 0.0 else
"Moderate anthropogenic noise" if ndsi_g > -0.5 else
"Strong anthropogenic noise"
)
aci_label = (
"Very high complexity" if aci_g > 10000 else
"High complexity" if aci_g > 5000 else
"Moderate complexity" if aci_g > 1000 else
"Low complexity"
)
clipped_note = (
f"\n> ⚠️ File longer than {MAX_DUR_S}s β€” analysis performed on first {MAX_DUR_S}s only.\n"
if clipped else ""
)
report = f"""{clipped_note}
## πŸ“‹ Analysis Report
### 🎡 Basic Information
| Parameter | Value |
|-----------|-------|
| Duration | {duration:.2f} s |
| Sample Rate | {sr:,} Hz |
| Total Samples | {len(y):,} |
| Processing Mode | Mono |
| Analysis Windows | {n_win} Γ— {win_len/sr:.1f} s |
---
### πŸ“ˆ Amplitude Statistics
| Parameter | Value |
|-----------|-------|
| RMS Level | {rms_db_val:.1f} dBFS |
| Peak Level | {peak_db_val:.1f} dBFS |
| Dynamic Range | {dyn_range:.1f} dB |
| Zero Crossing Rate | {zcr_mean:.5f} |
| Detected Onsets | {len(onset_times)} events |
---
### 🌊 Spectral Features (mean over recording)
| Feature | Value |
|---------|-------|
| Spectral Centroid | {sp_c_mean:.1f} Hz |
| Spectral Bandwidth | {sp_bw_mean:.1f} Hz |
| Spectral Rolloff (85%) | {sp_ro_mean:.1f} Hz |
| Spectral Flatness | {sp_fl_mean:.5f} |
| Top 5 Dominant Freqs | {', '.join(f'{f:.0f} Hz' for f in top5)} |
---
### 🧬 Acoustic Indices (whole recording)
| Index | Value | Interpretation |
|-------|-------|----------------|
| **ACI** | {aci_g:.1f} | {aci_label} β€” higher = more varied amplitude patterns |
| **BI** | {bi_g:.2f} | Biological activity intensity in 2–8 kHz band |
| **NDSI** | {ndsi_g:.3f} | {ndsi_label} |
| **ADI** | {adi_g:.3f} | Shannon diversity across frequency bands |
| **Hf** (Spectral Entropy) | {Hf:.4f} | 0 = tonal, 1 = uniform spectrum |
| **Ht** (Temporal Entropy) | {Ht:.4f} | 0 = impulsive, 1 = stationary |
| **H** (Total Entropy) | {H_total:.4f} | Combined soundscape heterogeneity |
---
### 🐠 Marine Coral Reef Frequency Guide
| Band | Range | Typical Sources |
|------|-------|-----------------|
| Low | 20 – 1,000 Hz | Fish choruses, breaking waves, vessel traffic |
| Snapping Shrimp | 1 – 5 kHz | *Alpheid* snapping shrimp β€” reef health indicator |
| High Biotic | 5 – 20 kHz | Small crustaceans, urchins, high-frequency fish |
> **Reef health note:** Healthy reefs typically show strong broadband energy from snapping shrimp (1–20 kHz crackling), high ACI, and positive NDSI. Degraded reefs tend to be quieter and more tonally uniform.
---
*Indices: ACI (Pieretti et al. 2011) Β· BI (Boelman et al. 2007) Β· NDSI (Kasten et al. 2012) Β· ADI (Villanueva-Rivera et al. 2011) Β· H (Sueur et al. 2008)*
"""
return fig1, fig2, fig3, fig4, fig5, report
# ──────────────────────────────────────────────────────────────────────────────
# Gradio Interface
# ──────────────────────────────────────────────────────────────────────────────
CSS = """
body, .gradio-container {
background: linear-gradient(160deg, #071324 0%, #0b1e35 60%, #071324 100%) !important;
}
h1 { font-size: 2rem !important; }
.gr-button { font-weight: 600; }
footer { display: none !important; }
"""
_HERE = os.path.dirname(os.path.abspath(__file__))
_DATA = os.path.join(_HERE, "..", "data")
EXAMPLES = [
[os.path.join(_DATA, "Invertebrates", "Snapping Shrimp.wav")],
[os.path.join(_DATA, "Invertebrates", "Ghost Crab: Gastric Mill Stridulation.wav")],
[os.path.join(_DATA, "Mammal", "Humpback Whale Song.wav")],
[os.path.join(_DATA, "Mammal", "Fish", "Red Grouper Vocalization.wav")],
]
# Filter to only examples that actually exist (avoids errors on HuggingFace)
EXAMPLES = [e for e in EXAMPLES if os.path.isfile(e[0])]
with gr.Blocks(title="🌊 Marine Soundscape Analyzer", css=CSS,
theme=gr.themes.Base(
primary_hue="cyan",
secondary_hue="blue",
neutral_hue="slate",
font=gr.themes.GoogleFont("Inter"),
)) as demo:
gr.Markdown("""
# 🌊 Marine Soundscape Analyzer
**Coral Reef Acoustic Analysis Tool**
Upload a hydrophone recording to generate spectrograms, power spectral density, acoustic indices,
and a full analysis report β€” tailored for coral reef soundscape monitoring.
Supported formats: **WAV Β· MP3 Β· FLAC Β· OGG Β· AIFF** Β· Maximum analysed duration: **5 minutes**
""")
with gr.Row(equal_height=True):
with gr.Column(scale=3):
audio_in = gr.Audio(label="πŸ“ Upload Sound File", type="filepath")
with gr.Column(scale=1):
gr.Markdown("""
### Acoustic Indices
| Index | What it measures |
|-------|-----------------|
| **ACI** | Amplitude complexity |
| **BI** | Biological activity |
| **NDSI** | Biotic vs anthropogenic |
| **ADI** | Frequency diversity |
| **Hf / Ht** | Spectral / temporal entropy |
""")
analyze_btn = gr.Button("πŸ” Analyse Recording", variant="primary", size="lg")
with gr.Tabs():
with gr.Tab("πŸ“Š Waveform"):
plot_wave = gr.Plot()
with gr.Tab("πŸ”Š Spectrograms"):
plot_spec = gr.Plot()
with gr.Tab("πŸ“‘ Frequency Analysis + MFCC"):
plot_freq = gr.Plot()
with gr.Tab("🧬 Acoustic Indices"):
plot_idx = gr.Plot()
with gr.Tab("πŸ“ Temporal Features"):
plot_temp = gr.Plot()
with gr.Tab("πŸ“‹ Report"):
report_out = gr.Markdown()
analyze_btn.click(
fn=analyze,
inputs=[audio_in],
outputs=[plot_wave, plot_spec, plot_freq, plot_idx, plot_temp, report_out],
)
if EXAMPLES:
gr.Examples(
examples=EXAMPLES,
inputs=[audio_in],
label="🎧 Example Marine Recordings",
)
gr.Markdown("""
---
*Built for marine bioacoustic research Β· References: Pieretti et al. 2011, Boelman et al. 2007, Kasten et al. 2012, Villanueva-Rivera et al. 2011, Sueur et al. 2008*
""")
if __name__ == "__main__":
demo.launch(share=False)