class AudioFormat(GrooveClass): """Groove Audio Format""" _ffitype = 'struct GrooveAudioFormat *' sample_rate = utils.property_convert('sample_rate', from_cdef=int, doc="""Sample rate in Hz""") channel_layout = utils.property_convert( 'channel_layout', from_cdef=ChannelLayout.__values__.get, doc="""ChanelLayout for the audio format""") sample_format = utils.property_convert( 'sample_fmt', from_cdef=SampleFormat.__values__.get, doc="""SampleFormat for the audio format""") def __init__(self): self._obj = ffi.new(self._ffitype) def __eq__(self, rhs): if not isinstance(rhs, AudioFormat): return False return lib.groove_audio_formats_equal(self._obj, rhs._obj) == 1 def clone(self, other): """Set this AudioFormat equal to another format""" self.sample_rate = other.sample_rate self.channel_layout = other.channel_layout self.sample_format = other.sample_format
class LoudnessDetector(GrooveClass): """pass""" _ffitype = 'struct GrooveLoudnessDetector *' info_queue_size = utils.property_convert( 'info_queue_size', int, doc="""Maximum number of items to store in this LoudnessDetector's queue This defaults to MAX_INT, meaning that the loudness detector will cause the decoder to decode the entire playlist. If you want to instead, for example, obtain loudness info at the same time as playback, you might set this value to 1. """) sink_buffer_size = utils.property_convert( 'sink_buffer_size', int, doc="""How big the sink buffer should be, in sample frames LoudnessDetector defaults this to 8192 """) disable_album = utils.property_convert( 'disable_album', bool, doc="""Set True to only compute track loudness This is faster and requires less memory than computing both. LoudnessDetector defaults this to False """) @property def playlist(self): """Playlist to generate loudness info for""" return self._playlist @playlist.setter def playlist(self, value): if self._playlist: assert lib.groove_loudness_detector_detach(self._obj) == 0 self._playlist = None if value is not None: assert lib.groove_loudness_detector_attach(self._obj, value._obj) == 0 self._playlist = value def __init__(self): # TODO: error handling obj = lib.groove_loudness_detector_create() assert obj != ffi.NULL self._obj = ffi.gc(obj, lib.groove_loudness_detector_destroy) self._playlist = None def __del__(self): # Make sure playlist gets detached before we loose the obj if self.playlist is not None: self.playlist = None def __iter__(self): info_obj = ffi.new('struct GrooveLoudnessDetectorInfo *') pitem = True while pitem: status = lib.groove_loudness_detector_info_get( self._obj, info_obj, True) assert status >= 0 if status != 1: break loudness = float(info_obj.loudness) peak = float(info_obj.peak) duration = float(info_obj.duration) if info_obj.item == ffi.NULL: pitem = None else: pitem = self.playlist._pitem(info_obj.item) yield LoudnessDetectorInfo(loudness, peak, duration, pitem) def info_peek(self, block=False): """Check if info is ready""" result = lib.groove_loudness_detector_info_peek(self._obj, block) assert result >= 0 return bool(result) def position(self): """Get the current position of the printer head Returns: A tuple of (playlist_item, seconds). If the playlist is empty playlist_item will be None and seconds will be -1.0 """ pitem_obj_ptr = ffi.new('struct GroovePlaylistItem **') seconds = ffi.new('double *') lib.groove_loudness_detector_position(self._obj, pitem_obj_ptr, seconds) if pitem_obj_ptr[0] == ffi.NULL: pitem = None else: pitem = self.playlist._pitem(pitem_obj_ptr[0]) return pitem, float(seconds[0])
class Sink(GrooveClass): """Groove Sink""" _ffitype = 'struct GrooveSink *' BufferClass = Buffer disable_resample = utils.property_convert( 'disable_resample', bool, doc="""Set this flag to ignore audio_format. If you set this flag, the buffers you pull from this sink could have any audio format. """) buffer_sample_count = utils.property_convert( 'buffer_sample_count', int, doc="""Number of frames to pull into a buffer If set to the default of 0, groove will choose a sample count based on efficiency. """) buffer_size = utils.property_convert( 'buffer_size', int, doc="""Buffer queue size in frames, default 8192""") @property def audio_format(self): """Set this to the audio format you want the sink to output""" return AudioFormat._from_obj(self._obj.format) @property def gain(self): """Volume adjustment for the audio sink It is recommended to leave this at 1.0 and adjust the playlist/item gain instead """ return self._obj.gain @gain.setter def gain(self, value): lib.groove_sink_set_gain(self._obj, value) @property def playlist(self): return self._playlist @playlist.setter def playlist(self, value): # TODO: better exception handling if self._obj.playlist != ffi.NULL: assert lib.groove_sink_detach(self._obj) == 0 if value is not None: assert lib.groove_sink_attach(self._obj, value._obj) == 0 self._playlist = value @property def bytes_per_sec(self): """Automatically computed from audio format when attached""" return self._obj.bytes_per_sec @classmethod def _from_obj(cls, obj): instance, created = super(Sink, cls)._from_obj(obj) if created: # TODO: is this safe? libgroove uses these callbacks internally # but when it does I think the sink is not exposed instance._attach_callbacks() return instance, created def __init__(self): # TODO: better exception handling obj = lib.groove_sink_create() assert obj != ffi.NULL self._obj = ffi.gc(obj, lib.groove_sink_destroy) self._attach_callbacks() self._playlist = None def _attach_callbacks(self): self._obj.flush = lib.groove_sink_callback_flush self._obj.purge = lib.groove_sink_callback_purge self._obj.pause = lib.groove_sink_callback_pause self._obj.play = lib.groove_sink_callback_play def on_flush(self): """Called when the audio queue is flushed. For example if you seek to a different location in the song. """ pass def on_purge(self, playlist_item): """Called when a playlist item is deleted. Take this opportunity to remove all references to the PlaylistItem. """ pass def on_pause(self): """Called when a playlist is paused""" pass def on_play(self): """Called when a playlist is played""" pass def buffer_peek(self, block=True): """Returns True if a buffer is ready, False if not""" value = lib.groove_sink_buffer_peek(self._obj, block) assert value >= 0 return value == 1 def get_buffer(self, block=False): """Get the buffer on the sink If no buffer is ready, this raises `groove.Buffer.NotReady` If the end of the playlist is reached, this raises `groove.Buffer.End` If block is True and no buffer is ready, this may block indefinately """ # TODO: add timeout, might have to be done in libgroove to be safe buff_obj_ptr = ffi.new('struct GrooveBuffer **') value = lib.groove_sink_buffer_get(self._obj, buff_obj_ptr, block) assert value >= 0 if value == _constants.GROOVE_BUFFER_NO: raise Buffer.NotReady() elif value == _constants.GROOVE_BUFFER_END: raise Buffer.End() elif value == _constants.GROOVE_BUFFER_YES: buff = self.BufferClass._from_obj(buff_obj_ptr[0]) buff.sink = self return buff raise Exception('Unknown value %s from groove_sink_buffer_get' % value)
class Encoder(GrooveClass): """Groove Encoder""" _ffitype = 'struct GrooveEncoder *' BufferClass = Buffer format_short_name = utils.property_char_ptr( 'format_short_name', """Short name of the format Optional - choose a short name for the format to help libgroove choose which format to use. Use `avconfig -formats` to get a list of possibilities """) codec_short_name = utils.property_char_ptr( 'codec_short_name', """Short name of the codec Optional - choose a short name for the codec to help libgroove choose which codec to use. Use `avconfig -codecs` to get a list of possibilities """) filename = utils.property_char_ptr( 'filename', """An example filename Optional - provide an example filename to help libgroove guess which format/codec to use. """) mime_type = utils.property_char_ptr( 'mime_type', """A mime type string Optional - provide a mime type string to help libgroove guess which format/codec to use. """) bit_rate = utils.property_convert( 'bit_rate', int, doc="""Target encoding quality in bits per second Select encoding quality by choosing a target bit rate in bits per second. Note that typically you see this expressed in "kbps", such as 320kbps or 128kbps. Surprisingly, in this circumstance 1 kbps is 1000 bps, *not* 1024 bps as you would expect. This defaults to 256000. """) sink_buffer_size = utils.property_convert( 'sink_buffer_size', int, doc="""How big the sink buffer should be, in sample frames. This defaults to 8192. """) encoded_buffer_size = utils.property_convert( 'encoded_buffer_size', int, doc="""How big the encoded buffer should be, in bytes. This defaults to 16384. """) @property def actual_audio_format(self): fmt_obj = ffi.addressof(self._obj.actual_audio_format) fmt, _ = AudioFormat._from_obj(fmt_obj) return fmt @property def target_audio_format(self): fmt_obj = ffi.addressof(self._obj.target_audio_format) fmt, _ = AudioFormat._from_obj(fmt_obj) return fmt @property def gain(self): """The volume adjustment to make to this player. It is recommended to leave this at 1.0 and instead adjust the gain of the underlying playlist. """ return float(self._obj.gain) @gain.setter def gain(self, value): lib.groove_encoder_set_gain(self._obj, value) @property def playlist(self): return self._playlist @playlist.setter def playlist(self, value): # TODO: better exception handling if self._playlist: assert lib.groove_encoder_detach(self._obj) == 0 self._playlist = None assert lib.groove_encoder_attach(self._obj, value._obj) == 0 self._playlist = value def __init__(self): # TODO: better exception handling obj = lib.groove_encoder_create() assert obj != ffi.NULL self._obj = ffi.gc(obj, lib.groove_encoder_destroy) self._playlist = None @property def disable_resample(self): """Set this flag to ignore audio_format. If you set this flag, the buffers you pull from this could have any audio format. """ return self._obj.disable_resample @disable_resample.setter def disable_resample(self, value): if value: self._obj.disable_resample = 1 @property def buffer_sample_count(self): """Number of frames to pull into a buffer If set to the default of 0, groove will choose a sample count based on efficiency. """ return self._obj.buffer_sample_count @buffer_sample_count.setter def buffer_sample_count(self, value): self._obj.buffer_sample_count = value @property def buffer_size(self): """Buffer queue size in frames, default 8192""" return self._obj.buffer_size @buffer_size.setter def buffer_size(self, value): self._obj.buffer_size = value @property def gain(self): """Volume adjustment for the audio It is recommended to leave this at 1.0 and adjust the playlist/item gain instead """ return self._obj.gain @gain.setter def gain(self, value): lib.groove_encoder_set_gain(self._obj, value) @property def playlist(self): return self._playlist @playlist.setter def playlist(self, value): # TODO: better exception handling if self._obj.playlist != ffi.NULL: assert lib.groove_encoder_detach(self._obj) == 0 if value is not None: assert lib.groove_encoder_attach(self._obj, value._obj) == 0 self._playlist = value @property def bytes_per_sec(self): """Automatically computed from audio format when attached""" return self._obj.bytes_per_sec def buffer_peek(self, block=True): """Returns True if a buffer is ready, False if not""" value = lib.groove_encoder_buffer_peek(self._obj, 1 if block else 0) assert value >= 0 return value == 1 def get_buffer(self, block=False): """Get the buffer on the encoder If no buffer is ready, this raises `groove.Buffer.NotReady` If the end of the playlist is reached, this raises `groove.Buffer.End` If block is True and no buffer is ready, this may block indefinately """ # TODO: add timeout, might have to be done in libgroove to be safe buff_obj_ptr = ffi.new('struct GrooveBuffer **') value = lib.groove_encoder_buffer_get(self._obj, buff_obj_ptr, 1 if block else 0) assert value >= 0 if value == _constants.GROOVE_BUFFER_NO: raise Buffer.NotReady() elif value == _constants.GROOVE_BUFFER_END: raise Buffer.End() elif value == _constants.GROOVE_BUFFER_YES: buff, _ = self.BufferClass._from_obj(buff_obj_ptr[0]) buff.encoder = self return buff raise Exception('Unknown value %s from groove_encoder_buffer_get' % value) def get_tags(self, flags=0): """Get the tags for an encoder Args: flags (int) Bitmask of tag flags Returns: A dictionary of `name: value` pairs. Both `name` and `value` will be type `bytes`. """ # Have to make a GrooveTag** so cffi doesn't try to sizeof GrooveTag gtag_ptr = ffi.new('struct GrooveTag **') gtag = gtag_ptr[0] tags = OrderedDict() while True: gtag = lib.groove_encoder_metadata_get(self._obj, b'', gtag, flags) if gtag == ffi.NULL: break key = ffi.string(lib.groove_tag_key(gtag)) value = ffi.string(lib.groove_tag_value(gtag)) tags[key] = value return tags def set_tags(self, tagdict, flags=0): """Shortcut to set each flag in tagdict This will overwrite existing tags, but will not delete existing tags that are not listed in tagdict. To delete a tag, set its value to `None` """ for k, v in tagdict.items(): self.set_tag(k, v, flags) def set_tag(self, key, value, flags=0): """Set tag `key` to `value` If `value` is `None`, the tag will be deleted. `key` and `value` must be type `bytes`. """ if value is None: value = ffi.NULL status = lib.groove_encoder_metadata_set(self._obj, key, value, flags) return status def decode_position(self): """Get the current position of the decode head Returns: A tuple of (playlist_item, seconds). If the playlist is empty playlist_item will be None and seconds will be -1.0 """ pitem_obj_ptr = ffi.new('struct GroovePlaylistItem **') seconds = ffi.new('double *') lib.groove_encoder_position(self._obj, pitem_obj_ptr, seconds) return self._pitem(pitem_obj_ptr[0]), float(seconds[0])
class Fingerprinter(GrooveClass): """Use this to find out the unique id of an audio track""" _ffitype = 'struct GrooveFingerprinter *' @classmethod def encode(cls, fp): """Compress and base64-encode a raw fingerprint""" # TODO: error handling efp_obj_ptr = ffi.new('char **') fp_obj = ffi.new('int32_t[]', fp) assert lib.groove_fingerprinter_encode(fp_obj, len(fp), efp_obj_ptr) == 0 # copy the result to python and free the c obj result = ffi.string(efp_obj_ptr[0]) lib.groove_fingerprinter_dealloc(efp_obj_ptr[0]) return result @classmethod def decode(cls, encoded_fp): """Uncompress and base64-decode a raw fingerprint""" efp_obj = ffi.new('char[]', encoded_fp) fp_obj_ptr = ffi.new('int32_t **') size_obj_ptr = ffi.new('int *') assert lib.groove_fingerprinter_decode(efp_obj, fp_obj_ptr, size_obj_ptr) == 0 # copy the result to python and free the c obj fp_obj = fp_obj_ptr[0] result = [int(fp_obj[n]) for n in range(size_obj_ptr[0])] lib.groove_fingerprinter_dealloc(fp_obj) return result info_queue_size = utils.property_convert('info_queue_size', int, doc="""Maximum number of items to store in this Fingerprinter's queue This defaults to MAX_INT, meaning that fingerprinter will cause the decoder to decode the entire playlist. If you want instead, for example, obtain fingerprints at the same time as playback, you might set this value to 1. """) sink_buffer_size = utils.property_convert('sink_buffer_size', int, doc="""How big the sink buffer should be, in sample frames This defaults to 8192. """) @property def playlist(self): """Playlist to generate fingerprints for""" return self._playlist @playlist.setter def playlist(self, value): if self._playlist: assert lib.groove_fingerprinter_detach(self._obj) == 0 self._playlist = None if value is not None: assert lib.groove_fingerprinter_attach(self._obj, value._obj) == 0 self._playlist = value def __init__(self, base64_encode=True): # TODO: error handling obj = lib.groove_fingerprinter_create() assert obj != ffi.NULL self._obj = ffi.gc(obj, lib.groove_fingerprinter_destroy) self._playlist = None self.base64_encode = base64_encode def __del__(self): # Make sure playlist gets detached before we loose the obj if self.playlist is not None: self.playlist = None def __iter__(self): info_obj = ffi.new('struct GrooveFingerprinterInfo *'); while True: status = lib.groove_fingerprinter_info_get(self._obj, info_obj, True) assert status >= 0 if status != 1 or info_obj.item == ffi.NULL: break fp_obj = info_obj.fingerprint fp_size_obj = info_obj.fingerprint_size if self.base64_encode: efp_obj_ptr = ffi.new('char **') assert lib.groove_fingerprinter_encode(fp_obj, fp_size_obj, efp_obj_ptr) == 0 fp = ffi.string(efp_obj_ptr[0]) lib.groove_fingerprinter_dealloc(efp_obj_ptr[0]) else: fp = [int(fp_obj[n]) for n in range(fp_size_obj)] duration = float(info_obj.duration) pitem = self.playlist._pitem(info_obj.item) lib.groove_fingerprinter_free_info(info_obj) yield FingerprinterInfo(fp, duration, pitem) def info_peek(self, block=False): """Check if info is ready""" result = lib.groove_fingerprinter_info_peek(self._obj, block) assert result >= 0 return bool(result) def position(self): """Get the current position of the printer head Returns: A tuple of (playlist_item, seconds). If the playlist is empty playlist_item will be None and seconds will be -1.0 """ pitem_obj_ptr = ffi.new('struct GroovePlaylistItem **') seconds = ffi.new('double *') lib.groove_fingerprinter_position(self._obj, pitem_obj_ptr, seconds) if pitem_obj_ptr[0] == ffi.NULL: pitem = None else: pitem = self.playlist._pitem(pitem_obj_ptr[0]) return pitem, float(seconds[0])
class Player(GrooveClass): _ffitype = 'struct GroovePlayer *' dummy_device = Device(_constants.GROOVE_PLAYER_DUMMY_DEVICE, 'dummy') default_device = Device(_constants.GROOVE_PLAYER_DEFAULT_DEVICE, 'default') @classmethod def list_devices(cls): """Get the list of available devices This may trigger a complete redetect of available hardware """ devices = [ Player.dummy_device, Player.default_device, ] device_count = lib.groove_device_count() for n in range(device_count): name = lib.groove_device_name(n) if name == ffi.NULL: continue name = ffi.string(name).decode('utf-8') devices.append(Device(n, name)) return devices device_buffer_size = utils.property_convert('device_buffer_size', int, doc="""How big the device buffer should be, in sample frames Must be a power of 2, defaults to 1024 """) sink_buffer_size = utils.property_convert('sink_buffer_size', int, doc="""How big the sink buffer should be, in sample frames Defaults to 8192 """) use_exact_audio_format = utils.property_convert('use_exact_audio_format', bool, doc="""Force the device to play with the format of the media If true, `target_audio_format` and `actual_audio_format` are ignored and no resampling, channel layout remapping, or sample format conversion will occur. The audio device will be reopened with exact parameters whenever necessary. """) @property def device(self): """Device for playback, defaults to Player.dummy_device""" return self._device @device.setter def device(self, value): self._obj.device_index = value.index self._device = value @property def target_audio_format(self): """The desired audio format to open the device with Defaults to 44100Hz, signed 16-bit int, stereo These are preferences; if a setting cannot be used, a substitute will be used instead. `actual_audio_format` is set to the actual values. """ fmt, _ = AudioFormat._from_obj(self._obj.target_audio_format) return fmt @property def actual_audio_format(self): """Set to the actual audio format you get when you open the device""" fmt, _ = AudioFormat._from_obj(self._obj.actual_audio_format) return fmt @property def gain(self): """The volume adjustment to make to this player It is recommended to leave this at 1.0 and instead adjust the gain of the underlying playlist. """ return self._obj.gain @gain.setter def gain(self, value): assert lib.groove_player_set_gain(self._obj, value) == 0 @property def playlist(self): """Playlist to play""" return self._playlist @playlist.setter def playlist(self, value): if self._playlist: assert lib.groove_player_detach(self._obj) == 0 self._playlist = None if value is not None: assert lib.groove_player_attach(self._obj, value._obj) == 0 self._playlist = value def __init__(self): # TODO: error handling obj = lib.groove_player_create() assert obj != ffi.NULL self._obj = ffi.gc(obj, lib.groove_player_destroy) self._playlist = None self.device = Player.dummy_device def __del__(self): # Make sure playlist gets detached before we loose the obj if self.playlist is not None: self.playlist = None def event_get(self, block=False): """Get player event""" event_obj = ffi.new('union GroovePlayerEvent *') result = lib.groove_player_event_get(self._obj, event_obj, block) assert result >= 0 if result == 0: return None return PlayerEvent.__values__[event_obj.type] def event_peek(self, block=False): """Check if event is ready""" result = lib.groove_player_event_peek(self._obj, block) assert result >= 0 return bool(result) def position(self): """Get the current position of the printer head Returns: A tuple of (playlist_item, seconds). If the playlist is empty playlist_item will be None and seconds will be -1.0 """ pitem_obj_ptr = ffi.new('struct GroovePlaylistItem **') seconds = ffi.new('double *') lib.groove_player_position(self._obj, pitem_obj_ptr, seconds) if pitem_obj_ptr[0] == ffi.NULL: pitem = None else: pitem = self.playlist._pitem(pitem_obj_ptr[0]) return pitem, float(seconds[0])