Vansh Chugh commited on
Commit
f330184
·
1 Parent(s): b399d0a

initial deploy

Browse files
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ beat-this-repo/
README.md CHANGED
@@ -1,13 +1,14 @@
1
  ---
2
  title: Beat This
3
- emoji: 👀
4
  colorFrom: blue
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Beat This
3
+ emoji: 🥁
4
  colorFrom: blue
5
  colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 5.28.0
8
+ python_version: '3.11'
9
  app_file: app.py
10
  pinned: false
11
+ license: mit
12
  ---
13
 
14
+ Beat and downbeat tracking with [Beat This!](https://github.com/CPJKU/beat_this), accessible in HARP.
SOURCES.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Sources — beat-this
2
+
3
+ - Source repo: https://github.com/CPJKU/beat_this
4
+ - Paper: https://arxiv.org/abs/2407.21658
app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ sys.stdout.reconfigure(line_buffering=True)
4
+
5
+ try:
6
+ import spaces
7
+ except ImportError:
8
+ # keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
9
+ class spaces:
10
+ class GPU:
11
+ def __init__(self, func=None, duration=60):
12
+ self.func = func
13
+
14
+ def __call__(self, *args, **kwargs):
15
+ if self.func is not None:
16
+ return self.func(*args, **kwargs)
17
+ func = args[0]
18
+ return func
19
+
20
+
21
+ from pyharp import ModelCard, build_endpoint
22
+ from pyharp.labels import LabelList, OutputLabel
23
+
24
+ import gradio as gr
25
+ import torch
26
+
27
+ from beat_this.inference import File2Beats
28
+
29
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
30
+
31
+ # built on CPU at import time so a broken checkpoint fails fast in the logs;
32
+ # moved to DEVICE lazily on first request, since ZeroGPU only allows CUDA
33
+ # calls made inside an @spaces.GPU-decorated call.
34
+ model = File2Beats(checkpoint_path="final0.ckpt", device="cpu", dbn=False)
35
+ model_ready = DEVICE == "cpu"
36
+
37
+ model_card = ModelCard(
38
+ name="Beat This!",
39
+ description="Detects the beat and downbeat (start-of-bar) positions in a piece of music.",
40
+ author="Francesco Foscarin, Jan Schlüter",
41
+ tags=["beat tracking", "rhythm"],
42
+ )
43
+
44
+
45
+ @spaces.GPU
46
+ @torch.inference_mode()
47
+ def process_fn(input_audio_path: str) -> LabelList:
48
+ """Finds beat and downbeat times in the input audio and returns them as labeled markers."""
49
+ global model, model_ready
50
+ if not model_ready:
51
+ model = File2Beats(checkpoint_path="final0.ckpt", device=DEVICE, dbn=False)
52
+ model_ready = True
53
+
54
+ beats, downbeats = model(input_audio_path)
55
+ downbeat_times = set(downbeats.tolist())
56
+
57
+ output_labels = LabelList()
58
+ for t in beats:
59
+ label = "downbeat" if float(t) in downbeat_times else "beat"
60
+ output_labels.append(OutputLabel(t=float(t), label=label))
61
+ return output_labels
62
+
63
+
64
+ with gr.Blocks() as demo:
65
+ input_components = [
66
+ gr.Audio(type="filepath", label="Input Audio").harp_required(True),
67
+ ]
68
+ output_components = [
69
+ gr.JSON(label="Beats").set_info(
70
+ "Detected beat and downbeat times, labeled \"beat\" or \"downbeat\" (start of bar)."
71
+ ),
72
+ ]
73
+
74
+ build_endpoint(
75
+ model_card=model_card,
76
+ input_components=input_components,
77
+ output_components=output_components,
78
+ process_fn=process_fn,
79
+ )
80
+
81
+ if __name__ == "__main__":
82
+ demo.queue().launch(pwa=True)
beat_this/__init__.py ADDED
File without changes
beat_this/inference.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ import numpy as np
4
+ import soxr
5
+ import torch
6
+ import torch.nn.functional as F
7
+
8
+ from beat_this.model.beat_tracker import BeatThis
9
+ from beat_this.model.postprocessor import Postprocessor
10
+ from beat_this.preprocessing import LogMelSpect, load_audio
11
+ from beat_this.utils import replace_state_dict_key, save_beat_tsv
12
+
13
+ CHECKPOINT_URL = "https://cloud.cp.jku.at/public.php/dav/files/7ik4RrBKTS273gp"
14
+
15
+
16
+ def load_checkpoint(checkpoint_path: str, device: str | torch.device = "cpu") -> dict:
17
+ """
18
+ Load a BeatThis checkpoint as a dictionary.
19
+
20
+ Args:
21
+ checkpoint_path (str, optional): The path to the checkpoint. Can be a local path, a URL, or a shortname.
22
+ device (torch.device or str): The device to load the model on.
23
+
24
+ Returns:
25
+ dict: The loaded checkpoint dictionary.
26
+ """
27
+ try:
28
+ # try interpreting as local file name
29
+ weights_only = {"weights_only": True} if torch.__version__ >= "2" else {}
30
+ return torch.load(checkpoint_path, map_location=device, **weights_only)
31
+ except FileNotFoundError:
32
+ try:
33
+ if not (
34
+ str(checkpoint_path).startswith("https://")
35
+ or str(checkpoint_path).startswith("http://")
36
+ ):
37
+ # interpret it as a name of one of our checkpoints
38
+ checkpoint_url = f"{CHECKPOINT_URL}/{checkpoint_path}.ckpt"
39
+ file_name = f"beat_this-{checkpoint_path}.ckpt"
40
+ else:
41
+ # try interpreting as a URL
42
+ checkpoint_url = checkpoint_path
43
+ file_name = None
44
+ return torch.hub.load_state_dict_from_url(
45
+ checkpoint_url,
46
+ file_name=file_name,
47
+ map_location=device,
48
+ )
49
+ except Exception:
50
+ raise ValueError(
51
+ "Could not load the checkpoint given the provided name",
52
+ checkpoint_path,
53
+ )
54
+
55
+
56
+ def load_model(
57
+ checkpoint_path: str | None = "final0", device: str | torch.device = "cpu"
58
+ ) -> BeatThis:
59
+ """
60
+ Load a BeatThis model from a checkpoint.
61
+
62
+ Args:
63
+ checkpoint_path (str, optional): The path to the checkpoint. Can be a local path, a URL, or a shortname.
64
+ device (torch.device or str): The device to load the model on.
65
+
66
+ Returns:
67
+ BeatThis: The loaded model.
68
+ """
69
+ if checkpoint_path is not None:
70
+ checkpoint = load_checkpoint(checkpoint_path, device)
71
+ # Retrieve the model hyperparameters as it could be the small model
72
+ hparams = checkpoint["hyper_parameters"]
73
+ # Filter only those hyperparameters that apply to the model itself
74
+ hparams = {
75
+ k: v
76
+ for k, v in hparams.items()
77
+ if k in set(inspect.signature(BeatThis).parameters)
78
+ }
79
+ # Create the uninitialized model
80
+ model = BeatThis(**hparams)
81
+ # The PLBeatThis (LightningModule) state_dict contains the BeatThis
82
+ # state_dict under the "model." prefix; remove the prefix to load it
83
+ state_dict = replace_state_dict_key(checkpoint["state_dict"], "model.", "")
84
+ model.load_state_dict(state_dict)
85
+ else:
86
+ model = BeatThis()
87
+ return model.to(device).eval()
88
+
89
+
90
+ def zeropad(spect: torch.Tensor, left: int = 0, right: int = 0):
91
+ """
92
+ Pads a tensor spectrogram matrix of shape (time x bins) with `left` frames in the beginning and `right` frames in the end.
93
+ """
94
+ if left == 0 and right == 0:
95
+ return spect
96
+ else:
97
+ return F.pad(spect, (0, 0, left, right), "constant", 0)
98
+
99
+
100
+ def split_piece(
101
+ spect: torch.Tensor,
102
+ chunk_size: int,
103
+ border_size: int = 6,
104
+ avoid_short_end: bool = True,
105
+ ):
106
+ """
107
+ Split a tensor spectrogram matrix of shape (time x bins) into time chunks of `chunk_size` and return the chunks and starting positions.
108
+ The `border_size` is the number of frames assumed to be discarded in the predictions on either side (since the model was not trained on the input edges due to the max-pool in the loss).
109
+ To cater for this, the first and last chunk are padded by `border_size` on the beginning and end, respectively, and consecutive chunks overlap by `border_size`.
110
+ If `avoid_short_end` is true, the last chunk start is shifted left to ends at the end of the piece, therefore the last chunk can potentially overlap with previous chunks more than border_size, otherwise it will be a shorter segment.
111
+ If the piece is shorter than `chunk_size`, avoid_short_end is ignored and the piece is returned as a single shorter chunk.
112
+
113
+ Args:
114
+ spect (torch.Tensor): The input spectrogram tensor of shape (time x bins).
115
+ chunk_size (int): The size of the chunks to produce.
116
+ border_size (int, optional): The size of the border to overlap between chunks. Defaults to 6.
117
+ avoid_short_end (bool, optional): If True, the last chunk is shifted left to end at the end of the piece. Defaults to True.
118
+ """
119
+ # generate the start and end indices
120
+ starts = np.arange(
121
+ -border_size, len(spect) - border_size, chunk_size - 2 * border_size
122
+ )
123
+ if avoid_short_end and len(spect) > chunk_size - 2 * border_size:
124
+ # if we avoid short ends, move the last index to the end of the piece - (chunk_size - border_size)
125
+ starts[-1] = len(spect) - (chunk_size - border_size)
126
+ # generate the chunks
127
+ chunks = [
128
+ zeropad(
129
+ spect[max(start, 0) : min(start + chunk_size, len(spect))],
130
+ left=max(0, -start),
131
+ right=max(0, min(border_size, start + chunk_size - len(spect))),
132
+ )
133
+ for start in starts
134
+ ]
135
+ return chunks, starts
136
+
137
+
138
+ def aggregate_prediction(
139
+ pred_chunks: list,
140
+ starts: list,
141
+ full_size: int,
142
+ chunk_size: int,
143
+ border_size: int,
144
+ overlap_mode: str,
145
+ device: str | torch.device,
146
+ ) -> tuple[torch.Tensor, torch.Tensor]:
147
+ """
148
+ Aggregates the predictions for the whole piece based on the given prediction chunks.
149
+
150
+ Args:
151
+ pred_chunks (list): List of prediction chunks, where each chunk is a dictionary containing 'beat' and 'downbeat' predictions.
152
+ starts (list): List of start positions for each prediction chunk.
153
+ full_size (int): Size of the full piece.
154
+ chunk_size (int): Size of each prediction chunk.
155
+ border_size (int): Size of the border to be discarded from each prediction chunk.
156
+ overlap_mode (str): Mode for handling overlapping predictions. Can be 'keep_first' or 'keep_last'.
157
+ device (torch.device): Device to be used for the predictions.
158
+
159
+ Returns:
160
+ tuple: A tuple containing the aggregated beat predictions and downbeat predictions as torch tensors for the whole piece.
161
+ """
162
+ if border_size > 0:
163
+ # cut the predictions to discard the border
164
+ pred_chunks = [
165
+ {
166
+ "beat": pchunk["beat"][border_size:-border_size],
167
+ "downbeat": pchunk["downbeat"][border_size:-border_size],
168
+ }
169
+ for pchunk in pred_chunks
170
+ ]
171
+ # aggregate the predictions for the whole piece
172
+ piece_prediction_beat = torch.full((full_size,), -1000.0, device=device)
173
+ piece_prediction_downbeat = torch.full((full_size,), -1000.0, device=device)
174
+ if overlap_mode == "keep_first":
175
+ # process in reverse order, so predictions of earlier excerpts overwrite later ones
176
+ pred_chunks = reversed(list(pred_chunks))
177
+ starts = reversed(list(starts))
178
+ for start, pchunk in zip(starts, pred_chunks):
179
+ piece_prediction_beat[
180
+ start + border_size : start + chunk_size - border_size
181
+ ] = pchunk["beat"]
182
+ piece_prediction_downbeat[
183
+ start + border_size : start + chunk_size - border_size
184
+ ] = pchunk["downbeat"]
185
+ return piece_prediction_beat, piece_prediction_downbeat
186
+
187
+
188
+ def split_predict_aggregate(
189
+ spect: torch.Tensor,
190
+ chunk_size: int,
191
+ border_size: int,
192
+ overlap_mode: str,
193
+ model: torch.nn.Module,
194
+ ) -> dict:
195
+ """
196
+ Function for pieces that are longer than the training length of the model.
197
+ Split the input piece into chunks, run the model on them, and aggregate the predictions.
198
+ The spect is supposed to be a torch tensor of shape (time x bins), i.e., unbatched, and the output is also unbatched.
199
+
200
+ Args:
201
+ spect (torch.Tensor): the input piece
202
+ chunk_size (int): the length of the chunks
203
+ border_size (int): the size of the border that is discarded from the predictions
204
+ overlap_mode (str): how to handle overlaps between chunks
205
+ model (torch.nn.Module): the model to run
206
+
207
+ Returns:
208
+ dict: the model framewise predictions for the hole piece as a dictionary containing 'beat' and 'downbeat' predictions.
209
+ """
210
+ # split the piece into chunks
211
+ chunks, starts = split_piece(
212
+ spect, chunk_size, border_size=border_size, avoid_short_end=True
213
+ )
214
+ # run the model
215
+ pred_chunks = [model(chunk.unsqueeze(0)) for chunk in chunks]
216
+ # remove the extra dimension in beat and downbeat prediction due to batch size 1
217
+ pred_chunks = [
218
+ {"beat": p["beat"][0], "downbeat": p["downbeat"][0]} for p in pred_chunks
219
+ ]
220
+ piece_prediction_beat, piece_prediction_downbeat = aggregate_prediction(
221
+ pred_chunks,
222
+ starts,
223
+ spect.shape[0],
224
+ chunk_size,
225
+ border_size,
226
+ overlap_mode,
227
+ spect.device,
228
+ )
229
+ # save it to model_prediction
230
+ return {"beat": piece_prediction_beat, "downbeat": piece_prediction_downbeat}
231
+
232
+
233
+ class Spect2Frames:
234
+ """
235
+ Class for extracting framewise beat and downbeat predictions (logits) from a spectrogram.
236
+ """
237
+
238
+ def __init__(self, checkpoint_path="final0", device="cpu", float16=False):
239
+ super().__init__()
240
+ self.device = torch.device(device)
241
+ self.float16 = float16
242
+ self.model = load_model(checkpoint_path, self.device)
243
+
244
+ def spect2frames(self, spect):
245
+ with torch.inference_mode():
246
+ with torch.autocast(enabled=self.float16, device_type=self.device.type):
247
+ model_prediction = split_predict_aggregate(
248
+ spect=spect,
249
+ chunk_size=1500,
250
+ overlap_mode="keep_first",
251
+ border_size=6,
252
+ model=self.model,
253
+ )
254
+ return model_prediction["beat"].float(), model_prediction["downbeat"].float()
255
+
256
+ def __call__(self, spect):
257
+ return self.spect2frames(spect)
258
+
259
+
260
+ class Audio2Frames(Spect2Frames):
261
+ """
262
+ Class for extracting framewise beat and downbeat predictions (logits) from an audio tensor.
263
+ """
264
+
265
+ def __init__(self, checkpoint_path="final0", device="cpu", float16=False):
266
+ super().__init__(checkpoint_path, device, float16)
267
+ self.spect = LogMelSpect(device=self.device)
268
+
269
+ def signal2spect(self, signal, sr):
270
+ if signal.ndim == 2:
271
+ signal = signal.mean(1)
272
+ elif signal.ndim != 1:
273
+ raise ValueError(f"Expected 1D or 2D signal, got shape {signal.shape}")
274
+ if sr != 22050:
275
+ signal = soxr.resample(signal, in_rate=sr, out_rate=22050)
276
+ signal = torch.tensor(signal, dtype=torch.float32, device=self.device)
277
+ return self.spect(signal)
278
+
279
+ def __call__(self, signal, sr):
280
+ spect = self.signal2spect(signal, sr)
281
+ return self.spect2frames(spect)
282
+
283
+
284
+ class Audio2Beats(Audio2Frames):
285
+ """
286
+ Class for extracting beat and downbeat positions (in seconds) from an audio tensor.
287
+
288
+ Args:
289
+ checkpoint_path (str): Path to the model checkpoint file. It can be a local path, a URL, or a key from the CHECKPOINT_URL dictionary. Default is "final0", which will load the model trained on all data except GTZAN with seed 0.
290
+ device (str): Device to use for inference. Default is "cpu".
291
+ float16 (bool): Whether to use half precision floating point arithmetic. Default is False.
292
+ dbn (bool): Whether to use the madmom DBN for post-processing. Default is False.
293
+ """
294
+
295
+ def __init__(
296
+ self, checkpoint_path="final0", device="cpu", float16=False, dbn=False
297
+ ):
298
+ super().__init__(checkpoint_path, device, float16)
299
+ self.frames2beats = Postprocessor(type="dbn" if dbn else "minimal")
300
+
301
+ def __call__(self, signal, sr):
302
+ beat_logits, downbeat_logits = super().__call__(signal, sr)
303
+ return self.frames2beats(beat_logits, downbeat_logits)
304
+
305
+
306
+ class File2Beats(Audio2Beats):
307
+ def __call__(self, audio_path):
308
+ signal, sr = load_audio(audio_path)
309
+ return super().__call__(signal, sr)
310
+
311
+
312
+ class File2File(File2Beats):
313
+ def __call__(self, audio_path, output_path):
314
+ downbeats, beats = super().__call__(audio_path)
315
+ save_beat_tsv(downbeats, beats, output_path)
beat_this/model/__init__.py ADDED
File without changes
beat_this/model/beat_tracker.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model definitions for the Beat This! beat tracker.
3
+ """
4
+
5
+ import contextlib
6
+ from collections import OrderedDict
7
+
8
+ import torch
9
+ from einops import rearrange
10
+ from einops.layers.torch import Rearrange
11
+ from rotary_embedding_torch import RotaryEmbedding
12
+ from torch import nn
13
+
14
+ from beat_this.model import roformer
15
+ from beat_this.utils import replace_state_dict_key
16
+
17
+
18
+ class BeatThis(nn.Module):
19
+ """
20
+ A neural network model for beat tracking. It is composed of three main components:
21
+ - a frontend that processes the input spectrogram,
22
+ - a series of transformer blocks that process the output of the frontend,
23
+ - a head that produces the final beat and downbeat predictions.
24
+
25
+ Args:
26
+ spect_dim (int): The dimension of the input spectrogram (default: 128).
27
+ transformer_dim (int): The dimension of the main transformer blocks (default: 512).
28
+ ff_mult (int): The multiplier for the feed-forward dimension in the transformer blocks (default: 4).
29
+ n_layers (int): The number of transformer blocks (default: 6).
30
+ head_dim (int): The dimension of each attention head for the partial transformers in the frontend and the transformer blocks (default: 32).
31
+ stem_dim (int): The out dimension of the stem convolutional layer (default: 32).
32
+ dropout (dict): A dictionary specifying the dropout rates for different parts of the model
33
+ (default: {"frontend": 0.1, "transformer": 0.2}).
34
+ sum_head (bool): Whether to use a SumHead for the final predictions (default: True) or plain independent projections.
35
+ partial_transformers (bool): Whether to include partial frequency- and time-transformers in the frontend (default: True)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ spect_dim: int = 128,
41
+ transformer_dim: int = 512,
42
+ ff_mult: int = 4,
43
+ n_layers: int = 6,
44
+ head_dim: int = 32,
45
+ stem_dim: int = 32,
46
+ dropout: dict = {"frontend": 0.1, "transformer": 0.2},
47
+ sum_head: bool = True,
48
+ partial_transformers: bool = True,
49
+ ):
50
+ super().__init__()
51
+ # shared rotary embedding for frontend blocks and transformer blocks
52
+ rotary_embed = RotaryEmbedding(head_dim)
53
+
54
+ # create the frontend
55
+ # - stem
56
+ stem = self.make_stem(spect_dim, stem_dim)
57
+ spect_dim //= 4 # frequencies were convolved with stride 4
58
+ # - three frontend blocks
59
+ frontend_blocks = []
60
+ dim = stem_dim
61
+ for _ in range(3):
62
+ frontend_blocks.append(
63
+ self.make_frontend_block(
64
+ dim,
65
+ dim * 2,
66
+ partial_transformers,
67
+ head_dim,
68
+ rotary_embed,
69
+ dropout["frontend"],
70
+ )
71
+ )
72
+ dim *= 2
73
+ spect_dim //= 2 # frequencies were convolved with stride 2
74
+ frontend_blocks = nn.Sequential(*frontend_blocks)
75
+ # - linear projection to transformer dimensionality
76
+ concat = Rearrange("b c f t -> b t (c f)")
77
+ linear = nn.Linear(dim * spect_dim, transformer_dim)
78
+ self.frontend = nn.Sequential(
79
+ OrderedDict(stem=stem, blocks=frontend_blocks, concat=concat, linear=linear)
80
+ )
81
+
82
+ # create the transformer blocks
83
+ assert (
84
+ transformer_dim % head_dim == 0
85
+ ), "transformer_dim must be divisible by head_dim"
86
+ n_heads = transformer_dim // head_dim
87
+ self.transformer_blocks = roformer.Transformer(
88
+ dim=transformer_dim,
89
+ depth=n_layers,
90
+ heads=n_heads,
91
+ attn_dropout=dropout["transformer"],
92
+ ff_dropout=dropout["transformer"],
93
+ rotary_embed=rotary_embed,
94
+ ff_mult=ff_mult,
95
+ dim_head=head_dim,
96
+ norm_output=True,
97
+ )
98
+
99
+ # create the output heads
100
+ if sum_head:
101
+ self.task_heads = SumHead(transformer_dim)
102
+ else:
103
+ self.task_heads = Head(transformer_dim)
104
+
105
+ # init all weights
106
+ self.apply(self._init_weights)
107
+
108
+ @staticmethod
109
+ def make_stem(spect_dim: int, stem_dim: int) -> nn.Module:
110
+ return nn.Sequential(
111
+ OrderedDict(
112
+ rearrange_tf=Rearrange("b t f -> b f t"),
113
+ bn1d=nn.BatchNorm1d(spect_dim),
114
+ add_channel=Rearrange("b f t -> b 1 f t"),
115
+ conv2d=nn.Conv2d(
116
+ in_channels=1,
117
+ out_channels=stem_dim,
118
+ kernel_size=(4, 3),
119
+ stride=(4, 1),
120
+ padding=(0, 1),
121
+ bias=False,
122
+ ),
123
+ bn2d=nn.BatchNorm2d(stem_dim),
124
+ activation=nn.GELU(),
125
+ )
126
+ )
127
+
128
+ @staticmethod
129
+ def make_frontend_block(
130
+ in_dim: int,
131
+ out_dim: int,
132
+ partial_transformers: bool = True,
133
+ head_dim: int | None = 32,
134
+ rotary_embed: RotaryEmbedding | None = None,
135
+ dropout: float = 0.1,
136
+ ) -> nn.Module:
137
+ if partial_transformers and (head_dim is None or rotary_embed is None):
138
+ raise ValueError(
139
+ "Must specify head_dim and rotary_embed for using partial_transformers"
140
+ )
141
+ return nn.Sequential(
142
+ OrderedDict(
143
+ partial=(
144
+ PartialFTTransformer(
145
+ dim=in_dim,
146
+ dim_head=head_dim,
147
+ n_head=in_dim // head_dim,
148
+ rotary_embed=rotary_embed,
149
+ dropout=dropout,
150
+ )
151
+ if partial_transformers
152
+ else nn.Identity()
153
+ ),
154
+ # conv block
155
+ conv2d=nn.Conv2d(
156
+ in_channels=in_dim,
157
+ out_channels=out_dim,
158
+ kernel_size=(2, 3),
159
+ stride=(2, 1),
160
+ padding=(0, 1),
161
+ bias=False,
162
+ ),
163
+ # out_channels : 64, 128, 256
164
+ # freqs : 16, 8, 4 (due to the stride=2)
165
+ norm=nn.BatchNorm2d(out_dim),
166
+ activation=nn.GELU(),
167
+ )
168
+ )
169
+
170
+ @staticmethod
171
+ def _init_weights(module: nn.Module):
172
+ if isinstance(module, (nn.Linear, nn.Conv1d)):
173
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
174
+ if module.bias is not None:
175
+ torch.nn.init.zeros_(module.bias)
176
+ elif isinstance(module, nn.Conv2d):
177
+ torch.nn.init.kaiming_normal_(
178
+ module.weight, mode="fan_out", nonlinearity="relu"
179
+ )
180
+ if module.bias is not None:
181
+ torch.nn.init.zeros_(module.bias)
182
+ elif isinstance(module, nn.Embedding):
183
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
184
+ if module.padding_idx is not None:
185
+ with torch.no_grad():
186
+ module.weight[module.padding_idx].fill_(0)
187
+
188
+ def forward(self, x):
189
+ x = self.frontend(x)
190
+ x = self.transformer_blocks(x)
191
+ x = self.task_heads(x)
192
+ return x
193
+
194
+ def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs):
195
+ # remove _orig_mod prefixes for compiled models
196
+ state_dict = replace_state_dict_key(state_dict, "_orig_mod.", "")
197
+ super()._load_from_state_dict(state_dict, prefix, *args, **kwargs)
198
+
199
+ def state_dict(self, *args, **kwargs):
200
+ state_dict = super().state_dict(*args, **kwargs)
201
+ # remove _orig_mod prefixes for compiled models
202
+ state_dict = replace_state_dict_key(state_dict, "_orig_mod.", "")
203
+ return state_dict
204
+
205
+
206
+ class PartialRoformer(nn.Module):
207
+ """
208
+ Takes a (batch, channels, freqs, time) input, applies self-attention and
209
+ a feed-forward block either only across frequencies or only across time.
210
+ Returns a tensor of the same shape as the input.
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ dim: int,
216
+ dim_head: int,
217
+ n_head: int,
218
+ direction: str,
219
+ rotary_embed: RotaryEmbedding,
220
+ dropout: float,
221
+ ):
222
+ super().__init__()
223
+
224
+ assert dim % dim_head == 0, "dim must be divisible by dim_head"
225
+ assert dim // dim_head == n_head, "n_head must be equal to dim // dim_head"
226
+ self.direction = direction[0].lower()
227
+ if self.direction not in "ft":
228
+ raise ValueError(f"direction must be F or T, got {direction}")
229
+ self.attn = roformer.Attention(
230
+ dim,
231
+ heads=n_head,
232
+ dim_head=dim_head,
233
+ dropout=dropout,
234
+ rotary_embed=rotary_embed,
235
+ )
236
+ self.ff = roformer.FeedForward(dim, dropout=dropout)
237
+
238
+ def forward(self, x):
239
+ b = len(x)
240
+ if self.direction == "f":
241
+ pattern = "(b t) f c"
242
+ elif self.direction == "t":
243
+ pattern = "(b f) t c"
244
+ x = rearrange(x, f"b c f t -> {pattern}")
245
+ x = x + self.attn(x)
246
+ x = x + self.ff(x)
247
+ x = rearrange(x, f"{pattern} -> b c f t", b=b)
248
+ return x
249
+
250
+
251
+ class PartialFTTransformer(nn.Module):
252
+ """
253
+ Takes a (batch, channels, freqs, time) input, applies self-attention and
254
+ a feed-forward block once across frequencies and once across time. Same
255
+ as applying two PartialRoformer() in sequence, but encapsulated in a single
256
+ module. Returns a tensor of the same shape as the input.
257
+ """
258
+
259
+ def __init__(
260
+ self,
261
+ dim: int,
262
+ dim_head: int,
263
+ n_head: int,
264
+ rotary_embed: RotaryEmbedding,
265
+ dropout: float,
266
+ ):
267
+ super().__init__()
268
+
269
+ assert dim % dim_head == 0, "dim must be divisible by dim_head"
270
+ assert dim // dim_head == n_head, "n_head must be equal to dim // dim_head"
271
+ # frequency directed partial transformer
272
+ self.attnF = roformer.Attention(
273
+ dim,
274
+ heads=n_head,
275
+ dim_head=dim_head,
276
+ dropout=dropout,
277
+ rotary_embed=rotary_embed,
278
+ )
279
+ self.ffF = roformer.FeedForward(dim, dropout=dropout)
280
+ # time directed partial transformer
281
+ self.attnT = roformer.Attention(
282
+ dim,
283
+ heads=n_head,
284
+ dim_head=dim_head,
285
+ dropout=dropout,
286
+ rotary_embed=rotary_embed,
287
+ )
288
+ self.ffT = roformer.FeedForward(dim, dropout=dropout)
289
+
290
+ def forward(self, x):
291
+ b = len(x)
292
+ # frequency directed partial transformer
293
+ x = rearrange(x, "b c f t -> (b t) f c")
294
+ x = x + self.attnF(x)
295
+ x = x + self.ffF(x)
296
+ # time directed partial transformer
297
+ x = rearrange(x, "(b t) f c ->(b f) t c", b=b)
298
+ x = x + self.attnT(x)
299
+ x = x + self.ffT(x)
300
+ x = rearrange(x, "(b f) t c -> b c f t", b=b)
301
+ return x
302
+
303
+
304
+ class SumHead(nn.Module):
305
+ """
306
+ A PyTorch module that produces the final beat and downbeat prediction logits.
307
+ The beats are a sum of all beats and all downbeats predictions, to reduce the prediction
308
+ of downbeats which are not beats.
309
+ """
310
+
311
+ def __init__(self, input_dim):
312
+ super().__init__()
313
+ self.beat_downbeat_lin = nn.Linear(input_dim, 2)
314
+
315
+ def forward(self, x):
316
+ beat_downbeat = self.beat_downbeat_lin(x)
317
+ # separate beat from downbeat
318
+ beat, downbeat = rearrange(beat_downbeat, "b t c -> c b t", c=2)
319
+ # aggregate beats and downbeats prediction
320
+ # autocast to float16 disabled to avoid numerical issues causing NaNs
321
+ if hasattr(
322
+ torch.amp, "is_autocast_available"
323
+ ) and not torch.amp.is_autocast_available(beat.device.type):
324
+ # but do not try disabling if the device does not support autocast
325
+ disable_autocast = contextlib.nullcontext()
326
+ else:
327
+ disable_autocast = torch.autocast(beat.device.type, enabled=False)
328
+ with disable_autocast:
329
+ beat = beat.float() + downbeat.float()
330
+ return {"beat": beat, "downbeat": downbeat}
331
+
332
+
333
+ class Head(nn.Module):
334
+ """
335
+ A PyToch module that produces the final beat and downbeat prediction logits with independent linear layers outputs.
336
+ """
337
+
338
+ def __init__(self, input_dim):
339
+ super().__init__()
340
+ self.beat_downbeat_lin = nn.Linear(input_dim, 2)
341
+
342
+ def forward(self, x):
343
+ beat_downbeat = self.beat_downbeat_lin(x)
344
+ # separate beat from downbeat
345
+ beat, downbeat = rearrange(beat_downbeat, "b t c -> c b t", c=2)
346
+ return {"beat": beat, "downbeat": downbeat}
beat_this/model/postprocessor.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from concurrent.futures import ThreadPoolExecutor
2
+
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from einops import rearrange
7
+
8
+
9
+ class Postprocessor:
10
+ """Postprocessor for the beat and downbeat predictions of the model.
11
+ The postprocessor takes the (framewise) model predictions (beat and downbeats) and the padding mask,
12
+ and returns the postprocessed beat and downbeat as list of times in seconds.
13
+ The beats and downbeats can be 1D arrays (for only 1 piece) or 2D arrays, if a batch of pieces is considered.
14
+ The output dimensionality is the same as the input dimensionality.
15
+ Two types of postprocessing are implemented:
16
+ - minimal: a simple postprocessing that takes the maximum of the framewise predictions,
17
+ and removes adjacent peaks.
18
+ - dbn: a postprocessing based on the Dynamic Bayesian Network proposed by Böck et al.
19
+ Args:
20
+ type (str): the type of postprocessing to apply. Either "minimal" or "dbn". Default is "minimal".
21
+ fps (int): the frames per second of the model framewise predictions. Default is 50.
22
+ """
23
+
24
+ def __init__(self, type: str = "minimal", fps: int = 50):
25
+ assert type in ["minimal", "dbn"]
26
+ self.type = type
27
+ self.fps = fps
28
+ if type == "dbn":
29
+ from madmom.features.downbeats import DBNDownBeatTrackingProcessor
30
+
31
+ self.dbn = DBNDownBeatTrackingProcessor(
32
+ beats_per_bar=[3, 4],
33
+ min_bpm=55.0,
34
+ max_bpm=215.0,
35
+ fps=self.fps,
36
+ transition_lambda=100,
37
+ )
38
+
39
+ def __call__(
40
+ self,
41
+ beat: torch.Tensor,
42
+ downbeat: torch.Tensor,
43
+ padding_mask: torch.Tensor | None = None,
44
+ ) -> tuple[np.ndarray, np.ndarray]:
45
+ """
46
+ Apply postprocessing to the input beat and downbeat tensors. Works with batched and unbatched inputs.
47
+ The output is a list of times in seconds, or a list of lists of times in seconds, if the input is batched.
48
+
49
+ Args:
50
+ beat (torch.Tensor): The input beat tensor.
51
+ downbeat (torch.Tensor): The input downbeat tensor.
52
+ padding_mask (torch.Tensor, optional): The padding mask tensor. Defaults to None.
53
+
54
+ Returns:
55
+ torch.Tensor: The postprocessed beat tensor.
56
+ torch.Tensor: The postprocessed downbeat tensor.
57
+ """
58
+ batched = False if beat.ndim == 1 else True
59
+ if padding_mask is None:
60
+ padding_mask = torch.ones_like(beat, dtype=torch.bool)
61
+
62
+ # if beat and downbeat are 1D tensors, add a batch dimension
63
+ if not batched:
64
+ beat = beat.unsqueeze(0)
65
+ downbeat = downbeat.unsqueeze(0)
66
+ padding_mask = padding_mask.unsqueeze(0)
67
+
68
+ if self.type == "minimal":
69
+ postp_beat, postp_downbeat = self.postp_minimal(
70
+ beat, downbeat, padding_mask
71
+ )
72
+ elif self.type == "dbn":
73
+ postp_beat, postp_downbeat = self.postp_dbn(beat, downbeat, padding_mask)
74
+ else:
75
+ raise ValueError("Invalid postprocessing type")
76
+
77
+ # remove the batch dimension if it was added
78
+ if not batched:
79
+ postp_beat = postp_beat[0]
80
+ postp_downbeat = postp_downbeat[0]
81
+
82
+ # update the model prediction dict
83
+ return postp_beat, postp_downbeat
84
+
85
+ def postp_minimal(self, beat, downbeat, padding_mask):
86
+ # concatenate beat and downbeat in the same tensor of shape (B, T, 2)
87
+ packed_pred = rearrange(
88
+ [beat, downbeat], "c b t -> b t c", b=beat.shape[0], t=beat.shape[1], c=2
89
+ )
90
+ # set padded elements to -1000 (= probability zero even in float64) so they don't influence the maxpool
91
+ pred_logits = packed_pred.masked_fill(~padding_mask.unsqueeze(-1), -1000)
92
+ # reshape to (2*B, T) to apply max pooling
93
+ pred_logits = rearrange(pred_logits, "b t c -> (c b) t")
94
+ # pick maxima within +/- 70ms
95
+ pred_peaks = pred_logits.masked_fill(
96
+ pred_logits != F.max_pool1d(pred_logits, 7, 1, 3), -1000
97
+ )
98
+ # keep maxima with over 0.5 probability (logit > 0)
99
+ pred_peaks = pred_peaks > 0
100
+ # rearrange back to two tensors of shape (B, T)
101
+ beat_peaks, downbeat_peaks = rearrange(
102
+ pred_peaks, "(c b) t -> c b t", b=beat.shape[0], t=beat.shape[1], c=2
103
+ )
104
+ # run the piecewise operations
105
+ with ThreadPoolExecutor() as executor:
106
+ postp_beat, postp_downbeat = zip(
107
+ *executor.map(
108
+ self._postp_minimal_item, beat_peaks, downbeat_peaks, padding_mask
109
+ )
110
+ )
111
+ return postp_beat, postp_downbeat
112
+
113
+ def _postp_minimal_item(self, padded_beat_peaks, padded_downbeat_peaks, mask):
114
+ """Function to compute the operations that must be computed piece by piece, and cannot be done in batch."""
115
+ # unpad the predictions by truncating the padding positions
116
+ beat_peaks = padded_beat_peaks[mask]
117
+ downbeat_peaks = padded_downbeat_peaks[mask]
118
+ # pass from a boolean array to a list of times in frames.
119
+ beat_frame = torch.nonzero(beat_peaks).cpu().numpy()[:, 0]
120
+ downbeat_frame = torch.nonzero(downbeat_peaks).cpu().numpy()[:, 0]
121
+ # remove adjacent peaks
122
+ beat_frame = deduplicate_peaks(beat_frame, width=1)
123
+ downbeat_frame = deduplicate_peaks(downbeat_frame, width=1)
124
+ # convert from frame to seconds
125
+ beat_time = beat_frame / self.fps
126
+ downbeat_time = downbeat_frame / self.fps
127
+ # move the downbeat to the nearest beat
128
+ if (
129
+ len(beat_time) > 0
130
+ ): # skip if there are no beats, like in the first training steps
131
+ for i, d_time in enumerate(downbeat_time):
132
+ beat_idx = np.argmin(np.abs(beat_time - d_time))
133
+ downbeat_time[i] = beat_time[beat_idx]
134
+ # remove duplicate downbeat times (if some db were moved to the same position)
135
+ downbeat_time = np.unique(downbeat_time)
136
+ return beat_time, downbeat_time
137
+
138
+ def postp_dbn(self, beat, downbeat, padding_mask):
139
+ beat_prob = beat.double().sigmoid()
140
+ downbeat_prob = downbeat.double().sigmoid()
141
+ # limit lower and upper bound, since 0 and 1 create problems in the DBN
142
+ epsilon = 1e-5
143
+ beat_prob = beat_prob * (1 - epsilon) + epsilon / 2
144
+ downbeat_prob = downbeat_prob * (1 - epsilon) + epsilon / 2
145
+ with ThreadPoolExecutor() as executor:
146
+ postp_beat, postp_downbeat = zip(
147
+ *executor.map(
148
+ self._postp_dbn_item, beat_prob, downbeat_prob, padding_mask
149
+ )
150
+ )
151
+ return postp_beat, postp_downbeat
152
+
153
+ def _postp_dbn_item(self, padded_beat_prob, padded_downbeat_prob, mask):
154
+ """Function to compute the operations that must be computed piece by piece, and cannot be done in batch."""
155
+ # unpad the predictions by truncating the padding positions
156
+ beat_prob = padded_beat_prob[mask]
157
+ downbeat_prob = padded_downbeat_prob[mask]
158
+ # build an artificial multiclass prediction, as suggested by Böck et al.
159
+ # again we limit the lower bound to avoid problems with the DBN
160
+ epsilon = 1e-5
161
+ combined_act = np.vstack(
162
+ (
163
+ np.maximum(
164
+ beat_prob.cpu().numpy() - downbeat_prob.cpu().numpy(), epsilon / 2
165
+ ),
166
+ downbeat_prob.cpu().numpy(),
167
+ )
168
+ ).T
169
+ # run the DBN
170
+ dbn_out = self.dbn(combined_act)
171
+ postp_beat = dbn_out[:, 0]
172
+ postp_downbeat = dbn_out[dbn_out[:, 1] == 1][:, 0]
173
+ return postp_beat, postp_downbeat
174
+
175
+
176
+ def deduplicate_peaks(peaks, width=1) -> np.ndarray:
177
+ """
178
+ Replaces groups of adjacent peak frame indices that are each not more
179
+ than `width` frames apart by the average of the frame indices.
180
+ """
181
+ result = []
182
+ peaks = map(int, peaks) # ensure we get ordinary Python int objects
183
+ try:
184
+ p = next(peaks)
185
+ except StopIteration:
186
+ return np.array(result)
187
+ c = 1
188
+ for p2 in peaks:
189
+ if p2 - p <= width:
190
+ c += 1
191
+ p += (p2 - p) / c # update mean
192
+ else:
193
+ result.append(p)
194
+ p = p2
195
+ c = 1
196
+ result.append(p)
197
+ return np.array(result)
beat_this/model/roformer.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Transformer with rotary position embedding, adapted from Phil Wang's repository
3
+ at https://github.com/lucidrains/BS-RoFormer (under MIT License).
4
+ """
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from einops import rearrange
9
+ from torch import nn
10
+ from torch.nn import Module, ModuleList
11
+
12
+ # helper functions
13
+
14
+
15
+ def exists(val):
16
+ return val is not None
17
+
18
+
19
+ # norm
20
+
21
+
22
+ class RMSNorm(Module):
23
+ def __init__(self, size, dim=-1):
24
+ super().__init__()
25
+ self.scale = size**0.5
26
+ if dim >= 0:
27
+ raise ValueError(f"dim must be negative, got {dim}")
28
+ self.gamma = nn.Parameter(torch.ones((size,) + (1,) * (abs(dim) - 1)))
29
+ self.dim = dim
30
+
31
+ def forward(self, x):
32
+ return F.normalize(x, dim=self.dim) * self.scale * self.gamma
33
+
34
+
35
+ # feedforward
36
+
37
+
38
+ class FeedForward(Module):
39
+ def __init__(
40
+ self,
41
+ dim,
42
+ mult=4,
43
+ dropout=0.0,
44
+ dim_out=None,
45
+ ):
46
+ super().__init__()
47
+ if dim_out is None:
48
+ dim_out = dim
49
+ dim_inner = int(dim * mult)
50
+ self.activation = nn.GELU()
51
+ self.net = nn.Sequential(
52
+ RMSNorm(dim),
53
+ nn.Linear(dim, dim_inner),
54
+ self.activation,
55
+ nn.Dropout(dropout),
56
+ nn.Linear(dim_inner, dim_out),
57
+ nn.Dropout(dropout),
58
+ )
59
+
60
+ def forward(self, x):
61
+ return self.net(x)
62
+
63
+
64
+ # attention
65
+
66
+
67
+ class Attend(nn.Module):
68
+ def __init__(self, dropout=0.0, scale=None):
69
+ super().__init__()
70
+ self.dropout = dropout
71
+ self.scale = scale
72
+
73
+ def forward(self, q, k, v):
74
+ if exists(self.scale):
75
+ default_scale = q.shape[-1] ** -0.5
76
+ q = q * (self.scale / default_scale)
77
+
78
+ return F.scaled_dot_product_attention(
79
+ q, k, v, dropout_p=self.dropout if self.training else 0.0
80
+ )
81
+
82
+
83
+ class Attention(Module):
84
+ def __init__(
85
+ self,
86
+ dim,
87
+ heads=8,
88
+ dim_head=64,
89
+ dropout=0.0,
90
+ rotary_embed=None,
91
+ gating=True,
92
+ ):
93
+ super().__init__()
94
+ self.heads = heads
95
+ self.scale = dim_head**-0.5
96
+ dim_inner = heads * dim_head
97
+
98
+ self.rotary_embed = rotary_embed
99
+
100
+ self.attend = Attend(dropout=dropout)
101
+
102
+ self.norm = RMSNorm(dim)
103
+ self.to_qkv = nn.Linear(dim, dim_inner * 3, bias=False)
104
+
105
+ if gating:
106
+ self.to_gates = nn.Linear(dim, heads)
107
+ else:
108
+ self.to_gates = None
109
+
110
+ self.to_out = nn.Sequential(
111
+ nn.Linear(dim_inner, dim, bias=False), nn.Dropout(dropout)
112
+ )
113
+
114
+ def forward(self, x):
115
+ x = self.norm(x)
116
+
117
+ q, k, v = rearrange(
118
+ self.to_qkv(x), "b n (qkv h d) -> qkv b h n d", qkv=3, h=self.heads
119
+ )
120
+
121
+ if exists(self.rotary_embed):
122
+ q = self.rotary_embed.rotate_queries_or_keys(q)
123
+ k = self.rotary_embed.rotate_queries_or_keys(k)
124
+
125
+ out = self.attend(q, k, v)
126
+
127
+ if exists(self.to_gates):
128
+ gates = self.to_gates(x)
129
+ out = out * rearrange(gates, "b n h -> b h n 1").sigmoid()
130
+
131
+ out = rearrange(out, "b h n d -> b n (h d)")
132
+ return self.to_out(out)
133
+
134
+
135
+ # Roformer
136
+
137
+
138
+ class Transformer(Module):
139
+ def __init__(
140
+ self,
141
+ *,
142
+ dim,
143
+ depth,
144
+ dim_head=32,
145
+ heads=16,
146
+ attn_dropout=0.1,
147
+ ff_dropout=0.1,
148
+ ff_mult=4,
149
+ norm_output=True,
150
+ rotary_embed=None,
151
+ gating=True,
152
+ ):
153
+ super().__init__()
154
+ self.layers = ModuleList([])
155
+
156
+ for _ in range(depth):
157
+ ff = FeedForward(dim=dim, mult=ff_mult, dropout=ff_dropout)
158
+ self.layers.append(
159
+ ModuleList(
160
+ [
161
+ Attention(
162
+ dim=dim,
163
+ dim_head=dim_head,
164
+ heads=heads,
165
+ dropout=attn_dropout,
166
+ rotary_embed=rotary_embed,
167
+ gating=gating,
168
+ ),
169
+ ff,
170
+ ]
171
+ )
172
+ )
173
+
174
+ self.norm = RMSNorm(dim) if norm_output else nn.Identity()
175
+
176
+ def forward(self, x):
177
+ for attn, ff in self.layers:
178
+ x = attn(x) + x
179
+ x = ff(x) + x
180
+ x = self.norm(x)
181
+ return x
beat_this/preprocessing.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torchaudio
4
+
5
+
6
+ def load_audio(path, dtype="float64"):
7
+ try:
8
+ waveform, samplerate = torchaudio.load(path, channels_first=False)
9
+ waveform = np.asanyarray(waveform.squeeze().numpy(), dtype=dtype)
10
+ return waveform, samplerate
11
+ except Exception:
12
+ # in case torchaudio fails, try soundfile
13
+ try:
14
+ import soundfile as sf
15
+
16
+ return sf.read(path, dtype=dtype)
17
+ except Exception:
18
+ # some files are not readable by soundfile, try madmom
19
+ try:
20
+ import madmom
21
+
22
+ return madmom.io.load_audio_file(str(path), dtype=dtype)
23
+ except Exception:
24
+ raise RuntimeError(f'Could not load audio from "{path}".')
25
+
26
+
27
+ class LogMelSpect(torch.nn.Module):
28
+ def __init__(
29
+ self,
30
+ sample_rate=22050,
31
+ n_fft=1024,
32
+ hop_length=441,
33
+ f_min=30,
34
+ f_max=11000,
35
+ n_mels=128,
36
+ mel_scale="slaney",
37
+ normalized="frame_length",
38
+ power=1,
39
+ log_multiplier=1000,
40
+ device="cpu",
41
+ ):
42
+ super().__init__()
43
+ self.spect_class = torchaudio.transforms.MelSpectrogram(
44
+ sample_rate=sample_rate,
45
+ n_fft=n_fft,
46
+ hop_length=hop_length,
47
+ f_min=f_min,
48
+ f_max=f_max,
49
+ n_mels=n_mels,
50
+ mel_scale=mel_scale,
51
+ normalized=normalized,
52
+ power=power,
53
+ ).to(device)
54
+ self.log_multiplier = log_multiplier
55
+
56
+ def forward(self, x):
57
+ """Input is a waveform as a monodimensional array of shape T,
58
+ output is a 2D log mel spectrogram of shape (F,128)."""
59
+ return torch.log1p(self.log_multiplier * self.spect_class(x).T)
beat_this/utils.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import chain
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+
6
+
7
+ def index_to_framewise(index, length):
8
+ """Convert an index to a framewise sequence"""
9
+ sequence = np.zeros(length, dtype=bool)
10
+ sequence[index] = True
11
+ return sequence
12
+
13
+
14
+ def filename_to_augmentation(filename):
15
+ """Convert a filename to an augmentation factor."""
16
+ parts = Path(filename).stem.split("_")
17
+ augmentations = {}
18
+ for part in parts[1:]:
19
+ if part.startswith("ps"):
20
+ augmentations["shift"] = int(part[2:])
21
+ elif part.startswith("ts"):
22
+ augmentations["stretch"] = int(part[2:])
23
+ return augmentations
24
+
25
+
26
+ def infer_beat_numbers(beats: np.ndarray, downbeats: np.ndarray) -> np.ndarray:
27
+ """
28
+ From beat and downbeat times, infer a number for each beat such that each downbeat
29
+ is associated with a 1 and beats in between are counted upwards.
30
+ The function requires that all downbeats are also listed as beats.
31
+
32
+ Args:
33
+ beats (numpy.ndarray): Array of beat positions in seconds (including downbeats).
34
+ downbeats (numpy.ndarray): Array of downbeat positions in seconds.
35
+
36
+ Returns:
37
+ numbers (numpy.ndarray): Array of integer beat numbers.
38
+ """
39
+ # check if all downbeats are beats
40
+ if not np.all(np.isin(downbeats, beats)):
41
+ raise ValueError("Not all downbeats are beats.")
42
+
43
+ # handle pickup measure, by considering the beat count of the first full measure
44
+ if len(downbeats) >= 2:
45
+ # find the number of beats between the first two downbeats
46
+ first_downbeat, second_downbeat = np.searchsorted(beats, downbeats[:2])
47
+ beats_in_first_measure = second_downbeat - first_downbeat
48
+ # find the number of beats before the first downbeat
49
+ pickup_beats = first_downbeat
50
+ # derive where to start counting
51
+ if pickup_beats < beats_in_first_measure:
52
+ start_counter = beats_in_first_measure - pickup_beats
53
+ else:
54
+ print(
55
+ "WARNING: There are more beats in the pickup measure than in the first measure. The beat count will start from 2 without trying to estimate the length of the pickup measure."
56
+ )
57
+ start_counter = 1
58
+ else:
59
+ print(
60
+ "WARNING: There are less than two downbeats in the predictions. Something may be wrong. The beat count will start from 2 without trying to estimate the length of the pickup measure."
61
+ )
62
+ start_counter = 1
63
+
64
+ # assemble the beat numbers
65
+ numbers = []
66
+ counter = start_counter
67
+ downbeats = chain(downbeats, [-1])
68
+ next_downbeat = next(downbeats)
69
+ for beat in beats:
70
+ if beat == next_downbeat:
71
+ counter = 1
72
+ next_downbeat = next(downbeats)
73
+ else:
74
+ counter += 1
75
+ numbers.append(counter)
76
+ return np.asarray(numbers)
77
+
78
+
79
+ def save_beat_tsv(beats: np.ndarray, downbeats: np.ndarray, outpath: str) -> None:
80
+ """
81
+ Save beat information to a tab-separated file in the standard .beats format:
82
+ each line has a time in seconds, a tab, and a beat number (1 = downbeat).
83
+ The function requires that all downbeats are also listed as beats.
84
+
85
+ Args:
86
+ beats (numpy.ndarray): Array of beat positions in seconds (including downbeats).
87
+ downbeats (numpy.ndarray): Array of downbeat positions in seconds.
88
+ outpath (str): Path to the output TSV file.
89
+
90
+ Returns:
91
+ None
92
+ """
93
+ # infer beat numbers
94
+ numbers = infer_beat_numbers(beats, downbeats)
95
+
96
+ # write the beat file
97
+ Path(outpath).parent.mkdir(parents=True, exist_ok=True)
98
+ try:
99
+ with open(outpath, "w") as f:
100
+ f.writelines(f"{beat}\t{number}\n" for beat, number in zip(beats, numbers))
101
+ except KeyboardInterrupt:
102
+ outpath.unlink() # avoid half-written files
103
+
104
+
105
+ def replace_state_dict_key(state_dict: dict, old: str, new: str):
106
+ """Replaces `old` in all keys of `state_dict` with `new`."""
107
+ keys = list(state_dict.keys()) # take snapshot of the keys
108
+ for key in keys:
109
+ if old in key:
110
+ state_dict[key.replace(old, new)] = state_dict.pop(key)
111
+ return state_dict
final0.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c328b45f59d8dd3dff219253ff6a8d6482be57d0133a29140e2febbf8eb8331
3
+ size 81058141
model.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "beat-this",
3
+ "package_dir": "beat_this",
4
+ "entry_point": "beat_this.inference.File2Beats",
5
+ "checkpoint": {"repo": "teamup-tech/beat-this", "filename": "final0.ckpt", "size_mb": 81}
6
+ }
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/TEAMuP-dev/pyharp.git@develop
2
+ # model-specific deps below:
3
+ numpy>=1.20
4
+ torch>=2
5
+ torchaudio
6
+ einops
7
+ rotary-embedding-torch
8
+ soxr
9
+ soundfile