Buffers¶
Buffers are arrays of sample data. Like sound files, they have one or more channels, and one or more frames. They can be read from disk (or written back to it), created empty and populated later, or synthesized by the server - typically to create window functions or waveforms for wavetable synthesis. They’re also used when streaming audio to or from soundfiles too large to load into memory at once.
Like buses, scsynth boots with a fixed number of buffers available.
Unlike buses, these buffers can be reconfigured with different channel counts
and durations on allocation. The Buffer
class provides a proxy to the buffer datastructure residing in a
running scsynth process, and the
BufferGroup
class models a contiguous
block of buffers.
Lifecycle¶
Buffers can only be added to running servers, so let’s create one and boot it:
>>> server = supriya.Server().boot()
Note
Scores
are neither online
nor offline, so you can add buffers to them whenever you like.
Creation¶
Buffers can be allocated empty or immediately filled with all or part of a soundfile read from disk.
Note
You must specify the number of channels and frames when allocating a buffer.
These properties cannot be changed during the lifetime of the buffer.
Allocate an empty buffer with
add_buffer()
and print its
repr to the terminal:
>>> buffer_ = server.add_buffer(channel_count=1, frame_count=512)
>>> buffer_
Buffer(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, completion=Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(AllocateBuffer(buffer_id=0, frame_count=512, channel_count=1, on_completion=None), ...)]), requests=[]))
The repr shows the buffer’s type (Buffer
), its ID (0
) and indicates it has been allocated (+
).
Now, allocate a contiguous group of four empty buffers with
add_buffer_group()
and print its
repr to the terminal:
>>> buffer_group = server.add_buffer_group(
... count=4, channel_count=1, frame_count=512
... )
>>> buffer_group
BufferGroup(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1, count=4)
The repr shows the buffer group’s type (BufferGroup
), the ID of the first buffer in the group (1
) and indicates it has
been allocated (+
).
Note
Why use a BufferGroup
?
While you could allocate multiple single buffers, allocating a group of
buffers in a single operation guarantees that the IDs of the buffers are
contiguous. Some UGens that operator on buffers, like the
wavetable oscillator VOsc
, expect that the
buffers they operate over are contiguously allocated.
The buffer group’s free()
method also guarantees that those IDs are released back to the allocator
pool simultaneously.
Creation from files¶
Let’s locate a soundfile:
>>> file_path = supriya.assets_path / "audio/birds/birds-01.wav"
Allocate a buffer from a soundfile by passing a value to file_path
when
using add_buffer()
:
>>> buffer_ = server.add_buffer(file_path=file_path)
Let’s plot it, and play it:
>>> supriya.plot(buffer_)
>>> supriya.play(buffer_)
Note that channel_count
and frame_count
were omitted; we’re taking the
full set of channels and frames from the source file when reading its contents
into the buffer.
We can allocate a buffer from a partial soundfile by passing a combination of
channel_count
, frame_count
and starting_frame
parameters. Let’s
allocate a buffer from the middle of that soundfile, plot it and play it:
>>> buffer_ = server.add_buffer(
... file_path=file_path, frame_count=8192, starting_frame=33091 // 2
... )
>>> supriya.plot(buffer_)
>>> supriya.play(buffer_)
Let’s grab another soundfile, this time an octophonic one:
>>> file_path = supriya.assets_path / "audio/sine_440hz_44100sr_16bit_octo.wav"
Allocating a buffer from this soundfile shows it contains eight channels:
>>> server.add_buffer(file_path=file_path)
Buffer(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=7, completion=Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(AllocateReadBuffer(buffer_id=7, path=PosixPath('/home/runner/work/supriya/supriya/supriya/assets/audio/sine_440hz_44100sr_16bit_octo.wav'), starting_frame=0, frame_count=0, on_completion=None), ...)]), requests=[]))
We can allocate a buffer from a subset of those channels by passing the number
of channels to grab via the channel_count
parameter:
>>> server.add_buffer(channel_indices=[0, 1], file_path=file_path)
Buffer(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=8, completion=Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(AllocateReadBufferChannel(buffer_id=8, path=PosixPath('/home/runner/work/supriya/supriya/supriya/assets/audio/sine_440hz_44100sr_16bit_octo.wav'), channel_indices=[0, 1], starting_frame=0, frame_count=0, on_completion=None), ...)]), requests=[]))
Todo
Implement server.add_buffer_group(file_paths=[..., ..., ...])
Deletion¶
Free a buffer with:
>>> buffer_.free()
Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(FreeBuffer(buffer_id=Buffer(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=6, completion=Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(AllocateReadBuffer(buffer_id=6, path=PosixPath('/home/runner/work/supriya/supriya/supriya/assets/audio/birds/birds-01.wav'), starting_frame=16545, frame_count=8192, on_completion=None), ...)]), requests=[])), on_completion=None), ...)]), requests=[])
Free a buffer group with:
>>> buffer_group.free()
Disk IO¶
.read()
.write()
Inspection¶
.buffer_id
.__int__()
>>> buffer_ = server.add_buffer(channel_count=2, frame_count=512)
>>> buffer_.id_
9
>>> int(buffer_)
9
Querying¶
.query()
>>> buffer_ = server.add_buffer(channel_count=2, frame_count=512)
>>> buffer_.query()
BufferInfo(items=[BufferInfo.Item(buffer_id=10, frame_count=0, channel_count=0, sample_rate=0.0)])
Getting¶
.get()
.get_range()
>>> buffer_.get(0, 2, 4)
{0: 0.0, 2: 0.0, 4: 0.0}
Buffer UGens¶
BufChannels
BufDur
BufFrames
BufRateScale
BufSampleRate
BufSamples
Interaction¶
Setting¶
.set()
.set_range()
Filling¶
Given a single-channel buffer with 1024 samples:
>>> buffer_ = server.add_buffer(channel_count=1, frame_count=128)
.fill()
.generate()
Copying¶
.copy()
Zeroing¶
.zero()
Normalizing¶
.normalize()
Integration¶
Referencing¶
Buffer IO¶
BufRd and BufWr
PlayBuf and RecordBuf
Continuous Disk IO¶
DiskIn and DiskOut
VDiskIn
leaving_open
.close()
Wavetable synthesis¶
SuperCollider provides a number of wavetable
oscillators, including Osc
,
COsc
, VOsc
, and
VOsc3
All of these UGens accept a buffer_id
argument, pointing to
a buffer filled with some waveform to use as their source material. The
interpolation algorithm used by these oscillators has one important
requirement: the waveforms must be in SuperCollider’s “wavetable format”.
We can ensure the buffer contents are in wavetable format when using any of
the .fill_...()
methods by setting as_wavetable=True
.
Grab a fresh buffer:
>>> buffer_ = server.add_buffer(channel_count=1, frame_count=128)
… and compare the following calls against the non-wavetable versions demonstrated earlier:
>>> buffer_.generate(
... command_name="cheby",
... amplitudes=[1.0, 0.5, 0.25],
... as_wavetable=True,
... )
>>> supriya.plot(buffer_)
>>> buffer_.generate(
... command_name="sine1",
... amplitudes=[1.0, 0.5, 0.25],
... as_wavetable=True,
... )
>>> supriya.plot(buffer_)
>>> buffer_.generate(
... command_name="sine2",
... amplitudes=[1.0, 0.5, 0.25],
... frequencies=[1, 3, 5],
... as_wavetable=True,
... )
>>> supriya.plot(buffer_)
>>> buffer_.generate(
... command_name="sine3",
... amplitudes=[1.0, 0.5, 0.25],
... frequencies=[1, 3, 5],
... phases=[0.0, 0.333, 0.666],
... as_wavetable=True,
... )
>>> supriya.plot(buffer_)
While /b_gen
may be able to create waveforms in the expected wavetable
format, there’s no functionality built into scsynth to load arbitrary
soundfiles and convert them into wavetable format in the process, or to copy an
existing buffer’s contents into another buffer and convert.
Todo
Implement wavetable utilities for loading arbitrary audio.
Configuration¶
The maximum number of buffers available in a context is controlled by its options.
Options.buffer_count