from functools import lru_cache from typing import List import librosa import numpy as np from mugen import paths from mugen.events import EventList, Event class AudioEvent(Event): """ An event in some audio """ pass class End(AudioEvent): """ The end of some audio """ pass class Beat(AudioEvent): """ A beat in some audio """ pass class WeakBeat(Beat): """ A weak beat in some audio """ pass class Onset(AudioEvent): """ An onset in some audio """ pass class Audio: """ Wraps the audio ouput from librosa, providing access to extra features Attributes ---------- file Loaded audio file samples Audio samples sample_rate Audio sample rate duration Audio duration (seconds) """ file: str sample_rate: int samples: np.ndarray duration: float def __init__(self, file: str, *, sample_rate: int = 44100): """ Parameters ---------- file Audio file to load """ self.file = file self.samples, self.sample_rate = librosa.load(file, sr=sample_rate) self.duration = librosa.get_duration(y=self.samples, sr=self.sample_rate) def __repr__(self): filename = paths.filename_from_path(self.file) return f'<Audio, file: {filename}, duration: {self.duration}>' def beats(self, trim: bool = False) -> EventList: """ Gets beat events Parameters ---------- trim Label weak leading and trailing beats separately Returns ------- Detected beat events from the audio """ untrimmed_beats = self._beats() untrimmed_beats = EventList([Beat(beat) for beat in untrimmed_beats], end=self.duration) if not trim: beats = untrimmed_beats else: trimmed_beats = self._beats(trim=True) trimmed_leading_beats = [beat for beat in untrimmed_beats.locations if beat < trimmed_beats[0]] trimmed_trailing_beats = [beat for beat in untrimmed_beats.locations if beat > trimmed_beats[-1]] # Mark leading & trailing trimmed beats as weak beats trimmed_beats = EventList([Beat(beat) for beat in trimmed_beats], end=self.duration) trimmed_leading_beats = EventList([WeakBeat(beat) for beat in trimmed_leading_beats], end=self.duration) trimmed_trailing_beats = EventList([WeakBeat(beat) for beat in trimmed_trailing_beats], end=self.duration) beats = trimmed_leading_beats + trimmed_beats + trimmed_trailing_beats return beats @lru_cache(maxsize=None) def _beats(self, trim: bool = False) -> List[float]: """ Gets beat locations using librosa's beat tracker Parameters ---------- trim Whether to discard weak beats Returns ------- Beat locations """ if trim: tempo, beats = librosa.beat.beat_track(y=self.samples, sr=self.sample_rate, units='time', trim=True) else: tempo, beats = librosa.beat.beat_track(y=self.samples, sr=self.sample_rate, units='time', trim=False) return beats def onsets(self, backtrack: bool = False) -> EventList: """ Gets onset events Parameters ---------- backtrack Shift onset events back to the nearest local minimum of energy Returns ------- Detected onset events from the audio """ if not backtrack: onsets = self._onsets() else: onsets = self._onsets(backtrack=True) onsets = EventList([Onset(onset) for onset in onsets], end=self.duration) return onsets @lru_cache(maxsize=None) def _onsets(self, backtrack: bool = False): """ Gets onset locations using librosa's onset detector. Parameters ---------- backtrack Whether to shift onset events back to the nearest local minimum of energy Returns ------- Onset locations """ if backtrack: onsets = librosa.beat.onset.onset_detect(y=self.samples, sr=self.sample_rate, units='time', backtrack=True) else: onsets = librosa.beat.onset.onset_detect(y=self.samples, sr=self.sample_rate, units='time', backtrack=False) return onsets