Hello, world!, context-agnostic¶
Info
See the full example source code on GitHub.
Let’s revisit the first “hello, world!” example and apply the concept of contexts to create the same C-major chord in non-realtime, “sync” realtime and “async” realtime contexts.
We’ll demonstrate how some code can be “context-agnostic”: it doesn’t care if you’re running it live or using it to create a pre-recorded audio file offline.
This might seem a little abstract, but it’s helpful for code re-use, and is foundational for more complex audio applications. Remember how commercial DAWs let you both play your song live and also render it to disk as quickly as possible? Context-agnostic performance logic is part of unlocking that.
Performance logic¶
Let’s extract some performance logic out of the first “hello, world!” example.
We can pull out the logic for starting the chord into its own function, passing the context in as an argument, and returning the list of synths when we’re done:
def play_synths(context: supriya.Context) -> list[supriya.Synth]:
"""
Play a C-major chord on ``context``.
"""
# Define a C-major chord in Hertz
frequencies = [261.63, 329.63, 392.00]
# Create an empty list to store synths in:
synths: list[supriya.Synth] = []
# Add the default synthdef to the server and open a "completion" context
# manager to group further commands for when the synthdef finishes loading:
with context.add_synthdefs(supriya.default):
# Loop over the frequencies:
for frequency in frequencies:
# Create a synth using the default synthdef and the frequency
# and add it to the list of synths:
synths.append(
context.add_synth(synthdef=supriya.default, frequency=frequency)
)
return synths
Note
Look closely at the context usage when starting the chord: we don’t see any
usage of at()
to specify when the
chord plays. Realtime and non-realtime contexts have subtly different
semantics around timestamps both in terms of whether timestamps are
mandatory and what the exact time in the timestamp should be. Because of
those different semantics, we’ll treat setting up the
Moment
as a context-specific
implementation detail external to this function.
And we can pull out the logic for stopping the chord into its own function, passing a list of synths in as an argument, and return nothing because there’s nothing else to be done:
def stop_synths(synths: list[supriya.Synth]) -> None:
"""
Stop ``synths``.
"""
# Loop over the synths and free them
for synth in synths:
synth.free()
Nice. We’ve decoupled playing the chord from stopping the chord, and we’ve removed any notion of a concrete context type. These functions can work with both servers and scores interchangeably.
Context management¶
While having context-agnostic logic is great, we still need specific contexts to run our context-agnostic code with.
Let’s create separate functions for playing our C-major chord: two realtime
functions using a Server
and
AsyncServer
respectively, and one
non-realtime function using a Score
.
Threaded realtime¶
Performing with a threaded server is simple, and looks very much like an
abridged version of the main()
function from the very first example:
def run_threaded() -> None:
"""
Run the example on a realtime threaded
:py:class:`~supriya.contexts.realtime.Server`.
"""
# Create a server and boot it:
server = supriya.Server().boot()
# Start an OSC bundle to run immediately:
with server.at():
# Start playing the synths
synths = play_synths(context=server)
# Let the notes play for 4 seconds:
time.sleep(4)
# Loop over the synths and free them:
stop_synths(synths)
# Wait a second for the notes to fade out:
time.sleep(1)
# Quit the server:
server.quit()
Async realtime¶
Performing with an async server is virtually identical to performing with a
threaded server. The only differences are the server’s class name, and a
sprinkling of async
and await
keywords (and yes, await
asyncio.sleep()
instead of time.sleep()
):
async def run_async() -> None:
"""
Run the example on an realtime async
:py:class:`~supriya.contexts.realtime.AsyncServer`.
"""
# Create an async server and boot it:
server = await supriya.AsyncServer().boot()
# Start an OSC bundle to run immediately:
with server.at():
# Start playing the synths:
synths = play_synths(context=server)
# Let the notes play for 4 seconds:
await asyncio.sleep(4)
# Loop over the synths and free them:
stop_synths(synths)
# Wait a second for the notes to fade out:
await asyncio.sleep(1)
# Quit the async server:
await server.quit()
Booting, quitting and sleeping are all async
operations in this paradigm,
but sending messages to the context doesn’t change at all. “Writes” or
“mutations” against the server are never an asynchronous operation. We just
fire them off.
Non-realtime¶
Performing with a non-realtime score is a little different. While the score doesn’t need to be booted or quit, we do have to be absolutely explicit about when everything happens. Non-realtime contexts have no concept of “now”, so every moment needs to have an explicit timestamp.
We open a moment at 0
seconds and start playing the synths. We don’t need
to sleep since nothing is happening live, so instead we open a second moment at
4
seconds to stop playing the synths. And to handle the same 1
second
fade out that the realtime functions achieve via a final sleep(1)
, we just
open a third moment at 5
seconds and… do nothing. The final no-op
operation against the context just signals to non-realtime rendering that we
should keep processing audio until that timestamp.
def run_nonrealtime() -> None:
"""
Run the example on a non-realtime
:py:class:`~supriya.contexts.nonrealtime.Score`.
"""
# Create a score with stereo outputs:
score = supriya.Score(output_bus_channel_count=2)
# Start an OSC bundle to run at 0 seconds:
with score.at(0):
# Start playing the synths:
synths = play_synths(context=score)
# Start an OSC bundle to run at 4 seconds:
with score.at(4):
# Loop over the synths and free them:
stop_synths(synths)
# Start an OSC bundle to run at 5 seconds:
with score.at(5):
# A no-op message to tell the score there's nothing left to do:
score.do_nothing()
# Render the score to disk and open the soundfile:
supriya.play(score)
Note
The timestamps used with scores are all absolute, starting from 0
seconds.
You don’t need to open moments in any particular order: the moment at 5
seconds could’ve been used before the moments at 4
or 0
. But
stopping the synths had to be applied after staring them, because we still
needed Synth
objects with IDs to act
against.
Timestamps can be visited multiple times too, allowing you to make multiple passes against the same score. Additional commands run at the same timestamp will be appended to the sequence of commands to be run at that timestamp.
The call at the end to play()
renders the score to disk
and opens the result in your default audio player.
Scripting¶
Now that we have three different ways of creating contexts to use our context-agnostic performance logic, we need a way to yoke them into a script.
Our main()
function parses some
command-line arguments, and then calls the appropriate run_...()
function
depending on what we passed to the CLI on invocation:
def main(args: list[str] | None = None) -> None:
"""
The example entry-point function.
"""
parsed_args = parse_args(args)
if parsed_args.realtime_threaded:
run_threaded()
elif parsed_args.realtime_async:
asyncio.run(run_async())
elif parsed_args.nonrealtime:
run_nonrealtime()
And, for simplicity, we’ll wrap up the CLI argument parsing into its own
function, using Python’s argparse
module to create an argument
parser, create a mandatory mutually-exclusive group of flags, and then parse
whatever CLI arguments were passed when we run the script:
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
"""
Parse CLI arguments.
"""
parser = argparse.ArgumentParser(
description="Play a C-major chord via different kinds of contexts"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--realtime-threaded",
action="store_true",
help="use a realtime threaded Server",
)
group.add_argument(
"--realtime-async",
action="store_true",
help="use a realtime asyncio-enabled AsyncServer",
)
group.add_argument(
"--nonrealtime", action="store_true", help="use a non-realtime Score"
)
return parser.parse_args(args)
Invocation¶
You can invoke the script with …
josephine@laptop:~/supriya$ python -m examples.hello_world_contexts --help
usage: __main__.py [-h] (--realtime-threaded | --realtime-async |
--nonrealtime)
Play a C-major chord via different kinds of contexts
options:
-h, --help show this help message and exit
--realtime-threaded use a realtime threaded Server
--realtime-async use a realtime asyncio-enabled AsyncServer
--nonrealtime use a non-realtime Score
… and you’ll see the options necessary to properly run it.
For example:
josephine@laptop:~/supriya$ python -m examples.hello_world_contexts --realtime-threaded