Last active
May 31, 2024 19:43
-
-
Save joaoreboucas1/f66ef205825c97b5e82d432a514e5534 to your computer and use it in GitHub Desktop.
Command-line tool in Python that synchronizes a funk beat into a song (must be .wav format)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from math import floor | |
from pathlib import Path | |
import sys | |
import os | |
import numpy as np | |
import librosa | |
import librosa.beat | |
import soundfile as sf | |
def synchronize(file_path_1, file_path_2, output_path="mashup.wav", second_iteration=False): | |
""" | |
Synchronizes the audio in `file_path_2` to the tempo of `file_path_1` and also does phase alignment. Saves the result to `output_path`. | |
""" | |
file_path_1 = Path(file_path_1) | |
file_path_2 = Path(file_path_2) | |
print(f"Synchronizing {file_path_2} into {file_path_1}") | |
# Load the audio file | |
audio_1, sr_1 = librosa.load(file_path_1) | |
audio_2, sr_2 = librosa.load(file_path_2) | |
# Estimate the tempo (BPM) of the audio file | |
onset_env_1 = librosa.onset.onset_strength(y=audio_1, sr=sr_1) | |
bpm_1 = librosa.beat.tempo(onset_envelope=onset_env_1, sr=sr_1)[0] | |
print(f"{file_path_1} has {bpm_1} BPM") | |
onset_env_2 = librosa.onset.onset_strength(y=audio_2, sr=sr_2) | |
bpm_2 = librosa.beat.tempo(onset_envelope=onset_env_2, sr=sr_2)[0] | |
print(f"{file_path_2} has {bpm_2} BPM") | |
stretch_factor = bpm_1/bpm_2 | |
stretched_audio_2 = librosa.effects.time_stretch(audio_2, rate=stretch_factor) | |
onset_env_2 = librosa.onset.onset_strength(y=stretched_audio_2, sr=sr_2) | |
beat_index_1 = np.argmax(onset_env_1) | |
beat_index_2 = np.argmax(onset_env_2) | |
time_shift_samples = beat_index_1 - beat_index_2 | |
aligned_audio_2 = np.roll(stretched_audio_2, -time_shift_samples) | |
if len(audio_1) > len(aligned_audio_2): | |
mashup = 0.5 * audio_1[:len(aligned_audio_2)] + 0.5 * aligned_audio_2 | |
else: | |
mashup = 0.5 * audio_1 + 0.5 * aligned_audio_2[:len(audio_1)] | |
if not second_iteration: | |
sf.write("temp.wav", aligned_audio_2, sr_2) | |
synchronize(file_path_1, "temp.wav", output_path, second_iteration=True) | |
else: | |
os.remove("temp.wav") | |
print(f"Saving mashup to {output_path}") | |
sf.write(output_path, mashup, sr_1) | |
if len(sys.argv) != 4: | |
print(f"Usage: python {sys.argv[0]} TARGET_FILE BEAT_FILE OUTPUT_FILE") | |
exit(1) | |
song = sys.argv[1] | |
beat = sys.argv[2] | |
output = sys.argv[3] | |
synchronize(song, beat, output) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment