Skip to content

Commit

Permalink
add more example code to README
Browse files Browse the repository at this point in the history
  • Loading branch information
dsuedholt committed Jan 9, 2025
1 parent 1ac937a commit 7085ea0
Showing 1 changed file with 142 additions and 11 deletions.
153 changes: 142 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ The bindings consist of two parts:
- C++ wrapper classes (contained in `src/wrappers/`) encapsulate SDT's C API, especially the manual memory management
- [nanobind](https://github.com/wjakob/nanobind) bindings (contained in `src/pysdt.cpp`) make the wrappers available in Python

Proper documentation for the Python library will be added at some point. Until then, the [SDT documentation](https://skat-vg.github.io/SDT/) should be helpful as well, since the bindings follow the C API and module structure very closely. Getter/setter pairs are usually captured as Python `@properties`.
Proper documentation for the Python library will be added at some point. Until then, refer to examples below. The [SDT documentation](https://skat-vg.github.io/SDT/) should be helpful as well, since the bindings follow the C API and module structure very closely. Getter/setter pairs are usually captured as Python `@properties`.

### Features
## Features

Most of the SDT functionality is available in the bindings, **except for**
- OSC support
- JSON export/import
- anything with a straightforward `numpy` equivalent, e.g. array means or FFT calculation

### Installation
## Installation

Packaged wheels are available for Linux, MacOS and Windows.

Expand All @@ -32,7 +32,7 @@ cd SDT && git submodule update --init 3rdparty/json-builder 3rdparty/json-parser
cd .. && pip install .
```

### Usage
## Usage

By default, **the global sampling rate is set to 0 Hz**. It is important to set the global sampling rate explicitly at the start of your script. If you change the sampling rate later on, you may need to call `update()` on some objects; check the [SDT documentation](https://skat-vg.github.io/SDT/).

Expand All @@ -43,21 +43,152 @@ import pysdt
import numpy as np

sr = 44100

pysdt.common.set_sampling_rate(sr)
pysdt.common.set_samplerate(sr)

# buffer length of an internal comb filter is a constructor argument
motor = pysdt.dcmotor.DCMotor(1024)

# properties bind to getRpm / setRpm etc
motor.rpm = 3000
motor.load = 0.5
motor.rpm = 4000
motor.load = 0.2

# motor.gear_ratio = ...
# motor.coils = ...
# see https://skat-vg.github.io/SDT/group__dcmotor.html for more parameters

result = np.zeros(sr)
audio = np.zeros(sr)
for i in range(sr):
result[i] = motor.dsp()
audio[i] = motor.dsp()

# we have sound! can now process, play or save it, e.g:
import soundfile as sf
sf.write("engine.wav", result, sr)
sf.write("engine.wav", audio, sr)
```

### `dsp(...)` methods

Most SDT classes have a `dsp()` method that expects to be called once per sample, but the signature can vary. Here are some examples that demonstrate how PySDT wraps these depending on their signature in the C API.

```python
import pysdt
import numpy as np

sr = 44100
pysdt.common.set_samplerate(sr)

audio = np.zeros(sr)

# C signature: double dsp(...)
# return single floating point number
bubble = pysdt.liquids.Bubble()
bubble.radius = 0.001
bubble.rise_factor = 0.1
bubble.depth = 0.7

rev = pysdt.effects.Reverb(sr)
rev.time = 0.5
rev.update()

for i in range(sr):
if i % (sr // 4) == 0:
bubble.trigger()
bubble_out = bubble.dsp()
audio[i] = rev.dsp(bubble_out * 100)


# C signature: void dsp(..., double *out, ...)
# return multiple floating point numbers
expl = pysdt.gases.Explosion(sr, sr)
expl.blast_time = 0.1
expl.scatter_time = 4
expl.dispersion = 0.5
expl.distance = 10
expl.wave_speed = 340.2
expl.wind_speed = 600

expl.trigger()

for i in range(sr):
wave, wind = expl.dsp()
audio[i] = 0.5 * wave + 0.5 * wind

# C signature: int dsp(..., double *out, double in)
# This one is mostly used in SDTAnalysis / pysdt.analysis
# It expects to be called every sample,
# but will only provide output when a frame has been filled
# returns Tuple[bool, np.ndarray[float]]
audio = np.cos(440 * 2 * np.pi * np.arange(sr) / sr) # simple sine wave
win_size = 1024
pitch = pysdt.analysis.Pitch(win_size)
pitch.overlap = 0.5

f0s, confs = [], []
for i in range(sr):
has_values, values = pitch.dsp(audio[i])
if has_values:
f0, conf = values
f0s.append(f0)
confs.append(conf)
```

### Interactions

Interactors apply forces to resonators and can optionally couple two resonators together. Internally, the `dsp()` method of an interactor will call the `dsp()` method of the resonators it interacts with. After the interactor's `dsp()` method was called, audio can be read from the position value of the resonators' pickups.

The following code replicates the example shown in the helpfile of the Pd `scraping~` object:

```python
import pysdt
import numpy as np

sr = 44100
pysdt.common.set_samplerate(sr)

audio = np.zeros(sr)

n_modes = 3
n_pickups = 1

res = pysdt.resonators.Resonator(n_modes, n_pickups)

freqs = [500, 1300, 1700]
decays = [0.03, 0.02, 0.01]
pickups = [100, 100, 100]
weights = [1, 1, 1]

res.active_modes = 3

for i in range(n_modes):
res.set_frequency(i, freqs[i])
res.set_decay(i, decays[i])
res.set_gain(0, i, pickups[i])
res.set_weight(i, weights[i])

res.fragment_size = 1

res.update()

impact = pysdt.interactors.Impact()
impact.stiffness = 1e8
impact.dissipation = 0.8
impact.shape = 1.5

impact.first_point = 0
impact.second_point = 0

impact.first_resonator = res

scraping = pysdt.control.Scraping()
scraping.velocity = 1
scraping.grain = 0.001
scraping.force = 2

lop = pysdt.filters.OnePole()
lop.lowpass(20)

for i in range(sr):
noise = pysdt.oscillators.white_noise()
scrape_force = scraping.dsp(lop.dsp(noise)) * 10
impact.dsp(scrape_force, 0, 0, 0, 0, 0)
audio[i] = res.get_position(0) * 50000
```

0 comments on commit 7085ea0

Please sign in to comment.