Fork me on GitHub
Release:
Date:
1.3
Feb 14, 2012
Flattr Mktoc

Table Of Contents

Download

Get latest source archive,
mktoc-1.3.tar.gz, or install with:

pip install mktoc --upgrade --user

Found a Bug?

Fill out a report on the issue tracker.

Source code for mktoc.disc

#  Copyright (c) 2011, Patrick C. McGinty
#
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the Simplified BSD License.
#
#  See LICENSE text for more details.
"""
   mktoc.disc
   ~~~~~~~~~~

   A set of classes for representation of audio CD information.

   The following are a list of the classes provided in this module:

   * :class:`Disc`
   * :class:`Track`
   * :class:`TrackIndex`
"""

import os
import re
import wave
import logging
import itertools as itr

from mktoc.base import *

__all__ = [ 'Disc', 'Track', 'TrackIndex' ]

log = logging.getLogger('mktoc.disc')


[docs]class Disc( object ): """ Stores audio disc metadata values such as album title, performer, genre. """ #: Disc mode string for single session MODE_SINGLE_SESSION = 'CD_DA' #: Disc mode string for multi sessions MODE_MULTI_SESSION = 'CD_ROM_XA' #: String representing the Catalog Id of a disc. catalog = None #: String representing the release year of a disc. date = None #: String representing the DiscID value of a disc. This value is assumed to #: be correct and not verified by any internal logic. discid = None #: String representing the genre of a disc genre = None #: String representing the performer or artist of a disc. performer = None #: String representing the album title of a disc. title = None def __init__(self): self.is_multisession = False def __str__(self): raise NotImplementedError def __unicode__(self): """Return a string of TOC formatted disc information.""" out = [u'%s' % self._mode] if self.catalog: out += [u'CATALOG "%s"' % self.catalog] out += [u'CD_TEXT { LANGUAGE_MAP { 0:EN }\n\tLANGUAGE 0 {'] if self.title: out += [u'\t\tTITLE "%s"' % self.title] if self.performer: out += [u'\t\tPERFORMER "%s"' % self.performer] if self.discid: out += [u'\t\tDISC_ID "%s"' % self.discid] out += [u'}}'] return u'\n'.join(out)
[docs] def set_field(self, name, value): """ Set a disc field value as a class attributes. This method provides additional formating to any data fields. For example, removing quoting and checking the field name. :param name: Field name to set :type name: str :param value: Value of field :type value: str :return: :data:`True` if field is written to the class data, or :data:`False` """ name = name.lower() if hasattr(self,name): setattr(self,name,value.strip('"')) return True return False
@property def is_multisession(self): """ :return: :data:`True` if disc is defined as multi-sesssion. """ return self._mode == self.MODE_MULTI_SESSION @is_multisession.setter
[docs] def is_multisession(self,val): """ Change the mode of a disc to either 'single-session' or 'multi-session'. :param val: :data:`True` if disc is multi-session, :data:`False` otherwise. :type val: bool """ if val: self._mode = self.MODE_MULTI_SESSION else: self._mode = self.MODE_SINGLE_SESSION
[docs]class Track( object ): """ Holds track metadata values such as title and performer. Each :class:`Track` object contains a list of :class:`TrackIndex`\s that specifies the audio data associated with the track. """ #: :data:`True` or :data:`False`, indicates *Digital Copy Protection* #: flag on the track. dcp = False #: :data:`True` or :data:`False`, indicates *Four Channel Audio* flag #: on the track. four_ch = False #: list of :class:`TrackIndex` objects. Every track has at #: least one :class:`TrackIndex` and possibly more. The #: :class:`TrackIndex` defines a length of audio data or property in #: the track. The fist :class:`TrackIndex` can be pre-gap data. Only #: one audio file can be associated with a :class:`TrackIndex`, so if a #: track is composed of multiple audio files, there will be an >= #: number of :class:`TrackIndex`\s. indexes = None #: :data:`True` or :data:`False`, indicates if a track is binary data #: and not audio. Data tracks will not produce any text when printed. is_data = False #: String representing ISRC value of the track. isrc = None #: Integer initialized to the value of the track number. num = None #: String representing the track artist. performer = None #: :data:`True` or :data:`False`, indicates *Pre-Emphasis* flag on the #: track. pre = False #: :class:`_TrackTime` value that indicates the pre-gap value of the #: current track. The pre-gap is a time length at the beginning of a #: track that will cause a CD player to count up from a negative time #: value before changing the track index number. The starting pre-gap #: value of a track is essentially the final audio at the end of the #: previous track. However, there is more than one way to designate #: the pregap in a track, therefore this variable is only used if the #: first :class:`TrackIndex` in the track contains more than just the #: pre-gap audio. pregap = None #: String representing the title of the track. title = None def __init__(self,num,is_data=False): """ :param num: Track index in the audio CD. :type num: int :param is_data: :data:`True` if track is data instead of audio. :type is_data: bool """ # create an empty list of :class:`TrackIndex` objects, and # assign track number. self.indexes = [] # list of indexes in the track self.num = num self.is_data = is_data def __str__(self): raise NotImplementedError def __unicode__(self): """Return the TOC formated representation of the :class:`Track` object including the :class:`TrackIndex` objects. Data tracks will not generate any output.""" if self.is_data: return '' # do not output to TOC out = [u'\n//Track %d' % self.num] out += [u'TRACK AUDIO'] if self.isrc: out += [u'\tISRC "%s"' % self.isrc] if self.dcp: out += [u'\tCOPY'] if self.four_ch: out += [u'\tFOUR_CHANNEL_AUDIO'] if self.pre: out += [u'\tPRE_EMPHASIS'] out += [u'\tCD_TEXT { LANGUAGE 0 {'] if self.title: out += [u'\t\tTITLE "%s"' % self.title] if self.performer: out += [u'\t\tPERFORMER "%s"' % self.performer] out += [u'\t}}'] if self.pregap: out += [u'\tPREGAP %s' % self.pregap] for idx in self.indexes: out.append( unicode(idx) ) return u'\n'.join(out)
[docs] def set_field(self, name, value): """ Set a track field value as a class attributes. This method provides additional formating to any data fields. For example, removing quoting and checking the field name. :param name: Field name to set :type name: str :param value: Value of field :type value: str :return: :data:`True` if field is written to the class data, or :data:`False` """ name = name.lower() if hasattr(self,name): if isinstance(value,basestring): setattr(self,name,value.strip('"')) elif isinstance(value,bool): setattr(self,name,value) return True return False
[docs]class TrackIndex(object): """ Represent an *index* of an audio CD track. Specifically, information about a location, length and type of audio data. :class:`Track` objects can have one or more :class:`TrackIndex`\s to represent the audio data belonging to the track. .. rubric:: Constants .. data:: PREAUDIO Indicates a :class:`TrackIndex` that is pre-gap audio data only. .. data:: AUDIO Indicates a :class:`TrackIndex` of standard audio data or both pre-gap and audio. .. data:: INDEX Indicates a :class:`TrackIndex` after the start of a time stamp of a previous :const:`AUDIO` :class:`TrackIndex` object. There is no audio data associated with *INDEX* :class:`TrackIndex`\s. .. data:: START Same as :const:`INDEX`, but :class:`TrackIndex` preceding it is was the pre-gap audio of the same :class:`Track`. .. rubric:: Attributes .. attribute:: file_ String representing a WAV file's path and name. This is used to read the audio data of the :class:`TrackIndex`. .. attribute:: len_ Empty string or :class:`_TrackTime` value that specifies the number of audio frames associated with the :class:`TrackIndex`. By default, this value will equal the total length of the WAV data, but might be truncated if the track starts after, or ends before the WAV data. .. attribute:: num Integer specifying the location of the :class:`TrackIndex` in the track. The first index *num* is always 0. .. attribute:: time :class:`_TrackTime` value that specifies the starting time index of the :class:`TrackIndex` object relative to the start of the audio data. Usually this value is ``0``. """ #: Enum of valid :class:`TrackIndex` types. PREAUDIO, AUDIO, INDEX, START, DATA = range(5) #: Integer set to :const:`PREAUDIO` or :const:`AUDIO` or :const:`INDEX` or #: :const:`START`. Indicate the mode of :class:`TrackIndex` object. cmd = AUDIO def __init__(self, num, time, file_, len_=None): """ If possible the sample count of the :class:`TrackIndex` is calculated by reading the WAV audio data. :param num: Index number position in the :class:`Track`, starting at 0. :type num: int :param time: The indexes starting offset in the audio data file. :type time: :class:`_TrackTime` :param file_: String representing the path location of a WAV file associated with this index. :type file_: str :param len_: Track length in format supported by :class:`_TrackTime`. :type len_: str, tuple, int (see :class:`_TrackTime`) """ self.file_ = file_ self.num = int(num) self.time = _TrackTime(time) if len_: self.len_ = _TrackTime(len_) else: # set length to maximum possible for now (total - start) file_len = self._file_len(self.file_) if file_len: self.len_ = file_len - self.time else: self.len_ = '' log.debug( 'creating index %s' % repr(self) ) def __repr__(self): """Return a string used for debug logging.""" return ("'%s, %s'" % (self.file_, self.time)).encode('utf-8') def __str__(self): raise NotImplementedError def __unicode__(self): """Return the TOC formated string representation of the :class:`TrackIndex` object.""" out = [] if self.cmd == self.DATA: return u'' # do not output to TOC if self.cmd in [self.AUDIO, self.PREAUDIO]: out += [u'\tAUDIOFILE "%(file_)s" %(time)s %(len_)s' % self.__dict__] elif self.cmd == self.INDEX: out += [u'\tINDEX %(time)s' % self.__dict__] elif self.cmd == self.START: out += [u'\tSTART %(len_)s' % self.__dict__] else: raise Exception # add start command for pregap audio if self.cmd == self.PREAUDIO: out += [u'\tSTART'] return u'\n'.join(out) def _file_len(self,file_): """Returns the number of audio samples in the WAV file, *file_*. Called during __init__. If *file_* can not be opened, :data:`None` is returned. :param file_: a file name string relative to the cwd referencing a WAV file. :type file_: str :rtype: :class:`_TrackTime` of audio samples or :data:`None`""" if not (file_ and os.path.exists(file_)): return None w = wave.open(file_) frames = w.getnframes() / (w.getframerate()/75) w.close() return _TrackTime(frames)
class _TrackTime(object): """ Container class to represent the sample count or position in audio data. Allows mathematical operations to be easily performed on time positions. """ #: Defines the number of audio *Frames Per Second* _FPS = 75 #: Defines the number of *Seconds Per Minute* _SPM = 60 #: Defines the number of audio *Frames Per Minute* _FPM = _FPS * _SPM #: :class:`tuple` that stores the minutes, seconds, and frames values. The #: combination of these values can be used to calculate the total frame #: count. _time = None def __init__(self, arg=None): """Initializes the :class:`_TrackTime` object, normalizing the input data. :param arg: Variable representation of the value of the :class:`_TrackTime` object. The allowed formats are: a. String in the format *MM:SS:FF* b. Tuple in the format (M,S,F) c. Integer of the total frame length d. :data:`None`, object is initialized to 0 length :type arg: str, :class:`tuple`, int, :data:`None`""" if isinstance(arg,basestring): # extract time from string val = [int(x) for x in arg.split(':')] self._time = tuple(val) elif isinstance(arg,tuple): # assume arg is correct format self._time = arg elif isinstance(arg,(int,long)): # convert frame count to min,sec,frames min_,fr = divmod(arg, self._FPM) sec,fr = divmod(fr, self._FPS) self._time = (min_,sec,fr) else: # set time to '0:0:0' (zero) self._time = tuple([0]*3) def __repr__(self): """Return string value of the :class:`_TrackTime` in format *MM:SS:FF*.""" return '%02d:%02d:%02d' % self._time def __ne__(self, other): """Return :data:`True` if objects are NOT equal.""" return self._time != other._time def __eq__(self, other): """Return :data:`True` if objects are equal.""" return self._time == other._time def __sub__(self, other): """Return result of *self* - *other*.""" mn,sc,fr = map(lambda x,y: x-y, self._time, other._time) if fr<0: sc-=1; fr+=self._FPS if sc<0: mn-=1; sc+=self._SPM if mn<0: raise UnderflowError, \ 'Track time calculation resulted in a negative value' return _TrackTime((mn,sc,fr)) @property def frames(self): """Convert min,sec,frame to total frames.""" return sum( [x*y for x,y in zip(self._time,[self._FPM,self._FPS,1]) ])