Heartbeat / app.py
j-tobias
small updates on plots
16d4314
raw
history blame
9.31 kB
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import gradio as gr
import numpy as np
import itertools
import librosa
import os
example_dir = "Examples"
example_files = [os.path.join(example_dir, f) for f in os.listdir(example_dir) if f.endswith(('.wav', '.mp3', '.ogg'))]
example_pairs = [list(pair) for pair in itertools.combinations(example_files, 2)][:25] # Limit to 5 pairs
# GENERAL HELPER FUNCTIONS
def getaudiodata(audio:gr.Audio)->tuple[int,np.ndarray]:
# Extract audio data and sample rate
sr, audiodata = audio
# Ensure audiodata is a numpy array
if not isinstance(audiodata, np.ndarray):
audiodata = np.array(audiodata)
# Check if audio is mono or stereo
if len(audiodata.shape) > 1:
# If stereo, convert to mono by averaging channels
audiodata = np.mean(audiodata, axis=1)
audiodata = np.astype(audiodata, np.float16)
return sr, audiodata
# HELPER FUNCTIONS FOR SINGLE AUDIO ANALYSIS
def getBeats(audiodata:np.ndarray, sr:int):
# Compute onset envelope
onset_env = librosa.onset.onset_strength(y=audiodata, sr=sr)
# Detect beats
tempo, beats = librosa.beat.beat_track(onset_envelope=onset_env, sr=sr)
# Convert beat frames to time
beattimes = librosa.frames_to_time(beats, sr=sr)
return tempo[0], beattimes
def plotCombined(audiodata, sr):
# Create subplots
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1,
subplot_titles=('Audio Waveform', 'Spectrogram'))
# Waveform plot
time = (np.arange(0, len(audiodata)) / sr)*2
fig.add_trace(
go.Scatter(x=time, y=audiodata, mode='lines', name='Waveform', line=dict(color='blue', width=1)),
row=1, col=1
)
# Spectrogram plot
D = librosa.stft(audiodata)
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
times = librosa.times_like(S_db)
freqs = librosa.fft_frequencies(sr=sr)
fig.add_trace(
go.Heatmap(z=S_db, x=times, y=freqs, colorscale='Viridis',
zmin=S_db.min(), zmax=S_db.max(), colorbar=dict(title='Magnitude (dB)')),
row=2, col=1
)
# Update layout
fig.update_layout(
height=800, width=900,
title_text="Audio Analysis",
)
fig.update_xaxes(title_text="Time (s)", row=2, col=1)
fig.update_yaxes(title_text="Amplitude", row=1, col=1)
fig.update_yaxes(title_text="Frequency (Hz)", type="log", row=2, col=1)
return fig
def plotbeatshist(tempo, beattimes):
# Calculate beat durations
beat_durations = np.diff(beattimes)
# Create histogram
fig = go.Figure()
fig.add_trace(go.Histogram(
x=beat_durations,
nbinsx=60, # You can adjust the number of bins as needed
name='Beat Durations'
))
# Add vertical line for average beat duration
avg_duration = 60 / tempo # Convert tempo (BPM) to seconds
fig.add_vline(x=avg_duration, line_dash="dash", line_color="red",
annotation_text=f"Average: {avg_duration:.2f}s",
annotation_position="top right")
# Update layout
fig.update_layout(
title_text='Histogram of Beat Durations',
xaxis_title_text='Beat Duration (seconds)',
yaxis_title_text='Count',
bargap=0.05, # gap between bars
)
return fig
def analyze_single(audio:gr.Audio):
# Extract audio data and sample rate
sr, audiodata = getaudiodata(audio)
# Now you have:
# - audiodata: a 1D numpy array containing the audio samples
# - sr: the sample rate of the audio
# Your analysis code goes here
# For example, you could print basic information:
print(f"Audio length: {len(audiodata) / sr:.2f} seconds")
print(f"Sample rate: {sr} Hz")
zcr = librosa.feature.zero_crossing_rate(audiodata)[0]
print(f"Mean Zero Crossing Rate: {np.mean(zcr):.4f}")
# Calculate RMS Energy
rms = librosa.feature.rms(y=audiodata)[0]
print(f"Mean RMS Energy: {np.mean(rms):.4f}")
tempo, beattimes = getBeats(audiodata, sr)
spectogram_wave = plotCombined(audiodata, sr)
beats_histogram = plotbeatshist(tempo, beattimes)
# Return your analysis results
results = f"""
- Audio length: {len(audiodata) / sr:.2f} seconds
- Sample rate: {sr} Hz
- Mean Zero Crossing Rate: {np.mean(zcr):.4f}
- Mean RMS Energy: {np.mean(rms):.4f}
- Tempo: {tempo:.4f}
- Beats: {beattimes}
- Beat durations: {np.diff(beattimes)}
- Mean Beat Duration: {np.mean(np.diff(beattimes)):.4f}
"""
return results, spectogram_wave, beats_histogram
#-----------------------------------------------
#-----------------------------------------------
# HELPER FUNCTIONS FOR DUAL AUDIO ANALYSIS
def analyze_double(audio1:gr.Audio, audio2:gr.Audio):
sr1, audiodata1 = getaudiodata(audio1)
sr2, audiodata2 = getaudiodata(audio2)
combinedfig = plotCombineddouble(audiodata1, sr1, audiodata2, sr2)
return combinedfig
def plotCombineddouble(audiodata1, sr1, audiodata2, sr2):
# Create subplots
fig = make_subplots(rows=2, cols=2, shared_xaxes=True, vertical_spacing=0.1,
subplot_titles=['Audio 1 Waveform','Audio 2 Audio Waveform', 'Audio 1 Spectrogram', 'Audio 2 Spectrogram'])
# Waveform plot
time = (np.arange(0, len(audiodata1)) / sr1)*2
fig.add_trace(
go.Scatter(x=time, y=audiodata1, mode='lines', line=dict(color='blue', width=1), showlegend=False),
row=1, col=1
)
# Spectrogram plot
D = librosa.stft(audiodata1)
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
times = librosa.times_like(S_db)
freqs = librosa.fft_frequencies(sr=sr1)
fig.add_trace(
go.Heatmap(z=S_db, x=times, y=freqs, colorscale='Viridis',
zmin=S_db.min(), zmax=S_db.max(), showlegend=False),#, colorbar=dict(title='Magnitude (dB)')),
row=2, col=1
)
# Waveform plot
time = (np.arange(0, len(audiodata2)) / sr2)*2
fig.add_trace(
go.Scatter(x=time, y=audiodata2, mode='lines', line=dict(color='blue', width=1), showlegend=False),
row=1, col=2
)
# Spectrogram plot
D = librosa.stft(audiodata2)
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
times = librosa.times_like(S_db)
freqs = librosa.fft_frequencies(sr=sr2)
fig.add_trace(
go.Heatmap(z=S_db, x=times, y=freqs, colorscale='Viridis',
zmin=S_db.min(), zmax=S_db.max(), showlegend=False),#, colorbar=dict(title='Magnitude (dB)')),
row=2, col=2
)
# Update layout
fig.update_layout(
height=800, width=1200,
title_text="Audio Analysis",
showlegend=False
)
fig.update_xaxes(title_text="Time (s)", row=2, col=1)
fig.update_yaxes(title_text="Amplitude", row=1, col=1)
fig.update_yaxes(title_text="Frequency (Hz)", type="log", row=2, col=1)
fig.update_xaxes(title_text="Time (s)", row=2, col=2)
fig.update_yaxes(title_text="Amplitude", row=1, col=2)
fig.update_yaxes(title_text="Frequency (Hz)", type="log", row=2, col=2)
return fig
with gr.Blocks() as app:
gr.Markdown("# Heartbeat")
gr.Markdown("This App helps to analyze and extract Information from Heartbeats")
gr.Markdown("""
- Beat (mean) (average heartbeat duration)
- S1, S2 (mean) (average S1,S2 duration)
- mean - herzschlag (synthesised) - Bild (Wave & Spectogram)
- FFT & Mel Spectogram
- Plot of Wave & Spectogram (Beats annotated)
""")
with gr.Tab("Single Audio"):
audiofile = gr.Audio(
label="Audio of a Heartbeat",
sources="upload")
analyzebtn = gr.Button("analyze")
results = gr.Markdown()
spectogram_wave = gr.Plot()
beats_histogram = gr.Plot()
analyzebtn.click(analyze_single, audiofile, [results, spectogram_wave, beats_histogram])
gr.Examples(
examples=example_files,
inputs=audiofile,
outputs=[results, spectogram_wave],
fn=analyze_single,
cache_examples=False
)
gr.Markdown("""### Open TODO's
- Create Histogram for Beat durations
- classify Beat's into S1 and S2
- synthesise the mean Beat S1 & S2""")
with gr.Tab("Two Audios"):
with gr.Row():
audioone = gr.Audio(
label="Audio of a Heartbeat",
sources="upload")
audiotwo = gr.Audio(
label="Audio of a Heartbeat",
sources="upload")
analyzebtn2 = gr.Button("analyze & compare")
with gr.Accordion("Results",open=False):
results2 = gr.Markdown()
spectogram_wave2 = gr.Plot()
analyzebtn2.click(analyze_double, inputs=[audioone,audiotwo], outputs=spectogram_wave2)
# Add gr.Examples for the Two Audios tab
gr.Examples(
examples=example_pairs, # Create pairs of the same file for demonstration
inputs=[audioone, audiotwo],
outputs=spectogram_wave2,
fn=analyze_double,
cache_examples=False
)
if __name__ == "__main__":
app.launch()