Skip to content

Commit

Permalink
Documentation: Add Buffer docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ideoforms committed Jan 14, 2024
1 parent 3be54f5 commit 08686a8
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 29 deletions.
47 changes: 47 additions & 0 deletions docs/buffer/access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Accessing in memory

The floating-point samples within a SignalFlow `Buffer` can be read and written directly from Python.

## Using get and set methods

The `get()` and `set()` methods can be used to access individual samples, indexed by channel and frame offset.

```python
# Create a 2-channel buffer
buf = Buffer(2, 256)

# Set the sample in channel 1 at index 20 to 0.5
buf.set(1, 20, 0.5)

# Confirm that the sample is set correctly
assert buf.get(1, 20) == 0.5
```

## As a numpy array

The `.data` property of a `Buffer` points to a numpy array of shape `(num_channels, num_frames)`, which can be used to read or write the buffer's data in real time.

```python
import time

# Create and play a one-second silent buffer
buf = Buffer(2, graph.sample_rate)
player = BufferPlayer(buf, loop=True)
player.play()

# Gradually add crackles to the buffer, which will be heard in real-time
while True:
buf.data[0][np.random.randint(0, graph.sample_rate)] = 1.0
buf.data[1][np.random.randint(0, graph.sample_rate)] = 1.0
time.sleep(1)
```

## Filling a buffer with the result of a function

Just like when [creating a buffer](creating.md#initialising-a-buffer-with-the-result-of-a-function), an existing buffer can be filled with the output of a Python function.



---

[→ Next: Arithmetic operators](operators.md)
83 changes: 83 additions & 0 deletions docs/buffer/creating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Creating a Buffer

A Buffer can be created from:

- [a sound file](#loading-a-buffer-from-a-sound-file)
- [an array of samples](#creating-a-buffer-from-an-array-of-samples)
- [a specified dimension](#creating-an-empty-buffer)
- [the result of a function](#initialising-a-buffer-with-the-result-of-a-function)

## Loading a buffer from a sound file

To load an audio buffer from a sound file, pass the file path to Buffer's constructor.

```python
# Load and play a buffer
buf = Buffer("filename.wav")
player = BufferPlayer(buf)
player.play()
```

The type of the audio file is automatically inferred from the type and contents. Supported formats include `wav`, `aif`, `mp3`, `ogg`, `flac`, and many other audio formats.

Interally, file I/O is handled by `libsndfile`. For a full list of supported files, see the [libsndfile documentation](http://www.mega-nerd.com/libsndfile/).

---

## Creating a buffer from an array of samples

To create and initialise a buffer from an existing array of samples, pass the array to Buffer's constructor. Both native Python arrays and `numpy` arrays are supported.

Note that audio samples should always range between `-1.0` and `1.0` to avoid distortion.

```python
# Initialise a buffer from a native 1D array containing a sawtooth wave
samples = [(n % 100) / 100 - 0.5 for n in range(44100)]
buf = Buffer(samples)
player = BufferPlayer(buf)
player.play()
```

If the array is 1D, a mono buffer will be created. If the array is 2D, a multichannel buffer will be created.

```python
# Initialise a buffer from a numpy 2D array containing a stereo sine wave
import numpy as np

t = np.linspace(0, 1, 44100)
stereo = np.array([np.sin(220 * t * np.pi * 2),
np.sin(225 * t * np.pi * 2)])
buf = Buffer(stereo * 0.1)
player = BufferPlayer(buf)
player.play()
```

---

## Creating an empty buffer

An empty buffer can be initialised by specifying its dimensions. All samples will be initialised to zero.

```python
# Create an empty buffer with 2 channels containing 44100 samples each.
buf = Buffer(2, 44100)
```

---

## Initialising a buffer with the result of a function

A buffer can also be populated with the result of a Python function, which takes a single argument containing the index of the frame to be filled.

```python
# Create a buffer containing a 440Hz ping
import numpy as np
buf = Buffer(1, graph.sample_rate,
lambda frame: np.sin(frame * 440 * np.pi * 2 / graph.sample_rate) * (1 - frame / graph.sample_rate))
player = BufferPlayer(buf)
player.play()
```

---

[→ Next: Saving and exporting a buffer](exporting.md)
17 changes: 17 additions & 0 deletions docs/buffer/exporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Saving and exporting a buffer

## Saving to a sound file

To export a buffer's audio contents to a sound file, use the `save()` method:

```python
import numpy as np
buf = Buffer(np.sin(np.linspace(0, 1, graph.sample_rate) * 440 * np.pi * 2))
buf.save("buffer.wav")
```

The output format will be automatically detected from the filename extension. Supported formats are presently `wav`, `aif` and `flac`.

---

[→ Next: Passing a buffer as an input to a node or patch](input.md)
27 changes: 12 additions & 15 deletions docs/buffer/index.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
# Buffer
# Buffers

!!! warning
This documentation is a work-in-progress and may have sections that are missing or incomplete.
A `Buffer` is an area of memory that stores single-channel or multi-channel data, which may represent an audio waveform or any other type of signal.

A `Buffer` is an allocated area of memory that can be used to store single-channel or multi-channel data, which may represent an audio waveform or any other type of signal.

- A Buffer can be created from a sound file, an array of samples, or with an empty contents
- A Buffer can be passed to a Node or Patch as an input
- A Buffer can be exported to a sound file
- A Buffer's data can be directly accessed in memory as a numpy array, or by get/set methods
- The contents of a buffer can be combined with arithmetic operators
- Properties
- Buffer interpolation modes
- 2D buffers
- Buffer applications: Sample recording and playback, control recording and playback, envelopes, waveshapers
- The total Buffer memory usage can be queried
- A Buffer can be [created](creating.md) from a sound file, an array of samples, a specified dimension, or the result of a function
- A Buffer can be [saved to a sound file](exporting.md)
- A Buffer can be [passed to a Node or Patch](input.md) as an input
- Buffer [sample access](access.md) can be performed by get/set/fill methods, or directly as a numpy array
- Buffers can be modified, combined and queried with standard [arithmetic operators](operators.md)
- Buffers can be queried for a number of [properties](properties.md), including interpolation modes and total memory usage
- _TODO_: Different Buffer subclasses exist for specific operations, including `Buffer2D`, `WaveshaperBuffer` and `EnvelopeBuffer`
- _TODO_: Playing a buffer, including sample rate conversion and interpolation
- _TODO_: Recording and rendering audio into a Buffer

10 changes: 10 additions & 0 deletions docs/buffer/input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Passing a buffer as an input to a node or patch

See:

- [Node: Buffer inputs](../node/inputs.md#buffer-inputs)
- [Patch: Buffer inputs](../patch/inputs.md#buffer-inputs)

---

[→ Next: Accessing a buffer's data in memory](access.md)
24 changes: 24 additions & 0 deletions docs/buffer/operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Arithmetic operators

Buffers, [like nodes](../node/operators.md), can be manipulated using Python's standard arithmetic operators.

For example, to attenuate a buffer, it can be multiplied by a constant value. A new `Buffer` object is returned, with the same dimensions as the original, scaled by the coefficient.

```python
input_buffer = Buffer("input.wav")
scaled_buffer = input_buffer * 0.5
# `scaled_buffer` now contains an attenuated version of `input_buffer`
```

Below is a full list of operators supported by SignalFlow `Buffer` objects.

| Operator | Node class |
|----------|------------|
| `+` | Add |
| `-` | Subtract |
| `*` | Multiply |
| `/` | Divide |

---

[→ Next: Buffer properties](properties.md)
16 changes: 16 additions & 0 deletions docs/buffer/properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Buffer properties

A `Buffer` has a number of read-only properties which can be used to query its status at a given moment in time.

| Property | Type | Description |
|--------------------|-------|------------------------------------------------------------------------------------|
| num_frames | int | The number of frames (samples) in the buffer. |
| channels | int | The number of channels in the buffer. |
| sample_rate | int | The sample rate of the buffer. |
| duration | float | The duration of the buffer, in seconds. |
| filename | str | If the buffer has been loaded from/saved to a file, the absolute path to the file. |
| interpolation_mode | str | The interpolation mode of the buffer. [LINK] |

---

[→ Next: Multichannel](multichannel.md)
1 change: 0 additions & 1 deletion docs/graph/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ To print the current configuration to stdout:
graph.config.print()
```


---

[→ Next: Graph status and properties](properties.md)
32 changes: 31 additions & 1 deletion docs/node/inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,40 @@ output.play()
The third type of input supported by nodes is the [buffer](../buffer/index.md). Nodes often take buffer inputs as sources of audio samples. They are also useful as sources of envelope shape data (for example, to shape the grains of a Granulator), or general control data (for example, recording motion patterns from a `MouseX` input).

```python
buffer = Buffer("../audio/stereo-count.wav")
buffer = Buffer("audio/stereo-count.wav")
player = BufferPlayer(buffer, loop=True)
```

A buffer input cannot be set using the [same property shorthand as audio-rate inputs](#audio-rate-inputs); instead, the `set_buffer` method should be used.

```python
new_buffer = Buffer("audio/example.wav")
player.set_buffer("buffer", new_buffer)
```

_TODO: Should this be `set_input` for consistency with Patch?_

## Enumerating a node's inputs

To list the potential and actual inputs of a node, the `.inputs` property returns a `dict` of key-value pairs:

```python
>>> player.inputs
{
'clock': <signalflow.Impulse at 0x107778eb0>,
'end_time': None,
'loop': <signalflow.Constant at 0x12a4cd4b0>,
'rate': <signalflow.Constant at 0x12a4cd330>,
'start_time': None
}
```

Any constant-valued inputs are wrapped inside a special `Constant` node class. The value contained by a `Constant` can be accessed with its `.value` property.

```python
>>> player.inputs["rate"].value
1.0
```
---

[→ Next: Operators](operators.md)
22 changes: 11 additions & 11 deletions docs/node/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

A `Node` has a number of read-only properties which can be used to query its status at a given moment in time.

| Property | Type | Description |
|----------|--|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | str | Short alphanumeric string that identifies the type of node (for example, `asr-envelope`) |
| num_output_channels | int | The number of output channels that the node generates. |
| num_input_channels | int | The number of input channels that the node takes. Note that most nodes have `matches_input_channels` set, meaning that their `num_input_channels` will be automatically increased according to their inputs. To learn more, see [Nodes: Multichannel](multichannel.md). |
| matches_input_channels | bool | Whether the node automatically increases its `num_input_channels` based on its inputs. To learn more, see [Nodes: Multichannel](multichannel.md). |
| has_variable_inputs | bool | Whether the node supports an [arbitrary number of audio-rate inputs](inputs.md#variable-input-nodes) |
| output_buffer | numpy.ndarray | Contains the Node's most recent audio output, in `float32` samples. The buffer is indexed by `channel` x `frame`, so to obtain the 32nd sample in the first channel, query: `node.output_buffer[0][31]`. |
| inputs | dict | A dict containing all of the `Node`'s audio-rate inputs. Note that buffer inputs are not currently included within this dict. |
| state | int | The Node's current playback state, which can be one of `SIGNALFLOW_NODE_STATE_ACTIVE` and `SIGNALFLOW_NODE_STATE_STOPPED`. The `STOPPED` state only applies to those nodes which have a finite duration (e.g. `ASREnvelope`, or `BufferPlayer` with looping disabled) and have reached the end of playback. Nodes continue to have a state of `ACTIVE` whether or not they are connected to the graph. |
| patch | Patch | Indicates the [Patch](../patch/index.md) that the node is part of, or None if the Node does not belong to a Patch. |
| Property | Type | Description |
|------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | str | Short alphanumeric string that identifies the type of node (for example, `asr-envelope`) |
| num_output_channels | int | The number of output channels that the node generates. |
| num_input_channels | int | The number of input channels that the node takes. Note that most nodes have `matches_input_channels` set, meaning that their `num_input_channels` will be automatically increased according to their inputs. To learn more, see [Nodes: Multichannel](multichannel.md). |
| matches_input_channels | bool | Whether the node automatically increases its `num_input_channels` based on its inputs. To learn more, see [Nodes: Multichannel](multichannel.md). |
| has_variable_inputs | bool | Whether the node supports an [arbitrary number of audio-rate inputs](inputs.md#variable-input-nodes) |
| output_buffer | numpy.ndarray | Contains the Node's most recent audio output, in `float32` samples. The buffer is indexed by `channel` x `frame`, so to obtain the 32nd sample in the first channel, query: `node.output_buffer[0][31]`. |
| inputs | dict | A dict containing all of the `Node`'s audio-rate inputs. Note that buffer inputs are not currently included within this dict. |
| state | int | The Node's current playback state, which can be one of `SIGNALFLOW_NODE_STATE_ACTIVE` and `SIGNALFLOW_NODE_STATE_STOPPED`. The `STOPPED` state only applies to those nodes which have a finite duration (e.g. `ASREnvelope`, or `BufferPlayer` with looping disabled) and have reached the end of playback. Nodes continue to have a state of `ACTIVE` whether or not they are connected to the graph. |
| patch | Patch | Indicates the [Patch](../patch/index.md) that the node is part of, or None if the Node does not belong to a Patch. |

### Monitoring a node's output

Expand Down
8 changes: 7 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ nav:
- Inputs: patch/inputs.md
- Operators: patch/operators.md
- Properties: patch/properties.md
- Exporting and importing patches: patch/exporting.md
- Exporting and importing: patch/exporting.md
- Auto-free and memory management: patch/auto-free.md
- Buffers:
- Buffers: buffer/index.md
- Creating and loading: buffer/creating.md
- Saving and exporting: buffer/exporting.md
- Passing to a node or patch: buffer/input.md
- Accessing a buffer's contents: buffer/access.md
- Operators: buffer/operators.md
- Properties: buffer/properties.md
- Reference library:
- "Reference library": library/index.md
- "Analysis": library/analysis/index.md
Expand Down

0 comments on commit 08686a8

Please sign in to comment.