__author__ = 'maartenbreddels'
import os
import mmap
import itertools
import functools
import collections
import logging
import numpy as np
import numpy.ma
import vaex
import astropy.table
import astropy.units
from vaex.utils import ensure_string
import astropy.io.fits as fits
import re
import six
from vaex.dataset import DatasetLocal, DatasetArrays
import vaex.dataset
import vaex.file
from vaex.expression import Expression
from vaex.dataset_mmap import DatasetMemoryMapped

logger = logging.getLogger("vaex.file")

dataset_type_map = {}

# h5py doesn't want to build at readthedocs
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
try:
    import h5py
except:
    if not on_rtd:
        raise

def _try_unit(unit):
    try:
        unit = astropy.units.Unit(str(unit))
        if not isinstance(unit, astropy.units.UnrecognizedUnit):
            return unit
    except:
        # logger.exception("could not parse unit: %r", unit)
        pass
    try:
        unit_mangle = re.match(".*\[(.*)\]", str(unit)).groups()[0]
        unit = astropy.units.Unit(unit_mangle)
    except:
        pass  # logger.exception("could not parse unit: %r", unit)
    if isinstance(unit, six.string_types):
        return None
    elif isinstance(unit, astropy.units.UnrecognizedUnit):
        return None
    else:
        return unit

class Hdf5MemoryMapped(DatasetMemoryMapped):
    """Implements the vaex hdf5 file format"""

    def __init__(self, filename, write=False):
        super(Hdf5MemoryMapped, self).__init__(filename, write=write)
        self.h5file = h5py.File(self.filename, "r+" if write else "r")
        self.h5table_root_name = None
        self._version = 1
        try:
            self._load()
        finally:
            self.h5file.close()

    def write_meta(self):
        """ucds, descriptions and units are written as attributes in the hdf5 file, instead of a seperate file as
         the default :func:`Dataset.write_meta`.
         """
        with h5py.File(self.filename, "r+") as h5file_output:
            h5table_root = h5file_output[self.h5table_root_name]
            if self.description is not None:
                h5table_root.attrs["description"] = self.description
            h5columns = h5table_root if self._version == 1 else h5table_root['columns']
            for column_name in self.columns.keys():
                h5dataset = None
                if column_name in h5columns:
                    h5dataset = h5columns[column_name]
                else:
                    for group in h5columns.values():
                        if 'type' in group.attrs:
                            if group.attrs['type'] in ['csr_matrix']:
                                for name, column in group.items():
                                    if name == column_name:
                                        h5dataset = column
                if h5dataset is None:
                    raise ValueError('column {} not found'.format(column_name))
                for name, values in [("ucd", self.ucds), ("unit", self.units), ("description", self.descriptions)]:
                    if column_name in values:
                        value = ensure_string(values[column_name], cast=True)
                        h5dataset.attrs[name] = value
                    else:
                        if name in h5columns.attrs:
                            del h5dataset.attrs[name]

    @classmethod
    def create(cls, path, N, column_names, dtypes=None, write=True):
        """Create a new (empty) hdf5 file with columns given by column names, of length N

        Optionally, numpy dtypes can be passed, default is floats
        """

        dtypes = dtypes or [np.float] * len(column_names)

        if N == 0:
            raise ValueError("Cannot export empty table")
        with h5py.File(path, "w") as h5file_output:
            h5data_output = h5file_output.require_group("data")
            for column_name, dtype in zip(column_names, dtypes):
                shape = (N,)
                print(dtype)
                if dtype.type == np.datetime64:
                    array = h5file_output.require_dataset("/data/%s" % column_name, shape=shape, dtype=np.int64)
                    array.attrs["dtype"] = dtype.name
                else:
                    array = h5file_output.require_dataset("/data/%s" % column_name, shape=shape, dtype=dtype)
                array[0] = array[0]  # make sure the array really exists
        return Hdf5MemoryMapped(path, write=write)

    @classmethod
    def can_open(cls, path, *args, **kwargs):
        h5file = None
        try:
            with open(path, "rb") as f:
                signature = f.read(4)
                hdf5file = signature == b"\x89\x48\x44\x46"
        except:
            logger.error("could not read 4 bytes from %r", path)
            return
        if hdf5file:
            try:
                h5file = h5py.File(path, "r")
            except:
                logger.exception("could not open file as hdf5")
                return False
            if h5file is not None:
                with h5file:
                    return ("data" in h5file) or ("columns" in h5file) or ("table" in h5file)
            else:
                logger.debug("file %s has no data or columns group" % path)
        return False

    @classmethod
    def get_options(cls, path):
        return []

    @classmethod
    def option_to_args(cls, option):
        return []

    def _load(self):
        if "data" in self.h5file:
            self._load_columns(self.h5file["/data"])
            self.h5table_root_name = "/data"
        if "table" in self.h5file:
            self._version = 2
            self._load_columns(self.h5file["/table"])
            self.h5table_root_name = "/table"
        # TODO: shall we rename it vaex... ?
        # if "vaex" in self.h5file:
        # self.load_columns(self.h5file["/vaex"])
        # h5table_root = "/vaex"
        if "columns" in self.h5file:
            self._load_columns(self.h5file["/columns"])
            self.h5table_root_name = "/columns"
        if "properties" in self.h5file:
            self._load_variables(self.h5file["/properties"])  # old name, kept for portability
        if "variables" in self.h5file:
            self._load_variables(self.h5file["/variables"])
        if "axes" in self.h5file:
            self._load_axes(self.h5file["/axes"])
        self.update_meta()
        self.update_virtual_meta()

    # def
    def _load_axes(self, axes_data):
        for name in axes_data:
            axis = axes_data[name]
            logger.debug("loading axis %r" % name)
            offset = axis.id.get_offset()
            shape = axis.shape
            assert len(shape) == 1  # ony 1d axes
            # print name, offset, len(axis), axis.dtype
            self.addAxis(name, offset=offset, length=len(axis), dtype=axis.dtype)
            # self.axis_names.append(axes_data)
            # self.axes[name] = np.array(axes_data[name])

    def _load_variables(self, h5variables):
        for key, value in list(h5variables.attrs.items()):
            self.variables[key] = value

    def _map_hdf5_array(self, data, mask=None):
        offset = data.id.get_offset()
        if offset is None:
            raise Exception("columns doesn't really exist in hdf5 file")
        shape = data.shape
        dtype = data.dtype
        if "dtype" in data.attrs:
            # ignore the special str type, which is not a numpy dtype
            if data.attrs["dtype"] != "str":
                dtype = data.attrs["dtype"]
                if dtype == 'utf32':
                    dtype = np.dtype('U' + str(data.attrs['dlength']))
        #self.addColumn(column_name, offset, len(data), dtype=dtype)
        array = self._map_array(data.id.get_offset(), dtype=dtype, length=len(data))
        if mask is not None:
            mask_array = self._map_hdf5_array(mask)
            return np.ma.array(array, mask=mask_array, shrink=False)
            assert ar.mask is mask_array, "masked array was copied"
        else:
            return array

    def _load_columns(self, h5data, first=[]):
        # print h5data
        # make sure x y x etc are first

        finished = set()
        if "description" in h5data.attrs:
            self.description = ensure_string(h5data.attrs["description"])
        # hdf5, or h5py doesn't keep the order of columns, so manually track that, also enables reordering later
        h5columns = h5data if self._version == 1 else h5data['columns']
        if "column_order" in h5columns.attrs:
            column_order = ensure_string(h5columns.attrs["column_order"]).split(",")
        else:
            column_order = []
        # for name in list(h5columns):
        #     if name not in column_order:
        #         column_order.append(name)
        # for column_name in column_order:
            # if column_name in h5columns and column_name not in finished:
        for group_name in list(h5columns):
            group = h5columns[group_name]
            if 'type' in group.attrs:
                if group.attrs['type'] in ['csr_matrix']:
                    from scipy.sparse import csc_matrix, csr_matrix
                    class csr_matrix_nocheck(csr_matrix):
                        def check_format(self, *args, **kwargs):
                            pass
                    data = self._map_hdf5_array(group['data'])
                    indptr = self._map_hdf5_array(group['indptr'])
                    indices = self._map_hdf5_array(group['indices'])
                    #column_names = ensure_string(group.attrs["column_names"]).split(",")
                    # make sure we keep the original order
                    groups = [(name, value) for name, value in group.items() if isinstance(value, h5py.Group)]
                    column_names = [None] * len(groups)
                    for name, column in groups:
                        column_names[column.attrs['column_index']] = name
                    matrix = csr_matrix_nocheck((data, indices, indptr), shape=(len(indptr)-1, len(column_names)))
                    assert matrix.data is data
                    # assert matrix.indptr is indptr
                    assert matrix.indices is indices
                    self.add_columns(column_names, matrix)
            else:
                column_name = group_name
                column = h5columns[column_name]
                if "ucd" in column.attrs:
                    self.ucds[column_name] = ensure_string(column.attrs["ucd"])
                if "description" in column.attrs:
                    self.descriptions[column_name] = ensure_string(column.attrs["description"])
                if "unit" in column.attrs:
                    try:
                        unitname = ensure_string(column.attrs["unit"])
                        if unitname and unitname != "None":
                            self.units[column_name] = _try_unit(unitname)
                    except:
                        logger.exception("error parsing unit: %s", column.attrs["unit"])
                if "units" in column.attrs:  # Amuse case
                    unitname = ensure_string(column.attrs["units"])
                    logger.debug("amuse unit: %s", unitname)
                    if unitname == "(0.01 * system.get('S.I.').base('length'))":
                        self.units[column_name] = astropy.units.Unit("cm")
                    if unitname == "((0.01 * system.get('S.I.').base('length')) * (system.get('S.I.').base('time')**-1))":
                        self.units[column_name] = astropy.units.Unit("cm/s")
                    if unitname == "(0.001 * system.get('S.I.').base('mass'))":
                        self.units[column_name] = astropy.units.Unit("gram")

                    if unitname == "system.get('S.I.').base('length')":
                        self.units[column_name] = astropy.units.Unit("m")
                    if unitname == "(system.get('S.I.').base('length') * (system.get('S.I.').base('time')**-1))":
                        self.units[column_name] = astropy.units.Unit("m/s")
                    if unitname == "system.get('S.I.').base('mass')":
                        self.units[column_name] = astropy.units.Unit("kg")
                data = column if self._version == 1 else column['data']
                if hasattr(data, "dtype"):
                    if "dtype" in data.attrs and data.attrs["dtype"] == "str":
                        indices = self._map_hdf5_array(column['indices'])
                        bytes = self._map_hdf5_array(data)
                        if "null_bitmap" in column:
                            null_bitmap = self._map_hdf5_array(column['null_bitmap'])
                        else:
                            null_bitmap = None
                        from vaex.column import ColumnStringArrow
                        self.add_column(column_name, ColumnStringArrow(indices, bytes, null_bitmap=null_bitmap))
                    else:
                        shape = data.shape
                        if True:  # len(shape) == 1:
                            dtype = data.dtype
                            if "dtype" in data.attrs:
                                dtype = data.attrs["dtype"]
                            logger.debug("adding column %r with dtype %r", column_name, dtype)
                            # self.addColumn(column_name, offset, len(data), dtype=dtype)
                            if self._version > 1 and 'mask' in column:
                                self.add_column(column_name, self._map_hdf5_array(data, column['mask']))
                            else:
                                self.add_column(column_name, self._map_hdf5_array(data))
                        else:
                            transposed = shape[1] < shape[0]
                            self.addRank1(column_name, offset, shape[1], length1=shape[0], dtype=data.dtype, stride=1, stride1=1, transposed=transposed)
        all_columns = dict(**self.columns)
        # print(all_columns, column_order)
        self.column_names = []
        for name in column_order:
            self.columns[name] = all_columns.pop(name)
            self.column_names.append(name)
        # add the rest
        for name, col in all_columns.items():
            self.columns[name] = col
            self.column_names.append(name)

    def close(self):
        super(Hdf5MemoryMapped, self).close()
        self.h5file.close()

    def __expose_array(self, hdf5path, column_name):
        array = self.h5file[hdf5path]
        array[0] = array[0]  # without this, get_offset returns None, probably the array isn't really created
        offset = array.id.get_offset()
        self.remap()
        self.addColumn(column_name, offset, len(array), dtype=array.dtype)

    def __add_column(self, column_name, dtype=np.float64, length=None):
        array = self.h5data.create_dataset(column_name, shape=(self._length if length is None else length,), dtype=dtype)
        array[0] = array[0]  # see above
        offset = array.id.get_offset()
        self.h5file.flush()
        self.remap()
        self.addColumn(column_name, offset, len(array), dtype=array.dtype)

dataset_type_map["h5vaex"] = Hdf5MemoryMapped

class AmuseHdf5MemoryMapped(Hdf5MemoryMapped):
    """Implements reading Amuse hdf5 files `amusecode.org <http://amusecode.org/>`_"""

    def __init__(self, filename, write=False):
        super(AmuseHdf5MemoryMapped, self).__init__(filename, write=write)

    @classmethod
    def can_open(cls, path, *args, **kwargs):
        h5file = None
        try:
            h5file = h5py.File(path, "r")
        except:
            return False
        if h5file is not None:
            with h5file:
                return ("particles" in h5file)  # or ("columns" in h5file)
        return False

    def _load(self):
        particles = self.h5file["/particles"]
        for group_name in particles:
            # import pdb
            # pdb.set_trace()
            group = particles[group_name]
            self._load_columns(group["attributes"])

            column_name = "keys"
            column = group[column_name]
            offset = column.id.get_offset()
            self.addColumn(column_name, offset, len(column), dtype=column.dtype)
        self.update_meta()
        self.update_virtual_meta()

dataset_type_map["amuse"] = AmuseHdf5MemoryMapped

gadget_particle_names = "gas halo disk bulge stars dm".split()

class Hdf5MemoryMappedGadget(DatasetMemoryMapped):
    """Implements reading `Gadget2 <http://wwwmpa.mpa-garching.mpg.de/gadget/>`_ hdf5 files """

    def __init__(self, filename, particle_name=None, particle_type=None):
        if "#" in filename:
            filename, index = filename.split("#")
            index = int(index)
            particle_type = index
            particle_name = gadget_particle_names[particle_type]
        elif particle_type is not None:
            self.particle_name = gadget_particle_names[self.particle_type]
            self.particle_type = particle_type
        elif particle_name is not None:
            if particle_name.lower() in gadget_particle_names:
                self.particle_type = gadget_particle_names.index(particle_name.lower())
                self.particle_name = particle_name.lower()
            else:
                raise ValueError("particle name not supported: %r, expected one of %r" % (particle_name, " ".join(gadget_particle_names)))
        else:
            raise Exception("expected particle type or name as argument, or #<nr> behind filename")
        super(Hdf5MemoryMappedGadget, self).__init__(filename)
        self.particle_type = particle_type
        self.particle_name = particle_name
        self.name = self.name + "-" + self.particle_name
        h5file = h5py.File(self.filename, 'r')
        # for i in range(1,4):
        key = "/PartType%d" % self.particle_type
        if key not in h5file:
            raise KeyError("%s does not exist" % key)
        particles = h5file[key]
        for name in list(particles.keys()):
            # name = "/PartType%d/Coordinates" % i
            data = particles[name]
            if isinstance(data, h5py.highlevel.Dataset):  # array.shape
                array = data
                shape = array.shape
                if len(shape) == 1:
                    offset = array.id.get_offset()
                    if offset is not None:
                        self.addColumn(name, offset, data.shape[0], dtype=data.dtype)
                else:
                    if name == "Coordinates":
                        offset = data.id.get_offset()
                        if offset is None:
                            print((name, "is not of continuous layout?"))
                            sys.exit(0)
                        bytesize = data.dtype.itemsize
                        self.addColumn("x", offset, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("y", offset + bytesize, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("z", offset + bytesize * 2, data.shape[0], dtype=data.dtype, stride=3)
                    elif name == "Velocity":
                        offset = data.id.get_offset()
                        self.addColumn("vx", offset, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("vy", offset + bytesize, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("vz", offset + bytesize * 2, data.shape[0], dtype=data.dtype, stride=3)
                    elif name == "Velocities":
                        offset = data.id.get_offset()
                        self.addColumn("vx", offset, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("vy", offset + bytesize, data.shape[0], dtype=data.dtype, stride=3)
                        self.addColumn("vz", offset + bytesize * 2, data.shape[0], dtype=data.dtype, stride=3)
                    else:
                        logger.error("unsupported column: %r of shape %r" % (name, array.shape))
        if "Header" in h5file:
            for name in "Redshift Time_GYR".split():
                if name in h5file["Header"].attrs:
                    value = h5file["Header"].attrs[name].decode("utf-8")
                    logger.debug("property[{name!r}] = {value}".format(**locals()))
                    self.variables[name] = value
                    # self.property_names.append(name)

        name = "particle_type"
        value = particle_type
        logger.debug("property[{name}] = {value}".format(**locals()))
        self.variables[name] = value
        # self.property_names.append(name)

    @classmethod
    def can_open(cls, path, *args, **kwargs):
        if len(args) == 2:
            particleName = args[0]
            particleType = args[1]
        elif "particle_name" in kwargs:
            particle_type = gadget_particle_names.index(kwargs["particle_name"].lower())
        elif "particle_type" in kwargs:
            particle_type = kwargs["particle_type"]
        elif "#" in path:
            filename, index = path.split("#")
            particle_type = gadget_particle_names[index]
        else:
            return False
        h5file = None
        try:
            h5file = h5py.File(path, "r")
        except:
            return False
        has_particles = False
        # for i in range(1,6):
        key = "/PartType%d" % particle_type
        exists = key in h5file
        h5file.close()
        return exists

        # has_particles = has_particles or (key in h5file)
        # return has_particles

    @classmethod
    def get_options(cls, path):
        return []

    @classmethod
    def option_to_args(cls, option):
        return []

dataset_type_map["gadget-hdf5"] = Hdf5MemoryMappedGadget

class MemoryMappedGadget(DatasetMemoryMapped):
    def __init__(self, filename):
        super(MemoryMappedGadget, self).__init__(filename)
        # h5file = h5py.File(self.filename)
        import vaex.file.gadget
        length, posoffset, veloffset, header = vaex.file.gadget.getinfo(filename)
        self.addColumn("x", posoffset, length, dtype=np.float32, stride=3)
        self.addColumn("y", posoffset + 4, length, dtype=np.float32, stride=3)
        self.addColumn("z", posoffset + 8, length, dtype=np.float32, stride=3)

        self.addColumn("vx", veloffset, length, dtype=np.float32, stride=3)
        self.addColumn("vy", veloffset + 4, length, dtype=np.float32, stride=3)
        self.addColumn("vz", veloffset + 8, length, dtype=np.float32, stride=3)

dataset_type_map["gadget-plain"] = MemoryMappedGadget


"""
Taken from
From: https://github.com/keflavich/agpy/blob/master/agpy/readcol.py
License: Copyright (c) 2009 Adam Ginsburg

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

readcol.py by Adam Ginsburg (keflavich@gmail.com)

readcol is meant to emulate IDL's readcol.pro, but is more capable and
flexible.  It is not a particularly "pythonic" program since it is not modular.
For a modular ascii table reader, http://cxc.harvard.edu/contrib/asciitable/ is
probably better.  This single-function code is probably more intuitive to an
end-user, though.
"""
import string,re,sys
import numpy
from collections import OrderedDict
try:
    from scipy.stats import mode
    hasmode = True
except ImportError:
    #print "scipy could not be imported.  Your table must have full rows."
    hasmode = False
except ValueError:
    #print "error"
    hasmode = False

def readcol(filename,skipline=0,skipafter=0,names=False,fsep=None,twod=True,
        fixedformat=None,asdict=False,comment='#',verbose=True,nullval=None,
        asStruct=False,namecomment=True,removeblanks=False,header_badchars=None,
        asRecArray=False):
    """
    The default return is a two dimensional float array.  If you want a list of
    columns output instead of a 2D array, pass 'twod=False'.  In this case,
    each column's data type will be automatically detected.

    Example usage:
    CASE 1) a table has the format:
     X    Y    Z
    0.0  2.4  8.2
    1.0  3.4  5.6
    0.7  3.2  2.1
    ...
    names,(x,y,z)=readcol("myfile.tbl",names=True,twod=False)
    or
    x,y,z=readcol("myfile.tbl",skipline=1,twod=False)
    or
    names,xx = readcol("myfile.tbl",names=True)
    or
    xxdict = readcol("myfile.tbl",asdict=True)
    or
    xxstruct = readcol("myfile.tbl",asStruct=True)

    CASE 2) no title is contained into the table, then there is
    no need to skipline:
    x,y,z=readcol("myfile.tbl")

    CASE 3) there is a names column and then more descriptive text:
     X      Y     Z
    (deg) (deg) (km/s)
    0.0    2.4   8.2
    1.0    3.4.  5.6
    ...
    then use:
    names,x,y,z=readcol("myfile.tbl",names=True,skipline=1,twod=False)
    or
    x,y,z=readcol("myfile.tbl",skipline=2,twod=False)

    INPUTS:
        fsep - field separator, e.g. for comma separated value (csv) files
        skipline - number of lines to ignore at the start of the file
        names - read / don't read in the first line as a list of column names
                can specify an integer line number too, though it will be
                the line number after skipping lines
        twod - two dimensional or one dimensional output
        nullval - if specified, all instances of this value will be replaced
           with a floating NaN
        asdict - zips names with data to create a dict with column headings
            tied to column data.  If asdict=True, names will be set to True
        asStruct - same as asdict, but returns a structure instead of a dictionary
            (i.e. you call struct.key instead of struct['key'])
        fixedformat - if you have a fixed format file, this is a python list of
            column lengths.  e.g. the first table above would be [3,5,5].  Note
            that if you specify the wrong fixed format, you will get junk; if your
            format total is greater than the line length, the last entries will all
            be blank but readcol will not report an error.
        namecomment - assumed that "Name" row is on a comment line.  If it is not -
            e.g., it is the first non-comment line, change this to False
        removeblanks - remove all blank entries from split lines.  This can cause lost
            data if you have blank entries on some lines.
        header_badchars - remove these characters from a header before parsing it
            (helpful for IPAC tables that are delimited with | )

    If you get this error: "scipy could not be imported.  Your table must have
    full rows." it means readcol cannot automatically guess which columns
    contain data.  If you have scipy and columns of varying length, readcol will
    read in all of the rows with length=mode(row lengths).
    """
    with open(filename,'r') as f:
        f = f.readlines()

        null=[f.pop(0) for i in range(skipline)]

        commentfilter = make_commentfilter(comment)

        if not asStruct:
            asStruct = asRecArray

        if namecomment is False and (names or asdict or asStruct):
            while 1:
                line = f.pop(0)
                if line[0] != comment:
                    nameline = line
                    if header_badchars:
                        for c in header_badchars:
                            nameline = nameline.replace(c,' ')
                    nms=nameline.split(fsep)
                    break
                elif len(f) == 0:
                    raise Exception("No uncommented lines found.")
        else:
            if names or asdict or asStruct:
                # can specify name line
                if type(names) == type(1):
                    nameline = f.pop(names)
                else:
                    nameline = f.pop(0)
                if nameline[0]==comment:
                    nameline = nameline[1:]
                if header_badchars:
                    for c in header_badchars:
                        nameline = nameline.replace(c,' ')
                nms=list([name.strip() for name in nameline.split(fsep)])

        null=[f.pop(0) for i in range(skipafter)]

        if fixedformat:
            myreadff = lambda x: readff(x,fixedformat)
            splitarr = list(map(myreadff,f))
            splitarr = list(filter(commentfilter,splitarr))
        else:
            fstrip = list(map(str.strip,f))
            fseps = [ fsep for i in range(len(f)) ]
            splitarr = list(map(str.split,fstrip,fseps))
            if removeblanks:
                for i in range(splitarr.count([''])):
                    splitarr.remove([''])

            splitarr = list(filter(commentfilter,splitarr))

            # check to make sure each line has the same number of columns to avoid
            # "ValueError: setting an array element with a sequence."
            nperline = list(map(len,splitarr))
            if hasmode:
                ncols,nrows = mode(nperline)
                if nrows != len(splitarr):
                    if verbose:
                        print("Removing %i rows that don't match most common length %i.  \
                         \n%i rows read into array." % (len(splitarr) - nrows,ncols,nrows))
                    for i in range(len(splitarr)-1,-1,-1):  # need to go backwards
                        if nperline[i] != ncols:
                            splitarr.pop(i)

        try:
            x = numpy.asarray( splitarr , dtype='float')
        except ValueError:
            if verbose:
                print("WARNING: reading as string array because %s array failed" % 'float')
            try:
                x = numpy.asarray( splitarr , dtype='S')
            except ValueError:
                if hasmode:
                    raise Exception( "ValueError when converting data to array." + \
                            "  You have scipy.mode on your system, so this is " + \
                            "probably not an issue of differing row lengths." )
                else:
                    raise Exception( "Conversion to array error.  You probably " + \
                            "have different row lengths and scipy.mode was not " + \
                            "imported." )

        if nullval is not None:
            x[x==nullval] = numpy.nan
            x = get_autotype(x)

        if asdict or asStruct:
            mydict = OrderedDict(zip(nms,x.T))
            for k,v in mydict.items():
                mydict[k] = get_autotype(v)
            if asdict:
                return mydict
            elif asRecArray:
                return Struct(mydict).as_recarray()
            elif asStruct:
                return Struct(mydict)
        elif names and twod:
            return nms,x
        elif names:
            # if not returning a twod array, try to return each vector as the spec. type
            return nms,[ get_autotype(x.T[i]) for i in range(x.shape[1]) ]
        else:
            if twod:
                return x
            else:
                return [ get_autotype(x.T[i]) for i in range(x.shape[1]) ]

def get_autotype(arr):
    """
    Attempts to return a numpy array converted to the most sensible dtype
    Value errors will be caught and simply return the original array
    Tries to make dtype int, then float, then no change
    """
    try:
        narr = arr.astype('float')
        if (narr < sys.maxsize).all() and (narr % 1).sum() == 0:
            return narr.astype('int')
        else:
            return narr
    except ValueError:
        return arr

class Struct(object):
    """
    Simple struct intended to take a dictionary of column names -> columns
    and turn it into a struct by removing special characters
    """
    def __init__(self,namedict):
        R = re.compile('\W')  # find and remove all non-alphanumeric characters
        for k in namedict.keys():
            v = namedict.pop(k)
            if k[0].isdigit():
                k = 'n'+k
            namedict[R.sub('',k)] = v
        self.__dict__ = namedict

    def add_column(self,name,data):
        """
        Add a new column (attribute) to the struct
        (will overwrite anything with the same name)
        """
        self.__dict__[name] = data

    def as_recarray(self):
        """ Convert into numpy recordarray """
        dtype = [(k,v.dtype) for k,v in self.__dict__.iteritems()]
        R = numpy.recarray(len(self.__dict__[k]),dtype=dtype)
        for key in self.__dict__:
            R[key] = self.__dict__[key]
        return R

    def __getitem__(self, key):
        return self.__dict__[key]

def readff(s,format):
    """
    Fixed-format reader
    Pass in a single line string (s) and a format list,
    which needs to be a python list of string lengths
    """

    F = numpy.array([0]+format).cumsum()
    bothF = zip(F[:-1],F[1:])
    strarr = [s[l:u] for l,u in bothF]

    return strarr

def make_commentfilter(comment):
    if comment is not None:
        def commentfilter(a):
            try: return comment.find(a[0][0])
            except: return -1
        return commentfilter
    else: # always return false
        return lambda x: -1


import re
import string
import sys
import time

PY2 = sys.version_info[0] == 2
if not PY2:
  text_type = str
  string_types = (str,)
  unichr = chr
else:
  text_type = unicode
  string_types = (str, unicode)
  unichr = unichr

COMMENT = object()

def load_properties(fh, mapping=dict):
  """
    Reads properties from a Java .properties file.

    Returns a dict (or provided mapping) of properties.

    :param fh: a readable file-like object
    :param mapping: mapping type to load properties into
  """
  return mapping(iter_properties(fh))

def store_properties(fh, props, comment=None, timestamp=True):
  """
    Writes properties to the file in Java properties format.

    :param fh: a writable file-like object
    :param props: a mapping (dict) or iterable of key/value pairs
    :param comment: comment to write to the beginning of the file
    :param timestamp: boolean indicating whether to write a timestamp comment
  """
  if comment is not None:
    write_comment(fh, comment)

  if timestamp:
    write_comment(fh, time.strftime('%a %b %d %H:%M:%S %Z %Y'))

  if hasattr(props, 'keys'):
    for key in props:
      write_property(fh, key, props[key])
  else:
    for key, value in props:
      write_property(fh, key, value)

def write_comment(fh, comment):
  """
    Writes a comment to the file in Java properties format.

    Newlines in the comment text are automatically turned into a continuation
    of the comment by adding a "#" to the beginning of each line.

    :param fh: a writable file-like object
    :param comment: comment string to write
  """
  _require_string(comment, 'comments')
  fh.write(_escape_comment(comment))
  fh.write(b'\n')

def _require_string(value, name):
  if isinstance(value, string_types):
    return

  valid_types = ' or '.join(cls.__name__ for cls in string_types)
  raise TypeError('%s must be %s, but got: %s %r'
                  % (name, valid_types, type(value), value))

def write_property(fh, key, value):
  """
    Write a single property to the file in Java properties format.

    :param fh: a writable file-like object
    :param key: the key to write
    :param value: the value to write
  """
  if key is COMMENT:
    write_comment(fh, value)
    return

  _require_string(key, 'keys')
  _require_string(value, 'values')

  fh.write(_escape_key(key))
  fh.write(b'=')
  fh.write(_escape_value(value))
  fh.write(b'\n')

def iter_properties(fh, comments=False):
  """
    Incrementally read properties from a Java .properties file.

    Yields tuples of key/value pairs.

    If ``comments`` is `True`, comments will be included with ``jprops.COMMENT``
    in place of the key.

    :param fh: a readable file-like object
    :param comments: should include comments (default: False)
  """
  for line in _property_lines(fh):
    key, value = _split_key_value(line)
    if key is not COMMENT:
      key = _unescape(key)
    elif not comments:
      continue
    yield key, _unescape(value)

################################################################################
# Helpers for property parsing/writing
################################################################################

_COMMENT_CHARS = '#!'
_COMMENT_CHARS_BYTES = bytearray(_COMMENT_CHARS, 'ascii')
_LINE_PATTERN = re.compile(br'^\s*(?P<body>.*?)(?P<backslashes>\\*)$')
_KEY_TERMINATORS_EXPLICIT = '=:'
_KEY_TERMINATORS = _KEY_TERMINATORS_EXPLICIT + string.whitespace

_KEY_TERMINATORS_EXPLICIT_BYTES = bytearray(_KEY_TERMINATORS_EXPLICIT, 'ascii')
_KEY_TERMINATORS_BYTES = bytearray(_KEY_TERMINATORS, 'ascii')

_escapes = {
  't': '\t',
  'n': '\n',
  'f': '\f',
  'r': '\r',
}
_escapes_rev = dict((v, '\\' + k) for k,v in _escapes.items())
for c in '\\' + _COMMENT_CHARS + _KEY_TERMINATORS_EXPLICIT:
  _escapes_rev.setdefault(c, '\\' + c)

def _unescape(value):
  # all values required to be latin-1 encoded bytes
  value = value.decode('latin-1')

  # if not native string (e.g. PY2) try converting it back
  if not isinstance(value, str):
    try:
      value = value.encode('ascii')
    except UnicodeEncodeError:
      # cannot be represented in ASCII so leave it as unicode type
      pass

  def unirepl(m):
    backslashes = m.group(1)
    charcode = m.group(2)

    # if preceded by even number of backslashes, the \u is escaped
    if len(backslashes) % 2 == 0:
      return m.group(0)

    c = unichr(int(charcode, 16))
    # if unicode decodes to '\', re-escape it to unescape in the second step
    if c == '\\':
      c = u'\\\\'

    return backslashes + c

  value = re.sub(r'(\\+)u([0-9a-fA-F]{4})', unirepl, value)

  def bslashrepl(m):
    code = m.group(1)
    return _escapes.get(code, code)

  return re.sub(r'\\(.)', bslashrepl, value)

def _escape_comment(comment):
  comment = comment.replace('\r\n', '\n').replace('\r', '\n')
  comment = re.sub(r'\n(?![#!])', '\n#', comment)
  if isinstance(comment, text_type):
    comment = re.sub(u'[\u0100-\uffff]', _unicode_replace, comment)
    comment = comment.encode('latin-1')
  return b'#' + comment

def _escape_key(key):
  return _escape(key, _KEY_TERMINATORS)

def _escape_value(value):
  tail = value.lstrip()
  if len(tail) == len(value):
    return _escape(value)

  if tail:
    head = value[:-len(tail)]
  else:
    head = value

  # escape any leading whitespace, but leave other spaces intact
  return _escape(head, string.whitespace) + _escape(tail)

def _escape(value, chars=''):
  escape_chars = set(_escapes_rev)
  escape_chars.update(chars)
  escape_pattern = '[%s]' % re.escape(''.join(escape_chars))

  def esc(m):
    c = m.group(0)
    return _escapes_rev.get(c) or '\\' + c
  value = re.sub(escape_pattern, esc, value)

  value = re.sub(u'[\u0000-\u0019\u007f-\uffff]', _unicode_replace, value)

  return value.encode('latin-1')

def _unicode_replace(m):
  c = m.group(0)
  return r'\u%.4x' % ord(c)

def _split_key_value(line):
  if line[0] in _COMMENT_CHARS_BYTES:
    return COMMENT, line[1:]

  escaped = False
  key_buf = bytearray()

  line_orig = line
  line = bytearray(line)

  for idx, c in enumerate(line):
    if not escaped and c in _KEY_TERMINATORS_BYTES:
      key_terminated_fully = c in _KEY_TERMINATORS_EXPLICIT_BYTES
      break

    key_buf.append(c)
    escaped = c == ord('\\')

  else:
    # no key terminator, key is full line & value is blank
    return line_orig, b''

  value = line[idx+1:].lstrip()
  if not key_terminated_fully and value[:1] in _KEY_TERMINATORS_EXPLICIT_BYTES:
    value = value[1:].lstrip()

  return bytes(key_buf), bytes(value)

def _universal_newlines(fp):
  """
    Wrap a file to convert newlines regardless of whether the file was opened
    with the "universal newlines" option or not.
  """
  # if file was opened with universal newline support we don't need to convert
  if 'U' in getattr(fp, 'mode', ''):
    for line in fp:
      yield line
  else:
    for line in fp:
      line = line.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
      for piece in line.split(b'\n'):
        yield piece

def _property_lines(fp):
  buf = bytearray()
  for line in _universal_newlines(fp):
    m = _LINE_PATTERN.match(line)

    body = m.group('body')
    backslashes = m.group('backslashes')

    if len(backslashes) % 2 == 0:
      body += backslashes
      continuation = False
    else:
      body += backslashes[:-1]
      continuation = True

    if not body:
      continue

    buf.extend(body)

    if not continuation:
      yield bytes(buf)
      buf = bytearray()


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Version info"""

import sys
import importlib

short_version = '0.6'
version = '0.6.3'

def __get_mod_version(modname):

    try:
        if modname in sys.modules:
            mod = sys.modules[modname]
        else:
            mod = importlib.import_module(modname)
        try:
            return mod.__version__
        except AttributeError:
            return 'installed, no version number available'

    except ImportError:
        return None

def show_versions():
    '''Return the version information for all librosa dependencies.'''

    core_deps = ['audioread',
                 'numpy',
                 'scipy',
                 'sklearn',
                 'joblib',
                 'decorator',
                 'six',
                 'soundfile',
                 'resampy',
                 'numba']

    extra_deps = ['numpydoc',
                  'sphinx',
                  'sphinx_rtd_theme',
                  'sphinxcontrib.versioning',
                  'sphinx-gallery',
                  'pytest',
                  'pytest-mpl',
                  'pytest-cov',
                  'matplotlib']

    print('INSTALLED VERSIONS')
    print('------------------')
    print('python: {}\n'.format(sys.version))
    print('librosa: {}\n'.format(version))
    for dep in core_deps:
        print('{}: {}'.format(dep, __get_mod_version(dep)))
    print('')
    for dep in extra_deps:
        print('{}: {}'.format(dep, __get_mod_version(dep)))
    pass


#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''Deprecation utilities'''

import inspect
import warnings

class Deprecated(object):
    '''A dummy class to catch usage of deprecated variable names'''
    def __repr__(self):
        return '<DEPRECATED parameter>'

def rename_kw(old_name, old_value, new_name, new_value,
              version_deprecated, version_removed):
    '''Handle renamed arguments.

    Parameters
    ----------
    old_name : str
    old_value
        The name and value of the old argument

    new_name : str
    new_value
        The name and value of the new argument

    version_deprecated : str
        The version at which the old name became deprecated

    version_removed : str
        The version at which the old name will be removed

    Returns
    -------
    value
        - `new_value` if `old_value` of type `Deprecated`
        - `old_value` otherwise

    Warnings
    --------
    if `old_value` is not of type `Deprecated`

    '''
    if isinstance(old_value, Deprecated):
        return new_value
    else:
        stack = inspect.stack()
        dep_func = stack[1]
        caller = stack[2]

        warnings.warn_explicit("{:s}() keyword argument '{:s}' has been "
                               "renamed to '{:s}' in version {:}."
                               "\n\tThis alias will be removed in version "
                               "{:}.".format(dep_func[3],
                                             old_name, new_name,
                                             version_deprecated,
                                             version_removed),
                               category=DeprecationWarning,
                               filename=caller[1],
                               lineno=caller[2])

        return old_value


#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''Fast Fourier Transform (FFT) library container'''

__all__ = ['get_fftlib', 'set_fftlib']

# Object to hold FFT interfaces
__FFTLIB = None

def set_fftlib(lib=None):
    '''Set the FFT library used by librosa.

    Parameters
    ----------
    lib : None or module
        Must implement an interface compatible with `numpy.fft`.
        If `None`, reverts to `numpy.fft`.

    Examples
    --------
    Use `pyfftw`:

    >>> import pyfftw
    >>> librosa.set_fftlib(pyfftw.interfaces.numpy_fft)

    Reset to default `numpy` implementation

    >>> librosa.set_fftlib()

    '''

    global __FFTLIB
    if lib is None:
        from numpy import fft
        lib = fft

    __FFTLIB = lib

def get_fftlib():
    '''Get the FFT library currently used by librosa

    Returns
    -------
    fft : module
        The FFT library currently used by librosa.
        Must API-compatible with `numpy.fft`.
    '''
    global __FFTLIB
    return __FFTLIB

# Set the FFT library to numpy's, by default
set_fftlib(None)


#!/usr/bin/env python
'''
CREATED:2013-02-11 18:37:30 by Brian McFee <brm2132@columbia.edu>

Track beat events in an audio file

Usage:   ./beat_tracker.py [-h] input_file.mp3    output_beats.csv
'''
from __future__ import print_function

import argparse
import sys
import librosa

def beat_track(input_file, output_csv):
    '''Beat tracking function

    :parameters:
      - input_file : str
          Path to input audio file (wav, mp3, m4a, flac, etc.)

      - output_file : str
          Path to save beat event timestamps as a CSV file
    '''

    print('Loading ', input_file)
    y, sr = librosa.load(input_file, sr=22050)

    # Use a default hop size of 512 samples @ 22KHz ~= 23ms
    hop_length = 512

    # This is the window length used by default in stft
    print('Tracking beats')
    tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=hop_length)

    print('Estimated tempo: {:0.2f} beats per minute'.format(tempo))

    # save output
    # 'beats' will contain the frame numbers of beat events.
    beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=hop_length)

    print('Saving output to ', output_csv)
    librosa.output.times_csv(output_csv, beat_times)
    print('done!')

def process_arguments(args):
    '''Argparse function to get the program parameters'''

    parser = argparse.ArgumentParser(description='Beat tracking example')

    parser.add_argument('input_file',
                        action='store',
                        help='path to the input file (wav, mp3, etc)')

    parser.add_argument('output_file',
                        action='store',
                        help='path to the output file (csv of beat times)')

    return vars(parser.parse_args(args))

if __name__ == '__main__':
    # Get the parameters
    parameters = process_arguments(sys.argv[1:])

    # Run the beat tracker
    beat_track(parameters['input_file'], parameters['output_file'])


#!/usr/bin/env python
'''CREATED:2014-05-22 16:43:44 by Brian McFee <brm2132@columbia.edu>

Pitch-shift a recording to be in A440 tuning.

Usage: ./adjust_tuning.py [-h] input_file output_file
'''
from __future__ import print_function

import argparse
import sys
import librosa

def adjust_tuning(input_file, output_file):
    '''Load audio, estimate tuning, apply pitch correction, and save.'''
    print('Loading ', input_file)
    y, sr = librosa.load(input_file)

    print('Separating harmonic component ... ')
    y_harm = librosa.effects.harmonic(y)

    print('Estimating tuning ... ')
    # Just track the pitches associated with high magnitude
    tuning = librosa.estimate_tuning(y=y_harm, sr=sr)

    print('{:+0.2f} cents'.format(100 * tuning))
    print('Applying pitch-correction of {:+0.2f} cents'.format(-100 * tuning))
    y_tuned = librosa.effects.pitch_shift(y, sr, -tuning)

    print('Saving tuned audio to: ', output_file)
    librosa.output.write_wav(output_file, y_tuned, sr)

def process_arguments(args):
    '''Argparse function to get the program parameters'''

    parser = argparse.ArgumentParser(description='Tuning adjustment example')

    parser.add_argument('input_file',
                        action='store',
                        help='path to the input file (wav, mp3, etc)')

    parser.add_argument('output_file',
                        action='store',
                        help='path to store the output wav')

    return vars(parser.parse_args(args))

if __name__ == '__main__':
    # Get the parameters
    params = process_arguments(sys.argv[1:])

    # Run the beat tracker
    adjust_tuning(params['input_file'], params['output_file'])


#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''Time and frequency utilities'''

import re

import numpy as np
import six

from ..util.exceptions import ParameterError

__all__ = ['frames_to_samples', 'frames_to_time',
           'samples_to_frames', 'samples_to_time',
           'time_to_samples', 'time_to_frames',
           'note_to_hz', 'note_to_midi',
           'midi_to_hz', 'midi_to_note',
           'hz_to_note', 'hz_to_midi',
           'hz_to_mel', 'hz_to_octs',
           'mel_to_hz',
           'octs_to_hz',
           'fft_frequencies',
           'cqt_frequencies',
           'mel_frequencies',
           'tempo_frequencies',
           'A_weighting',
           'samples_like',
           'times_like']

def frames_to_samples(frames, hop_length=512, n_fft=None):
    """Converts frame indices to audio sample indices.

    Parameters
    ----------
    frames     : number or np.ndarray [shape=(n,)]
        frame index or vector of frame indices

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `n_fft / 2`
        to counteract windowing effects when using a non-centered STFT.

    Returns
    -------
    times : number or np.ndarray
        time (in samples) of each given frame number:
        `times[i] = frames[i] * hop_length`

    See Also
    --------
    frames_to_time : convert frame indices to time values
    samples_to_frames : convert sample indices to frame indices

    Examples
    --------
    >>> y, sr = librosa.load(librosa.util.example_audio_file())
    >>> tempo, beats = librosa.beat.beat_track(y, sr=sr)
    >>> beat_samples = librosa.frames_to_samples(beats)
    """

    offset = 0
    if n_fft is not None:
        offset = int(n_fft // 2)

    return (np.asanyarray(frames) * hop_length + offset).astype(int)

def samples_to_frames(samples, hop_length=512, n_fft=None):
    """Converts sample indices into STFT frames.

    Examples
    --------
    >>> # Get the frame numbers for every 256 samples
    >>> librosa.samples_to_frames(np.arange(0, 22050, 256))
    array([ 0,  0,  1,  1,  2,  2,  3,  3,  4,  4,  5,  5,  6,  6,
            7,  7,  8,  8,  9,  9, 10, 10, 11, 11, 12, 12, 13, 13,
           14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20,
           21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
           28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34, 34,
           35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 40, 40, 41, 41,
           42, 42, 43])

    Parameters
    ----------
    samples : int or np.ndarray [shape=(n,)]
        sample index or vector of sample indices

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `- n_fft / 2`
        to counteract windowing effects in STFT.

        .. note:: This may result in negative frame indices.

    Returns
    -------
    frames : int or np.ndarray [shape=(n,), dtype=int]
        Frame numbers corresponding to the given times:
        `frames[i] = floor( samples[i] / hop_length )`

    See Also
    --------
    samples_to_time : convert sample indices to time values
    frames_to_samples : convert frame indices to sample indices
    """

    offset = 0
    if n_fft is not None:
        offset = int(n_fft // 2)

    samples = np.asanyarray(samples)
    return np.floor((samples - offset) // hop_length).astype(int)

def frames_to_time(frames, sr=22050, hop_length=512, n_fft=None):
    """Converts frame counts to time (seconds).

    Parameters
    ----------
    frames     : np.ndarray [shape=(n,)]
        frame index or vector of frame indices

    sr         : number > 0 [scalar]
        audio sampling rate

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `n_fft / 2`
        to counteract windowing effects when using a non-centered STFT.

    Returns
    -------
    times : np.ndarray [shape=(n,)]
        time (in seconds) of each given frame number:
        `times[i] = frames[i] * hop_length / sr`

    See Also
    --------
    time_to_frames : convert time values to frame indices
    frames_to_samples : convert frame indices to sample indices

    Examples
    --------
    >>> y, sr = librosa.load(librosa.util.example_audio_file())
    >>> tempo, beats = librosa.beat.beat_track(y, sr=sr)
    >>> beat_times = librosa.frames_to_time(beats, sr=sr)
    """

    samples = frames_to_samples(frames,
                                hop_length=hop_length,
                                n_fft=n_fft)

    return samples_to_time(samples, sr=sr)

def time_to_frames(times, sr=22050, hop_length=512, n_fft=None):
    """Converts time stamps into STFT frames.

    Parameters
    ----------
    times : np.ndarray [shape=(n,)]
        time (in seconds) or vector of time values

    sr : number > 0 [scalar]
        audio sampling rate

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `- n_fft / 2`
        to counteract windowing effects in STFT.

        .. note:: This may result in negative frame indices.

    Returns
    -------
    frames : np.ndarray [shape=(n,), dtype=int]
        Frame numbers corresponding to the given times:
        `frames[i] = floor( times[i] * sr / hop_length )`

    See Also
    --------
    frames_to_time : convert frame indices to time values
    time_to_samples : convert time values to sample indices

    Examples
    --------
    Get the frame numbers for every 100ms

    >>> librosa.time_to_frames(np.arange(0, 1, 0.1),
    ...                         sr=22050, hop_length=512)
    array([ 0,  4,  8, 12, 17, 21, 25, 30, 34, 38])

    """

    samples = time_to_samples(times, sr=sr)

    return samples_to_frames(samples, hop_length=hop_length, n_fft=n_fft)

def time_to_samples(times, sr=22050):
    '''Convert timestamps (in seconds) to sample indices.

    Parameters
    ----------
    times : number or np.ndarray
        Time value or array of time values (in seconds)

    sr : number > 0
        Sampling rate

    Returns
    -------
    samples : int or np.ndarray [shape=times.shape, dtype=int]
        Sample indices corresponding to values in `times`

    See Also
    --------
    time_to_frames : convert time values to frame indices
    samples_to_time : convert sample indices to time values

    Examples
    --------
    >>> librosa.time_to_samples(np.arange(0, 1, 0.1), sr=22050)
    array([    0,  2205,  4410,  6615,  8820, 11025, 13230, 15435,
           17640, 19845])

    '''

    return (np.asanyarray(times) * sr).astype(int)

def samples_to_time(samples, sr=22050):
    '''Convert sample indices to time (in seconds).

    Parameters
    ----------
    samples : np.ndarray
        Sample index or array of sample indices

    sr : number > 0
        Sampling rate

    Returns
    -------
    times : np.ndarray [shape=samples.shape, dtype=int]
        Time values corresponding to `samples` (in seconds)

    See Also
    --------
    samples_to_frames : convert sample indices to frame indices
    time_to_samples : convert time values to sample indices

    Examples
    --------
    Get timestamps corresponding to every 512 samples

    >>> librosa.samples_to_time(np.arange(0, 22050, 512))
    array([ 0.   ,  0.023,  0.046,  0.07 ,  0.093,  0.116,  0.139,
            0.163,  0.186,  0.209,  0.232,  0.255,  0.279,  0.302,
            0.325,  0.348,  0.372,  0.395,  0.418,  0.441,  0.464,
            0.488,  0.511,  0.534,  0.557,  0.58 ,  0.604,  0.627,
            0.65 ,  0.673,  0.697,  0.72 ,  0.743,  0.766,  0.789,
            0.813,  0.836,  0.859,  0.882,  0.906,  0.929,  0.952,
            0.975,  0.998])
    '''

    return np.asanyarray(samples) / float(sr)

def note_to_hz(note, **kwargs):
    '''Convert one or more note names to frequency (Hz)

    Examples
    --------
    >>> # Get the frequency of a note
    >>> librosa.note_to_hz('C')
    array([ 16.352])
    >>> # Or multiple notes
    >>> librosa.note_to_hz(['A3', 'A4', 'A5'])
    array([ 220.,  440.,  880.])
    >>> # Or notes with tuning deviations
    >>> librosa.note_to_hz('C2-32', round_midi=False)
    array([ 64.209])

    Parameters
    ----------
    note : str or iterable of str
        One or more note names to convert

    kwargs : additional keyword arguments
        Additional parameters to `note_to_midi`

    Returns
    -------
    frequencies : number or np.ndarray [shape=(len(note),)]
        Array of frequencies (in Hz) corresponding to `note`

    See Also
    --------
    midi_to_hz
    note_to_midi
    hz_to_note
    '''
    return midi_to_hz(note_to_midi(note, **kwargs))

def note_to_midi(note, round_midi=True):
    '''Convert one or more spelled notes to MIDI number(s).

    Notes may be spelled out with optional accidentals or octave numbers.

    The leading note name is case-insensitive.

    Sharps are indicated with ``#``, flats may be indicated with ``!`` or ``b``.

    Parameters
    ----------
    note : str or iterable of str
        One or more note names.

    round_midi : bool
        - If `True`, allow for fractional midi notes
        - Otherwise, round cent deviations to the nearest note

    Returns
    -------
    midi : float or np.array
        Midi note numbers corresponding to inputs.

    Raises
    ------
    ParameterError
        If the input is not in valid note format

    See Also
    --------
    midi_to_note
    note_to_hz

    Examples
    --------
    >>> librosa.note_to_midi('C')
    12
    >>> librosa.note_to_midi('C#3')
    49
    >>> librosa.note_to_midi('f4')
    65
    >>> librosa.note_to_midi('Bb-1')
    10
    >>> librosa.note_to_midi('A!8')
    116
    >>> # Lists of notes also work
    >>> librosa.note_to_midi(['C', 'E', 'G'])
    array([12, 16, 19])

    '''

    if not isinstance(note, six.string_types):
        return np.array([note_to_midi(n, round_midi=round_midi) for n in note])

    pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
    acc_map = {'#': 1, '': 0, 'b': -1, '!': -1}

    match = re.match(r'^(?P<note>[A-Ga-g])'
                     r'(?P<accidental>[#b!]*)'
                     r'(?P<octave>[+-]?\d+)?'
                     r'(?P<cents>[+-]\d+)?$',
                     note)
    if not match:
        raise ParameterError('Improper note format: {:s}'.format(note))

    pitch = match.group('note').upper()
    offset = np.sum([acc_map[o] for o in match.group('accidental')])
    octave = match.group('octave')
    cents = match.group('cents')

    if not octave:
        octave = 0
    else:
        octave = int(octave)

    if not cents:
        cents = 0
    else:
        cents = int(cents) * 1e-2

    note_value = 12 * (octave + 1) + pitch_map[pitch] + offset + cents

    if round_midi:
        note_value = int(np.round(note_value))

    return note_value

def midi_to_note(midi, octave=True, cents=False):
    '''Convert one or more MIDI numbers to note strings.

    MIDI numbers will be rounded to the nearest integer.

    Notes will be of the format 'C0', 'C#0', 'D0', ...

    Examples
    --------
    >>> librosa.midi_to_note(0)
    'C-1'
    >>> librosa.midi_to_note(37)
    'C#2'
    >>> librosa.midi_to_note(-2)
    'A#-2'
    >>> librosa.midi_to_note(104.7)
    'A7'
    >>> librosa.midi_to_note(104.7, cents=True)
    'A7-30'
    >>> librosa.midi_to_note(list(range(12, 24)))
    ['C0', 'C#0', 'D0', 'D#0', 'E0', 'F0', 'F#0', 'G0', 'G#0', 'A0', 'A#0', 'B0']

    Parameters
    ----------
    midi : int or iterable of int
        Midi numbers to convert.

    octave: bool
        If True, include the octave number

    cents: bool
        If true, cent markers will be appended for fractional notes.
        Eg, `midi_to_note(69.3, cents=True)` == `A4+03`

    Returns
    -------
    notes : str or iterable of str
        Strings describing each midi note.

    Raises
    ------
    ParameterError
        if `cents` is True and `octave` is False

    See Also
    --------
    midi_to_hz
    note_to_midi
    hz_to_note
    '''

    if cents and not octave:
        raise ParameterError('Cannot encode cents without octave information.')

    if not np.isscalar(midi):
        return [midi_to_note(x, octave=octave, cents=cents) for x in midi]

    note_map = ['C', 'C#', 'D', 'D#',
                'E', 'F', 'F#', 'G',
                'G#', 'A', 'A#', 'B']

    note_num = int(np.round(midi))
    note_cents = int(100 * np.around(midi - note_num, 2))

    note = note_map[note_num % 12]

    if octave:
        note = '{:s}{:0d}'.format(note, int(note_num / 12) - 1)
    if cents:
        note = '{:s}{:+02d}'.format(note, note_cents)

    return note

def midi_to_hz(notes):
    """Get the frequency (Hz) of MIDI note(s)

    Examples
    --------
    >>> librosa.midi_to_hz(36)
    65.406

    >>> librosa.midi_to_hz(np.arange(36, 48))
    array([  65.406,   69.296,   73.416,   77.782,   82.407,
             87.307,   92.499,   97.999,  103.826,  110.   ,
            116.541,  123.471])

    Parameters
    ----------
    notes       : int or np.ndarray [shape=(n,), dtype=int]
        midi number(s) of the note(s)

    Returns
    -------
    frequency   : number or np.ndarray [shape=(n,), dtype=float]
        frequency (frequencies) of `notes` in Hz

    See Also
    --------
    hz_to_midi
    note_to_hz
    """

    return 440.0 * (2.0 ** ((np.asanyarray(notes) - 69.0)/12.0))

def hz_to_midi(frequencies):
    """Get MIDI note number(s) for given frequencies

    Examples
    --------
    >>> librosa.hz_to_midi(60)
    34.506
    >>> librosa.hz_to_midi([110, 220, 440])
    array([ 45.,  57.,  69.])

    Parameters
    ----------
    frequencies   : float or np.ndarray [shape=(n,), dtype=float]
        frequencies to convert

    Returns
    -------
    note_nums     : number or np.ndarray [shape=(n,), dtype=float]
        MIDI notes to `frequencies`

    See Also
    --------
    midi_to_hz
    note_to_midi
    hz_to_note
    """

    return 12 * (np.log2(np.asanyarray(frequencies)) - np.log2(440.0)) + 69

def hz_to_note(frequencies, **kwargs):
    '''Convert one or more frequencies (in Hz) to the nearest note names.

    Parameters
    ----------
    frequencies : float or iterable of float
        Input frequencies, specified in Hz

    kwargs : additional keyword arguments
        Arguments passed through to `midi_to_note`

    Returns
    -------
    notes : list of str
        `notes[i]` is the closest note name to `frequency[i]`
        (or `frequency` if the input is scalar)

    See Also
    --------
    hz_to_midi
    midi_to_note
    note_to_hz

    Examples
    --------
    Get a single note name for a frequency

    >>> librosa.hz_to_note(440.0)
    ['A5']

    Get multiple notes with cent deviation

    >>> librosa.hz_to_note([32, 64], cents=True)
    ['C1-38', 'C2-38']

    Get multiple notes, but suppress octave labels

    >>> librosa.hz_to_note(440.0 * (2.0 ** np.linspace(0, 1, 12)),
    ...                    octave=False)
    ['A', 'A#', 'B', 'C', 'C#', 'D', 'E', 'F', 'F#', 'G', 'G#', 'A']

    '''
    return midi_to_note(hz_to_midi(frequencies), **kwargs)

def hz_to_mel(frequencies, htk=False):
    """Convert Hz to Mels

    Examples
    --------
    >>> librosa.hz_to_mel(60)
    0.9
    >>> librosa.hz_to_mel([110, 220, 440])
    array([ 1.65,  3.3 ,  6.6 ])

    Parameters
    ----------
    frequencies   : number or np.ndarray [shape=(n,)] , float
        scalar or array of frequencies
    htk           : bool
        use HTK formula instead of Slaney

    Returns
    -------
    mels        : number or np.ndarray [shape=(n,)]
        input frequencies in Mels

    See Also
    --------
    mel_to_hz
    """

    frequencies = np.asanyarray(frequencies)

    if htk:
        return 2595.0 * np.log10(1.0 + frequencies / 700.0)

    # Fill in the linear part
    f_min = 0.0
    f_sp = 200.0 / 3

    mels = (frequencies - f_min) / f_sp

    # Fill in the log-scale part

    min_log_hz = 1000.0                         # beginning of log region (Hz)
    min_log_mel = (min_log_hz - f_min) / f_sp   # same (Mels)
    logstep = np.log(6.4) / 27.0                # step size for log region

    if frequencies.ndim:
        # If we have array data, vectorize
        log_t = (frequencies >= min_log_hz)
        mels[log_t] = min_log_mel + np.log(frequencies[log_t]/min_log_hz) / logstep
    elif frequencies >= min_log_hz:
        # If we have scalar data, heck directly
        mels = min_log_mel + np.log(frequencies / min_log_hz) / logstep

    return mels

def mel_to_hz(mels, htk=False):
    """Convert mel bin numbers to frequencies

    Examples
    --------
    >>> librosa.mel_to_hz(3)
    200.

    >>> librosa.mel_to_hz([1,2,3,4,5])
    array([  66.667,  133.333,  200.   ,  266.667,  333.333])

    Parameters
    ----------
    mels          : np.ndarray [shape=(n,)], float
        mel bins to convert
    htk           : bool
        use HTK formula instead of Slaney

    Returns
    -------
    frequencies   : np.ndarray [shape=(n,)]
        input mels in Hz

    See Also
    --------
    hz_to_mel
    """

    mels = np.asanyarray(mels)

    if htk:
        return 700.0 * (10.0**(mels / 2595.0) - 1.0)

    # Fill in the linear scale
    f_min = 0.0
    f_sp = 200.0 / 3
    freqs = f_min + f_sp * mels

    # And now the nonlinear scale
    min_log_hz = 1000.0                         # beginning of log region (Hz)
    min_log_mel = (min_log_hz - f_min) / f_sp   # same (Mels)
    logstep = np.log(6.4) / 27.0                # step size for log region

    if mels.ndim:
        # If we have vector data, vectorize
        log_t = (mels >= min_log_mel)
        freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel))
    elif mels >= min_log_mel:
        # If we have scalar data, check directly
        freqs = min_log_hz * np.exp(logstep * (mels - min_log_mel))

    return freqs

def hz_to_octs(frequencies, A440=440.0):
    """Convert frequencies (Hz) to (fractional) octave numbers.

    Examples
    --------
    >>> librosa.hz_to_octs(440.0)
    4.
    >>> librosa.hz_to_octs([32, 64, 128, 256])
    array([ 0.219,  1.219,  2.219,  3.219])

    Parameters
    ----------
    frequencies   : number >0 or np.ndarray [shape=(n,)] or float
        scalar or vector of frequencies
    A440          : float
        frequency of A440 (in Hz)

    Returns
    -------
    octaves       : number or np.ndarray [shape=(n,)]
        octave number for each frequency

    See Also
    --------
    octs_to_hz
    """
    return np.log2(np.asanyarray(frequencies) / (float(A440) / 16))

def octs_to_hz(octs, A440=440.0):
    """Convert octaves numbers to frequencies.

    Octaves are counted relative to A.

    Examples
    --------
    >>> librosa.octs_to_hz(1)
    55.
    >>> librosa.octs_to_hz([-2, -1, 0, 1, 2])
    array([   6.875,   13.75 ,   27.5  ,   55.   ,  110.   ])

    Parameters
    ----------
    octaves       : np.ndarray [shape=(n,)] or float
        octave number for each frequency
    A440          : float
        frequency of A440

    Returns
    -------
    frequencies   : number or np.ndarray [shape=(n,)]
        scalar or vector of frequencies

    See Also
    --------
    hz_to_octs
    """
    return (float(A440) / 16)*(2.0**np.asanyarray(octs))

def fft_frequencies(sr=22050, n_fft=2048):
    '''Alternative implementation of `np.fft.fftfreq`

    Parameters
    ----------
    sr : number > 0 [scalar]
        Audio sampling rate

    n_fft : int > 0 [scalar]
        FFT window size

    Returns
    -------
    freqs : np.ndarray [shape=(1 + n_fft/2,)]
        Frequencies `(0, sr/n_fft, 2*sr/n_fft, ..., sr/2)`

    Examples
    --------
    >>> librosa.fft_frequencies(sr=22050, n_fft=16)
    array([     0.   ,   1378.125,   2756.25 ,   4134.375,
             5512.5  ,   6890.625,   8268.75 ,   9646.875,  11025.   ])

    '''

    return np.linspace(0,
                       float(sr) / 2,
                       int(1 + n_fft//2),
                       endpoint=True)

def cqt_frequencies(n_bins, fmin, bins_per_octave=12, tuning=0.0):
    """Compute the center frequencies of Constant-Q bins.

    Examples
    --------
    >>> # Get the CQT frequencies for 24 notes, starting at C2
    >>> librosa.cqt_frequencies(24, fmin=librosa.note_to_hz('C2'))
    array([  65.406,   69.296,   73.416,   77.782,   82.407,   87.307,
             92.499,   97.999,  103.826,  110.   ,  116.541,  123.471,
            130.813,  138.591,  146.832,  155.563,  164.814,  174.614,
            184.997,  195.998,  207.652,  220.   ,  233.082,  246.942])

    Parameters
    ----------
    n_bins  : int > 0 [scalar]
        Number of constant-Q bins

    fmin    : float > 0 [scalar]
        Minimum frequency

    bins_per_octave : int > 0 [scalar]
        Number of bins per octave

    tuning : float in `[-0.5, +0.5)`
        Deviation from A440 tuning in fractional bins (cents)

    Returns
    -------
    frequencies : np.ndarray [shape=(n_bins,)]
        Center frequency for each CQT bin
    """

    correction = 2.0**(float(tuning) / bins_per_octave)
    frequencies = 2.0**(np.arange(0, n_bins, dtype=float) / bins_per_octave)

    return correction * fmin * frequencies

def mel_frequencies(n_mels=128, fmin=0.0, fmax=11025.0, htk=False):
    """Compute an array of acoustic frequencies tuned to the mel scale.

    The mel scale is a quasi-logarithmic function of acoustic frequency
    designed such that perceptually similar pitch intervals (e.g. octaves)
    appear equal in width over the full hearing range.

    Because the definition of the mel scale is conditioned by a finite number
    of subjective psychoaoustical experiments, several implementations coexist
    in the audio signal processing literature [1]_. By default, librosa replicates
    the behavior of the well-established MATLAB Auditory Toolbox of Slaney [2]_.
    According to this default implementation,  the conversion from Hertz to mel is
    linear below 1 kHz and logarithmic above 1 kHz. Another available implementation
    replicates the Hidden Markov Toolkit [3]_ (HTK) according to the following formula:

    `mel = 2595.0 * np.log10(1.0 + f / 700.0).`

    The choice of implementation is determined by the `htk` keyword argument: setting
    `htk=False` leads to the Auditory toolbox implementation, whereas setting it `htk=True`
    leads to the HTK implementation.

    .. [1] Umesh, S., Cohen, L., & Nelson, D. Fitting the mel scale.
        In Proc. International Conference on Acoustics, Speech, and Signal Processing
        (ICASSP), vol. 1, pp. 217-220, 1998.

    .. [2] Slaney, M. Auditory Toolbox: A MATLAB Toolbox for Auditory
        Modeling Work. Technical Report, version 2, Interval Research Corporation, 1998.

    .. [3] Young, S., Evermann, G., Gales, M., Hain, T., Kershaw, D., Liu, X.,
        Moore, G., Odell, J., Ollason, D., Povey, D., Valtchev, V., & Woodland, P.
        The HTK book, version 3.4. Cambridge University, March 2009.

    See Also
    --------
    hz_to_mel
    mel_to_hz
    librosa.feature.melspectrogram
    librosa.feature.mfcc

    Parameters
    ----------
    n_mels    : int > 0 [scalar]
        Number of mel bins.

    fmin      : float >= 0 [scalar]
        Minimum frequency (Hz).

    fmax      : float >= 0 [scalar]
        Maximum frequency (Hz).

    htk       : bool
        If True, use HTK formula to convert Hz to mel.
        Otherwise (False), use Slaney's Auditory Toolbox.

    Returns
    -------
    bin_frequencies : ndarray [shape=(n_mels,)]
        Vector of n_mels frequencies in Hz which are uniformly spaced on the Mel
        axis.

    Examples
    --------
    >>> librosa.mel_frequencies(n_mels=40)
    array([     0.   ,     85.317,    170.635,    255.952,
              341.269,    426.586,    511.904,    597.221,
              682.538,    767.855,    853.173,    938.49 ,
             1024.856,   1119.114,   1222.042,   1334.436,
             1457.167,   1591.187,   1737.532,   1897.337,
             2071.84 ,   2262.393,   2470.47 ,   2697.686,
             2945.799,   3216.731,   3512.582,   3835.643,
             4188.417,   4573.636,   4994.285,   5453.621,
             5955.205,   6502.92 ,   7101.009,   7754.107,
             8467.272,   9246.028,  10096.408,  11025.   ])

    """

    # 'Center freqs' of mel bands - uniformly spaced between limits
    min_mel = hz_to_mel(fmin, htk=htk)
    max_mel = hz_to_mel(fmax, htk=htk)

    mels = np.linspace(min_mel, max_mel, n_mels)

    return mel_to_hz(mels, htk=htk)

def tempo_frequencies(n_bins, hop_length=512, sr=22050):
    '''Compute the frequencies (in beats-per-minute) corresponding
    to an onset auto-correlation or tempogram matrix.

    Parameters
    ----------
    n_bins : int > 0
        The number of lag bins

    hop_length : int > 0
        The number of samples between each bin

    sr : number > 0
        The audio sampling rate

    Returns
    -------
    bin_frequencies : ndarray [shape=(n_bins,)]
        vector of bin frequencies measured in BPM.

        .. note:: `bin_frequencies[0] = +np.inf` corresponds to 0-lag

    Examples
    --------
    Get the tempo frequencies corresponding to a 384-bin (8-second) tempogram

    >>> librosa.tempo_frequencies(384)
    array([      inf,  2583.984,  1291.992, ...,     6.782,
               6.764,     6.747])
    '''

    bin_frequencies = np.zeros(int(n_bins), dtype=np.float)

    bin_frequencies[0] = np.inf
    bin_frequencies[1:] = 60.0 * sr / (hop_length * np.arange(1.0, n_bins))

    return bin_frequencies

# A-weighting should be capitalized: suppress the naming warning
def A_weighting(frequencies, min_db=-80.0):     # pylint: disable=invalid-name
    '''Compute the A-weighting of a set of frequencies.

    Parameters
    ----------
    frequencies : scalar or np.ndarray [shape=(n,)]
        One or more frequencies (in Hz)

    min_db : float [scalar] or None
        Clip weights below this threshold.
        If `None`, no clipping is performed.

    Returns
    -------
    A_weighting : scalar or np.ndarray [shape=(n,)]
        `A_weighting[i]` is the A-weighting of `frequencies[i]`

    See Also
    --------
    perceptual_weighting

    Examples
    --------

    Get the A-weighting for CQT frequencies

    >>> import matplotlib.pyplot as plt
    >>> freqs = librosa.cqt_frequencies(108, librosa.note_to_hz('C1'))
    >>> aw = librosa.A_weighting(freqs)
    >>> plt.plot(freqs, aw)
    >>> plt.xlabel('Frequency (Hz)')
    >>> plt.ylabel('Weighting (log10)')
    >>> plt.title('A-Weighting of CQT frequencies')

    '''

    # Vectorize to make our lives easier
    frequencies = np.asanyarray(frequencies)

    # Pre-compute squared frequency
    f_sq = frequencies**2.0

    const = np.array([12200, 20.6, 107.7, 737.9])**2.0

    weights = 2.0 + 20.0 * (np.log10(const[0]) + 4 * np.log10(frequencies)
                            - np.log10(f_sq + const[0])
                            - np.log10(f_sq + const[1])
                            - 0.5 * np.log10(f_sq + const[2])
                            - 0.5 * np.log10(f_sq + const[3]))

    if min_db is not None:
        weights = np.maximum(min_db, weights)

    return weights

def times_like(X, sr=22050, hop_length=512, n_fft=None, axis=-1):
    """Return an array of time values to match the time axis from a feature matrix.

    Parameters
    ----------
    X : np.ndarray or scalar
        - If ndarray, X is a feature matrix, e.g. STFT, chromagram, or mel spectrogram.
        - If scalar, X represents the number of frames.

    sr : number > 0 [scalar]
        audio sampling rate

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `n_fft / 2`
        to counteract windowing effects when using a non-centered STFT.

    axis : int [scalar]
        The axis representing the time axis of X.
        By default, the last axis (-1) is taken.

    Returns
    -------
    times : np.ndarray [shape=(n,)]
        ndarray of times (in seconds) corresponding to each frame of X.

    See Also
    --------
    samples_like : Return an array of sample indices to match the time axis from a feature matrix.

    Examples
    --------
    Provide a feature matrix input:

    >>> y, sr = librosa.load(librosa.util.example_audio_file())
    >>> X = librosa.stft(y)
    >>> times = librosa.times_like(X)
    >>> times
    array([  0.00000000e+00,   2.32199546e-02,   4.64399093e-02, ...,
             6.13935601e+01,   6.14167800e+01,   6.14400000e+01])

    Provide a scalar input:

    >>> n_frames = 2647
    >>> times = librosa.times_like(n_frames)
    >>> times
    array([  0.00000000e+00,   2.32199546e-02,   4.64399093e-02, ...,
             6.13935601e+01,   6.14167800e+01,   6.14400000e+01])
    """
    samples = samples_like(X, hop_length=hop_length, n_fft=n_fft, axis=axis)
    return samples_to_time(samples, sr=sr)

def samples_like(X, hop_length=512, n_fft=None, axis=-1):
    """Return an array of sample indices to match the time axis from a feature matrix.

    Parameters
    ----------
    X : np.ndarray or scalar
        - If ndarray, X is a feature matrix, e.g. STFT, chromagram, or mel spectrogram.
        - If scalar, X represents the number of frames.

    hop_length : int > 0 [scalar]
        number of samples between successive frames

    n_fft : None or int > 0 [scalar]
        Optional: length of the FFT window.
        If given, time conversion will include an offset of `n_fft / 2`
        to counteract windowing effects when using a non-centered STFT.

    axis : int [scalar]
        The axis representing the time axis of X.
        By default, the last axis (-1) is taken.

    Returns
    -------
    samples : np.ndarray [shape=(n,)]
        ndarray of sample indices corresponding to each frame of X.

    See Also
    --------
    times_like : Return an array of time values to match the time axis from a feature matrix.

    Examples
    --------
    Provide a feature matrix input:

    >>> y, sr = librosa.load(librosa.util.example_audio_file())
    >>> X = librosa.stft(y)
    >>> samples = librosa.samples_like(X)
    >>> samples
    array([      0,     512,    1024, ..., 1353728, 1354240, 1354752])

    Provide a scalar input:

    >>> n_frames = 2647
    >>> samples = librosa.samples_like(n_frames)
    >>> samples
    array([      0,     512,    1024, ..., 1353728, 1354240, 1354752])
    """
    if np.isscalar(X):
        frames = np.arange(X)
    else:
        frames = np.arange(X.shape[axis])
    return frames_to_samples(frames, hop_length=hop_length, n_fft=n_fft)