'''
Audio Output
=====
Load an audio sound and play it with::
from kivy.core.audio_output import SoundLoader
sound = SoundLoader.load('mytest.wav')
if sound:
print("Sound found at %s" % sound.source)
print("Sound is %.3f seconds" % sound.length)
sound.play()
You should not use the Sound class directly. The class returned by
:func:`SoundLoader.load` will be the best sound provider for that particular
file type, so it might return different Sound classes depending the file type.
Event dispatching and state changes
-----------------------------------
Audio is often processed in parallel to your code. This means you often need to
enter the Kivy :func:`eventloop <kivy.base.EventLoopBase>` in order to allow
events and state changes to be dispatched correctly.
You seldom need to worry about this as Kivy apps typically always
require this event loop for the GUI to remain responsive, but it is good to
keep this in mind when debugging or running in a
`REPL <https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop>`_
(Read-eval-print loop).
.. versionchanged:: 1.10.0
The pygst and gi providers have been removed.
.. versionchanged:: 1.8.0
There are now 2 distinct Gstreamer implementations: one using Gi/Gst
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
working only for Python 2 + Gstreamer 0.10.
.. note::
The core audio library does not support recording audio. If you require
this functionality, please refer to the
`audiostream <https://github.com/kivy/audiostream>`_ extension.
Provider selection
------------------
.. versionadded:: 3.0.0
By default, Kivy automatically selects an audio provider based on platform
defaults and file type. You can override this to use a specific provider.
Querying available providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To see which providers are available on your system::
from kivy.core.audio_output import SoundLoader
print(SoundLoader.available_providers()) # e.g., ['sdl3', 'ffpyplayer']
Using ``audio_output_provider`` parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Specify a provider when loading a sound::
from kivy.core.audio_output import SoundLoader
# Load with SDL3 provider
sound = SoundLoader.load('music.mp3', audio_output_provider='sdl3')
# Load with ffpyplayer provider
sound = SoundLoader.load('music.mp3', audio_output_provider='ffpyplayer')
Strict mode
~~~~~~~~~~~
By default, if a requested provider is unavailable or fails, Kivy logs a warning
and falls back to other providers. Enable strict mode to raise exceptions instead::
import os
os.environ['KIVY_PROVIDER_STRICT'] = '1'
import kivy
In strict mode:
- Invalid provider names raise ``ValueError``
- Provider load failures raise ``Exception``
- No fallback to other providers occurs
This is useful during development to catch configuration errors immediately.
'''
__all__ = ('Sound', 'SoundLoader')
import os
from kivy.logger import Logger
from kivy.event import EventDispatcher
from kivy.core import core_register_libs, load_with_provider_selection, \
get_provider_modules, make_provider_tuple
from kivy.resources import resource_find
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
AliasProperty, BooleanProperty, BoundedNumericProperty
from kivy.utils import platform, path_to_str
from kivy.setupconfig import USE_SDL3
from sys import float_info
[docs]
class SoundLoader:
'''Load a sound, using the best loader for the given file type.
'''
_classes = []
_loaders_by_name = {} # O(1) lookup by provider name
[docs]
@staticmethod
def register(classobj):
'''Register a new class to load the sound.'''
Logger.debug('Audio: register %s' % classobj.__name__)
# Require explicit _provider_name attribute (validate BEFORE adding to list)
name = getattr(classobj, '_provider_name', None)
if name is None:
raise ValueError(
f'{classobj.__name__} must define a _provider_name class attribute'
)
SoundLoader._classes.append(classobj)
SoundLoader._loaders_by_name[name.lower()] = classobj
[docs]
@staticmethod
def available_providers():
'''Return a list of available audio provider names.
The returned names can be used with the ``audio_provider`` parameter.
.. versionadded:: 3.0.0
:returns: List of provider name strings (e.g., ['sdl3', 'ffpyplayer'])
'''
return list(SoundLoader._loaders_by_name.keys())
[docs]
@staticmethod
def load(filename, audio_output_provider=None) -> "Sound":
'''Load a sound, and return a Sound() instance.
:param filename: Path to the audio file to load.
:param audio_output_provider: Optional provider name (e.g., 'sdl3',
'ffpyplayer'). If specified, only that provider will be used. Use
:meth:`available_providers` to get a list of available providers.
.. versionadded:: 3.0.0
:returns: A Sound instance, or None if no loader could handle the file.
:raises ValueError: If ``audio_output_provider`` is specified but not
found or doesn't support the file format (when KIVY_PROVIDER_STRICT=1).
.. versionchanged:: 3.0.0
`filename` may be a :class:`os.PathLike` (e.g. :class:`pathlib.Path`)
in addition to a ``str``.
'''
# Coerce os.PathLike -> str so extension parsing below is safe even
# when resource_find does not resolve the file.
filename = path_to_str(filename)
rfn = resource_find(filename)
if rfn is not None:
filename = rfn
ext = filename.split('.')[-1].lower()
if '?' in ext:
ext = ext.split('?')[0]
def check_compatibility(provider_class, extension):
"""Check if provider supports the given extension."""
return extension in provider_class.extensions()
def try_load(provider_class, fname):
"""Try to load sound with the given provider."""
try:
return provider_class(source=fname)
except Exception:
return None
def fallback_load():
"""Load using default provider priority."""
for classobj in SoundLoader._classes:
if ext in classobj.extensions():
try:
return classobj(source=filename)
except Exception:
continue
Logger.warning('Audio: Unable to find a loader for <%s>' % filename)
return None
return load_with_provider_selection(
filename=filename,
extension=ext,
provider_name=audio_output_provider,
providers_by_name=SoundLoader._loaders_by_name,
category_name='Audio',
check_compatibility=check_compatibility,
try_load=try_load,
fallback_load=fallback_load
)
[docs]
class Sound(EventDispatcher):
'''Represents a sound to play. This class is abstract, and cannot be used
directly.
Use SoundLoader to load a sound.
:Events:
`on_play`: None
Fired when the sound is played.
`on_stop`: None
Fired when the sound is stopped.
'''
_provider_name = None
# Internal provider name used for registration.
# This must be set by provider implementations. Use
# :meth:`SoundLoader.available_providers` to query available provider names.
source = StringProperty(None)
'''Filename / source of your audio file.
.. versionadded:: 1.3.0
:attr:`source` is a :class:`~kivy.properties.StringProperty` that defaults
to None and is read-only. Use the :meth:`SoundLoader.load` for loading
audio.
'''
volume = NumericProperty(1.)
'''Volume, in the range 0-1. 1 means full volume, 0 means mute.
.. versionadded:: 1.3.0
:attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
to 1.
'''
pitch = BoundedNumericProperty(1., min=float_info.epsilon)
'''Pitch of a sound. 2 is an octave higher, .5 one below. This is only
implemented for SDL3 audio provider yet.
.. versionadded:: 1.10.0
:attr:`pitch` is a :class:`~kivy.properties.NumericProperty` and defaults
to 1.
'''
state = OptionProperty('stop', options=('stop', 'play'))
'''State of the sound, one of 'stop' or 'play'.
.. versionadded:: 1.3.0
:attr:`state` is a read-only :class:`~kivy.properties.OptionProperty`.'''
loop = BooleanProperty(False)
'''Set to True if the sound should automatically loop when it finishes.
.. versionadded:: 1.8.0
:attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and defaults to
False.'''
__events__ = ('on_play', 'on_stop')
def on_source(self, instance, filename):
self.unload()
if filename is None:
return
self.load()
[docs]
def get_pos(self):
'''
Returns the current position of the audio file.
Returns 0 if not playing.
.. versionadded:: 1.4.1
'''
return 0
def _get_length(self):
return 0
length = property(lambda self: self._get_length(),
doc='Get length of the sound (in seconds).')
[docs]
def load(self):
'''Load the file into memory.'''
pass
[docs]
def unload(self):
'''Unload the file from memory.'''
pass
[docs]
def play(self):
'''Play the file.'''
self.state = 'play'
self.dispatch('on_play')
[docs]
def stop(self):
'''Stop playback.'''
self.state = 'stop'
self.dispatch('on_stop')
[docs]
def seek(self, position):
'''Go to the <position> (in seconds).
.. note::
Most sound providers cannot seek when the audio is stopped.
Play then seek.
'''
pass
def on_play(self):
pass
def on_stop(self):
pass
# Little trick here, don't activate gstreamer on window
# seem to have lot of crackle or something...
# Build platform-specific list from registry
all_providers = get_provider_modules('audio_output')
audio_libs = []
if platform == 'android':
audio_libs.append(make_provider_tuple('android', all_providers))
elif platform in ('macosx', 'ios'):
audio_libs.append(make_provider_tuple('avplayer', all_providers))
try:
from kivy.lib.gstplayer import GstPlayer # NOQA
audio_libs.append(make_provider_tuple('gstplayer', all_providers))
except ImportError:
pass
audio_libs.append(make_provider_tuple('ffpyplayer', all_providers))
if USE_SDL3:
audio_libs.append(make_provider_tuple('sdl3', all_providers))
libs_loaded = core_register_libs('audio_output', audio_libs)