├── src └── pyfastmp3decoder │ ├── __init__.py │ └── mp3decoder.py ├── requirements.txt ├── .gitmodules ├── testdata ├── test_mono.mp3 └── test_stereo.mp3 ├── pyproject.toml ├── .gitignore ├── MANIFEST.in ├── LICENSE ├── setup.py ├── setup.cfg ├── demo.py ├── README.md └── lib └── minimp3.pyx /src/pyfastmp3decoder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | pytest >= 3.3 3 | pytest-cov >= 2.0 4 | tox 5 | librosa 6 | numpy -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "minimp3"] 2 | path = minimp3 3 | url = https://github.com/lieff/minimp3.git 4 | -------------------------------------------------------------------------------- /testdata/test_mono.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neonbjb/pyfastmp3decoder/HEAD/testdata/test_mono.mp3 -------------------------------------------------------------------------------- /testdata/test_stereo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neonbjb/pyfastmp3decoder/HEAD/testdata/test_stereo.mp3 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 41.0", "wheel", "cython"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | lib/*.c 8 | 9 | # Distribution / packaging 10 | build/ 11 | dist/ 12 | *.egg-info/ 13 | .eggs 14 | 15 | # Sphinx documentation 16 | docs/_build/ 17 | 18 | # Testing 19 | .cache 20 | .hypothesis_cache 21 | .mypy_cache 22 | .pytest_cache 23 | .tox 24 | 25 | venv/ 26 | .venv/ 27 | 28 | # IDEs 29 | .idea 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | 4 | include pyproject.toml 5 | 6 | include minimp3/*.c minimp3/*.h 7 | include minimp3/LICENSE 8 | 9 | recursive-include * *.py *.pyx *.pyd 10 | 11 | recursive-exclude .eggs/ * 12 | recursive-exclude * __pycache__ 13 | recursive-exclude * *.py[co] 14 | 15 | recursive-exclude src *.so 16 | recursive-exclude lib *.c *.h 17 | 18 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache Software License 2.0 2 | 3 | Copyright (c) 2018, Paul Ganssle 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.extension import Extension 3 | 4 | try: 5 | from Cython.Build import cythonize 6 | except ImportError: 7 | def cythonize(*args, **kwargs): 8 | return [] 9 | 10 | 11 | extensions = [ 12 | Extension( 13 | "pyfastmp3decoder._backend", 14 | sources=["lib/*.pyx"], 15 | language="c", 16 | include_dirs=[ 17 | "lib", 18 | "minimp3" 19 | ], 20 | ) 21 | ] 22 | 23 | 24 | setup( 25 | packages=['pyfastmp3decoder'], 26 | package_dir={ 27 | 'pyfastmp3decoder': './src/pyfastmp3decoder', 28 | }, 29 | ext_modules=cythonize(extensions), 30 | ) 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyfastmp3decoder 3 | version = 0.0.1 4 | author = James Betker 5 | author_email = 'none@none' 6 | url = "https://github.com/neonbjb/pyfastmp3decoder" 7 | license = "Apache Software License 2.0" 8 | description = "A fast MP3 decoder for python, using minimp3" 9 | long_description = file: README.md 10 | python_requires = >=3.6 11 | classifiers = 12 | License :: OSI Approved :: Apache Software License 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.6 15 | Programming Language :: Python :: 3.7 16 | Programming Language :: Python :: 3.8 17 | 18 | [options] 19 | packages = find: 20 | install_requires = 21 | 22 | [options.packages.find] 23 | where = "src" 24 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from pyfastmp3decoder.mp3decoder import load_mp3 2 | import librosa 3 | import numpy 4 | import soundfile 5 | from time import time 6 | from tqdm import tqdm 7 | 8 | if __name__ == '__main__': 9 | from tqdm import tqdm # Not in requirements.txt 10 | 11 | # Demonstrates the decoding speed differences between librosa and this library. 12 | file = 'testdata/test_stereo.mp3' 13 | start = time() 14 | for k in tqdm(list(range(50))): 15 | pcm, sr = load_mp3(file) 16 | soundfile.write('demo_out.wav', numpy.transpose(pcm, (1,0)), sr) 17 | print(f'tinymp3 elapsed: {time() -start}') 18 | 19 | start = time() 20 | for k in tqdm(list(range(50))): 21 | pcm, sr = librosa.load(file) 22 | print(f'librosa elapsed: {time() -start}') -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pyfastmp3decoder: A fast MP3 decoder for python, using minimp3 2 | 3 | This project builds upon https://github.com/pyminimp3/pyminimp3 by fixing several bugs which 4 | make the latter repo unusable (thanks to an investigation by newdive@ for this). It also wraps 5 | the library in an API that mirrors the librosa API for loading MP3 files. 6 | 7 | On my system, this library loads MP3 files 4x faster than librosa or other libraries which 8 | use the FFMPEG backend. Smaller files see even greater speed boosts. 9 | 10 | ## Installation 11 | 12 | ``` 13 | pip install cython 14 | git clone https://github.com/neonbjb/pyfastmp3decoder.git 15 | cd pyfastmp3decoder 16 | git submodule update --init --recursive 17 | python setup.py install 18 | ``` 19 | 20 | ## Demo 21 | 22 | See demo.py. 23 | 24 | `python demo.py` 25 | 26 | ## License 27 | 28 | This project is licensed under the Apache Software License 2.0. 29 | -------------------------------------------------------------------------------- /src/pyfastmp3decoder/mp3decoder.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | import librosa 4 | from ._backend import load_frame 5 | import numpy 6 | 7 | 8 | def load_mp3(path, sample_rate=None, return_sr=True, return_stats=False): 9 | """ 10 | Decodes the MP3 file at path, returning the decoded file as a numpy float array as well as some extraneous data 11 | if requested (see params). Shape of output is (channels,pcm_length). 12 | 13 | :param path: MP3 file path. 14 | :param sample_rate: If specified, the returned data will be resampled to this sample rate. 15 | :param return_sr: If true, sample rate is returned alongside audio data. True by default to mirror librosa API. 16 | :param return_stats: If true, sample rate, channels and bitrate are returned alongside audio data. Mutually exclusive with return_sr. 17 | """ 18 | with open(path, 'rb') as f: 19 | hz, chans, bitrate = 0,0,0 20 | data = [] 21 | while True: 22 | pcm, h, c, b = load_frame(f) 23 | if len(pcm) == 0: 24 | break 25 | else: 26 | hz = h 27 | chans = c 28 | bitrate = b 29 | data.extend(pcm) 30 | 31 | np_pcm = numpy.asarray(data, dtype=float) / 32767 32 | if chans > 1: 33 | np_pcm = numpy.reshape(np_pcm, (np_pcm.shape[0]//chans, chans)) 34 | np_pcm = numpy.transpose(np_pcm, (1,0)) 35 | if sample_rate is not None and sample_rate != hz: 36 | np_pcm = librosa.resample(np_pcm, hz, sample_rate) 37 | hz = sample_rate 38 | if return_sr: 39 | return np_pcm, hz 40 | elif return_stats: 41 | return np_pcm, hz, chans, bitrate 42 | return np_pcm 43 | -------------------------------------------------------------------------------- /lib/minimp3.pyx: -------------------------------------------------------------------------------- 1 | """ 2 | Wrappers for the minimp3 library 3 | """ 4 | from array import array 5 | 6 | from libc.string cimport memcpy 7 | from cpython cimport array 8 | 9 | 10 | cdef extern from *: 11 | """ 12 | /* In order to get the minimp3 symbols to build, we must define the 13 | * MINIMP3_IMPLEMENTATION macro. 14 | */ 15 | #define MINIMP3_IMPLEMENTATION 16 | """ 17 | 18 | cdef extern from "minimp3.h": 19 | 20 | ctypedef struct mp3dec_frame_info_t: 21 | int frame_bytes 22 | int channels 23 | int hz 24 | int layer 25 | int bitrate_kbps 26 | 27 | 28 | ctypedef struct mp3dec_t: 29 | float mdct_overlap[1][1] 30 | float qmf_state[1] 31 | int reserv 32 | int free_format_bytes 33 | unsigned char header[1] 34 | unsigned char reserv_buf[1] 35 | 36 | cdef const size_t MINIMP3_MAX_SAMPLES_PER_FRAME 37 | 38 | void mp3dec_init(mp3dec_t* dec) 39 | 40 | int mp3dec_decode_frame(mp3dec_t* dec, unsigned char* mp3, int mp3_bytes, short* pcm, mp3dec_frame_info_t* info) 41 | 42 | 43 | # Initialize the decoder structure 44 | cdef mp3dec_t MP3_DECODER 45 | mp3dec_init(&MP3_DECODER) 46 | 47 | cdef size_t MAX_BUFFER_SIZE = MINIMP3_MAX_SAMPLES_PER_FRAME * 100 48 | 49 | cdef int decode_frame(unsigned char* buffer, int mp3_size, 50 | short *pcm, 51 | mp3dec_frame_info_t* frame_info) except -1: 52 | """ 53 | Attempt to decode a single frame 54 | 55 | The operation may fail if insufficient data are available to read a frame, 56 | when this happens, the function will either raise InsufficientData (in the 57 | event that a frame is either not done being read, or some data has been 58 | skipped), or InvalidData, in the event of another type of error. 59 | """ 60 | cdef int samples = mp3dec_decode_frame(&MP3_DECODER, 61 | buffer, 62 | mp3_size, 63 | pcm, 64 | frame_info) 65 | 66 | if frame_info.frame_bytes: 67 | if not samples: 68 | raise InsufficientData("Decoder skipped ID3 or invalid data") 69 | else: 70 | raise InvalidDataError("No data read into frame") 71 | 72 | return samples 73 | 74 | 75 | cdef size_t refill_buffer(mp3_fobj, array.array buffer, size_t buf_size): 76 | """ 77 | Load more data into the buffer array 78 | """ 79 | cdef int old_buffer_len = len(buffer) 80 | cdef int bytes_read = buf_size 81 | 82 | try: 83 | buffer.fromfile(mp3_fobj, buf_size) 84 | except EOFError: 85 | bytes_read = len(buffer) - old_buffer_len 86 | 87 | return bytes_read 88 | 89 | 90 | cpdef load_frame(mp3_fobj, size_t buf_size=MINIMP3_MAX_SAMPLES_PER_FRAME): 91 | """ 92 | Load a single frame from the file-like ``mp3_fobj`` 93 | 94 | ``mp3_fobj`` will be read in chunks of size ``buf_size`` and decoded until 95 | a full frame has been populated, at which point the function returns an 96 | an array of `short` containing the frame data. 97 | """ 98 | cdef mp3dec_frame_info_t frame_info 99 | 100 | # Buffers 101 | cdef array.array pcm = array.array('h') 102 | cdef array.array buffer = array.array('B') 103 | 104 | cdef size_t pos = mp3_fobj.tell() 105 | cdef size_t samples = 0 106 | cdef size_t mp3_size = 0 107 | 108 | while True: 109 | mp3_size = refill_buffer(mp3_fobj, buffer, buf_size) 110 | if mp3_size == 0: 111 | return array.array('h'), 0, 0, 0 112 | 113 | try: 114 | array.resize(pcm, MINIMP3_MAX_SAMPLES_PER_FRAME) 115 | bufferData = buffer.data.as_uchars[0:mp3_size] 116 | samples = decode_frame(bufferData, mp3_size, 117 | pcm.data.as_shorts, 118 | &frame_info) 119 | except InsufficientData: 120 | pass 121 | except InvalidDataError: 122 | mp3_fobj.seek(pos) 123 | else: 124 | break 125 | finally: 126 | pos += frame_info.frame_bytes 127 | 128 | cdef size_t num_vals 129 | if samples: 130 | num_vals = frame_info.channels * samples 131 | pcm = pcm[0:num_vals] 132 | 133 | mp3_fobj.seek(pos) 134 | return pcm, frame_info.hz, frame_info.channels, frame_info.bitrate_kbps 135 | 136 | 137 | class InsufficientData(ValueError): 138 | pass 139 | 140 | class InvalidDataError(ValueError): 141 | pass 142 | --------------------------------------------------------------------------------