Source code for madmom.io

# encoding: utf-8
"""
Input/output package.

"""

from __future__ import absolute_import, division, print_function

import io as _io
import contextlib

import numpy as np

from .audio import load_audio_file
from .midi import load_midi, write_midi
from ..utils import suppress_warnings, string_types

ENCODING = 'utf8'

# dtype for numpy structured arrays that contain labelled segments
# 'label' needs to be castable to str
SEGMENT_DTYPE = [('start', np.float), ('end', np.float), ('label', object)]


# overwrite the built-in open() to transparently apply some magic file handling
[docs]@contextlib.contextmanager def open_file(filename, mode='r'): """ Context manager which yields an open file or handle with the given mode and closes it if needed afterwards. Parameters ---------- filename : str or file handle File (handle) to open. mode: {'r', 'w'} Specifies the mode in which the file is opened. Yields ------ Open file (handle). """ # check if we need to open the file if isinstance(filename, string_types): f = fid = _io.open(filename, mode) else: f = filename fid = None # yield an open file handle yield f # close the file if needed if fid: fid.close()
[docs]@suppress_warnings def load_events(filename): """ Load a events from a text file, one floating point number per line. Parameters ---------- filename : str or file handle File to load the events from. Returns ------- numpy array Events. Notes ----- Comments (lines starting with '#') and additional columns are ignored, i.e. only the first column is returned. """ # read in the events, one per line events = np.loadtxt(filename, ndmin=2) # 1st column is the event's time, the rest is ignored return events[:, 0]
[docs]def write_events(events, filename, fmt='%.3f', delimiter='\t', header=None): """ Write the events to a file, one event per line. Parameters ---------- events : numpy array Events to be written to file. filename : str or file handle File to write the events to. fmt : str or sequence of strs, optional A single format (e.g. '%.3f'), a sequence of formats, or a multi-format string (e.g. '%.3f %.3f'), in which case `delimiter` is ignored. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. """ events = np.array(events) # reformat fmt to be a single string if needed if isinstance(fmt, (list, tuple)): fmt = delimiter.join(fmt) # write output with open_file(filename, 'wb') as f: # write header if header is not None: f.write(bytes(('# ' + header + '\n').encode(ENCODING))) # write events for e in events: try: string = fmt % tuple(e.tolist()) except AttributeError: string = e except TypeError: string = fmt % e f.write(bytes((string + '\n').encode(ENCODING))) f.flush()
load_onsets = load_events write_onsets = write_events
[docs]@suppress_warnings def load_beats(filename, downbeats=False): """ Load the beats from the given file, one beat per line of format 'beat_time' ['beat_number']. Parameters ---------- filename : str or file handle File to load the beats from. downbeats : bool, optional Load only downbeats instead of beats. Returns ------- numpy array Beats. """ values = np.loadtxt(filename, ndmin=1) if values.ndim > 1: if downbeats: # rows with a "1" in the 2nd column are downbeats return values[values[:, 1] == 1][:, 0] else: # 1st column is the beat time, the rest is ignored return values[:, 0] return values
[docs]def write_beats(beats, filename, fmt=None, delimiter='\t', header=None): """ Write the beats to a file. Parameters ---------- beats : numpy array Beats to be written to file. filename : str or file handle File to write the beats to. fmt : str or sequence of strs, optional A single format (e.g. '%.3f'), a sequence of formats (e.g. ['%.3f', '%d']), or a multi-format string (e.g. '%.3f %d'), in which case `delimiter` is ignored. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. """ if fmt is None and beats.ndim == 2: fmt = ['%.3f', '%d'] elif fmt is None: fmt = '%.3f' write_events(beats, filename, fmt, delimiter, header)
[docs]def load_downbeats(filename): """ Load the downbeats from the given file. Parameters ---------- filename : str or file handle File to load the downbeats from. Returns ------- numpy array Downbeats. """ return load_beats(filename, downbeats=True)
[docs]def write_downbeats(beats, filename, fmt=None, delimiter='\t', header=None): """ Write the downbeats to a file. Parameters ---------- beats : numpy array Beats or downbeats to be written to file. filename : str or file handle File to write the beats to. fmt : str or sequence of strs, optional A single format (e.g. '%.3f'), a sequence of formats (e.g. ['%.3f', '%d']), or a multi-format string (e.g. '%.3f %d'), in which case `delimiter` is ignored. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. Notes ----- If `beats` contains both time and number of the beats, they are filtered to contain only the downbeats (i.e. only the times of those beats with a beat number of 1). """ if beats.ndim == 2: beats = beats[beats[:, 1] == 1][:, 0] if fmt is None: fmt = '%.3f' write_events(beats, filename, fmt, delimiter, header)
[docs]@suppress_warnings def load_notes(filename): """ Load the notes from the given file, one note per line of format 'onset_time' 'note_number' ['duration' ['velocity']]. Parameters ---------- filename: str or file handle File to load the notes from. Returns ------- numpy array Notes. """ return np.loadtxt(filename, ndmin=2)
[docs]def write_notes(notes, filename, fmt=None, delimiter='\t', header=None): """ Write the notes to a file. Parameters ---------- notes : numpy array, shape (num_notes, 2) Notes, row format 'onset_time' 'note_number' ['duration' ['velocity']]. filename : str or file handle File to write the notes to. fmt : str or sequence of strs, optional A sequence of formats (e.g. ['%.3f', '%d', '%.3f', '%d']), or a multi-format string, e.g. '%.3f %d %.3f %d', in which case `delimiter` is ignored. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. Returns ------- numpy array Notes. """ # set default format if fmt is None: fmt = ['%.3f', '%d', '%.3f', '%d'] if not notes.ndim == 2: raise ValueError('unknown format for `notes`') # truncate format to the number of colums given fmt = delimiter.join(fmt[:notes.shape[1]]) # write the notes write_events(notes, filename, fmt=fmt, delimiter=delimiter, header=header)
[docs]def load_segments(filename): """ Load labelled segments from file, one segment per line. Each segment is of form <start> <end> <label>, where <start> and <end> are floating point numbers, and <label> is a string. Parameters ---------- filename : str or file handle File to read the labelled segments from. Returns ------- segments : numpy structured array Structured array with columns 'start', 'end', and 'label', containing the beginning, end, and label of segments. """ start, end, label = [], [], [] with open_file(filename) as f: for line in f: s, e, l = line.split() start.append(float(s)) end.append(float(e)) label.append(l) segments = np.zeros(len(start), dtype=SEGMENT_DTYPE) segments['start'] = start segments['end'] = end segments['label'] = label return segments
[docs]def write_segments(segments, filename, fmt=None, delimiter='\t', header=None): """ Write labelled segments to a file. Parameters ---------- segments : numpy structured array Labelled segments, one per row (column definition see SEGMENT_DTYPE). filename : str or file handle Output filename or handle. fmt : str or sequence of strs, optional A sequence of formats (e.g. ['%.3f', '%.3f', '%s']), or a multi-format string (e.g. '%.3f %.3f %s'), in which case `delimiter` is ignored. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. Returns ------- numpy structured array Labelled segments Notes ----- Labelled segments are represented as numpy structured array with three named columns: 'start' contains the start position (e.g. seconds), 'end' the end position, and 'label' the segment label. """ if fmt is None: fmt = ['%.3f', '%.3f', '%s'] write_events(segments, filename, fmt=fmt, delimiter=delimiter, header=header)
load_chords = load_segments write_chords = write_segments
[docs]def load_key(filename): """ Load the key from the given file. Parameters ---------- filename : str or file handle File to read key information from. Returns ------- str Key. """ with open_file(filename) as f: return f.read().strip()
[docs]def write_key(key, filename, header=None): """ Write key string to a file. Parameters ---------- key : str Key name. filename : str or file handle Output file. header : str, optional String that will be written at the beginning of the file as comment. Returns ------- key : str Key name. """ write_events([key], filename, fmt='%s', header=header)
[docs]def load_tempo(filename, split_value=1., sort=None, norm_strengths=None, max_len=None): """ Load tempo information from the given file. Tempo information must have the following format: 'main tempo' ['secondary tempo' ['relative_strength']] Parameters ---------- filename : str or file handle File to load the tempo from. split_value : float, optional Value to distinguish between tempi and strengths. `values` > `split_value` are interpreted as tempi [bpm], `values` <= `split_value` are interpreted as strengths. sort : bool, deprecated Sort the tempi by their strength. norm_strengths : bool, deprecated Normalize the strengths to sum 1. max_len : int, deprecated Return at most `max_len` tempi. Returns ------- tempi : numpy array, shape (num_tempi[, 2]) Array with tempi. If no strength is parsed, a 1-dimensional array of length 'num_tempi' is returned. If strengths are given, a 2D array with tempi (first column) and their relative strengths (second column) is returned. """ # try to load the data from file values = np.loadtxt(filename, ndmin=1) # split the filename according to their filename into tempi and strengths # TODO: this is kind of hack-ish, find a better solution tempi = values[values > split_value] strengths = values[values <= split_value] # make the strengths behave properly strength_sum = np.sum(strengths) # relative strengths are given (one less than tempi) if len(tempi) - len(strengths) == 1: strengths = np.append(strengths, 1. - strength_sum) if np.any(strengths < 0): raise AssertionError('strengths must be positive') # no strength is given, assume an evenly distributed one if strength_sum == 0: strengths = np.ones_like(tempi) / float(len(tempi)) # normalize the strengths if norm_strengths is not None: import warnings warnings.warn('`norm_strengths` is deprecated as of version 0.16 and ' 'will be removed in 0.18. Please normalize strengths ' 'separately.') strengths /= float(strength_sum) # tempi and strengths must have same length if len(tempi) != len(strengths): raise AssertionError('tempi and strengths must have same length') # order the tempi according to their strengths if sort: import warnings warnings.warn('`sort` is deprecated as of version 0.16 and will be ' 'removed in 0.18. Please sort the returned array ' 'separately.') # Note: use 'mergesort', because we want a stable sorting algorithm # which keeps the order of the keys in case of duplicate keys # but we need to apply this '(-strengths)' trick because we want # tempi with uniformly distributed strengths to keep their order sort_idx = (-strengths).argsort(kind='mergesort') tempi = tempi[sort_idx] strengths = strengths[sort_idx] # return at most 'max_len' tempi and their relative strength if max_len is not None: import warnings warnings.warn('`max_len` is deprecated as of version 0.16 and will be ' 'removed in 0.18. Please truncate the returned array ' 'separately.') return np.vstack((tempi[:max_len], strengths[:max_len])).T
[docs]def write_tempo(tempi, filename, delimiter='\t', header=None, mirex=None): """ Write the most dominant tempi and the relative strength to a file. Parameters ---------- tempi : numpy array Array with the detected tempi (first column) and their strengths (second column). filename : str or file handle Output file. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file as comment. mirex : bool, deprecated Report the lower tempo first (as required by MIREX). Returns ------- tempo_1 : float The most dominant tempo. tempo_2 : float The second most dominant tempo. strength : float Their relative strength. """ # make the given tempi a 2d array tempi = np.array(tempi, ndmin=2) # default values t1 = t2 = strength = np.nan # only one tempo was detected if len(tempi) == 1: t1 = tempi[0][0] strength = 1. # consider only the two strongest tempi and strengths elif len(tempi) > 1: t1, t2 = tempi[:2, 0] strength = tempi[0, 1] / sum(tempi[:2, 1]) # for MIREX, the lower tempo must be given first if mirex is not None: import warnings warnings.warn('`mirex` argument is deprecated as of version 0.16 ' 'and will be removed in version 0.17. Please sort the ' 'tempi manually') if t1 > t2: t1, t2, strength = t2, t1, 1. - strength # format as a numpy array and write to output out = np.array([t1, t2, strength], ndmin=2) write_events(out, filename, fmt=['%.2f', '%.2f', '%.2f'], delimiter=delimiter, header=header)