Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ audio pump holding up under live use such as a stompbox or a stage rig.
sample rather than at the top of the next block.
- An LC3 codec module beside `audiomp3`, frame by frame: encode PCM to one
frame, decode a frame (or a lost one) to PCM. Leaves room for LE Audio later.
- A level ramp on `audiomixer` voices: a level move ramps over a set number of
frames instead of stepping, so an effect can fade its wet path in or out
without a click when you switch it in mid-note.
- `audiopump.DriftResampler` converting down as well as up, with its filter's
cutoff following the output rate, so a 48 kHz stream can play on a
44.1 kHz output.
Expand Down
13 changes: 13 additions & 0 deletions docs/guides/swap-what-plays.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ port.play(bitcrusher) # heard on the next block; nothing rewires
port.source # what it is playing right now
```

To switch once the old source has had time to finish something, such as a
level walking down over 20 ms, give `after` a number of frames. The port
keeps playing what it plays for that many more frames and switches at the
start of the next pull. Until then `source` is still the old one, and a
`play()` replaces the waiting switch. A port nothing has pulled since it was
pointed at what it plays now switches at once, since that hasn't started.
Pointing it at the same source again doesn't count as a new start.

```python
mixer.voice[0].level = 0.0 # walks down over 960 frames
port.play(dry, after=960 + 256) # switches once the walk has landed
```


`Port` has no `stop()`, deliberately — a port always has a source. A port
pulled while it is already inside itself, which is what a component wrapping
Expand Down
42 changes: 42 additions & 0 deletions docs/how-it-works/upstream-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,48 @@ CircuitPython, newest first. For the current list of deliberate differences,
and how and when to resync with upstream, start at
[upstream-sync.md](upstream-sync.md).

## A level ramp on Mixer voices

`audiomixer.Mixer.level_ramp` is new here; CircuitPython's Mixer doesn't have
it. Set it to a number of frames and a voice's level or pan move walks from
the old level to the new one in a straight line over that many frames. Left
at 0, the default, the Mixer is CircuitPython's to the byte.

```python
mixer.level_ramp = 960 # 20 ms at 48 kHz
mixer.voice[0].level = 0.0 # walks down over the next 960 frames
mixer.voice[1].level = 1.0 # walks up over the same frames
```

CircuitPython 10.3.0 holds a level move until the voice's next zero crossing,
so a move can't step a single waveform mid-cycle. That isn't enough for a
crossfade. A dry voice and a wet one cross zero at different moments, and a
voice whose signal never crosses zero (a lopsided shaper's output, a DC
offset) waits until the end of a stretch of its source and then lands at
once. Either way the sum steps, and a crossfade moved in one call can click.
The walk lives in the node because it has to move once a sample, and Python
only runs when a control is called.

How it walks, in [`src/audiomixer/Mixer.c`](../../src/audiomixer/Mixer.c):

- One step per packed 32-bit word the mix-down scales: a frame in a stereo
Mixer, two frames in a mono one. The level is held as Q15 shifted up 15
more bits, so a step smaller than one Q15 unit still adds up, and the last
step lands exactly on the level asked for.
- A voice's first level, the one it starts at, lands as CircuitPython's does.
The walk starts from the next move, so a fresh Mixer's first block is the
same bytes with the ramp on or off.
- A move made during a walk starts a new walk from where the level stands.
- Level and pan are read once per stretch, as before. A level driven by a
`synthio.LFO` starts a new walk each time it's read, so it follows the LFO
a walk behind.

The CPython twin walks one int16 pair at a time, which is the same word.
[`tests/test_cpython_audiomixer_level_ramp.py`](../../tests/test_cpython_audiomixer_level_ramp.py)
holds the walk's shape, and `mixer_level_ramp_probe.py` holds MicroPython and
CPython to the same bytes through `verify_dsp.py`. CircuitPython doesn't run
it, because its Mixer has no `level_ramp`.

## `audiosampler`: a sample player, because a Mixer voice cannot change pitch

Nothing in CircuitPython does this. `audiomixer.MixerVoice` pulls a *source*
Expand Down
3 changes: 3 additions & 0 deletions docs/how-it-works/upstream-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ reason, and a resync should not take them.
Level and panning are always block inputs, and the ARM CMSIS intrinsics are
the portable C
([why](upstream-diff.md#audiomixer-synthio-block-input-made-unconditional-arm-cmsis-dropped-tier-3)).
`Mixer.level_ramp` is new: set it and a level or pan move walks instead of
waiting for a zero crossing
([why](upstream-diff.md#a-level-ramp-on-mixer-voices)).
- **audiospeed**: the Q16 rate rounds and the phase carries across a source
buffer
([rate](upstream-diff.md#audiospeed-the-q16-rate-rounds-here-upstream-truncates-audiodsp92),
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/filters-and-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ introduction to the biquad, see [Filter a sound](../guides/filter-a-sound.md).
The reasons these exist beside CircuitPython's own modules are in
[Additions beyond CircuitPython](../how-it-works/additions.md).

A level moved on a playing node walks to its new value in a straight line
over 20 ms instead of stepping, so turning it in one call doesn't click: the
`mix` of `Biquad`, `AllPass`, `Waveshaper` and `FeedbackDelay`, the
Waveshaper's `post_gain` and the Dynamics node's `makeup_db`. A node that
hasn't played yet, or has just been reset, takes its levels at once.

## `audiobiquad`


Expand All @@ -21,7 +27,7 @@ When a `Biquad`'s `frequency`, `Q`, `gain_db` or `mode` changes while it plays,
it glides to the new filter over about 20 ms instead of jumping, so a preset
change that drops the corner from 20 kHz to 20 Hz doesn't thump. A block input
glides the same way, so an LFO on `frequency` arrives about a block late.
`mix` still moves at once, and so does a filter that hasn't played yet.
A filter that hasn't played yet takes its new settings at once.

`mode` is one of `LOW_PASS`, `HIGH_PASS`, `BAND_PASS`, `NOTCH`,
`PEAKING_EQ`, `LOW_SHELF`, `HIGH_SHELF` — numbered exactly as
Expand Down
Loading
Loading