Convert sound of heart beat into beats per minute
45 次查看(过去 30 天)
显示 更早的评论
Hello,
I have never before used matlab. I am a biology graduate student and I have a bunch of audio files(.mp3) of bird heart rate recorded on a standard audio recorder. What I would like to do is convert the audio into beats per minute, ideally for each minute in the ~48 hours of audio for each bird such that I can look at averages over select periods of time. Is this possible in matlab? If more information is needed to answer this question let me know.
8 个评论
Joseph Tricarico
2024-2-1
Hi Elyssa,
I realise that this post is a few years old, but if you do this work in the future, this may be useful to you:
Joe T.
回答(1 个)
Star Strider
2019-2-21
I worked on that for a while, including attempting wavelet denoising, without success. The heartbeats are easily audible, however detecting them reliably using every signal processing techhnique I’m aware of is essentially impossible. There is too much noise even for wavelet decomposition, and it’s very difficult to isolate individual heartbeats.
Nevertheless, here’s my code (that works even though it doesn’t result in usable heartbeat information) so you can experiment with it:
filename = 'sample_HR_audio.mp3';
[HRA0, Fs0] = audioread(filename);
% HRA = resample(HRA0, 1, 10);
HRA = HRA0(1:10:end); % Decimate
Fs = Fs0/10;
L = numel(HRA);
Ts = 1/Fs; % Sampling Interval
Fn = Fs/2; % Nyquist Frequency
t = linspace(0, L, L)/Fs; % Time Vector
figure
plot(t, HRA)
grid
xlabel('Time (s)')
ylabel('Amplitude')
title('Recorded Avian Phonocardiogram')
% soundsc(HRA(1:fix(L/5)), Fs)
FT_HRA = fft(HRA)/L; % Fourier Transform
Fv = linspace(0, 1, fix(L/2)+1)*Fn; % Frequency Vector
Iv = 1:numel(Fv); % Index Vector
[Pks,Locs] = findpeaks(abs(FT_HRA(Iv))*2, 'MinPeakHeight',2E-4, 'MinPeakDistance',2500);
Freqs = Fv(Locs);
figure
plot(Fv, abs(FT_HRA(Iv))*2)
hold on
plot(Fv(Locs), Pks, '^r')
hold off
grid
xlim([0, 500])
xlabel('Frequency (Hz)')
ylabel('Amplitude')
title('Fourier Transform of Avian Phonocardiogram')
text(200, min(Pks), sprintf('%.1f Hz %7.1E\n', [Freqs(:) Pks]'), 'VerticalAlignment','bottom')
Wp = [20 40]/Fn; % Design Filter & Filter Signal
Ws = [15 45]/Fn;
Rp = 1;
Rs = 75;
[n,Wp] = ellipord(Wp, Ws, Rp, Rs);
[z,p,k] = ellip(n,Rp,Rs,Wp);
[sos,g] = zp2sos(z,p,k);
HRA_Filtered = filtfilt(sos, g, HRA);
[envh,envl] = envelope(HRA_Filtered, 5000, 'peak'); % Calculate Envelope
figure
plot(t, HRA_Filtered)
hold on
plot(t, envh, t, envl)
hold off
grid
xlabel('Time (s)')
ylabel('Amplitude')
title('Bandpass-Filtered Avian Phonocardiogram')
env_mean = mean([envh,-envl],2);
[PksB,LocB] = findpeaks(env_mean, 'MinPeakDistance',500);
HR_mean = numel(PksB)/max(t)*60;
figure
plot(t, env_mean)
hold on
plot(t(LocB), PksB, '^r')
hold off
grid
xlabel('Time (s)')
ylabel('Amplitude')
title('Peaks and Mean Envelope of Bandpass-Filtered Avian Phonocardiogram')
text(mean(t), mean(PksB)/4, sprintf('Mean Heart Rate = %.1f / min', HR_mean), 'HorizontalAlignment','center')
The decimation step is intended to speed up the computations, since I had significant problems with the length of the signal even on my Ryzen 7 1700 machine with 64GB memory. It is not absolutely necessary.
4 个评论
Star Strider
2024-2-1
I decided to take another look at that.
Try this —
Uz = unzip('sample_HR_audio.zip');
[HRA0, Fs0] = audioread(Uz{1});
L = numel(HRA0);
t = linspace(0, L-1, L).'/Fs0; % Create Time Vector
figure
plot(t, HRA0)
grid
xlabel('Time (s)')
ylabel('Amplitude')
[p,f,t] = pspectrum(HRA0,Fs0,'spectrogram');
figure
waterfall(f,t,p')
xlabel('Frequency (Hz)')
ylabel('Time (seconds)')
wtf = gca;
wtf.XDir = 'reverse';
colormap(turbo)
view([30 45])
xlim([0 2.5E+2])
figure
plot(t, p(1,:))
grid
xlabel('Time (s)')
ylabel('Magnitude')
p1_filt = sgolayfilt(p(1,:), 3, 51); % Multiband Empirical FIR Filter
[pks,locs] = findpeaks(p1_filt, 'MinPeakProminence',1E-3);
Beats = t(locs) % Beat Times (s)
IBI = diff(Beats) % Inter-beat Interval
HR = 60/mean(IBI) % Beats/Minute
figure
plot(t, p1_filt)
hold on
plot(t(locs), pks, 'r^')
hold off
grid
xline((60:60:240), '--k', compose('%2d minutes',1:4))
xlabel('Time (s)')
ylabel('Magnitude')
.
Joseph Tricarico
2024-2-19
Thank you for circling back to this after five years! This was a great step forward. As your work shows, there are some pretty loud spikes in the clip that make it tough to identify heart beats across the clip. I worked with one of the more steady sections (30–60s), and was able to get some of the false positives knocked out by filtering on a percentile of the magnitude. I also implemented minimum peak distance as the inverse of the maximum expected heart rate, which I assumed to be 200 BPM. I unexpectedly got a more accurate result without the Savitsky-Golay filter applied (but have only tried it on a couple of clips).
I plan to mess with some dynamic range compression and circle back to this in the future.
%% Set constants
START_S = 30;
END_S = START_S + 30;
BP_ORDER = 20;
BP_LO = 50;
BP_HI = 200;
% DOWNSAMPLE_DIVISOR = 50;
MIN_PEAK_PROMINENCE = 5E-5;
MIN_PEAK_DISTANCE = 200^-1*60; % i.e., inverse of beats per second
SG_ORDER = 3; % polynomial order to use for Savitzky-Golay FIR filter
SG_FRAMELEN = 51; % for S-G filter; must be odd number
%% Read audio file
Uz = unzip('sample_HR_audio.zip');
[signal, fs] = audioread(Uz{1});
fprintf('Sample rate is %d Hz.\n', fs);
fprintf('Band-pass FIR filter is %d-%d Hz.\n', BP_LO, BP_HI);
fprintf('Savitzky-Golay filter frame length is %d (%.6f s).\n', SG_FRAMELEN, SG_FRAMELEN / fs);
%% Clip
if exist('START_S', 'var')
start = fs*START_S + 1;
else
start = 1;
end
if exist('END_S', 'var')
end_ = fs * END_S;
else
end_ = numel(signal);
end
signal = signal(start:end_)*10; % add gain
sample_ct = numel(signal);
T = linspace(0, sample_ct-1, sample_ct).' / fs; % Create Time Vector
%% Graph unfiltered signal amplitude over time
figure('Name', 'Amplitude vs. time (unfiltered)');
plot(T, signal);
grid;
xlabel('Time (s)');
ylabel('Amplitude');
%% Apply bandpass filter (BPF)
bandpassFilter = designfilt('bandpassfir', ...
'FilterOrder', BP_ORDER, ...
'CutoffFrequency1', BP_LO, ...
'CutoffFrequency2', BP_HI, ...
'SampleRate', fs);
% signal = filtfilt(bandpassFilter, signal);
figure('Name', 'Amplitude vs. time (BPF)')
plot(T, signal);
grid;
xlabel('Time (s)');
ylabel('Amplitude');
%% TODO: Dynamic range control
%% Display power spectrum waterfall
[p,f,t] = pspectrum(signal, fs, 'spectrogram');
figure('Name', 'Power Spectrum Waterfall (BPF)')
waterfall(f,t,p')
xlabel('Frequency (Hz)')
ylabel('Time (seconds)')
wtf = gca;
wtf.XDir = 'reverse';
colormap(turbo)
view([30 45]) % something about the angle?
xlim([0 250]) % freq range to graph
%% Graph magnitude over time
titletext = 'Magnitude over time';
figure('Name', titletext);
hold on;
p1 = normalize(p(1,:));
plot(t, p1);
xlabel('Time (s)');
ylabel('Normalised magnitude');
pQ1 = prctile(p1, 25);
pQ2 = prctile(p1, 50);
pQ3 = prctile(p1, 75);
customPercentilePoint = 65;
customPercentile = prctile(p1, customPercentilePoint);
pMean = mean(p1);
% yline(pQ1, '--g', sprintf('Q1=%f', pQ1));
% yline(pQ2, '--g', sprintf('Median=%f', pQ2));
% yline(pQ3, '--g', sprintf('Q3=%f', pQ3));
% Yes I know this will say weird things like '73th percentile'
yline(customPercentile, '--r', sprintf('%dth percentile=%f', customPercentilePoint, customPercentile));
yline(pMean, '--b', sprintf('Mean=%f', pMean));
[pks, locs] = findpeaks(p1, ...
'MinPeakProminence', MIN_PEAK_PROMINENCE, ...
'MinPeakDistance', MIN_PEAK_DISTANCE, ...
'MinPeakHeight', customPercentile);
plot(t(locs), pks, 'r^');
hbCt = length(t(locs));
fprintf('\nCounted %d heart beats from magnitude:\n', length(t(locs)));
bpm = hbCt / (max(T)/60);
fprintf('Mean: %.1f BPM.\n', bpm);
%% Apply Savitsky-Golay filter (smooths noise); find peaks; make new fig.
p1_sg = sgolayfilt(p(1,:), SG_ORDER, SG_FRAMELEN); % Multiband Empirical FIR Filter
figure('Name', 'S-G filtered magnitude over time')
hold on; % going to plot ref lines & peaks
plot(t, p1_sg(1,:));
p1_sg_pctl25 = prctile(p1_sg(1,:), 25);
yline(p1_sg_pctl25, '--r', '25th percentile')
[pks, locs] = findpeaks(p1_sg, ...
'MinPeakProminence', MIN_PEAK_PROMINENCE, ...
'MinPeakDistance', MIN_PEAK_DISTANCE);
plot(t(locs), pks, 'r^');
hbCt = length(t(locs));
fprintf('\nCounted %d heart beats from S-G filtered magnitude:\n', hbCt);
bpm = hbCt / (max(T)/60);
fprintf('Mean: %.1f BPM.\n', bpm);
另请参阅
类别
在 Help Center 和 File Exchange 中查找有关 Audio and Video Data 的更多信息
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!