diff --git a/examples/palace-hrtf.py b/examples/palace-hrtf.py index 131ff97..7580411 100755 --- a/examples/palace-hrtf.py +++ b/examples/palace-hrtf.py @@ -57,7 +57,7 @@ def play(files: Iterable[str], device: str, hrtf_name: str, except ValueError: stderr.write(f'HRTF {hrtf_name!r} not found\n') - with Context(dev, attrs) as ctx, Source(ctx) as src: + with Context(dev, attrs) as ctx, Source() as src: if dev.hrtf_enabled: print(f'Using HRTF {dev.current_hrtf!r}') else: @@ -66,11 +66,11 @@ def play(files: Iterable[str], device: str, hrtf_name: str, for filename in files: try: - decoder = Decoder(ctx, filename) + decoder = Decoder(filename) except RuntimeError: stderr.write(f'Failed to open file: {filename}\n') continue - decoder.play(src, CHUNK_LEN, QUEUE_SIZE) + decoder.play(CHUNK_LEN, QUEUE_SIZE, src) print(f'Playing {filename} ({decoder.sample_type},', f'{decoder.channel_config}, {decoder.frequency} Hz)') diff --git a/examples/palace-play.py b/examples/palace-play.py index 17b970e..5080865 100755 --- a/examples/palace-play.py +++ b/examples/palace-play.py @@ -52,7 +52,7 @@ def play(files: Iterable[str], device: str) -> None: ctx.message_handler = LoadingBufferHandler() for filename in files: try: - buffer = Buffer(ctx, filename) + buffer = Buffer(filename) except RuntimeError: stderr.write(f'Failed to open file: {filename}\n') continue diff --git a/examples/palace-stdec.py b/examples/palace-stdec.py index 7f6c069..ae811ae 100755 --- a/examples/palace-stdec.py +++ b/examples/palace-stdec.py @@ -43,11 +43,11 @@ def pretty_time(seconds: float) -> str: def play(files: Iterable[str], device: str) -> None: """Load and play files on the given device.""" - with Device(device) as dev, Context(dev) as ctx: + with Device(device) as dev, Context(dev): print('Opened', dev.name) for filename in files: try: - buffer = Buffer(ctx, filename) + buffer = Buffer(filename) except RuntimeError: stderr.write(f'Failed to open file: {filename}\n') continue diff --git a/src/palace.pyx b/src/palace.pyx index e2da895..916cf13 100644 --- a/src/palace.pyx +++ b/src/palace.pyx @@ -71,7 +71,7 @@ __all__ = [ 'sample_types', 'channel_configs', 'device_names', 'decoder_factories', 'sample_size', 'sample_length', 'query_extension', 'thread_local', 'current_context', 'use_context', 'current_fileio', 'use_fileio', - 'Device', 'Context', 'Buffer', 'Source', 'SourceGroup', + 'Device', 'Context', 'Listener', 'Buffer', 'Source', 'SourceGroup', 'AuxiliaryEffectSlot', 'Effect', 'Decoder', 'BaseDecoder', 'FileIO', 'MessageHandler'] @@ -723,18 +723,28 @@ cdef class Context: cdef class Listener: - """Listener instance of the context, i.e each context - will only have one listener. + """Listener instance of the given context. + + It is recommended that application access the listener via + `Context.listener`, which avoid the overhead caused by the + creation of the wrapper object. Parameters ---------- - context : Context - The `context` on which the listener instance is to be created. + context : Optional[Context], optional + The context on which the listener instance is to be created. + By default `current_context()` is used. + + Raises + ------ + RuntimeError + If there is neither any context specified nor current. """ cdef alure.Listener impl - def __init__(self, context: Context) -> None: - self.impl = context.impl.get_listener() + def __init__(self, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.impl = ( context).impl.get_listener() def __bool__(self) -> bool: return self.impl @@ -797,11 +807,12 @@ cdef class Buffer: Parameters ---------- - context : Context - The context from which the buffer is to be created and cached. name : str Audio file or resource name. Multiple calls with the same name will return the same buffer. + context : Optional[Context], optional + The context from which the buffer is to be created and cached. + By default `current_context()` is used. Attributes ---------- @@ -811,20 +822,20 @@ cdef class Buffer: Raises ------ RuntimeError - If the buffer can neither be loaded - nor be found when `existed` is set. + If there is neither any context specified nor current. """ cdef alure.Buffer impl cdef Context context cdef readonly str name - def __init__(self, context: Context, name: str, - existed: bool = False) -> None: - self.impl = context.impl.find_buffer(name) - if not self: - decoder: Decoder = Decoder.smart(context, name) - self.impl = context.impl.create_buffer_from(name, decoder.pimpl) + def __init__(self, name: str, context: Optional[Context] = None) -> None: + if context is None: context = current_context() self.context, self.name = context, name + self.impl = self.context.impl.find_buffer(self.name) + if not self: + decoder: Decoder = Decoder.smart(self.name, self.context) + self.impl = self.context.impl.create_buffer_from( + self.name, decoder.pimpl) def __enter__(self) -> Buffer: return self @@ -869,27 +880,32 @@ cdef class Buffer: return f'{self.__class__.__name__}({self.name!r})' @staticmethod - def from_decoder(decoder: Decoder, context: Context, name: str) -> Buffer: + def from_decoder(decoder: Decoder, name: str, + context: Optional[Context] = None) -> Buffer: """Return a buffer created by reading the given decoder. Parameters ---------- decoder : Decoder The decoder from which the buffer is to be cached. - context : Context - The context from which the buffer is to be created. name : str The name to give to the buffer. It may alias an audio file, but it must not currently exist in the buffer cache. + context : Optional[Context], optional + The context from which the buffer is to be created. + By default `current_context()` is used. Raises ------ RuntimeError - If the buffer cannot be created. + If there is neither any context specified nor current; + or if `name` is already used for another buffer. """ + if context is None: context = current_context() buffer: Buffer = Buffer.__new__(Buffer) - buffer.impl = context.impl.create_buffer_from(name, decoder.pimpl) buffer.context, buffer.name = context, name + buffer.impl = buffer.context.impl.create_buffer_from( + buffer.name, decoder.pimpl) return buffer @getter @@ -1009,13 +1025,20 @@ cdef class Source: Parameters ---------- - context : Context + context : Optional[Context], optional The context from which the source is to be created. + By default `current_context()` is used. + + Raises + ------ + RuntimeError + If there is neither any context specified nor current. """ cdef alure.Source impl - def __init__(self, context: Context) -> None: - self.impl = context.impl.create_source() + def __init__(self, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.impl = ( context).impl.create_source() def __enter__(self) -> Source: return self @@ -1586,13 +1609,20 @@ cdef class SourceGroup: Parameters ---------- - context : Context + context : Optional[Context], optional The context from which the source group is to be created. + By default `current_context()` is used. + + Raises + ------ + RuntimeError + If there is neither any context specified nor current. """ cdef alure.SourceGroup impl - def __init__(self, context: Context) -> None: - self.impl = context.impl.create_source_group() + def __init__(self, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.impl = ( context).impl.create_source_group() def __enter__(self) -> SourceGroup: return self @@ -1730,18 +1760,20 @@ cdef class AuxiliaryEffectSlot: Parameters ---------- - context : Context + context : Optional[Context], optional The context to create the auxiliary effect slot. + By default `current_context()` is used. Raises ------ RuntimeError - If the effect slot can't be created. + If there is neither any context specified nor current. """ cdef alure.AuxiliaryEffectSlot impl - def __init__(self, context: Context) -> None: - self.impl = context.impl.create_auxiliary_effect_slot() + def __init__(self, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.impl = ( context).impl.create_auxiliary_effect_slot() def __enter__(self) -> AuxiliaryEffectSlot: return self @@ -1843,13 +1875,20 @@ cdef class Effect: Parameters ---------- - context : Context + context : Optional[Context], optional The context from which the effect is to be created. + By default `current_context()` is used. + + Raises + ------ + RuntimeError + If there is neither any context specified nor current. """ cdef alure.Effect impl - def __init__(self, context: Context) -> None: - self.impl = context.impl.create_effect() + def __init__(self, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.impl = ( context).impl.create_effect() def __enter__(self) -> Effect: return self @@ -1952,15 +1991,16 @@ cdef class Decoder: Parameters ---------- - context : Context - The context from which the decoder is to be created. name : str Audio file or resource name. + context : Optional[Context], optional + The context from which the decoder is to be created. + By default `current_context()` is used. Raises ------ RuntimeError - If decoder creation fails. + If there is neither any context specified nor current. See Also -------- @@ -1976,15 +2016,25 @@ cdef class Decoder: """ cdef shared_ptr[alure.Decoder] pimpl - def __init__(self, context: Context, name: str) -> None: - self.pimpl = context.impl.create_decoder(name) + def __init__(self, name: str, context: Optional[Context] = None) -> None: + if context is None: context = current_context() + self.pimpl = ( context).impl.create_decoder(name) @staticmethod - def smart(context: Context, name: str) -> Decoder: + def smart(name: str, context: Optional[Context] = None) -> Decoder: """Return the decoder created from the given resource name. This first tries user-registered decoder factories in lexicographical order, then fallback to the internal ones. + + Raises + ------ + RuntimeError + If there is neither any context specified nor current. + + See Also + -------- + decoder_factories : Simple object for storing decoder factories """ def find_resource(name, subst): if not name: raise RuntimeError('Failed to open file') @@ -1996,6 +2046,7 @@ cdef class Decoder: except FileNotFoundError: return find_resource(subst(name), subst) + if context is None: context = current_context() resource = find_resource( name, context.message_handler.resource_not_found) for decoder_factory in decoder_factories: @@ -2004,7 +2055,7 @@ cdef class Decoder: return decoder_factory(resource) except RuntimeError: continue - return Decoder(context, name) + return Decoder(name, context) @getter def frequency(self) -> int: @@ -2089,7 +2140,8 @@ cdef class Decoder: PyMem_RawFree(ptr) return samples - def play(self, source: Source, chunk_len: int, queue_size: int) -> None: + def play(self, chunk_len: int, queue_size: int, + source: Optional[Source] = None) -> Source: """Stream audio asynchronously from the decoder. The decoder must NOT have its `read` or `seek` called @@ -2097,8 +2149,6 @@ cdef class Decoder: Parameters ---------- - source : Source - The source object to play audio. chunk_len : int The number of sample frames to read for each chunk update. Smaller values will require more frequent updates and @@ -2107,8 +2157,16 @@ cdef class Decoder: The number of chunks to keep queued during playback. Smaller values use less memory while larger values improve protection against underruns. + source : Optional[Source], optional + The source object to play audio. If `None` is given, + a new one will be created from the current context. + + Returns + ------- + The source used for playing. """ - source.impl.play(self.pimpl, chunk_len, queue_size) + if source is None: source = Source() + ( source).impl.play(self.pimpl, chunk_len, queue_size) # Decoder interface