Skip to content

Commit

Permalink
add cli docs
Browse files Browse the repository at this point in the history
  • Loading branch information
oguzhan-yilmaz committed Dec 9, 2024
1 parent 7b32cf3 commit 18e5647
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 31 deletions.
113 changes: 82 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,155 @@

![pyCrossfade Logo](./assets/images/logo.png)


pyCrossfade is born out of a personal effort to create a customizable and beat-matched crossfade functionality.

---------
December 2024: ✨added CLI and Docker image✨

---

## CLI

Since the creation of this project, Python3 and dependencies got updated and stopped working with the pyCrossfade codebase.

### Docker Image

I've created a [Docker Image on ghcr.io](https://github.com/oguzhan-yilmaz/pyCrossfade/pkgs/container/pycrossfade) to help users getting the correct dependecy versions.

```bash
docker pull ghcr.io/oguzhan-yilmaz/pycrossfade:latest
```

### CLI Setup (for Linux)

To use the Docker image as a CLI, you'd need to:

- attach your `audios/` directory to the container
- attach a directory for persisting _pyCrossfade annotations_
- set some Env Vars for easier use.

This can be long and ugly, so the best thing to do is create a `alias` command.

Change the `MY_AUDIO_DIRECTORY` and add the following bash snippet to your `.bashrc` :

```bash
PYCROSSFADE_DIR="$HOME/.pycrossfade"
PYCROSSFADE_ANNOTATIONS_DIR="$PYCROSSFADE_DIR/pycrossfade-annotations"
# create the alias command, with your options
PYCROSSFADE_ANNOTATIONS_DIR="$PYCROSSFADE_DIR/annotations"

MY_AUDIO_DIRECTORY="!!CHANGEME!!"

# create the alias command
alias pycrossfade="mkdir -p $PYCROSSFADE_DIR \
&& mkdir -p $PYCROSSFADE_ANNOTATIONS_DIR \
&& docker run --rm -it \
-v "$(pwd):/app/audios" \
-v "$MY_AUDIO_DIRECTORY:/app/audios" \
-v $PYCROSSFADE_ANNOTATIONS_DIR:/app/pycrossfade_annotations \
-e ANNOTATIONS_DIRECTORY=/app/pycrossfade_annotations \
-e BASE_AUDIO_DIRECTORY=/app/audios/ \
ghcr.io/oguzhan-yilmaz/pycrossfade:latest"
```

### CLI Usage

```bash
pycrossfade
```

![pycrossfade CLI help page](./assets/images/cli_help_page.png)

```bash
pycrossfade crossfade --help
```

![pycrossfade CLI crossfade command help page](./assets/images/cli_crossfade_help_page.png)

#### CLI Commands

- `crossfade`: Crossfade between two songs
- `crossfade-many`: Crossfade between min. of 3 songs
- `song`: Process song and print metadata
- `extract`: Extract BPM, ReplayGain, Key/Scale etc.
- `mark-downbeats`: Play a beep sound on each downbeat
- `cut-song`: Cut a song between two downbeats

---------
---

## Older pyCrossfade

Before I added the CLI and Docker feature, I created the v0.1.0 tag. Access below:

- <https://github.com/oguzhan-yilmaz/pyCrossfade/releases/tag/v0.1.0>
- Older [Scripted Usage](docs/scripted-usage-deprecated.md) documentation

---

## About This Project

This project's main goal is to create seamless crossfade transitions between music files. This requires some DJ'ing abilities such as _bpm changing_, _beat-matching_ and _equalizer manipulation_.

#### Some Definitions on Music Domain

#### Some Definitions on Music Domain
- [Beat](https://en.wikipedia.org/wiki/Beat_(music))
In music and music theory, the beat is the basic unit of time, the pulse or regularly repeating event.
The beat is often defined as the rhythm listeners would tap their toes to when listening to a piece of music.
- [Beat](<https://en.wikipedia.org/wiki/Beat_(music)>)
In music and music theory, the beat is the basic unit of time, the pulse or regularly repeating event.
The beat is often defined as the rhythm listeners would tap their toes to when listening to a piece of music.

- [Bar (Measure)](https://en.wikipedia.org/wiki/Bar_(music))
- [Bar (Measure)](<https://en.wikipedia.org/wiki/Bar_(music)>)
In musical notation, a bar (or measure) is a segment of time corresponding to a specific number of beats, usually 4.

- [Downbeat](https://en.wikipedia.org/wiki/Beat_(music)#Downbeat_and_upbeat)
- [Downbeat](<https://en.wikipedia.org/wiki/Beat_(music)#Downbeat_and_upbeat>)
The downbeat is the first beat of the bar, i.e. number 1.

### About Madmom's Beat Tracking

Madmom's Beat Tracking takes a long time to run, 45-150 seconds depending on the music file. It gives a `numpy array` as output, so when madmom finishes calculating, pyCrossfade saves/caches the said `numpy array` in a text file named after the song, under the folder `pycrossfade_annotations`. This makes pyCrossfade robust while working with same songs by avoiding heavy calculations every time.

### BPM Matching
The creation of a transition requires two songs, called master and slave songs. Master song is the currently playing track and slave song refers to the next track.

The creation of a transition requires two songs, called master and slave songs. Master song is the currently playing track and slave song refers to the next track.

Master and slave tracks can be in different BPM's or speeds, so before applying crossfade, we have to gradually increase/decrease to master track's speed to match slave's speed. Let's say master song has 90 bpm, and slave song has 135 bpm. This makes slave song 1.5x faster than master song. If we were to suddenly increase the speed 1.5x that would be harsh on the listeners ear.

#### Gradually Time Stretching On Downbeats
Before applying crossfade, to match the bpm's of two songs, master song's speed is gradually increased on given number of downbeats. This ensures the listening experience quality. This works linearly as can be seen in the table below.

Before applying crossfade, to match the bpm's of two songs, master song's speed is gradually increased on given number of downbeats. This ensures the listening experience quality. This works linearly as can be seen in the table below.

##### Example

##### Example
```python
from pycrossfade.transition import crop_audio_and_dbeats \
time_stretch_gradually_in_downbeats
time_stretch_gradually_in_downbeats

from pycrossfade.song import Song
from pycrossfade.utils import save_audio

my_song = Song('some/path/to/a/song.mp3')

final_factor = 1.10 # times faster

# returns a new Song obj. cropped from my_song's between given parameter downbeats(or bars).
sample = crop_audio_and_dbeats(my_song, 50, 60) # sample of 10 bars
# returns a new Song obj. cropped from my_song's between given parameter downbeats(or bars).
sample = crop_audio_and_dbeats(my_song, 50, 60) # sample of 10 bars

# increases the sample song's speed gradually
sample_but_faster_every_beat = time_stretch_gradually_in_downbeats(sample, final_factor)

save_audio(sample_but_faster_every_beat, 'some/output/path.wav', file_format='wav', bit_rate=320)
```


| bars | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Final Factor |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| *Time Stretching Factor* | 1.01x | 1.02x | 1.03x | 1.04x | 1.05x | 1.06x | 1.07x | 1.08x | 1.09x | 1.10x | *1.10x* |


| bars | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Final Factor |
| ------------------------ | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ------------ |
| _Time Stretching Factor_ | 1.01x | 1.02x | 1.03x | 1.04x | 1.05x | 1.06x | 1.07x | 1.08x | 1.09x | 1.10x | _1.10x_ |

### Overview of the Transition

A simple visualization of all the processes would be like this:
> *master song* | *bpm matching* | *crossfade* | *slave song*

> ı||ı|ı||||ı||ı||||ı|||ı||ı||ı||ı||ıı||ı||ı|ıı||ı|ıı||ııııııııııııııııııı
> _master song_ | _bpm matching_ | _crossfade_ | _slave song_
> --------------------------------ııııııııııııııııııı||ı||ııı|||ı||ııı|||ı||ııı|ıı||||ıı
> ı||ı|ı||||ı||ı||||ı|||ı||ı||ı||ı||ıı||ı||ı|ıı||ı|ıı||ııııııııııııııııııı
> --------------------------------ııııııııııııııııııı||ı||ııı|||ı||ııı|||ı||ııı|ıı||||ıı
### pyCrossfade's Approach To Perfect Beat Matching

Human ear can catch minimal errors easily thus making beat-matching is extremely important for any transition. Beat-matching would be easy if every beat had regular timing, but producers are doing their best to [humanize their songs](https://www.izotope.com/en/learn/how-to-humanize-and-dehumanize-drums.html), not playing every beat in regular timing to get nonrobotic rhythms.

#### A Visual Explanation
Expand All @@ -113,18 +165,17 @@ Red lines are denoting every _bar_, or it's delimiter _downbeats_.
This is the second song with 20 bars.

![Escape.mp3 and Downbeats](./assets/images/Eyeillfals-Escape-Downbeats.png)
> First song's waveform is blue and it's bars denoted with red lines. Second song is shown with colors of orange and green.

When we put them on top of each other, we can see that their beats(red and green lines) is not matched, resulting in clashing of drums - or distorted audio.
> First song's waveform is blue and it's bars denoted with red lines. Second song is shown with colors of orange and green.
When we put them on top of each other, we can see that their beats(red and green lines) is not matched, resulting in clashing of drums - or distorted audio.

Even though they have same amount of bars, resulting plot shows that second song is shorter. This is beacuse they have different BPMs - or speeds.

If every song had regular beat timing, then beat-matching would be easy as just time stretching the other song to match their speeds. However, because of _humanizing_, every bar can be different in length. For this reason, pyCrossfade applies beat matching on the level of bars.

##### Beat Matching on the level of every bar


pyCrossfade lets you define every transition's length in bars, lets take it as _K_ bars. Then it gets _master song_'s last K bars, and _slave song_'s first K bars, and applies beat matching on each bar. This is ensures the created transition is perfectly beat-matched even if the songs are _humanized_ or not.

### Possible Improvements
Expand Down
Binary file added assets/images/cli_crossfade_help_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/cli_help_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions docs/scripted-usage-deprecated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Scripted Usage (deprecated)

## Installation

### Dependencies

This project requires *libsndfile*, *rubberband-cli*, *ffmpeg* to be installed on the system to work.


```bash
apt-get update && apt-get install -y libsndfile1 rubberband-cli ffmpeg
```

```bash
brew install libsndfile ffmpeg rubberband
```

-----

### Python Dependencies

#### Python Version

`madmom` package supports up to python 3.8, it's recommended to is this version.


#### Installation

To install the projects dependencies run:

```bash
pip install -r requirements.txt
```

*!* _if you get an error about `Cython` refer to [this tip.](https://github.com/oguzhan-yilmaz/pyCrossfade#a-note-on-the-python-dependencies)_

|Package|Used For|
|---|---|
|[Cython](https://github.com/cython/cython) | Required by _madmom_ package.|
|[Numpy](https://github.com/numpy/numpy)|Handling audio data.|
|[pyrubberband](https://github.com/bmcfee/pyrubberband)|Python wrapper for rubberband, a perfectly capable time stretcher & pitch shifter.|
|[yodel](https://github.com/rclement/yodel)|Audio frequency filtering capabilities.|
|[essentia](https://github.com/MTG/essentia)|Format resilient audio loading and it's [MIR](https://en.wikipedia.org/wiki/Music_information_retrieval) tools. |
|[madmom](https://github.com/CPJKU/madmom)|_State of the art_ beat-tracking.|

#### A note on the Python dependencies
Installing `madmom` package alone, if `Cython` package is not installed before hand, can fail. To solve this problem, you can `pip install cython` before installing madmom package.


----
## Example Usage

#### Transitioning Between Two Songs

```python
from pycrossfade import Song, crossfade, save_audio
# creating master and slave songs
master_song = Song('/path/to/master_song.mp3')
slave_song = Song('/path/to/slave_song.mp3')
# creating crossfade with bpm matching
output_audio = crossfade(master_song, slave_song, len_crossfade=8, len_time_stretch=8)
# saving the output
save_audio(output_audio, '/path/to/save/mix.wav')
```

#### Transitioning Between Multiple Songs

```python
from pycrossfade import Song, crossfade_multiple, save_audio
# creating songs
song_list = [
Song('/path/to/song_one.mp3'),
Song('/path/to/song_two.mp3'),
Song('/path/to/song_three.mp3'),
]
# creating crossfade with bpm matching
output_audio = crossfade_multiple(song_list, len_crossfade=16, len_time_stretch=8)
# saving the output
save_audio(output_audio, '/path/to/save/mix_multiple.wav')
```

#### Transitioning Between Songs On Specific Bars

```python
from pycrossfade import Song, crossfade_multiple, crop_audio_and_dbeats, import save_audio
# creating songs
song_one = Song('/path/to/song_one.mp3')
song_two = Song('/path/to/song_two.mp3')
song_three = Song('/path/to/song_three.mp3')

song_list = [
crop_audio_and_dbeats(song_one, 10, 35),
crop_audio_and_dbeats(song_two, 30, 55),
crop_audio_and_dbeats(song_three, 50, 75),
]
# creating crossfade with bpm matching
output_audio = crossfade_multiple(song_list, len_crossfade=8, len_time_stretch=8)
# saving the output
save_audio(output_audio, '/path/to/save/mix_multiple_specific_bars.wav')
```

0 comments on commit 18e5647

Please sign in to comment.