├── .coveragerc ├── .gitignore ├── .travis.yml ├── .travis_dependencies.sh ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── amen ├── __init__.py ├── audio.py ├── deformation.py ├── echo_nest_converter.py ├── example_audio │ ├── amen-mono.wav │ └── amen.wav ├── exceptions.py ├── feature.py ├── synthesize.py ├── timing.py ├── utils.py └── version.py ├── docs ├── Makefile ├── api.rst ├── conf.py ├── index.rst └── requirements.txt ├── examples ├── echo_nest_json.py ├── iterate.py ├── iterate_feature.py └── reverse.py ├── setup.py └── tests ├── test_audio.py ├── test_audio_features.py ├── test_audio_timings.py ├── test_deformation.py ├── test_echo_nest_converter.py ├── test_feature.py ├── test_synthesize.py ├── test_timing.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | *.ipynb 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | addons: 4 | apt: 5 | packages: 6 | - libav-tools 7 | - libsndfile1 8 | 9 | cache: 10 | directories: 11 | - $HOME/env 12 | 13 | language: python 14 | 15 | notifications: 16 | email: false 17 | 18 | python: 19 | - "2.7" 20 | - "3.5" 21 | - "3.6" 22 | - "3.7" 23 | 24 | before_install: 25 | - bash .travis_dependencies.sh 26 | - export PATH="$HOME/env/miniconda$TRAVIS_PYTHON_VERSION/bin:$PATH"; 27 | - hash -r 28 | - source activate test-environment 29 | 30 | install: 31 | # install your own package into the environment 32 | # pip install -e rather than setup.py, so that coverage can find the source 33 | - pip install -e ./ 34 | - pip install flake8 35 | - pip install pytest-cov 36 | # We can get rid of these once we get rid of 2.7 and 3.5 37 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then pip install black; fi 38 | - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then pip install black; fi 39 | 40 | script: 41 | - python --version 42 | # We can get rid of these once we get rid of 2.7 and 3.5 43 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then black -S --check --verbose .; fi 44 | - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then black -S --check --verbose .; fi 45 | - flake8 --ignore E501,E203 --exclude=tests,docs,build 46 | - pytest --cov=amen tests/ 47 | 48 | after_success: 49 | - coveralls 50 | - pip uninstall -y amen 51 | 52 | after_failure: 53 | - pip uninstall -y amen 54 | -------------------------------------------------------------------------------- /.travis_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ENV_NAME="test-environment" 4 | set -e 5 | 6 | conda_create () 7 | { 8 | 9 | hash -r 10 | conda config --set always_yes yes --set changeps1 no 11 | conda update -q conda 12 | conda config --add channels pypi 13 | conda info -a 14 | deps='pip numpy scipy pandas requests pytest coverage numpydoc matplotlib sphinx scikit-learn seaborn' 15 | 16 | conda create -q -n $ENV_NAME "python=$TRAVIS_PYTHON_VERSION" $deps 17 | } 18 | 19 | src="$HOME/env/miniconda$TRAVIS_PYTHON_VERSION" 20 | if [ ! -d "$src" ]; then 21 | mkdir -p $HOME/env 22 | pushd $HOME/env 23 | # Download miniconda packages 24 | wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 25 | 26 | # Install both environments 27 | bash miniconda.sh -b -p $src 28 | 29 | export PATH="$src/bin:$PATH" 30 | conda_create 31 | 32 | source activate $ENV_NAME 33 | 34 | pip install python-coveralls 35 | source deactivate 36 | popd 37 | else 38 | echo "Using cached dependencies" 39 | fi 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | * Please send all contributions / updates / etc via Pull Requests. 2 | * Please create an Issue for any problems that you encounter. 3 | * Please make sure that all tests pass in your PR. 4 | * Please add tests to your PR! Tests are great. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, algorithmic-music-exploration 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/algorithmic-music-exploration/amen.svg?branch=master)](https://travis-ci.org/algorithmic-music-exploration/amen) 2 | [![Coverage Status](https://coveralls.io/repos/github/algorithmic-music-exploration/amen/badge.svg?branch=master)](https://coveralls.io/github/algorithmic-music-exploration/amen?branch=master) 3 | [![Dependency Status](https://dependencyci.com/github/algorithmic-music-exploration/amen/badge)](https://dependencyci.com/github/algorithmic-music-exploration/amen) 4 | [![Documentation Status](https://readthedocs.org/projects/amen/badge/?version=latest)](http://amen.readthedocs.io/en/latest/?badge=latest) 5 | [![GitHub license](https://img.shields.io/badge/license-BSD-blue.svg)](https://raw.githubusercontent.com/algorithmic-music-exploration/amen/master/LICENSE) 6 | 7 | # amen 8 | A toolbox for algorithmic remixing, after Echo Nest Remix. 9 | 10 | # Platforms 11 | Amen is developed on Ubuntu 14.04 and higher. OS X should be workable. Windows users should install Ubuntu. 12 | 13 | # Installation 14 | Amen is pretty simple, but it stands on top of some complex stuff. 15 | 16 | If you are on OSX, go on to the install Anaconda step. If you are on Linux, you'll need to do some apt-getting: 17 | - `libsoundfile`: `sudo apt-get install libsndfile1` 18 | - `libavtools`: `sudo apt-get update && sudo apt-get install libav-tools` 19 | 20 | You should install Anaconda, (https://www.continuum.io/downloads) which will get you all of the dependencies. 21 | 22 | Then, install via pip: `pip install amen`. That should be it! 23 | 24 | (If you're a serious Python person, you can just get Amen from pip, without Anaconda - but that will require installing numpy, scipy, a fortran compiler, and so on.) 25 | 26 | # Testing the Installation 27 | After installation is finished, open up a Python interpreter and run the following (or run it from a file): 28 | ``` 29 | from amen.utils import example_audio_file 30 | from amen.audio import Audio 31 | from amen.synthesize import synthesize 32 | 33 | audio_file = example_audio_file() 34 | audio = Audio(audio_file) 35 | 36 | beats = audio.timings['beats'] 37 | beats.reverse() 38 | 39 | out = synthesize(beats) 40 | out.output('reversed.wav') 41 | ``` 42 | 43 | If all that works, you just need to play the resulting `reversed.wav` file, and you're on your way! 44 | 45 | # Examples 46 | 47 | We've got a few other examples in the `examples` folder - most involve editing a file based on the audio features thereof. We'll try to add more as we go. 48 | 49 | # Documentation 50 | 51 | You can read the docs at http://amen.readthedocs.io/en/latest! You can also build the docs locally, using [Sphinx](http://www.sphinx-doc.org). Just run `make` within the `docs` directory. 52 | 53 | # Contributing 54 | Welcome aboard! Please see CONTRIBUTING.md, or open an issue if things don't work right. 55 | 56 | # Thanks 57 | Amen owes a very large debt to Echo Nest Remix. Contributors to that most esteemed library include: 58 | * Chris Angelico 59 | * Yannick Antoine 60 | * Adam Baratz 61 | * Ryan Berdeen 62 | * Dave DesRoches 63 | * Dan Foreman-Mackey 64 | * Tristan Jehan 65 | * Joshua Lifton 66 | * Adam Lindsay 67 | * Alison Mandel 68 | * Nicola Montecchio 69 | * Rob Oschorn 70 | * Jason Sundram 71 | * Brian Whitman 72 | -------------------------------------------------------------------------------- /amen/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | AMEN 5 | 6 | A toolbox for algorithmic remixing, after Echo Nest Remix. 7 | ''' 8 | from .feature import Feature 9 | 10 | __all__ = ['audio', 'Feature', 'utils'] 11 | -------------------------------------------------------------------------------- /amen/audio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Audio analysis 6 | ''' 7 | 8 | import os 9 | import pandas as pd 10 | import numpy as np 11 | import soundfile as sf 12 | 13 | import librosa 14 | 15 | from .feature import Feature, FeatureCollection 16 | from .timing import TimingList 17 | 18 | 19 | class Audio(object): 20 | """ 21 | The base Audio object: wraps the ouput from librosa, and provides access to features 22 | 23 | Attributes 24 | ---------- 25 | sample_rate: number 26 | sample rate 27 | raw_samples: numpy array 28 | raw samples from the audio 29 | analysis_samples: numpy array 30 | downsampled samples for analysis 31 | num_channels: integer 32 | number of channels of the audio 33 | duration: float 34 | duration, in seconds 35 | features: dict 36 | collection of named feature objects 37 | """ 38 | 39 | def __init__( 40 | self, 41 | file_path=None, 42 | raw_samples=None, 43 | convert_to_mono=False, 44 | sample_rate=44100, 45 | analysis_sample_rate=22050, 46 | ): 47 | """ 48 | Audio constructor. 49 | Opens a file path, loads the audio with librosa, and prepares the features 50 | 51 | Parameters 52 | ---------- 53 | 54 | file_path: string 55 | path to the audio file to load 56 | 57 | raw_samples: np.array 58 | samples to use for audio output 59 | 60 | convert_to_mono: boolean 61 | (optional) converts the file to mono on loading 62 | 63 | sample_rate: number > 0 [scalar] 64 | (optional) sample rate to pass to librosa. 65 | 66 | 67 | Returns 68 | ------ 69 | An Audio object 70 | """ 71 | 72 | if file_path: 73 | y, sr = librosa.load(file_path, mono=convert_to_mono, sr=sample_rate) 74 | elif raw_samples is not None: 75 | # This assumes that we're passing in raw_samples 76 | # directly from another Audio's raw_samples. 77 | y = raw_samples 78 | sr = sample_rate 79 | 80 | self.file_path = file_path 81 | self.sample_rate = float(sr) 82 | self.analysis_sample_rate = float(analysis_sample_rate) 83 | self.num_channels = y.ndim 84 | self.duration = librosa.get_duration(y=y, sr=sr) 85 | 86 | self.analysis_samples = librosa.resample( 87 | librosa.to_mono(y), sr, self.analysis_sample_rate, res_type='kaiser_best' 88 | ) 89 | self.raw_samples = np.atleast_2d(y) 90 | 91 | self.zero_indexes = self._create_zero_indexes() 92 | self.features = self._create_features() 93 | self.timings = self._create_timings() 94 | 95 | def __repr__(self): 96 | file_name = os.path.split(self.file_path)[-1] 97 | args = file_name, self.duration 98 | return ''.format(*args) 99 | 100 | def output(self, filename, format=None): 101 | """ 102 | Write the samples out to the given filename. 103 | 104 | Parameters 105 | ---------- 106 | filename : str 107 | The path to write the audio on disk. 108 | This can be any format supported by `pysoundfile`, including 109 | `WAV`, `FLAC`, or `OGG` (but not `mp3`). 110 | 111 | format : str 112 | If provided, explicitly set the output encoding format. 113 | See `soundfile.available_formats`. 114 | """ 115 | sf.write(filename, self.raw_samples.T, int(self.sample_rate), format=format) 116 | 117 | def _create_zero_indexes(self): 118 | """ 119 | Create zero crossing indexes. 120 | We use these in synthesis, and it is easier to make them here. 121 | """ 122 | zero_indexes = [] 123 | for channel_index in range(self.num_channels): 124 | channel = self.raw_samples[channel_index] 125 | zero_crossings = librosa.zero_crossings(channel) 126 | zero_index = np.nonzero(zero_crossings)[0] 127 | zero_indexes.append(zero_index) 128 | return zero_indexes 129 | 130 | def _create_timings(self): 131 | """ 132 | Create timings in a timings dict. 133 | """ 134 | timings = {} 135 | timings['track'] = TimingList('track', [(0, self.duration)], self) 136 | timings['beats'] = TimingList('beats', self._get_beats(), self) 137 | timings['segments'] = TimingList('segments', self._get_segments(), self) 138 | return timings 139 | 140 | def _get_beats(self): 141 | """ 142 | Gets beats using librosa's beat tracker. 143 | """ 144 | _, beat_frames = librosa.beat.beat_track( 145 | y=self.analysis_samples, sr=self.analysis_sample_rate, trim=False 146 | ) 147 | 148 | # pad beat times to full duration 149 | f_max = librosa.time_to_frames(self.duration, sr=self.analysis_sample_rate) 150 | beat_frames = librosa.util.fix_frames(beat_frames, x_min=0, x_max=f_max) 151 | 152 | # convert frames to times 153 | beat_times = librosa.frames_to_time(beat_frames, sr=self.analysis_sample_rate) 154 | 155 | # make the list of (start, duration) tuples that TimingList expects 156 | starts_durs = [(s, t - s) for (s, t) in zip(beat_times, beat_times[1:])] 157 | 158 | return starts_durs 159 | 160 | def _get_segments(self): 161 | """ 162 | Gets Echo Nest style segments using librosa's onset detection and backtracking. 163 | """ 164 | 165 | onset_frames = librosa.onset.onset_detect( 166 | y=self.analysis_samples, sr=self.analysis_sample_rate, backtrack=True 167 | ) 168 | segment_times = librosa.frames_to_time( 169 | onset_frames, sr=self.analysis_sample_rate 170 | ) 171 | 172 | # make the list of (start, duration) tuples that TimingList expects 173 | starts_durs = [(s, t - s) for (s, t) in zip(segment_times, segment_times[1:])] 174 | 175 | return starts_durs 176 | 177 | def _create_features(self): 178 | """ 179 | Creates the FeatureCollection, and loads each feature. 180 | 181 | Parameters 182 | --------- 183 | 184 | Returns 185 | ----- 186 | FeatureCollection 187 | FeatureCollection with each Amen.Feature object named correctly. 188 | Note that _get_chroma returns a FeatureCollection of chroma features. 189 | """ 190 | features = FeatureCollection() 191 | features['centroid'] = self._get_centroid() 192 | features['amplitude'] = self._get_amplitude() 193 | features['timbre'] = self._get_timbre() 194 | features['chroma'] = self._get_chroma() 195 | features['tempo'] = self._get_tempo() 196 | return features 197 | 198 | def _get_centroid(self): 199 | """ 200 | Gets spectral centroid data from librosa, and returns it as a Feature 201 | 202 | Parameters 203 | --------- 204 | 205 | Returns 206 | ----- 207 | Feature 208 | """ 209 | centroids = librosa.feature.spectral_centroid(self.analysis_samples) 210 | data = self._convert_to_dataframe(centroids, ['spectral_centroid']) 211 | feature = Feature(data) 212 | return feature 213 | 214 | def _get_amplitude(self): 215 | """ 216 | Gets amplitude data from librosa, and returns it as a Feature 217 | 218 | Parameters 219 | --------- 220 | 221 | Returns 222 | ----- 223 | Feature 224 | """ 225 | amplitudes = librosa.feature.rms(self.analysis_samples) 226 | data = self._convert_to_dataframe(amplitudes, ['amplitude']) 227 | feature = Feature(data) 228 | return feature 229 | 230 | def _get_timbre(self): 231 | """ 232 | Gets timbre (MFCC) data, taking the first 20. 233 | Note that the keys to the Feature are "mffc_", 234 | to avoid having a dict-like object with numeric keys. 235 | 236 | Parameters 237 | --------- 238 | 239 | Returns 240 | ----- 241 | Feature 242 | """ 243 | mfccs = librosa.feature.mfcc( 244 | y=self.analysis_samples, sr=self.analysis_sample_rate, n_mfcc=12 245 | ) 246 | feature = FeatureCollection() 247 | for index, mfcc in enumerate(mfccs): 248 | data = self._convert_to_dataframe(mfcc, ['timbre']) 249 | key = 'mfcc_%s' % (index) 250 | feature[key] = Feature(data) 251 | 252 | return feature 253 | 254 | def _get_chroma(self): 255 | """ 256 | Gets chroma data from librosa, and returns it as a FeatureCollection, 257 | with 12 features. 258 | 259 | Parameters 260 | --------- 261 | 262 | Returns 263 | ----- 264 | FeatureCollection 265 | """ 266 | feature = FeatureCollection() 267 | pitch_names = ['c', 'c#', 'd', 'eb', 'e', 'f', 'f#', 'g', 'ab', 'a', 'bb', 'b'] 268 | chroma_cq = librosa.feature.chroma_cqt(self.analysis_samples) 269 | for chroma, pitch in zip(chroma_cq, pitch_names): 270 | data = self._convert_to_dataframe(chroma, [pitch]) 271 | feature[pitch] = Feature(data) 272 | 273 | # Enharmonic aliases 274 | feature['db'] = feature['c#'] 275 | feature['d#'] = feature['eb'] 276 | feature['gb'] = feature['f#'] 277 | feature['g#'] = feature['ab'] 278 | feature['a#'] = feature['bb'] 279 | 280 | return feature 281 | 282 | def _get_tempo(self): 283 | """ 284 | Gets tempo data from librosa, and returns it as a feature collection. 285 | Note that the tempo feature uses median aggregation, as opposed to the 286 | default mean. 287 | 288 | Parameters 289 | --------- 290 | 291 | Returns 292 | ----- 293 | FeatureCollection 294 | """ 295 | onset_env = librosa.onset.onset_strength( 296 | self.analysis_samples, sr=self.analysis_sample_rate 297 | ) 298 | tempo = librosa.beat.tempo( 299 | onset_envelope=onset_env, sr=self.analysis_sample_rate, aggregate=None 300 | ) 301 | data = self._convert_to_dataframe(tempo, ['tempo']) 302 | feature = Feature(data, aggregate=np.median) 303 | 304 | return feature 305 | 306 | @classmethod 307 | def _convert_to_dataframe(cls, feature_data, columns): 308 | """ 309 | Take raw librosa feature data, convert to a pandas dataframe. 310 | 311 | Parameters 312 | --------- 313 | feature_data: numpy array 314 | a N by T array, where N is the number of features, and T is the number of time dimensions 315 | 316 | columns: list [strings] 317 | a list of column names of length N, the same as the N dimension of feature_data 318 | 319 | Returns 320 | ----- 321 | pandas.DataFrame 322 | """ 323 | feature_data = feature_data.transpose() 324 | frame_numbers = np.arange(len(feature_data)) 325 | indexes = librosa.frames_to_time(frame_numbers) 326 | indexes = pd.to_timedelta(indexes, unit='s') 327 | data = pd.DataFrame(data=feature_data, index=indexes, columns=columns) 328 | return data 329 | -------------------------------------------------------------------------------- /amen/deformation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Deformation functions for Amen. 6 | All functions act on an Audio object and return a modified Audio object. 7 | """ 8 | 9 | import librosa 10 | 11 | from .audio import Audio 12 | 13 | 14 | def time_stretch(audio, rate): 15 | """ 16 | Wraps librosa's `time_stretch` function, and returns a new Audio object. 17 | Note that this folds to mono. 18 | 19 | Parameters 20 | --------- 21 | audio : Audio 22 | The Audio object to act on. 23 | 24 | rate : float 25 | The stretch factor. 26 | If rate is > 1, then the audio is sped up. 27 | If rate is < 1, then the audio is slowed down. 28 | A rate of `2.0` will result in audio that is twice as fast. 29 | """ 30 | stretched = librosa.effects.time_stretch( 31 | librosa.to_mono(audio.raw_samples), rate=rate 32 | ) 33 | stretched_audio = Audio(raw_samples=stretched, sample_rate=audio.sample_rate) 34 | 35 | return stretched_audio 36 | 37 | 38 | def pitch_shift(audio, steps, step_size=12): 39 | """ 40 | Wraps librosa's `pitch_shift` function, and returns a new Audio object. 41 | Note that this folds to mono. 42 | 43 | Parameters 44 | --------- 45 | audio : Audio 46 | The Audio object to act on. 47 | 48 | steps : float 49 | The pitch shift amount. 50 | The default unit is semitones, as set by `step_size`. 51 | 52 | step_size : float > 0 53 | The number of equal-tempered steps per octave. 54 | The default is semitones, as set by `step_size=12`. 55 | Quarter-tones, for example, would be `step_size=24`. 56 | """ 57 | shifted = librosa.effects.pitch_shift( 58 | librosa.to_mono(audio.raw_samples), 59 | audio.sample_rate, 60 | steps, 61 | bins_per_octave=step_size, 62 | ) 63 | stretched_audio = Audio(raw_samples=shifted, sample_rate=audio.sample_rate) 64 | 65 | return stretched_audio 66 | 67 | 68 | def harmonic_separation(audio, margin=3.0): 69 | """ 70 | Wraps librosa's `harmonic` function, and returns a new Audio object. 71 | Note that this folds to mono. 72 | 73 | Parameters 74 | --------- 75 | audio : Audio 76 | The Audio object to act on. 77 | 78 | margin : float 79 | The larger the margin, the larger the separation. 80 | The default is `3.0`. 81 | """ 82 | harmonic = librosa.effects.harmonic( 83 | librosa.to_mono(audio.raw_samples), margin=margin 84 | ) 85 | harmonic_audio = Audio(raw_samples=harmonic, sample_rate=audio.sample_rate) 86 | 87 | return harmonic_audio 88 | 89 | 90 | def percussive_separation(audio, margin=3.0): 91 | """ 92 | Wraps librosa's `percussive` function, and returns a new Audio object. 93 | Note that this folds to mono. 94 | 95 | Parameters 96 | --------- 97 | audio : Audio 98 | The Audio object to act on. 99 | 100 | margin : float 101 | The larger the margin, the larger the separation. 102 | The default is `3.0`. 103 | """ 104 | percussive = librosa.effects.percussive( 105 | librosa.to_mono(audio.raw_samples), margin=margin 106 | ) 107 | percussive_audio = Audio(raw_samples=percussive, sample_rate=audio.sample_rate) 108 | 109 | return percussive_audio 110 | -------------------------------------------------------------------------------- /amen/echo_nest_converter.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | import collections 4 | 5 | import amen.audio 6 | import amen.feature 7 | 8 | 9 | class AudioAnalysis(object): 10 | """ 11 | An Analysis object compatible with EchoNest Remix 12 | """ 13 | 14 | def __init__(self, audio_object): 15 | """ 16 | AudioAnalysis constructor. 17 | Create an EchoNest compatible Analysis object from an Audio object 18 | 19 | Parameters 20 | ---------- 21 | audio_object: Audio object 22 | 23 | Returns 24 | ------ 25 | An AudioAnalysis object 26 | """ 27 | self.audio = audio_object 28 | self.sections = AudioQuantumList(kind='sections') 29 | self.bars = AudioQuantumList(kind='bars') 30 | self.beats = AudioQuantumList(kind='beats') 31 | self.tatums = AudioQuantumList(kind='tatums') 32 | self.segments = AudioQuantumList(kind='segments') 33 | 34 | for key in self.audio.timings: 35 | # Track-level attributes are dealt with below 36 | if key == 'track': 37 | continue 38 | else: 39 | setattr(self, key, self._get_quantums(key)) 40 | 41 | # TODO: stub segments to be same as beats for now 42 | self.segments = self._get_quantums('beats') 43 | self.segments.kind = 'segments' 44 | for quantum in self.segments: 45 | quantum.kind = 'segment' 46 | 47 | # Set track-level attributes 48 | track_level = self.audio.timings['track'] 49 | self.tempo = self.audio.features['tempo'].at(track_level) 50 | 51 | def _get_quantums(self, kind): 52 | """ 53 | Format Audio Beats to be compatible with EchoNest Remix 54 | 55 | Parameters 56 | ---------- 57 | kind: String, one of {beats, bars, tatums, sections, segments} 58 | 59 | Returns 60 | ------ 61 | An AudioQuantumList 62 | """ 63 | q_list = AudioQuantumList(kind=kind) 64 | time_slices = self.audio.timings[kind] 65 | for time_slice in time_slices: 66 | features = {} 67 | for key, feature in self.audio.features.items(): 68 | feature = feature.at(time_slice) 69 | features[key] = feature 70 | quantum = AudioQuantum( 71 | kind[:-1], time_slice.time, time_slice.duration, features 72 | ) 73 | q_list.append(quantum) 74 | return q_list 75 | 76 | def as_serializable(self): 77 | """ 78 | Return this object as a serializable dict that contains only dicts, 79 | lists, and primitives. 80 | 81 | This may be useful, for instance, if you would like to use 82 | flask.jsonify(analysis.as_serializable()) 83 | instead of the built-in to_json() 84 | """ 85 | 86 | def as_dict(obj): 87 | # Convert timedeltas to float 88 | if isinstance(obj, datetime.timedelta): 89 | return obj.total_seconds() 90 | # Convert Audio objects to string 91 | if isinstance(obj, amen.audio.Audio): 92 | return obj.file_path 93 | # Convert Feature objects to list 94 | if isinstance(obj, amen.feature.Feature): 95 | return as_dict(obj.data.values.tolist()) 96 | if isinstance(obj, collections.Sequence): 97 | if isinstance(obj, (str, bytes)): 98 | return obj 99 | items = [] 100 | for item in obj: 101 | items.append(as_dict(item)) 102 | # Extract if list contains only 1 item 103 | while isinstance(items, collections.Sequence) and len(items) == 1: 104 | items = items[0] 105 | return items 106 | if hasattr(obj, '__dict__'): 107 | result = {} 108 | for key, value in obj.__dict__.items(): 109 | if key.startswith('_'): 110 | continue 111 | result[key] = as_dict(value) 112 | return result 113 | # default 114 | return obj 115 | 116 | return as_dict(self) 117 | 118 | def to_json(self): 119 | """ 120 | Return this object as a JSON encoded string. 121 | """ 122 | return json.dumps(self.as_serializable()) 123 | 124 | 125 | class AudioQuantum(object): 126 | def __init__(self, kind, start, duration, features, confidence=None): 127 | self.kind = kind 128 | self.start = start 129 | self.duration = duration 130 | self.confidence = confidence 131 | # Map Amen feature names to Remix names 132 | for feature, value in features.items(): 133 | key = feature 134 | if feature == 'chroma': 135 | # pitches are a list in Remix 136 | key = 'pitches' 137 | pitch_names = [ 138 | 'c', 139 | 'c#', 140 | 'd', 141 | 'eb', 142 | 'e', 143 | 'f', 144 | 'f#', 145 | 'g', 146 | 'ab', 147 | 'a', 148 | 'bb', 149 | 'b', 150 | ] 151 | value = [value[pitch] for pitch in pitch_names] 152 | elif feature == 'timbre': 153 | # timbre is a list of 12 PCA things in Remix 154 | # we take the first 12 MFCCs 155 | timbre_dimensions = range(12) 156 | value = [value["mfcc_%s" % t] for t in timbre_dimensions] 157 | 158 | setattr(self, key, value) 159 | # TODO: stub fake loudness_max values from RMS energy 160 | self.loudness_max = features.get('amplitude') 161 | self.loudness_max_time = start 162 | self.loudness_start = start 163 | 164 | 165 | class AudioQuantumList(list): 166 | def __init__(self, initial=[], kind=None): 167 | list.__init__(self) 168 | self.kind = kind 169 | self.extend(initial) 170 | -------------------------------------------------------------------------------- /amen/example_audio/amen-mono.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorithmic-music-exploration/amen/88ec1ce3e6646b9ce35db993f56db04fbc11e7fe/amen/example_audio/amen-mono.wav -------------------------------------------------------------------------------- /amen/example_audio/amen.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorithmic-music-exploration/amen/88ec1ce3e6646b9ce35db993f56db04fbc11e7fe/amen/example_audio/amen.wav -------------------------------------------------------------------------------- /amen/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | '''AMEN Exception classes''' 4 | 5 | 6 | class AmenError(Exception): 7 | """ 8 | The root amen exception class 9 | """ 10 | 11 | pass 12 | 13 | 14 | class SynthesizeError(AmenError): 15 | """ 16 | Exception class for errors in synthesize.py 17 | """ 18 | 19 | pass 20 | 21 | 22 | class FeatureError(AmenError): 23 | """ 24 | Exception class for errors in feature.py 25 | """ 26 | 27 | pass 28 | -------------------------------------------------------------------------------- /amen/feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | '''Container classes for feature analysis''' 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import six 7 | 8 | from .timing import TimeSlice 9 | from .exceptions import FeatureError 10 | 11 | 12 | class Feature(object): 13 | """ 14 | Core feature container object. Handles indexing and time-slicing. 15 | 16 | Attributes 17 | --------- 18 | 19 | Methods 20 | ------ 21 | at(time_slices) 22 | Resample the feature at the given TimeSlices 23 | """ 24 | 25 | def __init__(self, data, aggregate=np.mean, base=None, time_slices=None): 26 | """ 27 | Constructor for feature object 28 | 29 | Parameters 30 | ---------- 31 | data: pandas.DataFrame 32 | Time-indexed data frame of features 33 | 34 | aggregate: function 35 | resample-aggregation function or mapping 36 | 37 | Returns 38 | ------ 39 | A Feature object 40 | """ 41 | 42 | # Check that the arguments have the right types 43 | assert isinstance(data, pd.DataFrame) 44 | 45 | self.data = data 46 | self.aggregate = aggregate 47 | self.time_slices = time_slices 48 | # Not sure that this is the right way to do it - I feel like we're outsmarting pandas 49 | # pandas supports multiple keys in a dataframe, whereas this only allows one. 50 | # Should we replace FeatureCollection with something like that? 51 | self.name = data.keys()[0] 52 | 53 | if base is not None: 54 | assert isinstance(base, Feature) 55 | 56 | self.base = base 57 | 58 | def __repr__(self): 59 | args = self.name 60 | return ''.format(args) 61 | 62 | def __iter__(self): 63 | """ 64 | Wrapper to allow easy access to the internal data of the pandas dataframe 65 | """ 66 | for datum in self.data[self.name]: 67 | yield datum 68 | 69 | def __getitem__(self, x): 70 | """ 71 | Wrapper to allow easy access to the internal data of the pandas dataframe 72 | """ 73 | return self.data[self.name][x] 74 | 75 | def __len__(self): 76 | """ 77 | Wrapper to allow easy access to the internal data of the pandas dataframe 78 | """ 79 | return len(self.data[self.name]) 80 | 81 | def with_time(self): 82 | """ 83 | Allows iteration over a time-indexed feature and the associated timeslices. 84 | """ 85 | if self.time_slices is None: 86 | raise FeatureError("Feature has no time reference.") 87 | 88 | for i, datum in enumerate(self.data[self.name]): 89 | yield (self.time_slices[i], datum) 90 | 91 | def at(self, time_slices): 92 | """ 93 | Resample the data at a new time slice index. 94 | 95 | Parameters 96 | ---------- 97 | time_slices: TimeSlice or TimeSlice collection 98 | The time slices at which to index this feature object 99 | 100 | Returns 101 | ------- 102 | Feature 103 | The resampled feature data 104 | """ 105 | 106 | if self.base is not None: 107 | return self.base.at(time_slices) 108 | 109 | if isinstance(time_slices, TimeSlice): 110 | time_slices = [time_slices] 111 | 112 | # join the time slice values 113 | timed_data = pd.DataFrame(columns=self.data.columns) 114 | 115 | # make the new data 116 | for slice_t in time_slices: 117 | slice_index = (slice_t.time <= self.data.index) & ( 118 | self.data.index < slice_t.time + slice_t.duration 119 | ) 120 | timed_data.loc[slice_t.time] = self.aggregate( 121 | self.data[slice_index], axis=0 122 | ) 123 | 124 | # return the new feature object 125 | return Feature( 126 | data=timed_data, 127 | aggregate=self.aggregate, 128 | base=self, 129 | time_slices=time_slices, 130 | ) 131 | 132 | 133 | class FeatureCollection(dict): 134 | """ 135 | A dictionary of features. 136 | 137 | Delegates `.at` to the features it contains. 138 | 139 | Allows for selection of multiple keys, which returns a smaller feature collection. 140 | """ 141 | 142 | def at(self, time_slices): 143 | """ 144 | Resample each feature at a new time slice index. 145 | 146 | Parameters 147 | ---------- 148 | time_slices : TimeSlice or TimeSlice collection 149 | The time slices at which to index this feature object 150 | 151 | Returns 152 | ------- 153 | new_features : FeatureCollection 154 | The resampled feature data 155 | """ 156 | new_features = FeatureCollection() 157 | for key in self.keys(): 158 | new_features[key] = self[key].at(time_slices) 159 | return new_features 160 | 161 | def __iter__(self): 162 | """ 163 | Wrapper to avoid making the user deal with parallel lists 164 | """ 165 | key = list(self.keys())[0] 166 | length = len(self[key]) 167 | for i in range(length): 168 | res = {} 169 | for key, feature in self.items(): 170 | res[key] = feature.data[feature.name][i] 171 | yield res 172 | 173 | def __len__(self): 174 | """ 175 | Wrapper to avoid making the user deal with parallel lists 176 | """ 177 | key = list(self.keys())[0] 178 | feature = self[key] 179 | return len(feature) 180 | 181 | def with_time(self): 182 | """ 183 | Allows iteration over a time-indexed feature and the associated timeslices. 184 | """ 185 | key = list(self.keys())[0] 186 | length = len(self[key]) 187 | time_slices = self[key].time_slices 188 | 189 | if time_slices is None: 190 | raise FeatureError("FeatureCollection has no time reference.") 191 | 192 | for i in range(length): 193 | res = {} 194 | for key, feature in self.items(): 195 | res[key] = feature.data[feature.name][i] 196 | yield (time_slices[i], res) 197 | 198 | def get(self, keys): 199 | """ 200 | Get a subset of the keys in the correct feature collection 201 | 202 | Parameters 203 | ---------- 204 | keys : A string or list of strings 205 | The keys to return from the current feature collection 206 | 207 | Returns 208 | ------- 209 | new_features : FeatureCollection 210 | The subset of keys 211 | """ 212 | if isinstance(keys, six.string_types): 213 | keys = [keys] 214 | 215 | new_features = FeatureCollection() 216 | for key in keys: 217 | if key in self: 218 | new_features[key] = self[key] 219 | return new_features 220 | -------------------------------------------------------------------------------- /amen/synthesize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''Audio synthesis''' 4 | 5 | import types 6 | import pandas as pd 7 | from scipy.sparse import lil_matrix 8 | 9 | import librosa 10 | 11 | from .audio import Audio 12 | from .exceptions import SynthesizeError 13 | 14 | 15 | def _format_inputs(inputs): 16 | """ 17 | Organizes inputs to be a list of (TimeSlice, start_time) tuples, 18 | if they are not already of that form. 19 | # We may want to update this to support "properly" zipped lists of tuples. 20 | 21 | Acceptable forms are: 22 | - A list of TimeSlices - the TimeSlices will be concatenated. 23 | - A generator that returns (TimeSlice, start_time). 24 | - A tuple of (TimeSlices, start_times). 25 | """ 26 | if isinstance(inputs, list): 27 | time_index = pd.to_timedelta(0.0, 's') 28 | timings = [] 29 | for time_slice in inputs: 30 | timings.append(time_index) 31 | time_index = time_index + time_slice.duration 32 | 33 | return zip(inputs, timings) 34 | 35 | elif isinstance(inputs, tuple): 36 | return zip(inputs[0], inputs[1]) 37 | 38 | elif isinstance(inputs, types.GeneratorType): 39 | return inputs 40 | raise SynthesizeError('Invalid synthesis timing format: {}'.format(inputs)) 41 | 42 | 43 | def synthesize(inputs): 44 | """ 45 | Generate new Audio objects for output or further remixing. 46 | 47 | Parameters 48 | ---------- 49 | 50 | inputs: generator, list, or tuple. 51 | See _format_inputs for details on parsing inputs. 52 | 53 | Returns 54 | ------ 55 | An Audio object 56 | """ 57 | # First we organize our inputs. 58 | inputs = _format_inputs(inputs) 59 | 60 | max_time = 0.0 61 | sample_rate = 44100 62 | array_length = 20 * 60 # 20 minutes! 63 | array_shape = (2, sample_rate * array_length) 64 | sparse_array = lil_matrix(array_shape) 65 | 66 | initial_offset = 0 67 | for i, (time_slice, start_time) in enumerate(inputs): 68 | # if we have a mono file, we return stereo here. 69 | resampled_audio, left_offset, right_offset = time_slice.get_samples() 70 | 71 | # set the initial offset, so we don't miss the start of the array 72 | if i == 0: 73 | initial_offset = max(left_offset * -1, right_offset * -1) 74 | 75 | # get the target start and duration 76 | start_time = start_time.delta * 1e-9 77 | duration = time_slice.duration.delta * 1e-9 78 | 79 | # find the max time 80 | if start_time + duration > max_time: 81 | max_time = start_time + duration 82 | # error if we'd go too far 83 | if start_time + duration > array_length: 84 | raise SynthesizeError("Amen can only synthesize up to 20 minutes of audio.") 85 | 86 | # get the target start and end samples 87 | starting_sample, _ = librosa.time_to_samples( 88 | [start_time, start_time + duration], sr=time_slice.audio.sample_rate 89 | ) 90 | 91 | # figure out the actual starting and ending samples for each channel 92 | left_start = starting_sample + left_offset + initial_offset 93 | right_start = starting_sample + right_offset + initial_offset 94 | 95 | # add the data from each channel to the array 96 | sparse_array[ 97 | 0, left_start : left_start + len(resampled_audio[0]) 98 | ] += resampled_audio[0] 99 | sparse_array[ 100 | 1, right_start : right_start + len(resampled_audio[1]) 101 | ] += resampled_audio[1] 102 | 103 | max_samples = librosa.time_to_samples([max_time], sr=sample_rate) 104 | truncated_array = sparse_array[:, 0 : max_samples[0]].toarray() 105 | 106 | return Audio(raw_samples=truncated_array, sample_rate=sample_rate) 107 | -------------------------------------------------------------------------------- /amen/timing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | '''Timing interface''' 3 | 4 | from bisect import bisect_left, bisect_right 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | import librosa 10 | 11 | 12 | class TimeSlice(object): 13 | """ 14 | A slice of time: has a start time, a duration, and a reference to an Audio object. 15 | """ 16 | 17 | def __init__(self, time, duration, audio, unit='s'): 18 | self.time = pd.to_timedelta(time, unit=unit) 19 | self.duration = pd.to_timedelta(duration, unit=unit) 20 | self.audio = audio 21 | 22 | def __repr__(self): 23 | args = self.time.delta * 1e-9, self.duration.delta * 1e-9 24 | return ''.format(*args) 25 | 26 | def get_samples(self): 27 | """ 28 | Gets the samples corresponding to this TimeSlice from the parent audio object. 29 | """ 30 | start = self.time.delta * 1e-9 31 | duration = self.duration.delta * 1e-9 32 | starting_sample, ending_sample = librosa.time_to_samples( 33 | [start, start + duration], self.audio.sample_rate 34 | ) 35 | 36 | left_offsets, right_offsets = self._get_offsets( 37 | starting_sample, ending_sample, self.audio.num_channels 38 | ) 39 | 40 | samples = self._offset_samples( 41 | starting_sample, 42 | ending_sample, 43 | left_offsets, 44 | right_offsets, 45 | self.audio.num_channels, 46 | ) 47 | 48 | return samples, left_offsets[0], right_offsets[0] 49 | 50 | def _get_offsets(self, starting_sample, ending_sample, num_channels): 51 | """ 52 | Find the offset to the next zero-crossing, for each channel. 53 | """ 54 | offsets = [] 55 | for zero_index in self.audio.zero_indexes: 56 | index = bisect_left(zero_index, starting_sample) - 1 57 | if index < 0: 58 | starting_offset = 0 59 | else: 60 | starting_crossing = zero_index[index] 61 | starting_offset = starting_crossing - starting_sample 62 | 63 | index = bisect_left(zero_index, ending_sample) 64 | if index >= len(zero_index): 65 | ending_offset = 0 66 | else: 67 | zci = min(bisect_right(zero_index, ending_sample), len(zero_index) - 1) 68 | ending_crossing = zero_index[zci] 69 | ending_offset = ending_crossing - ending_sample 70 | 71 | offsets.append((starting_offset, ending_offset)) 72 | 73 | if num_channels == 1: 74 | results = (offsets[0], offsets[0]) 75 | elif num_channels == 2: 76 | results = (offsets[0], offsets[1]) 77 | 78 | return results 79 | 80 | def _offset_samples( 81 | self, starting_sample, ending_sample, left_offsets, right_offsets, num_channels 82 | ): 83 | """ 84 | Does the offset itself. 85 | """ 86 | left_slice = ( 87 | 0, 88 | slice(starting_sample + left_offsets[0], ending_sample + left_offsets[1]), 89 | ) 90 | right_slice = left_slice 91 | 92 | if num_channels == 2: 93 | right_slice = ( 94 | 1, 95 | slice( 96 | starting_sample + right_offsets[0], ending_sample + right_offsets[1] 97 | ), 98 | ) 99 | 100 | left_channel = self.audio.raw_samples[left_slice] 101 | right_channel = self.audio.raw_samples[right_slice] 102 | return np.array([left_channel, right_channel]) 103 | 104 | 105 | class TimingList(list): 106 | """ 107 | A list of TimeSlices. 108 | """ 109 | 110 | def __init__(self, name, timings, audio, unit='s'): 111 | 112 | super(self.__class__, self).__init__() 113 | 114 | self.name = name 115 | for (start, duration) in timings: 116 | time_slice = TimeSlice(start, duration, audio, unit=unit) 117 | self.append(time_slice) 118 | -------------------------------------------------------------------------------- /amen/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''Utilities''' 4 | 5 | import pkg_resources 6 | 7 | 8 | def example_audio_file(): 9 | """Get the included example file""" 10 | path = 'example_audio/amen.wav' 11 | return pkg_resources.resource_filename(__name__, path) 12 | 13 | 14 | def example_mono_audio_file(): 15 | """Get the included example file""" 16 | path = 'example_audio/amen-mono.wav' 17 | return pkg_resources.resource_filename(__name__, path) 18 | -------------------------------------------------------------------------------- /amen/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Version info""" 4 | 5 | short_version = '0.0' 6 | version = '0.0.4' 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/AMEN.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/AMEN.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/AMEN" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/AMEN" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | .. module:: amen 4 | 5 | Core API 6 | -------- 7 | 8 | Audio 9 | ===== 10 | .. autoclass:: amen.audio.Audio 11 | :members: 12 | 13 | Synthesis 14 | ========= 15 | .. automodule:: amen.synthesize 16 | :members: 17 | 18 | Advanced functionality 19 | ---------------------- 20 | 21 | Feature 22 | ====== 23 | .. automodule:: amen.feature 24 | :members: 25 | 26 | Timing 27 | ====== 28 | .. automodule:: amen.timing 29 | :members: 30 | 31 | Utilities 32 | ========= 33 | .. automodule:: amen.utils 34 | :members: 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # AMEN documentation build configuration file, created by 5 | # sphinx-quickstart on Sat May 21 16:07:22 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.autosummary', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.viewcode', 37 | 'numpydoc', 38 | ] 39 | 40 | from glob import glob 41 | 42 | autosummary_generate = glob('*.rst') 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | # source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = 'AMEN' 60 | copyright = '2016, AMEN dev team' 61 | author = 'AMEN dev team' 62 | 63 | import mock 64 | 65 | MOCK_MODULES = ['numpy', 'scipy', 'scipy.sparse', 'pandas', 'librosa', 'soundfile'] 66 | sys.modules.update((mod_name, mock.Mock()) for mod_name in MOCK_MODULES) 67 | 68 | 69 | # The version info for the project you're documenting, acts as replacement for 70 | # |version| and |release|, also used in various other places throughout the 71 | # built documents. 72 | # 73 | # The short X.Y version. 74 | import imp 75 | 76 | amen_version = imp.load_source('amen.version', '../amen/version.py') 77 | version = amen_version 78 | # The full version, including alpha/beta/rc tags. 79 | release = amen_version 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = None 87 | 88 | # There are two options for replacing |today|: either, you set today to some 89 | # non-false value, then it is used: 90 | # today = '' 91 | # Else, today_fmt is used as the format for a strftime call. 92 | # today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This patterns also effect to html_static_path and html_extra_path 97 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 98 | 99 | # The reST default role (used for this markup: `text`) to use for all 100 | # documents. 101 | # default_role = None 102 | 103 | # If true, '()' will be appended to :func: etc. cross-reference text. 104 | # add_function_parentheses = True 105 | 106 | # If true, the current module name will be prepended to all description 107 | # unit titles (such as .. function::). 108 | # add_module_names = True 109 | 110 | # If true, sectionauthor and moduleauthor directives will be shown in the 111 | # output. They are ignored by default. 112 | # show_authors = False 113 | 114 | # The name of the Pygments (syntax highlighting) style to use. 115 | pygments_style = 'sphinx' 116 | 117 | # A list of ignored prefixes for module index sorting. 118 | # modindex_common_prefix = [] 119 | 120 | # If true, keep warnings as "system message" paragraphs in the built documents. 121 | # keep_warnings = False 122 | 123 | # If true, `todo` and `todoList` produce output, else they produce nothing. 124 | todo_include_todos = False 125 | 126 | 127 | # -- Options for HTML output ---------------------------------------------- 128 | 129 | # The theme to use for HTML and HTML Help pages. See the documentation for 130 | # a list of builtin themes. 131 | html_theme = 'default' 132 | 133 | # Theme options are theme-specific and customize the look and feel of a theme 134 | # further. For a list of options available for each theme, see the 135 | # documentation. 136 | # html_theme_options = {} 137 | 138 | # Add any paths that contain custom themes here, relative to this directory. 139 | # html_theme_path = [] 140 | 141 | # The name for this set of Sphinx documents. 142 | # " v documentation" by default. 143 | # html_title = 'AMEN v0.1.0pre' 144 | 145 | # A shorter title for the navigation bar. Default is the same as html_title. 146 | # html_short_title = None 147 | 148 | # The name of an image file (relative to this directory) to place at the top 149 | # of the sidebar. 150 | # html_logo = None 151 | 152 | # The name of an image file (relative to this directory) to use as a favicon of 153 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 154 | # pixels large. 155 | # html_favicon = None 156 | 157 | # Add any paths that contain custom static files (such as style sheets) here, 158 | # relative to this directory. They are copied after the builtin static files, 159 | # so a file named "default.css" will overwrite the builtin "default.css". 160 | html_static_path = ['_static'] 161 | 162 | # Add any extra paths that contain custom files (such as robots.txt or 163 | # .htaccess) here, relative to this directory. These files are copied 164 | # directly to the root of the documentation. 165 | # html_extra_path = [] 166 | 167 | # If not None, a 'Last updated on:' timestamp is inserted at every page 168 | # bottom, using the given strftime format. 169 | # The empty string is equivalent to '%b %d, %Y'. 170 | # html_last_updated_fmt = None 171 | 172 | # If true, SmartyPants will be used to convert quotes and dashes to 173 | # typographically correct entities. 174 | # html_use_smartypants = True 175 | 176 | # Custom sidebar templates, maps document names to template names. 177 | # html_sidebars = {} 178 | 179 | # Additional templates that should be rendered to pages, maps page names to 180 | # template names. 181 | # html_additional_pages = {} 182 | 183 | # If false, no module index is generated. 184 | # html_domain_indices = True 185 | 186 | # If false, no index is generated. 187 | # html_use_index = True 188 | 189 | # If true, the index is split into individual pages for each letter. 190 | # html_split_index = False 191 | 192 | # If true, links to the reST sources are added to the pages. 193 | # html_show_sourcelink = True 194 | 195 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 196 | # html_show_sphinx = True 197 | 198 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 199 | # html_show_copyright = True 200 | 201 | # If true, an OpenSearch description file will be output, and all pages will 202 | # contain a tag referring to it. The value of this option must be the 203 | # base URL from which the finished HTML is served. 204 | # html_use_opensearch = '' 205 | 206 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 207 | # html_file_suffix = None 208 | 209 | # Language to be used for generating the HTML full-text search index. 210 | # Sphinx supports the following languages: 211 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 212 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 213 | # html_search_language = 'en' 214 | 215 | # A dictionary with options for the search language support, empty by default. 216 | # 'ja' uses this config value. 217 | # 'zh' user can custom change `jieba` dictionary path. 218 | # html_search_options = {'type': 'default'} 219 | 220 | # The name of a javascript file (relative to the configuration directory) that 221 | # implements a search results scorer. If empty, the default will be used. 222 | # html_search_scorer = 'scorer.js' 223 | 224 | # Output file base name for HTML help builder. 225 | htmlhelp_basename = 'AMENdoc' 226 | 227 | # -- Options for LaTeX output --------------------------------------------- 228 | 229 | latex_elements = { 230 | # The paper size ('letterpaper' or 'a4paper'). 231 | #'papersize': 'letterpaper', 232 | # The font size ('10pt', '11pt' or '12pt'). 233 | #'pointsize': '10pt', 234 | # Additional stuff for the LaTeX preamble. 235 | #'preamble': '', 236 | # Latex figure (float) alignment 237 | #'figure_align': 'htbp', 238 | } 239 | 240 | # Grouping the document tree into LaTeX files. List of tuples 241 | # (source start file, target name, title, 242 | # author, documentclass [howto, manual, or own class]). 243 | latex_documents = [ 244 | (master_doc, 'AMEN.tex', 'AMEN Documentation', 'AMEN dev team', 'manual') 245 | ] 246 | 247 | # The name of an image file (relative to this directory) to place at the top of 248 | # the title page. 249 | # latex_logo = None 250 | 251 | # For "manual" documents, if this is true, then toplevel headings are parts, 252 | # not chapters. 253 | # latex_use_parts = False 254 | 255 | # If true, show page references after internal links. 256 | # latex_show_pagerefs = False 257 | 258 | # If true, show URL addresses after external links. 259 | # latex_show_urls = False 260 | 261 | # Documents to append as an appendix to all manuals. 262 | # latex_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | # latex_domain_indices = True 266 | 267 | 268 | # -- Options for manual page output --------------------------------------- 269 | 270 | # One entry per manual page. List of tuples 271 | # (source start file, name, description, authors, manual section). 272 | man_pages = [(master_doc, 'amen', 'AMEN Documentation', [author], 1)] 273 | 274 | # If true, show URL addresses after external links. 275 | # man_show_urls = False 276 | 277 | 278 | # -- Options for Texinfo output ------------------------------------------- 279 | 280 | # Grouping the document tree into Texinfo files. List of tuples 281 | # (source start file, target name, title, author, 282 | # dir menu entry, description, category) 283 | texinfo_documents = [ 284 | ( 285 | master_doc, 286 | 'AMEN', 287 | 'AMEN Documentation', 288 | author, 289 | 'AMEN', 290 | 'One line description of project.', 291 | 'Miscellaneous', 292 | ) 293 | ] 294 | 295 | # Documents to append as an appendix to all manuals. 296 | # texinfo_appendices = [] 297 | 298 | # If false, no module index is generated. 299 | # texinfo_domain_indices = True 300 | 301 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 302 | # texinfo_show_urls = 'footnote' 303 | 304 | # If true, do not generate a @detailmenu in the "Top" node's menu. 305 | # texinfo_no_detailmenu = False 306 | 307 | 308 | # Example configuration for intersphinx: refer to the Python standard library. 309 | intersphinx_mapping = { 310 | 'python': ('http://docs.python.org/', None), 311 | 'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None), 312 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 313 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 314 | 'soundfile': ('http://pysoundfile.readthedocs.io/en/0.8.1/', None), 315 | 'librosa': ('http://librosa.github.io/librosa/', None), 316 | } 317 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. AMEN documentation master file, created by 2 | sphinx-quickstart on Sat May 21 16:07:22 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | AMEN 7 | ==== 8 | 9 | A toolbox for algorithmic remixing, after Echo Nest Remix. 10 | 11 | Platforms 12 | --------- 13 | Amen is developed on Ubuntu 14.04 and higher. OS X should be workable. Windows users should install Ubuntu. 14 | 15 | Installation 16 | ------------ 17 | Amen is pretty simple, but it stands on top of some complex stuff. 18 | 19 | If you are on Linux, you'll need ``libsoundfile``: ``sudo apt-get install libsndfile1``. If you're on OS X, read on. 20 | 21 | Next, you should install Anaconda, (https://www.continuum.io/downloads) which will get you all of the dependencies. 22 | 23 | Then, install via pip: ``pip install amen``. That should be it! 24 | 25 | (If you're a serious Python cat, you can just get Amen from pip, without Anaconda: but that will require installing numpy, scipy, a fortran compiler, and so on.) 26 | 27 | 28 | Testing the Installation 29 | ------------------------ 30 | After installation is finished, open up a Python interpreter and run the following (or run it from a file):: 31 | 32 | from amen.utils import example_audio_file 33 | from amen.audio import Audio 34 | from amen.synthesize import synthesize 35 | 36 | audio_file = example_audio_file() 37 | audio = Audio(audio_file) 38 | 39 | beats = audio.timings['beats'] 40 | beats.reverse() 41 | 42 | out = synthesize(beats) 43 | out.output('reversed.wav') 44 | 45 | If all that works, just play the resulting ``reversed.wav`` file, and you're on your way! 46 | 47 | 48 | 49 | API Reference 50 | ============= 51 | .. toctree:: 52 | :maxdepth: 2 53 | 54 | api 55 | 56 | Contribute 57 | ========== 58 | - `Issue Tracker `_ 59 | - `Source Code `_ 60 | 61 | 62 | Indices and tables 63 | ================== 64 | 65 | * :ref:`genindex` 66 | * :ref:`modindex` 67 | * :ref:`search` 68 | 69 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc>=0.5 2 | six 3 | -------------------------------------------------------------------------------- /examples/echo_nest_json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf=8 3 | 4 | """ 5 | echo_nest_json.py : Write Echo Nest shaped JSON to a file 6 | """ 7 | 8 | import json 9 | from amen.audio import Audio 10 | from amen.utils import example_audio_file 11 | from amen.echo_nest_converter import AudioAnalysis 12 | 13 | audio_file = example_audio_file() 14 | audio = Audio(audio_file) 15 | 16 | remix_audio = AudioAnalysis(audio) 17 | 18 | with open('remix-json.json', 'w') as f: 19 | json.dump(remix_audio.to_json(), f) 20 | -------------------------------------------------------------------------------- /examples/iterate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf=8 3 | 4 | """ 5 | iterate.py : Iterate through the beats of a song, and keep some, but not others 6 | """ 7 | 8 | from amen.audio import Audio 9 | from amen.utils import example_audio_file 10 | from amen.synthesize import synthesize 11 | 12 | audio_file = example_audio_file() 13 | audio = Audio(audio_file) 14 | 15 | beats = audio.timings['beats'] 16 | new_beats = [] 17 | for i, beat in enumerate(beats): 18 | if i % 4 == 0: 19 | new_beats.append(beat) 20 | 21 | out = synthesize(new_beats) 22 | out.output('iterate.wav') 23 | -------------------------------------------------------------------------------- /examples/iterate_feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf=8 3 | 4 | """ 5 | iterate_feature.py : Iterate through the beats of a song, and keep some, 6 | but not others, based on an audio feature. 7 | """ 8 | 9 | from amen.audio import Audio 10 | from amen.utils import example_audio_file 11 | from amen.synthesize import synthesize 12 | 13 | audio_file = example_audio_file() 14 | audio = Audio(audio_file) 15 | 16 | beats = audio.timings['beats'] 17 | amplitudes = audio.features['amplitude'].at(beats) 18 | 19 | new_beats = [] 20 | for beat, amp in amplitudes.with_time(): 21 | if amp > 2.5: 22 | new_beats.append(beat) 23 | 24 | out = synthesize(new_beats) 25 | out.output('iterate-feature.wav') 26 | -------------------------------------------------------------------------------- /examples/reverse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf=8 3 | 4 | """ 5 | reverse.py : Reverse the beats of a song. 6 | """ 7 | 8 | from amen.audio import Audio 9 | from amen.utils import example_audio_file 10 | from amen.synthesize import synthesize 11 | 12 | audio_file = example_audio_file() 13 | audio = Audio(audio_file) 14 | 15 | beats = audio.timings['beats'] 16 | beats.reverse() 17 | 18 | out = synthesize(beats) 19 | out.output('reversed.wav') 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import imp 4 | 5 | version = imp.load_source('amen.version', 'amen/version.py') 6 | setup( 7 | name='amen', 8 | version=version.version, 9 | description='Algorithmic music remixing', 10 | url='http://github.com/algorithmic-music-exploration/amen', 11 | download_url='http://github.com/algorithmic-music-exploration/amen/releases', 12 | packages=find_packages(), 13 | package_data={'amen': ['example_audio/*.wav']}, 14 | classifiers=[ 15 | "License :: OSI Approved :: ISC License (ISCL)", 16 | "Programming Language :: Python", 17 | "Development Status :: 3 - Alpha", 18 | "Intended Audience :: Developers", 19 | "Topic :: Multimedia :: Sound/Audio :: Analysis", 20 | "Programming Language :: Python :: 2", 21 | "Programming Language :: Python :: 2.7", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.5", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | ], 27 | keywords='audio music sound', 28 | license='ISC', 29 | install_requires=[ 30 | 'librosa >= 0.7.0', 31 | 'pandas >= 0.16.0', 32 | 'pysoundfile >= 0.8', 33 | 'six >= 1.10.0', 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_audio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import tempfile 6 | import numpy as np 7 | import librosa 8 | from amen.audio import Audio 9 | from amen.feature import FeatureCollection 10 | from amen.utils import example_audio_file 11 | 12 | EXAMPLE_FILE = example_audio_file() 13 | audio = Audio(EXAMPLE_FILE) 14 | mono_audio = Audio(EXAMPLE_FILE, convert_to_mono=True, sample_rate=44100) 15 | 16 | 17 | def test_default_sample_rate(): 18 | assert isinstance(audio.sample_rate, float) 19 | assert audio.sample_rate == 44100 20 | 21 | 22 | def test_default_channels(): 23 | assert audio.num_channels == 2 24 | 25 | 26 | def test_duration(): 27 | duration = audio.raw_samples.shape[-1] / float(audio.sample_rate) 28 | assert audio.duration == duration 29 | 30 | 31 | def test_file_path(): 32 | assert audio.file_path == EXAMPLE_FILE 33 | 34 | 35 | def test_sample_data(): 36 | y, sr = librosa.load(EXAMPLE_FILE) 37 | assert audio.analysis_samples.all() == y.all() 38 | 39 | 40 | def test_sample_rate(): 41 | assert mono_audio.sample_rate == 44100 42 | 43 | 44 | def test_channels(): 45 | assert mono_audio.num_channels == 1 46 | 47 | 48 | def test_audio_from_raw_samples(): 49 | new_audio = Audio(raw_samples=audio.raw_samples) 50 | assert np.allclose(new_audio.raw_samples, audio.raw_samples, rtol=1e-3, atol=1e-4) 51 | 52 | 53 | def test_zero_indexes(): 54 | channel = mono_audio.raw_samples[0] 55 | zero_crossings = librosa.zero_crossings(channel) 56 | zero_index = np.nonzero(zero_crossings)[0] 57 | assert mono_audio.zero_indexes[0].all() == zero_index.all() 58 | 59 | 60 | def test_output(): 61 | n, tempfilename = tempfile.mkstemp() 62 | audio.output(tempfilename, format='WAV') 63 | new_samples, new_sample_rate = librosa.load(tempfilename, sr=audio.sample_rate) 64 | os.unlink(tempfilename) 65 | 66 | assert np.allclose(audio.sample_rate, new_sample_rate) 67 | assert np.allclose(audio.raw_samples, new_samples, rtol=1e-3, atol=1e-4) 68 | -------------------------------------------------------------------------------- /tests/test_audio_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import tempfile 6 | import numpy as np 7 | import librosa 8 | from amen.audio import Audio 9 | from amen.feature import FeatureCollection 10 | from amen.utils import example_audio_file 11 | 12 | EXAMPLE_FILE = example_audio_file() 13 | audio = Audio(EXAMPLE_FILE) 14 | mono_audio = Audio(EXAMPLE_FILE, convert_to_mono=True, sample_rate=44100) 15 | 16 | 17 | def test_has_feature_collection(): 18 | assert type(mono_audio.features) == FeatureCollection 19 | 20 | 21 | def test_has_amplitude_feature(): 22 | res = librosa.feature.rms(mono_audio.analysis_samples)[0] 23 | assert mono_audio.features["amplitude"].data.iloc[0].item() == res[0] 24 | 25 | 26 | def test_has_centroid_feature(): 27 | res = librosa.feature.spectral_centroid(mono_audio.analysis_samples)[0] 28 | assert mono_audio.features["centroid"].data.iloc[0].item() == res[0] 29 | 30 | 31 | def test_has_timbre_feature(): 32 | res = librosa.feature.mfcc( 33 | y=mono_audio.analysis_samples, sr=mono_audio.analysis_sample_rate, n_mfcc=20 34 | )[0] 35 | assert mono_audio.features["timbre"]["mfcc_0"].data.iloc[0].item() == res[0] 36 | 37 | 38 | def test_has_chroma_feature(): 39 | res = librosa.feature.chroma_cqt(mono_audio.analysis_samples)[0] 40 | assert mono_audio.features["chroma"]["c"].data.iloc[0].item() == res[0] 41 | 42 | 43 | def test_has_chroma_feature_aliases(): 44 | res = librosa.feature.chroma_cqt(mono_audio.analysis_samples)[1] 45 | assert mono_audio.features["chroma"]["db"].data.iloc[0].item() == res[0] 46 | 47 | 48 | def test_has_tempo_feature(): 49 | onset_env = librosa.onset.onset_strength( 50 | mono_audio.analysis_samples, sr=mono_audio.analysis_sample_rate 51 | ) 52 | res = librosa.beat.tempo( 53 | onset_envelope=onset_env, sr=mono_audio.analysis_sample_rate, aggregate=None 54 | ) 55 | assert mono_audio.features["tempo"].data.iloc[0].item() == res[0] 56 | -------------------------------------------------------------------------------- /tests/test_audio_timings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from amen.audio import Audio 5 | from amen.utils import example_audio_file 6 | from amen.timing import TimingList 7 | 8 | EXAMPLE_FILE = example_audio_file() 9 | AUDIO = Audio(EXAMPLE_FILE) 10 | 11 | 12 | def test_track(): 13 | track = AUDIO.timings['track'] 14 | assert isinstance(track, TimingList) 15 | assert len(track) == 1 16 | 17 | 18 | def test_beats(): 19 | beats = AUDIO.timings['beats'] 20 | assert isinstance(beats, TimingList) 21 | assert len(beats) == 11 22 | 23 | 24 | def test_segments(): 25 | segments = AUDIO.timings['segments'] 26 | assert isinstance(segments, TimingList) 27 | assert len(segments) == 42 28 | -------------------------------------------------------------------------------- /tests/test_deformation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import librosa 6 | from amen.audio import Audio 7 | from amen.utils import example_audio_file 8 | from amen.deformation import time_stretch 9 | from amen.deformation import pitch_shift 10 | from amen.deformation import harmonic_separation 11 | from amen.deformation import percussive_separation 12 | 13 | EXAMPLE_FILE = example_audio_file() 14 | audio = Audio(EXAMPLE_FILE) 15 | mono_audio = Audio(EXAMPLE_FILE, convert_to_mono=True, sample_rate=44100) 16 | 17 | 18 | def test_time_stretch(): 19 | stretch_amount = 1.5 20 | time_stretch_audio = time_stretch(mono_audio, stretch_amount) 21 | test_time_stretch = librosa.effects.time_stretch( 22 | librosa.to_mono(mono_audio.raw_samples), stretch_amount 23 | ) 24 | test_time_stretch_audio = Audio( 25 | raw_samples=test_time_stretch, sample_rate=mono_audio.sample_rate 26 | ) 27 | assert np.allclose( 28 | time_stretch_audio.raw_samples, 29 | test_time_stretch_audio.raw_samples, 30 | rtol=1e-3, 31 | atol=1e-4, 32 | ) 33 | 34 | 35 | def test_pitch_shift(): 36 | shift_amount = 4 37 | step_size = 24 38 | pitch_shift_audio = pitch_shift(mono_audio, shift_amount, step_size=step_size) 39 | test_pitch_shift = librosa.effects.pitch_shift( 40 | librosa.to_mono(mono_audio.raw_samples), 41 | mono_audio.sample_rate, 42 | shift_amount, 43 | bins_per_octave=step_size, 44 | ) 45 | test_pitch_shift_audio = Audio( 46 | raw_samples=test_pitch_shift, sample_rate=mono_audio.sample_rate 47 | ) 48 | assert np.allclose( 49 | pitch_shift_audio.raw_samples, 50 | test_pitch_shift_audio.raw_samples, 51 | rtol=1e-3, 52 | atol=1e-4, 53 | ) 54 | 55 | 56 | def test_harmonic(): 57 | harmonic_audio = harmonic_separation(mono_audio) 58 | test_harmonic = librosa.effects.harmonic( 59 | librosa.to_mono(mono_audio.raw_samples), margin=3.0 60 | ) 61 | test_harmonic_audio = Audio( 62 | raw_samples=test_harmonic, sample_rate=mono_audio.sample_rate 63 | ) 64 | assert np.allclose( 65 | harmonic_audio.raw_samples, 66 | test_harmonic_audio.raw_samples, 67 | rtol=1e-3, 68 | atol=1e-4, 69 | ) 70 | 71 | 72 | def test_percussive(): 73 | percussive_audio = percussive_separation(mono_audio) 74 | test_percussive = librosa.effects.percussive( 75 | librosa.to_mono(mono_audio.raw_samples), margin=3.0 76 | ) 77 | test_percussive_audio = Audio( 78 | raw_samples=test_percussive, sample_rate=mono_audio.sample_rate 79 | ) 80 | assert np.allclose( 81 | percussive_audio.raw_samples, 82 | test_percussive_audio.raw_samples, 83 | rtol=1e-3, 84 | atol=1e-4, 85 | ) 86 | -------------------------------------------------------------------------------- /tests/test_echo_nest_converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | 5 | from amen.audio import Audio 6 | from amen.utils import example_audio_file 7 | from amen.echo_nest_converter import AudioAnalysis 8 | from amen.echo_nest_converter import AudioQuantum 9 | from amen.echo_nest_converter import AudioQuantumList 10 | 11 | EXAMPLE_FILE = example_audio_file() 12 | AUDIO = Audio(EXAMPLE_FILE) 13 | ANALYSIS = AudioAnalysis(AUDIO) 14 | 15 | 16 | def test_beats(): 17 | beats = ANALYSIS.beats 18 | assert isinstance(beats, AudioQuantumList), type(beats) 19 | for beat in beats: 20 | assert isinstance(beat, AudioQuantum), type(beat) 21 | 22 | 23 | def test_serializable(): 24 | serializable = ANALYSIS.as_serializable() 25 | try: 26 | json.dumps(serializable) 27 | except: 28 | assert False, 'as_serializable object cannot be parsed by JSON' 29 | 30 | 31 | def test_json(): 32 | encoded = ANALYSIS.to_json() 33 | try: 34 | deserialized = json.loads(encoded) 35 | except: 36 | assert False, 'to_json string cannot be parsed by JSON' 37 | quantums = ['sections', 'bars', 'beats', 'tatums', 'segments'] 38 | assert all(quantum in deserialized for quantum in quantums) 39 | for segment in deserialized['segments']: 40 | assert isinstance(segment['pitches'], list), type(segment['pitches']) 41 | assert isinstance(segment['timbre'], list), type(segment['timbre']) 42 | assert len(segment['timbre']) == 12 43 | assert isinstance(segment['loudness_max'], float), type(segment['loudness_max']) 44 | assert isinstance(segment['loudness_max_time'], float), type( 45 | segment['loudness_max_time'] 46 | ) 47 | assert isinstance(segment['loudness_start'], float), type( 48 | segment['loudness_start'] 49 | ) 50 | 51 | assert isinstance(deserialized['tempo'], float) 52 | -------------------------------------------------------------------------------- /tests/test_feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import librosa 5 | import numpy as np 6 | import pandas as pd 7 | import pytest 8 | 9 | from pandas.util.testing import assert_frame_equal 10 | 11 | from amen.audio import Audio 12 | from amen.feature import Feature 13 | from amen.feature import FeatureCollection 14 | from amen.timing import TimeSlice 15 | from amen.utils import example_audio_file 16 | from amen.exceptions import FeatureError 17 | 18 | EXAMPLE_FILE = example_audio_file() 19 | audio = Audio(EXAMPLE_FILE) 20 | 21 | test_times = np.linspace(0, 10, num=1000) 22 | test_index = pd.to_timedelta(test_times, unit='s') 23 | 24 | test_dataframe = pd.DataFrame(data=audio.analysis_samples[:1000], index=test_index) 25 | test_feature = Feature(test_dataframe) 26 | 27 | 28 | def test_data_validation(): 29 | with pytest.raises(AssertionError): 30 | f = Feature([1, 2, 3]) 31 | 32 | 33 | def test_data(): 34 | assert_frame_equal(test_feature.data, test_dataframe) 35 | 36 | 37 | def test_default_aggregate(): 38 | assert test_feature.aggregate == np.mean 39 | 40 | 41 | def test_default_base(): 42 | assert test_feature.base == None 43 | 44 | 45 | def test_default_name(): 46 | assert test_feature.name == test_dataframe.keys()[0] 47 | 48 | 49 | def test_aggregate(): 50 | test_feature = Feature(test_dataframe, aggregate=np.median) 51 | assert test_feature.aggregate == np.median 52 | 53 | 54 | def test_base(): 55 | base_feature = Feature(test_dataframe) 56 | test_feature = Feature(test_dataframe, base=base_feature) 57 | assert test_feature.base == base_feature 58 | 59 | 60 | def test_base_validation(): 61 | with pytest.raises(AssertionError): 62 | f = Feature(test_dataframe, np.mean, [1, 2, 3]) 63 | 64 | 65 | # Test list wrappers 66 | def test_iter(): 67 | looped_data = [] 68 | for d in test_feature: 69 | looped_data.append(d) 70 | assert looped_data == test_feature.data[test_feature.name].tolist() 71 | 72 | 73 | def test_getitem(): 74 | assert test_feature[0] == test_feature.data[test_feature.name][0] 75 | 76 | 77 | # Test __repr__ 78 | def test_repr(): 79 | repr_string = ''.format((test_feature.name)) 80 | assert test_feature.__repr__() == repr_string 81 | 82 | 83 | # Test at() 84 | time_slices = [TimeSlice(0, 0.5, audio), TimeSlice(1, 0.5, audio)] 85 | feature_at = test_feature.at(time_slices) 86 | 87 | test_slice = time_slices[0] 88 | slice_index = (test_slice.time <= test_feature.data.index) & ( 89 | test_feature.data.index < test_slice.time + test_slice.duration 90 | ) 91 | target_data = test_feature.aggregate(test_feature.data[slice_index], axis=0) 92 | 93 | 94 | def test_default_aggregate(): 95 | assert feature_at.aggregate == test_feature.aggregate 96 | 97 | 98 | def test_default_base(): 99 | assert feature_at.base == test_feature 100 | 101 | 102 | def test_default_data(): 103 | assert feature_at.data.loc[test_slice.time].all() == target_data.all() 104 | 105 | 106 | def test_default_length(): 107 | assert len(feature_at.data) == len(time_slices) 108 | 109 | 110 | def test_base_with_second_resample(): 111 | feature_again = feature_at.at(time_slices[0]) 112 | assert feature_at.base == test_feature 113 | 114 | 115 | def test_base_with_second_resample(): 116 | feature_again = feature_at.at(time_slices[0]) 117 | assert feature_again.data.loc[test_slice.time].all() == target_data.all() 118 | 119 | 120 | def test_with_single_slice(): 121 | feature_at = test_feature.at(time_slices[0]) 122 | assert len(feature_at.data) == 1 123 | 124 | 125 | # Test with_time 126 | def test_with_time_raises(): 127 | def test(): 128 | with pytest.raises(FeatureError): 129 | for beat, feature in test_feature.with_time(): 130 | pass 131 | 132 | 133 | def test_with_time_beats(): 134 | beats = [] 135 | for beat, feature in feature_at.with_time(): 136 | beats.append(beat) 137 | assert beats == time_slices 138 | 139 | 140 | def test_with_time_features(): 141 | looped_features = [] 142 | for feature in feature_at: 143 | looped_features.append(feature) 144 | 145 | features = [] 146 | for beat, feature in feature_at.with_time(): 147 | features.append(feature) 148 | assert features == looped_features 149 | 150 | 151 | # Test FeatureCollection 152 | feature_collection = FeatureCollection() 153 | feature_collection['test'] = test_feature 154 | feature_collection['another_test'] = test_feature 155 | 156 | 157 | def test_iter(): 158 | looped_data = [] 159 | for data in feature_collection: 160 | looped_data.append(data) 161 | 162 | test_data = [] 163 | length = len(test_feature) 164 | for i in range(length): 165 | res = {} 166 | for key, feature in feature_collection.items(): 167 | res[key] = feature.data[feature.name][i] 168 | test_data.append(res) 169 | assert res == looped_data[i] 170 | 171 | 172 | def test_len(): 173 | key = list(feature_collection.keys())[0] 174 | length = len(feature_collection[key]) 175 | assert len(feature_collection) == length 176 | 177 | 178 | def test_at(): 179 | feature_collection_at = feature_collection.at(time_slices) 180 | assert ( 181 | feature_collection_at['test'].data.loc[test_slice.time].all() 182 | == target_data.all() 183 | ) 184 | 185 | 186 | def test_get(): 187 | # Casting to list for Python 3 188 | new_feature_collection = feature_collection.get('another_test') 189 | assert list(new_feature_collection.keys()) == ['another_test'] 190 | 191 | 192 | def test_get_with_list(): 193 | # Casting to list for Python 3 194 | new_feature_collection = feature_collection.get(['another_test']) 195 | assert list(new_feature_collection.keys()) == ['another_test'] 196 | 197 | 198 | # Test with_time 199 | def test_feature_collection_with_time_raises(): 200 | def test(): 201 | with pytest.raises(FeatureError): 202 | for beat, feature in feature_collection.with_time(): 203 | pass 204 | 205 | 206 | def test_feature_collection_with_time_beats(): 207 | feature_collection_at = feature_collection.at(time_slices) 208 | beats = [] 209 | for beat, feature in feature_collection_at.with_time(): 210 | beats.append(beat) 211 | assert beats == time_slices 212 | 213 | 214 | def test_feature_collection_with_time_features(): 215 | feature_collection_at = feature_collection.at(time_slices) 216 | looped_features = [] 217 | for feature in feature_collection_at: 218 | looped_features.append(feature) 219 | 220 | features = [] 221 | for beat, feature in feature_collection_at.with_time(): 222 | features.append(feature) 223 | assert features == looped_features 224 | -------------------------------------------------------------------------------- /tests/test_synthesize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import six 5 | import pandas as pd 6 | import numpy as np 7 | import librosa 8 | import pytest 9 | from amen.audio import Audio 10 | from amen.utils import example_audio_file 11 | from amen.utils import example_mono_audio_file 12 | from amen.synthesize import _format_inputs 13 | from amen.synthesize import synthesize 14 | from amen.exceptions import SynthesizeError 15 | 16 | EXAMPLE_FILE = example_audio_file() 17 | audio = Audio(EXAMPLE_FILE) 18 | 19 | 20 | def test_format_inputs_length(): 21 | formatted_inputs = _format_inputs(audio.timings['beats']) 22 | formatted_inputs = list(formatted_inputs) 23 | assert len(audio.timings['beats']) == len(formatted_inputs) 24 | 25 | 26 | def test_format_inputs_list(): 27 | formatted_inputs = _format_inputs(audio.timings['beats']) 28 | formatted_inputs = list(formatted_inputs) 29 | beat = audio.timings['beats'][0] 30 | assert formatted_inputs[0] == (audio.timings['beats'][0], beat.time) 31 | 32 | 33 | def test_format_inputs_parallel_list(): 34 | times = [beat.time for beat in audio.timings['beats']] 35 | formatted_inputs = _format_inputs((audio.timings['beats'], times)) 36 | formatted_inputs = list(formatted_inputs) 37 | assert formatted_inputs[0] == (audio.timings['beats'][0], times[0]) 38 | 39 | 40 | def test_format_inputs_generator(): 41 | def the_generator(): 42 | for beat in audio.timings['beats']: 43 | yield beat, beat.time 44 | 45 | formatted_inputs = _format_inputs(the_generator()) 46 | assert six.next(formatted_inputs) == six.next(the_generator()) 47 | 48 | 49 | def test_synthesize_fails_if_too_long(): 50 | time = pd.to_timedelta(21 * 60, unit='s') 51 | with pytest.raises(SynthesizeError): 52 | res = synthesize(([audio.timings['beats'][5]], [time])) 53 | 54 | 55 | stereo_audio = audio 56 | EXAMPLE_MONO_FILE = example_mono_audio_file() 57 | mono_audio = Audio(EXAMPLE_MONO_FILE) 58 | 59 | 60 | def test_synthesize_returns_mono(): 61 | synthesized_audio = synthesize(mono_audio.timings['beats']) 62 | assert isinstance(synthesized_audio, Audio) 63 | 64 | 65 | def test_synthesize_returns_stereo(): 66 | synthesized_audio = synthesize(stereo_audio.timings['beats']) 67 | assert isinstance(synthesized_audio, Audio) 68 | 69 | 70 | def test_synthesize_sample_output_mono(): 71 | synthesized_audio = synthesize(mono_audio.timings['beats']) 72 | assert np.isclose( 73 | mono_audio.raw_samples[0][100], synthesized_audio.raw_samples[0][100] 74 | ) 75 | 76 | 77 | def test_synthesize_sample_output_stereo(): 78 | synthesized_audio = synthesize(stereo_audio.timings['beats']) 79 | assert np.isclose( 80 | stereo_audio.raw_samples[0][100], synthesized_audio.raw_samples[0][100] 81 | ) 82 | -------------------------------------------------------------------------------- /tests/test_timing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import librosa 7 | from amen.audio import Audio 8 | from amen.utils import example_audio_file 9 | from amen.utils import example_mono_audio_file 10 | from amen.timing import TimeSlice 11 | 12 | t = 5 13 | d = 10 14 | dummy_audio = None 15 | time_slice = TimeSlice(t, d, dummy_audio) 16 | 17 | 18 | def test_time(): 19 | assert time_slice.time == pd.to_timedelta(t, 's') 20 | 21 | 22 | def test_duration(): 23 | assert time_slice.duration == pd.to_timedelta(d, 's') 24 | 25 | 26 | def test_units(): 27 | time_slice = TimeSlice(t, d, dummy_audio, unit='ms') 28 | assert time_slice.time == pd.to_timedelta(t, 'ms') 29 | 30 | 31 | EXAMPLE_FILE = example_audio_file() 32 | stereo_audio = Audio(EXAMPLE_FILE) 33 | time_slice = TimeSlice(t, d, stereo_audio) 34 | EXAMPLE_MONO_FILE = example_mono_audio_file() 35 | mono_audio = Audio(EXAMPLE_FILE) 36 | 37 | 38 | def test_get_offsets(): 39 | left, right = time_slice._get_offsets(3, 4, stereo_audio.num_channels) 40 | assert left == (-1, 3) 41 | 42 | 43 | def test_offset_samples_mono(): 44 | res = mono_audio.timings['beats'][0]._offset_samples( 45 | 1, 2, (-1, 1), (-1, 1), mono_audio.num_channels 46 | ) 47 | assert res.shape == (2, 3) 48 | 49 | 50 | def test_offset_samples_stereo(): 51 | res = stereo_audio.timings['beats'][0]._offset_samples( 52 | 1, 2, (-1, 1), (-1, 1), stereo_audio.num_channels 53 | ) 54 | assert res.shape == (2, 3) 55 | 56 | 57 | def test_get_samples_shape_both_stereo_and_mono(): 58 | def get_samples(audio): 59 | beat = audio.timings['beats'][0] 60 | 61 | start = beat.time.delta * 1e-9 62 | duration = beat.duration.delta * 1e-9 63 | starting_sample, ending_sample = librosa.time_to_samples( 64 | [start, start + duration], beat.audio.sample_rate 65 | ) 66 | 67 | samples, left_offset, right_offset = beat.get_samples() 68 | left_offsets, right_offsets = beat._get_offsets( 69 | starting_sample, ending_sample, beat.audio.num_channels 70 | ) 71 | 72 | duration = beat.duration.delta * 1e-9 73 | starting_sample, ending_sample = librosa.time_to_samples( 74 | [0, duration], audio.sample_rate 75 | ) 76 | 77 | initial_length = ending_sample - starting_sample 78 | left_offset_length = initial_length - left_offsets[0] + left_offsets[1] 79 | right_offset_length = initial_length - right_offsets[0] + right_offsets[1] 80 | 81 | return samples, left_offset_length, right_offset_length 82 | 83 | mono_samples, mono_left_offset_length, mono_right_offset_length = get_samples( 84 | mono_audio 85 | ) 86 | assert len(mono_samples[0]) == mono_left_offset_length 87 | assert len(mono_samples[1]) == mono_right_offset_length 88 | 89 | stereo_samples, stereo_left_offset_length, stereo_right_offset_length = get_samples( 90 | stereo_audio 91 | ) 92 | assert len(stereo_samples[0]) == stereo_left_offset_length 93 | assert len(stereo_samples[1]) == stereo_right_offset_length 94 | 95 | 96 | def test_get_samples_audio(): 97 | def get_samples_audio(audio): 98 | beat = audio.timings['beats'][0] 99 | samples, left_offset, right_offset = beat.get_samples() 100 | 101 | start = beat.time.delta * 1e-9 102 | duration = beat.duration.delta * 1e-9 103 | starting_sample, ending_sample = librosa.time_to_samples( 104 | [start, start + duration], beat.audio.sample_rate 105 | ) 106 | left_offsets, right_offsets = beat._get_offsets( 107 | starting_sample, ending_sample, beat.audio.num_channels 108 | ) 109 | 110 | start_sample = left_offsets[0] * -1 111 | end_sample = len(samples[0]) - left_offsets[1] 112 | reset_samples = samples[0][start_sample:end_sample] 113 | 114 | original_samples = audio.raw_samples[0, starting_sample:ending_sample] 115 | 116 | return reset_samples, original_samples 117 | 118 | mono_reset_samples, mono_original_samples = get_samples_audio(mono_audio) 119 | assert np.array_equiv(mono_reset_samples, mono_original_samples) 120 | 121 | stereo_reset_samples, stereo_original_samples = get_samples_audio(stereo_audio) 122 | assert np.array_equiv(stereo_reset_samples, stereo_original_samples) 123 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import amen.utils 6 | 7 | 8 | def test_example_audio_file(): 9 | path = amen.utils.example_audio_file() 10 | path_array = path.split(os.path.sep) 11 | set_path = path_array[-2:] 12 | assert (set_path) == ['example_audio', 'amen.wav'] 13 | --------------------------------------------------------------------------------