diff --git a/lib/audioeffects/analogdelay.py b/lib/audioeffects/analogdelay.py index 74f35ead..82a27255 100644 --- a/lib/audioeffects/analogdelay.py +++ b/lib/audioeffects/analogdelay.py @@ -190,6 +190,8 @@ class AnalogDelay(_component.Component): **Level and tail.** Below Mix 1, an input peaking at or below floor(32767 (1 - Mix)) - 1 does not reach the rail. + Keep the input under -6 dBFS and the defaults and patches stay off the + rails. `tail_samples` is an upper bound on how long the repeats take to reach exact zero after your input stops. `reset()` empties the line and returns to patch 0. @@ -221,6 +223,11 @@ class AnalogDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, the defaults rail on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Feedback", "Mix", "Modulation", "Mod Rate", "Spread", "Sync", "Division") MACRO_MODES = { diff --git a/lib/audioeffects/bitcrusher.py b/lib/audioeffects/bitcrusher.py index d6f1972f..8eb13aee 100644 --- a/lib/audioeffects/bitcrusher.py +++ b/lib/audioeffects/bitcrusher.py @@ -1375,8 +1375,9 @@ class Bitcrusher(_component.Component): **Mix and Output.** Mix 0 is a wire at the source's own level. Output trims the whole blend: -6 dB is 6 dB down within 0.1 dB, at Mix 1 and - at a blend. The Effect controls page lists what the knobs span and the - patches. + at a blend. Keep the input under -3 dBFS and the defaults and patches + stay off the rails. The Effect controls page lists what the knobs span + and the patches. @@ -1400,6 +1401,11 @@ class Bitcrusher(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = 0 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at full scale, patch 1 rails on + #: a 50 Hz sine. + INPUT_CEILING_DBFS = -3 + MACRO_LABELS = ("Bits", "Rate", "Dither", "Band Limit", "Mix", "Output") MACRO_MODES = { 0: "UNIPOLAR", 1: "UNIPOLAR", 2: "UNIPOLAR", 3: "TOGGLE", diff --git a/lib/audioeffects/combfilter.py b/lib/audioeffects/combfilter.py index e3f16cb8..490f37a1 100644 --- a/lib/audioeffects/combfilter.py +++ b/lib/audioeffects/combfilter.py @@ -259,9 +259,9 @@ class CombFilter(_component.Component): **Glide.** Glide slides the tuning, so turning Frequency bends the note rather than clicking. - **Level.** A comb gains, so a loud input can reach the rail by design. - A plucked phrase is clean on every patch up to -9 dBFS; at -3 dBFS - Ringing Pitch, Dark Resonator, Single Slap and Filter Only rail. + **Level.** A comb gains, so a loud input can reach the rail by design. Keep + the input under -24 dBFS and the defaults and patches stay off the rails. + Feedback or Trim at its top can still push the output onto the rails. `reset()` empties the line. **Latency and tail.** It adds no latency at any setting. The tail @@ -294,6 +294,13 @@ class CombFilter(_component.Component): #: `tail_samples`, the last of its decay. TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -18 dBFS, patch 1 rails on + #: a DC step. + INPUT_CEILING_DBFS = -24 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (1, 4) + MACRO_LABELS = ("Frequency", "Feedback", "Mix", "Tone", "Trim", "Glide") MACRO_MODES = {0: "UNIPOLAR", 1: "UNIPOLAR", 2: "UNIPOLAR", 3: "UNIPOLAR", 4: "BIPOLAR", 5: "UNIPOLAR"} diff --git a/lib/audioeffects/digitaldelay.py b/lib/audioeffects/digitaldelay.py index 27be5cc2..d55f8acf 100644 --- a/lib/audioeffects/digitaldelay.py +++ b/lib/audioeffects/digitaldelay.py @@ -28,7 +28,8 @@ below Nyquist and sound the same. The dry sits at unity and the repeats add to it, so a hot input can reach the int16 rail. With Repeat Cut out and Mix below 1, an input that peaks at or below floor(32767 (1 - Mix)) - 1 cannot -reach the rail, at any Time or Feedback. +reach the rail, at any Time or Feedback. Keep the input under -6 dBFS and +the defaults and patches stay off the rails. **Limits shared by the family.** A control that jumps makes the output step: move it in small steps from the host if you need it smooth. When your source @@ -324,6 +325,11 @@ class DigitalDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, patch 3 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Feedback", "Mix", "Glide", "Sync", "Division", "Repeat Tone", "Repeat Cut") MACRO_MODES = { diff --git a/lib/audioeffects/graphiceq.py b/lib/audioeffects/graphiceq.py index 913c19bc..3a5d3361 100644 --- a/lib/audioeffects/graphiceq.py +++ b/lib/audioeffects/graphiceq.py @@ -217,7 +217,8 @@ class GraphicEQ(_component.Component): **Level.** Bands overlap and add: three adjacent sliders at +6 dB lift the middle one's frequency by more than 7 dB. Every section hands the next an int16, so leave headroom, and pull Volume down when you push - Gain up. + Gain up. A band, Gain or Volume at its top can push a full-scale input + onto the rails. **Patches.** Scooped Mids, Pushed Mids and Full Boost pull Volume down, so stepping through them you hear the curve rather than the level. A @@ -250,6 +251,9 @@ class GraphicEQ(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = _DESIGN_TAIL_SAMPLES + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) + MACRO_LABELS = ("31", "63", "125", "250", "500", "1k", "2k", "4k", "8k", "16k Shelf", "Gain", "Volume", "Band Q", "Constant Q") MACRO_MODES = {0: "BIPOLAR", 1: "BIPOLAR", 2: "BIPOLAR", 3: "BIPOLAR", diff --git a/lib/audioeffects/highpass.py b/lib/audioeffects/highpass.py index 08a91786..443d5ab0 100644 --- a/lib/audioeffects/highpass.py +++ b/lib/audioeffects/highpass.py @@ -8,12 +8,14 @@ **What it sounds like.** One knob decides where the bottom stops, and above that corner nothing is touched: rumble, handling noise, a subsonic DC offset and the mud under a vocal go, and the voice does not. The second knob is how -loud the corner itself stands - at Resonance 0.707 it is 3 dB down and the -cut is as gentle as a cut gets, at 2 it is 6 dB up and you can hear where you -put it, at 16 it is 24 dB up and the filter whistles at its own corner. The -Slope switch is a desk's: 12 dB/oct for a low-cut you do not want to notice, -24 dB/oct when the rumble has to actually leave. There is no drive and no -growl anywhere in it - a high-pass is linear. +loud the corner itself stands - at Resonance 0.707 it is 3 dB down and the cut +is as gentle as a cut gets, at 2 it is 6 dB up and you can hear where you put +it, at 16 it is 24 dB up and the filter whistles at its own corner. The Slope +switch is a desk's: 12 dB/oct for a low-cut you do not want to notice, 24 +dB/oct when the rumble has to actually leave. There is no drive and no growl +anywhere in it - a high-pass is linear. Keep the input under -9 dBFS and the +defaults and patches stay off the rails. Trim at its top can still push the +output onto the rails. **Three things the knobs do not do exactly, found by the Phase 2 gate audit and its patch sweep, and @@ -197,6 +199,13 @@ class HighPass(_component.Component): #: the drive stops. TAIL_SAMPLES = 632832 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -6 dBFS, patch 1 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -9 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (4,) + MACRO_LABELS = ("Frequency", "Resonance", "Slope", "Mix", "Trim") MACRO_MODES = {0: "UNIPOLAR", 1: "UNIPOLAR", 2: "TOGGLE", 3: "UNIPOLAR", 4: "BIPOLAR"} diff --git a/lib/audioeffects/ladderfilter.py b/lib/audioeffects/ladderfilter.py index b90298f0..41fb5964 100644 --- a/lib/audioeffects/ladderfilter.py +++ b/lib/audioeffects/ladderfilter.py @@ -79,9 +79,9 @@ class LadderFilter(_component.Component): no tail bound, and `reset()` stops it at any setting. **Levels.** The resonance peak and the drive both add gain, so no input - level is clean on every setting. A plucked phrase stays off the rails on - every patch to -10 dBFS; at -3 dBFS Growl With Drive rails. Turn Drive, - Resonance or your input down. + level is clean on every setting. Keep the input under -15 dBFS and the + defaults and patches stay off the rails. Drive at its top can still push + the output onto the rails. Turn Drive, Resonance or your input down. **On a board.** The class needs audiodsp's `audioladder`, and on a board without it construction raises `ImportError`. @@ -107,6 +107,13 @@ class LadderFilter(_component.Component): #: bounds the decay; at and above it the filter sustains by design. TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -12 dBFS, patch 4 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -15 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (2,) + MACRO_LABELS = ("Cutoff", "Resonance", "Drive", "Passband Comp", "Poles", "Oversample", "Mix") MACRO_MODES = {0: "UNIPOLAR", 1: "UNIPOLAR", 2: "UNIPOLAR", diff --git a/lib/audioeffects/lowpass.py b/lib/audioeffects/lowpass.py index 7d5bc5eb..9a568997 100644 --- a/lib/audioeffects/lowpass.py +++ b/lib/audioeffects/lowpass.py @@ -7,14 +7,15 @@ **What it sounds like.** One knob slides the whole curve along the frequency axis without changing its shape, and one knob decides how loud the corner -itself stands: at Resonance 0.707 the corner is 3 dB down and the curve is -the gentlest roll-off there is, at 2 it is 6 dB up and the filter starts to -whistle where you put it, at 16 it is 24 dB up and a click at the input rings -for seconds. There is no drive, no saturation and no growl anywhere in it - -a low-pass is linear, and the thing that makes a Moog a Moog belongs to +itself stands: at Resonance 0.707 the corner is 3 dB down and the curve is the +gentlest roll-off there is, at 2 it is 6 dB up and the filter starts to whistle +where you put it, at 16 it is 24 dB up and a click at the input rings for +seconds. There is no drive, no saturation and no growl anywhere in it - a +low-pass is linear, and the thing that makes a Moog a Moog belongs to `LadderFilter`. The Slope switch is a console's: 12 dB/oct is the analog prototype, 24 dB/oct is a second section behind it for when a rumble has to -actually go away. +actually go away. Keep the input under -3 dBFS and the defaults and patches +stay off the rails. Trim at its top can still push the output onto the rails. **Where those two sentences stop being exact.** The Phase 2 gate audit (2026-09-07) disconfirmed four of the five traits off the point they were @@ -172,6 +173,13 @@ class LowPass(_component.Component): #: input stopped outlived (279 149 at patch 4). TAIL_SAMPLES = 528261 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at full scale, patch 5 rails on + #: a 50 Hz sine. + INPUT_CEILING_DBFS = -3 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (4,) + MACRO_LABELS = ("Frequency", "Resonance", "Slope", "Mix", "Trim") MACRO_MODES = {0: "UNIPOLAR", 1: "UNIPOLAR", 2: "TOGGLE", 3: "UNIPOLAR", 4: "BIPOLAR"} diff --git a/lib/audioeffects/multibandcompressor.py b/lib/audioeffects/multibandcompressor.py index 5d68c552..cf118bf0 100644 --- a/lib/audioeffects/multibandcompressor.py +++ b/lib/audioeffects/multibandcompressor.py @@ -260,9 +260,11 @@ class MultibandCompressor(_component.Component): **Mix.** Mix crossfades the input with the summed bands, so between its stops it is parallel compression. Mix 0 is the source byte for byte, at - 22.05 kHz, 44.1 kHz and 48 kHz, mono and stereo, and moving off it does - not click. Brought back up from 0, it starts as it was built rather - than playing what the bands held. + 22.05 kHz, 44.1 kHz and 48 kHz, mono and stereo, and moving off it does not + click. Brought back up from 0, it starts as it was built rather than + playing what the bands held. Keep the input under -9 dBFS and the defaults + and patches stay off the rails. A band's Gain at its top can still push the + output onto the rails. **Latency and tail.** It adds no latency at any setting. The tail reaches exact zero, and `tail_samples` says when. @@ -304,6 +306,13 @@ class MultibandCompressor(_component.Component): #: coupling state" in docs/reference/component-api.md. TAIL_SAMPLES = 0 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -6 dBFS, patch 3 rails on + #: a 147 Hz square. + INPUT_CEILING_DBFS = -9 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (8, 9, 10) + MACRO_LABELS = ( "Crossover Low", "Crossover High", "Low Threshold", "Mid Threshold", "High Threshold", diff --git a/lib/audioeffects/multitapdelay.py b/lib/audioeffects/multitapdelay.py index dd3562f0..fb6d4499 100644 --- a/lib/audioeffects/multitapdelay.py +++ b/lib/audioeffects/multitapdelay.py @@ -225,6 +225,8 @@ class MultiTapDelay(_component.Component): Up to Mix 1 the dry passes at unity, and at Mix 2 the echoes play alone. At unity the dry is exact to the sample, mono and stereo, right up to the rails, on every interpreter. + Keep the input under -6 dBFS and the defaults and patches stay off the + rails. Mix 0 is a wire. Sync locks Time to Division of the host's beat, clamped to Time's span. With no host tempo, Time stays on the knob. @@ -273,6 +275,11 @@ class MultiTapDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, the defaults rail on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Pattern", "Heads", "Feedback", "Mix", "Tilt", "Repeat Tone", "Sync", "Division") MACRO_MODES = { diff --git a/lib/audioeffects/notch.py b/lib/audioeffects/notch.py index 039da4a5..7d163f8d 100644 --- a/lib/audioeffects/notch.py +++ b/lib/audioeffects/notch.py @@ -4,16 +4,17 @@ its dossier. The old `eq.py:Notch` is not consulted except for the seven defects the dossier's section 7 names. -**What it sounds like.** One knob says where, one says how wide, and what -comes out is the input with a slice taken out of it and nothing else -touched - a whistle, a feedback tone or a mains hum goes and the rest of the -mix does not move, because a notch is exactly unity at DC and at Nyquist by -construction. Width is a bandwidth, not a depth: at Q 0.7 the notch is -wider than the note it sits on and reads as a scoop; at Q 30 it is a few -hertz across and you hear the tone leave without hearing the music change. -Harmonics adds a second notch an octave up, half as wide in hertz, because -mains hum is a series and one notch at 50 or 60 Hz leaves the buzz. Depth is -a blend, not a filter control - at 0 the class is a wire. +**What it sounds like.** One knob says where, one says how wide, and what comes +out is the input with a slice taken out of it and nothing else touched - a +whistle, a feedback tone or a mains hum goes and the rest of the mix does not +move, because a notch is exactly unity at DC and at Nyquist by construction. +Width is a bandwidth, not a depth: at Q 0.7 the notch is wider than the note it +sits on and reads as a scoop; at Q 30 it is a few hertz across and you hear the +tone leave without hearing the music change. Harmonics adds a second notch an +octave up, half as wide in hertz, because mains hum is a series and one notch +at 50 or 60 Hz leaves the buzz. Depth is a blend, not a filter control - at 0 +the class is a wire. Keep the input under -6 dBFS and the defaults and patches +stay off the rails. Trim at its top can still push the output onto the rails. **Portability tier: audiodsp** (`REQUIRES = ("audiobiquad",)`). All three sections are `audiobiquad.Biquad`: float state, and a tail that reaches @@ -134,6 +135,13 @@ class Notch(_component.Component): #: input stopped outlived by up to twice. TAIL_SAMPLES = 965439 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, patch 1 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (4,) + MACRO_LABELS = ("Frequency", "Width", "Harmonics", "Depth", "Trim") MACRO_MODES = {0: "UNIPOLAR", 1: "UNIPOLAR", 2: "TOGGLE", 3: "UNIPOLAR", 4: "BIPOLAR"} diff --git a/lib/audioeffects/parametriceq.py b/lib/audioeffects/parametriceq.py index 103ace3f..59748c00 100644 --- a/lib/audioeffects/parametriceq.py +++ b/lib/audioeffects/parametriceq.py @@ -48,9 +48,11 @@ All of it is the clip, not the filter: the coefficients are reciprocal and proportional at every level, which is what -`tests/test_cpython_effects_parametriceq.py` reads. Drive this EQ from a -signal with headroom for the boost you dial, or trim into it, and the curves -are the ones the traits describe. +`tests/test_cpython_effects_parametriceq.py` reads. Drive this EQ from a signal +with headroom for the boost you dial, or trim into it, and the curves are the +ones the traits describe. Keep the input under -12 dBFS and the defaults and +patches stay off the rails. A boost or Output at its top can still push the +output onto the rails. **Cost.** Eight sections, always built, and no patch changes that. Measured 2026-09-07 on both boards at construction defaults - where patch 0 is a wire, @@ -216,6 +218,13 @@ class ParametricEQ(_component.Component): #: ceiling at every rate. Patch 0 is a wire and its tail is zero. TAIL_SAMPLES = 37696 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -9 dBFS, patch 3 rails on + #: a DC step. + INPUT_CEILING_DBFS = -12 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (1, 4, 6, 8, 11, 15) + MACRO_LABELS = ( "Low Freq", "Low Boost", "Low Atten", "Bell 1 Freq", "Bell 1 Gain", diff --git a/lib/audioeffects/phaser.py b/lib/audioeffects/phaser.py index 6a812276..e432ce8b 100644 --- a/lib/audioeffects/phaser.py +++ b/lib/audioeffects/phaser.py @@ -295,8 +295,9 @@ class Phaser(_component.Component): **Drive and Feedback.** Drive is 0.3 when you construct the class, and there the Feedback hump stays under 5 dB. Turn Drive to 0 and Feedback - lifts it more than 5 dB. The Phaser - lean patch is the default with - Drive at 0. + lifts it more than 5 dB. The Phaser - lean patch is the default with Drive + at 0. Keep the input under -9 dBFS and the defaults and patches stay off + the rails. Feedback at its top can still push the output onto the rails. **Sync.** With Sync on, the sweep locks to the host: Division picks the length of a sweep in bars, a bar is read from the host's time @@ -328,6 +329,13 @@ class reports the worst case, 552960 samples, for stages at their #: comment; musical settings die much sooner. Ceiling, next 2048. TAIL_SAMPLES = 552960 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -6 dBFS, patch 1 rails on + #: a 147 Hz square. + INPUT_CEILING_DBFS = -9 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (3,) + MACRO_LABELS = ("Rate", "Depth", "Centre", "Feedback", "Mix", "Stages", "Shape", "Drive", "Sync", "Division") MACRO_MODES = { diff --git a/lib/audioeffects/pingpongdelay.py b/lib/audioeffects/pingpongdelay.py index fff068e1..15de2dee 100644 --- a/lib/audioeffects/pingpongdelay.py +++ b/lib/audioeffects/pingpongdelay.py @@ -133,6 +133,8 @@ class PingPongDelay(_component.Component): input that peaks at or below floor(32767 (1 - Mix)) - 1 cannot reach the rail, at any Time, Feedback or Spread. Repeat Cut's high-pass overshoots, so with it in leave more room. + Keep the input under -6 dBFS and the defaults and patches stay off the + rails. **Limits shared by the family.** A control that jumps makes the output step: move it in small steps from the host if you need it smooth. When @@ -168,6 +170,11 @@ class PingPongDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, patch 3 rails on + #: a DC step. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Feedback", "Mix", "Spread", "First Side", "Sync", "Division", "Repeat Tone", "Repeat Cut") MACRO_MODES = { diff --git a/lib/audioeffects/reverb.py b/lib/audioeffects/reverb.py index 4ba3f1c1..634c83ea 100644 --- a/lib/audioeffects/reverb.py +++ b/lib/audioeffects/reverb.py @@ -43,11 +43,10 @@ **Input ceiling.** There is no input gain, and the tank's lines clamp at the rail on every write whatever Mix is: a steady 362 Hz tone at 8 000 LSB RMS -comes back more than 1 dB quieter in the tail than at 4 000. On 2 s of -uniform noise at 4 000 LSB RMS at Mix 1, no shipped patch reaches the rail -at 48, 44.1 or 22.05 kHz; Bright Chamber at 44.1 kHz is not claimed. -A plucked phrase stays off the rails on every patch to -4 dBFS; at -3 dBFS -Slow Bloom rails. +comes back more than 1 dB quieter in the tail than at 4 000. On 2 s of uniform +noise at 4 000 LSB RMS at Mix 1, no shipped patch reaches the rail at 48, 44.1 +or 22.05 kHz; Bright Chamber at 44.1 kHz is not claimed. Keep the input under +-12 dBFS and the defaults and patches stay off the rails. `tail_samples` bounds the frames until the output is exactly zero once your input stops: 222 868 frames at the defaults at 48 kHz. One int16 @@ -298,6 +297,11 @@ class Reverb(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -9 dBFS, patch 9 rails on + #: a 220 Hz square. + INPUT_CEILING_DBFS = -12 + MACRO_LABELS = ("Character", "Decay", "Size", "Predelay", "Diffusion", "Damping", "Bandwidth", "Low Cut", "Mod Depth", "Mod Rate", "Width", "Tone", "Mix") diff --git a/lib/audioeffects/slapbackdelay.py b/lib/audioeffects/slapbackdelay.py index ee8eafd1..b8980228 100644 --- a/lib/audioeffects/slapbackdelay.py +++ b/lib/audioeffects/slapbackdelay.py @@ -135,6 +135,8 @@ class SlapbackDelay(_component.Component): Up to Level 1 the dry passes untouched until the repeat arrives, however hard Saturation drives the repeat. A hot input can reach the rail, since the repeat adds to a dry at unity. + Keep the input under -6 dBFS and the defaults and patches stay off the + rails. At Repeats 0 there is one repeat and no second. Repeats above 0 sends the repeat round for more. Wow swings the repeat's pitch by the cents the knob reads, at a slow @@ -187,6 +189,11 @@ class SlapbackDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, patch 2 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Level", "Saturation", "Tone", "Wow", "Repeats") MACRO_MODES = { 0: "UNIPOLAR", diff --git a/lib/audioeffects/tapedelay.py b/lib/audioeffects/tapedelay.py index d81780f9..5abc7d10 100644 --- a/lib/audioeffects/tapedelay.py +++ b/lib/audioeffects/tapedelay.py @@ -35,7 +35,8 @@ shape. The darkening follows the tape's loss law only up to a band top that rises with the tape's speed. Record Level has no memory: tape hysteresis is not modelled. The RE-201's -Bass and Treble are not here. +Bass and Treble are not here. Keep the input under -6 dBFS and the defaults +and patches stay off the rails. **Limits shared by the family.** A control that jumps makes the output step: move it in small steps from the host if you need it smooth. When your source @@ -349,6 +350,11 @@ class TapeDelay(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = None + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -3 dBFS, the defaults rail on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -6 + MACRO_LABELS = ("Time", "Feedback", "Mix", "Glide", "Wow", "Flutter", "Record Level", "Spacing", "Spread", "Sync", "Division") MACRO_MODES = { diff --git a/lib/audioeffects/transientshaper.py b/lib/audioeffects/transientshaper.py index 138637f0..d53ff65a 100644 --- a/lib/audioeffects/transientshaper.py +++ b/lib/audioeffects/transientshaper.py @@ -130,10 +130,10 @@ class TransientShaper(_component.Component): 3 dB a second, Sustain -24 reaches about -16 dB. **Levels.** At its defaults the class is a wire. Attack and Sustain add - gain where they are turned up, so a hit that is already loud reaches - the rails. A plucked phrase stays off the rails on every patch to - -10 dBFS; at -3 dBFS Snap and Kick Punch rail. Turn Output down, or - your input. + gain where they are turned up, so a hit that is already loud reaches the + rails. Keep the input under -12 dBFS and the defaults and patches stay off + the rails. Attack at its top can still push the output onto the rails. Turn + Output down, or your input. **Timing.** Latency is zero at every setting, and so is the tail: silence in is silence out in the same frame. @@ -155,6 +155,13 @@ class TransientShaper(_component.Component): LATENCY_SAMPLES = 0 TAIL_SAMPLES = 0 + #: The defaults and every patch play the ceiling kit's materials + #: clean at this level; at -9 dBFS, patch 1 rails on + #: a 100 Hz square. + INPUT_CEILING_DBFS = -12 + #: Settings that clip by design: the rail check skips them. + CLIPS_AT_TOP = (0,) + MACRO_LABELS = ("Attack", "Sustain", "Output", "Attack Speed", "Sustain Hold") MACRO_MODES = {0: "BIPOLAR", 1: "BIPOLAR", 2: "BIPOLAR", diff --git a/tests/support/kit_ceiling.py b/tests/support/kit_ceiling.py index a024b03b..8c48badd 100644 --- a/tests/support/kit_ceiling.py +++ b/tests/support/kit_ceiling.py @@ -59,7 +59,11 @@ - `rail`: more than 8 frames in a row on a rail (|y| >= 32767) where the input was on neither `latency_samples` earlier nor in the last min(`latency_samples`, `HOLD`) frames: a hold that latched a rail - plays it for the length of the hold. + plays it for the length of the hold. Not judged where the class says + the setting clips by design: `CLIPS_AT_TOP`, a tuple of macro indexes, + excuses a cell whose knobs put one of them at 127 (Feedback at its top + runs away), and `CLIPS_BY_DESIGN = True` excuses every cell (a fuzz + clips at any level). A wrap is still judged there. - `silent`: the last second exactly zero while the input played through it. Not judged on the DC step, which a high-pass rightly takes to zero, nor where a macro whose label holds Level or Volume is at 0: an output @@ -230,8 +234,20 @@ def turned_down(cls, knobs): return False +def clips_by_design(cls, knobs): + """Whether the class says this cell's knobs clip by design (see `rail` + under **The bars**).""" + if getattr(cls, "CLIPS_BY_DESIGN", False): + return True + top = getattr(cls, "CLIPS_AT_TOP", ()) + for index, value in knobs or (): + if value == 127 and index in top: + return True + return False + + def judge(data, values, channels, rate, lat, which, seconds=SECONDS, - silent_ok=False): + silent_ok=False, rail_ok=False): out = kc.samples(data) reds = [] if which != "noise": @@ -239,7 +255,7 @@ def judge(data, values, channels, rate, lat, which, seconds=SECONDS, if hit is not None: reds.append("wrap at %d ch %d" % hit) run, where = kc.rail_run(out, values, channels, lat, HOLD) - if run > kc.RAIL_RUN: + if run > kc.RAIL_RUN and not rail_ok: reds.append("rail %d frames from %d" % (run, where)) if which != "dc" and not silent_ok: lo = (seconds - 1) * rate * channels @@ -316,7 +332,8 @@ def measure(cls, rate, channels, which, patch, knobs, options=None): e.set_macro(index, value) data = kc.render(e, seconds * rate) reds = judge(data, values, channels, rate, e.latency_samples, which, - seconds, turned_down(cls, knobs)) + seconds, turned_down(cls, knobs), + clips_by_design(cls, knobs)) finally: e.deinit() verdict = "RED(%s)" % "; ".join(reds) if reds else "ok" @@ -352,6 +369,23 @@ def run(cls, emit=print, quick=False, rates=None, channels=None, return cells.summary() +def stated_level_reds(cls, material, db=None, keys=None): + """The red cells on `material`, 48 kHz stereo, for the defaults and + every patch (or just `keys`), played at the class's stated level, or at + `db` to show the level above it is not clean. An effect's own test + holds its stated level with this: clean there, red a step up.""" + if db is not None: + cls = type(cls.__name__, (cls,), {"INPUT_CEILING_DBFS": db, + "__module__": cls.__module__}) + if keys is None: + keys = ["d"] + ["p%d" % p for p in sorted(cls.PATCHES)] + lines = [] + run(cls, emit=lines.append, quick=True, materials=(material,), + only=keys) + return [line for line in lines + if "|V:RED" in line and not line.startswith("SUMMARY")] + + if __name__ == "__main__": import kit kit.main(sys.argv[1:], only=(CHECK,)) diff --git a/tests/support/kit_planted.py b/tests/support/kit_planted.py index 46382b4a..c88fef51 100644 --- a/tests/support/kit_planted.py +++ b/tests/support/kit_planted.py @@ -151,6 +151,25 @@ class ClipKit(KitPlain): CURVE = staticmethod(_clip2) +class ClipAtTop(ClipKit): + """Ceiling: ClipKit saying its Gain at the top clips by design, so + only a cell with Gain at 127 is excused the rail.""" + NAME = "ClipAtTop" + CLIPS_AT_TOP = (1,) + + +class ClipsAlways(ClipKit): + """Ceiling: ClipKit saying it clips by design everywhere.""" + NAME = "ClipsAlways" + CLIPS_BY_DESIGN = True + + +class WrapsAlways(WrapKit): + """Ceiling: a wrap is never by design, even in a class that clips.""" + NAME = "WrapsAlways" + CLIPS_BY_DESIGN = True + + class DeadStop(KitPlain): """Ceiling: Gain's bottom stop is silence, as a level that went NaN on a board would be.""" @@ -713,6 +732,7 @@ def _apply_macro(self, index, position): PLANTS = {} for _cls in (KitPlain, Silent, LeakBand, WrapKit, WrapKitQuiet, ClipKit, DeadStop, + ClipAtTop, ClipsAlways, WrapsAlways, CondKit, MidWrap, ComboWrap, AllWrap, LowWrap, ImplKit, EchoTail, ShortTail, EventTail, NoneSilent, NoneDC, ReadbackOff, StopRaises, NanLevel, PatchStale, CtorNan, ClearsFirst, ClearsMacro, FlushKit, FlushOnly, DoubleReset, diff --git a/tests/test_cpython_effects_analogdelay.py b/tests/test_cpython_effects_analogdelay.py index 2378d64a..927d1634 100644 --- a/tests/test_cpython_effects_analogdelay.py +++ b/tests/test_cpython_effects_analogdelay.py @@ -40,6 +40,7 @@ class built as a wire. T1 is unmeasured and T2b, T4 and T5 are disconfirmed sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import audiocore # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 @@ -2446,6 +2447,15 @@ def test_another_macro_leaves_a_walk_at_its_rate(self): self.assertEqual(effect._delay._handed["delay_slew"], slew) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: d at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(AnalogDelay, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + AnalogDelay, "square", db=-3, keys=("d",))) + + # -------------------------------------------------------------------------- # The docstring's claims, each tied to the test that asserts it @@ -2453,6 +2463,9 @@ def test_another_macro_leaves_a_walk_at_its_rate(self): #: that asserts it). Every sentence in the docstring that makes a claim is #: here; one that could not be tied to a test was struck. CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("test_clean_at_the_stated_input_level",)), ("Time runs from 20 to 600 ms, Feedback from 0 to 0.99 and Mix from 0 " "to 2.", "test_the_knob_spans"), ("Up to Mix 1 the dry passes untouched until the first repeat arrives.", diff --git a/tests/test_cpython_effects_bitcrusher.py b/tests/test_cpython_effects_bitcrusher.py index f980694e..8490a24f 100644 --- a/tests/test_cpython_effects_bitcrusher.py +++ b/tests/test_cpython_effects_bitcrusher.py @@ -16,6 +16,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import audiocore # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import bitcrusher as rebuilt # noqa: E402 @@ -1252,7 +1253,19 @@ def test_construction_needs_its_four_modules(self): sys.modules[name] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # sine is where it rails first: p1 at 0 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(rebuilt.Bitcrusher, "sine"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + rebuilt.Bitcrusher, "sine", db=0, keys=("p1",))) + + CLAIMS = ( + ("Keep the input under -3 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("`Bitcrusher` — a linear hold, the SP-1200's two numbers as " "knobs.", ("TheSurface.test_six_macros_and_seven_patches",)), diff --git a/tests/test_cpython_effects_combfilter.py b/tests/test_cpython_effects_combfilter.py index db262126..7e34d9c5 100644 --- a/tests/test_cpython_effects_combfilter.py +++ b/tests/test_cpython_effects_combfilter.py @@ -27,6 +27,7 @@ import audiocore sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) +import kit_ceiling # noqa: E402 import kit_faults as faults # noqa: E402 from effects_measure import SAMPLE_RATE # noqa: E402 @@ -1190,23 +1191,6 @@ def test_a_mono_source_gets_the_same_tuning(self): self.assertLess(abs(cents(centroid(y, ideal), ideal)), 5.0) -class TheInputCeiling(unittest.TestCase): - """audiocomponents#192: the kit's plucked phrase is clean on every - patch at -9 dBFS, and rails at -3 dBFS on the four patches the - docstring names.""" - - def test_a_plucked_phrase_is_clean_to_minus_9_dbfs(self): - import kit_levels - self.assertEqual(kit_levels.red_patches(combfilter.CombFilter, -9), - []) - - def test_at_minus_3_dbfs_four_patches_rail(self): - import kit_levels - self.assertEqual(kit_levels.red_patches(combfilter.CombFilter, -3), - [1, 3, 4, 5]) - names = [combfilter.CombFilter.PATCHES[k][0] for k in (1, 3, 4, 5)] - self.assertEqual(names, ["Ringing Pitch", "Dark Resonator", - "Single Slap", "Filter Only"]) class TheClaims(unittest.TestCase): @@ -1235,7 +1219,19 @@ def test_construction_needs_its_two_modules(self): sys.modules[name] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # dc is where it rails first: p1 at -18 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(combfilter.CombFilter, "dc"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + combfilter.CombFilter, "dc", db=-18, keys=("p1",))) + + CLAIMS = ( + ("Keep the input under -24 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("A tuned comb: a delay line short enough to be a pitch, fed back on " "itself, so noise turns into a note and a chord turns metallic.", ("TheCombTunesFractionally.test_every_request_lands_within_five_cents", @@ -1286,11 +1282,7 @@ def test_construction_needs_its_two_modules(self): ("TheTuningKnobIsClickFree.test_the_boundary_is_not_findable", "TheTuningKnobIsClickFree.test_a_jumped_read_head_is_red")), ("A comb gains, so a loud input can reach the rail by design.", - ("TheInputCeiling.test_at_minus_3_dbfs_four_patches_rail",)), - ("A plucked phrase is clean on every patch up to -9 dBFS; at -3 dBFS " - "Ringing Pitch, Dark Resonator, Single Slap and Filter Only rail.", - ("TheInputCeiling.test_a_plucked_phrase_is_clean_to_minus_9_dbfs", - "TheInputCeiling.test_at_minus_3_dbfs_four_patches_rail")), + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("`reset()` empties the line.", ("ResetEmptiesTheLine.test_reset_leaves_the_line_silent_and_the_" "source_rendering",)), @@ -1344,7 +1336,7 @@ def test_every_claim_is_in_the_docstring_and_tested(self): # docstring does not carry, a test that does not exist. self.assertTrue(claim_problems(doc + " It reads 12 ms.")) self.assertTrue(claim_problems(_flat(doc).replace( - "up to -9 dBFS", "up to -6 dBFS"))) + "under -24 dBFS", "under -18 dBFS"))) self.assertTrue(claim_problems( doc, CLAIMS + (("Latency", ("TheClaims.test_nothing_here",)),))) diff --git a/tests/test_cpython_effects_digitaldelay.py b/tests/test_cpython_effects_digitaldelay.py index fcf332df..218748f8 100644 --- a/tests/test_cpython_effects_digitaldelay.py +++ b/tests/test_cpython_effects_digitaldelay.py @@ -87,6 +87,7 @@ class of the node's walk, and the edge test goes red at the old edge. A sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import audiocore # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 import lifecycle # noqa: E402 @@ -2902,12 +2903,24 @@ def touch(effect, _index=index): effect.deinit() +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p3 at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(dd.DigitalDelay, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + dd.DigitalDelay, "square", db=-3, keys=("p3",))) + + # -------------------------------------------------------------------------- # The docstring's claims #: Every claim the module docstring makes, word for word, and the tests that #: assert it ("Class.test_name", in this file). CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("Time is the delay, from 12.5 to 800 ms, and Feedback is how much of " "each repeat goes round again, up to 0.99.", ("T4TimeLaw.test_the_map_at_the_stops_and_between", diff --git a/tests/test_cpython_effects_highpass.py b/tests/test_cpython_effects_highpass.py index 85369d7a..e1a75b14 100644 --- a/tests/test_cpython_effects_highpass.py +++ b/tests/test_cpython_effects_highpass.py @@ -31,6 +31,7 @@ import synthio sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) +import kit_ceiling # noqa: E402 from effects_measure import SAMPLE_RATE, source # noqa: E402 from audioeffects import _component # noqa: E402 @@ -602,5 +603,14 @@ def test_deinit_releases_every_node_and_is_idempotent(self): built.output +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p1 at -6 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(highpass.HighPass, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + highpass.HighPass, "square", db=-6, keys=("p1",))) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_cpython_effects_ladderfilter.py b/tests/test_cpython_effects_ladderfilter.py index 2c2b6784..875bf094 100644 --- a/tests/test_cpython_effects_ladderfilter.py +++ b/tests/test_cpython_effects_ladderfilter.py @@ -1,7 +1,5 @@ -"""`LadderFilter`'s input ceiling: the kit's plucked phrase is clean on -every patch at -10 dBFS and rails at -3 dBFS on Growl With Drive alone -(audiocomponents#192), and the class docstring's claims, each tied to its -test by the `CLAIMS` table. +"""`LadderFilter`'s input ceiling and the class docstring's claims, each +tied to its test by the `CLAIMS` table. python -m unittest tests.test_cpython_effects_ladderfilter """ @@ -16,7 +14,7 @@ sys.path.insert(0, os.path.join(ROOT, "lib")) sys.path.insert(0, os.path.join(ROOT, "tests", "support")) -import kit_levels # noqa: E402 +import kit_ceiling # noqa: E402 import kit_probes as probes # noqa: E402 import numpy as np # noqa: E402 from audioeffects import ladderfilter # noqa: E402 @@ -52,14 +50,6 @@ def burst_then_silence(seconds=2.0): + [0] * int(2 * seconds * RATE)) -class TheInputCeiling(unittest.TestCase): - - def test_a_plucked_phrase_is_clean_to_minus_10_dbfs(self): - self.assertEqual(kit_levels.red_patches(LadderFilter, -10), []) - - def test_at_minus_3_dbfs_growl_with_drive_rails(self): - self.assertEqual(kit_levels.red_patches(LadderFilter, -3), [4]) - self.assertEqual(LadderFilter.PATCHES[4][0], "Growl With Drive") class TheClaims(unittest.TestCase): @@ -182,7 +172,19 @@ def test_construction_needs_audioladder(self): self.assertEqual(LadderFilter.REQUIRES, ("audioladder",)) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p4 at -12 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(LadderFilter, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + LadderFilter, "square", db=-12, keys=("p4",))) + + CLAIMS = ( + ("Keep the input under -15 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("`LadderFilter` - the Moog transistor ladder, rebuilt on " "`audioladder`.", ("TheClaims.test_construction_needs_audioladder",)), @@ -236,11 +238,7 @@ def test_construction_needs_audioladder(self): "TheClaims.test_reset_stops_the_tone")), ("The resonance peak and the drive both add gain, so no input level is " "clean on every setting.", - ("TheInputCeiling.test_at_minus_3_dbfs_growl_with_drive_rails",)), - ("A plucked phrase stays off the rails on every patch to -10 dBFS; at " - "-3 dBFS Growl With Drive rails.", - ("TheInputCeiling.test_a_plucked_phrase_is_clean_to_minus_10_dbfs", - "TheInputCeiling.test_at_minus_3_dbfs_growl_with_drive_rails")), + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("The class needs audiodsp's `audioladder`, and on a board without it " "construction raises `ImportError`.", ("TheClaims.test_construction_needs_audioladder",)), diff --git a/tests/test_cpython_effects_lowpass.py b/tests/test_cpython_effects_lowpass.py index ab7c96d7..e3959622 100644 --- a/tests/test_cpython_effects_lowpass.py +++ b/tests/test_cpython_effects_lowpass.py @@ -28,6 +28,7 @@ class clamps now, and the dossier's section 7 names the raise as the defect) import audioeffects sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) +import kit_ceiling # noqa: E402 from effects_measure import SAMPLE_RATE, source # noqa: E402 import kit_faults as faults # noqa: E402 @@ -671,6 +672,15 @@ def build_twelve(cls): lambda effect: float(effect._pole_two.mix), build_twelve) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # sine is where it rails first: p5 at 0 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(lowpass.LowPass, "sine"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + lowpass.LowPass, "sine", db=0, keys=("p5",))) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_cpython_effects_multiband.py b/tests/test_cpython_effects_multiband.py index 811182fb..7ededd7a 100644 --- a/tests/test_cpython_effects_multiband.py +++ b/tests/test_cpython_effects_multiband.py @@ -32,6 +32,7 @@ #: `audioeffects/multibandcompressor.py`; `rebuilt.ADOPTED` no longer lists #: it. These tests still import the home module so a planted-fault subclass #: is measured against this file, not only `create()`. +import kit_ceiling # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 from tools import effect_measurements as kit # noqa: E402 @@ -990,7 +991,19 @@ def test_construction_needs_its_three_modules(self): sys.modules[name] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # sq147 is where it rails first: p3 at -6 dBFS. + cls = multibandcompressor.MultibandCompressor + self.assertEqual(kit_ceiling.stated_level_reds(cls, "sq147"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + cls, "sq147", db=-6, keys=("p3",))) + + CLAIMS = ( + ("Keep the input under -9 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("Split-band compression: the mix split into two or three bands, each " "with its own compressor, then summed back flat.", ("Traits.test_m1_the_bands_sum_flat_at_three_bands", diff --git a/tests/test_cpython_effects_multitapdelay.py b/tests/test_cpython_effects_multitapdelay.py index 4fb9c67f..74260393 100644 --- a/tests/test_cpython_effects_multitapdelay.py +++ b/tests/test_cpython_effects_multitapdelay.py @@ -60,6 +60,7 @@ class built as a wire. The full spans, the three interpreters and the rates import audiomixer # noqa: E402 import audioroute # noqa: E402 import synthio # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 import audioeffects # noqa: E402 @@ -2953,6 +2954,15 @@ def test_the_tail_reading_is_red_on_a_bound_one_lap_short(self): self.assertGreater(planted, short, (rate, channels)) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: d at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(mtd.MultiTapDelay, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + mtd.MultiTapDelay, "square", db=-3, keys=("d",))) + + # -------------------------------------------------------------------------- # The docstring's claims, each tied to the test that asserts it @@ -2961,6 +2971,9 @@ def test_the_tail_reading_is_red_on_a_bound_one_lap_short(self): #: claim is here; one that could not be tied to a test was struck (the #: trial fixer's dated note in the dossier lists them). CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("test_clean_at_the_stated_input_level",)), ("Measured at v0.6.3, patch 4 costs 5.567 ms a block on the " "ESP32-S3, where a block lasts 5.333 ms, so it needs a P4-class " "board: the ESP32-P4 runs it in 3.865 ms.", diff --git a/tests/test_cpython_effects_notch.py b/tests/test_cpython_effects_notch.py index 0cca27bc..7c7510ec 100644 --- a/tests/test_cpython_effects_notch.py +++ b/tests/test_cpython_effects_notch.py @@ -31,6 +31,7 @@ class actually built and not a re-derivation of RBJ standing beside it. The import synthio sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) +import kit_ceiling # noqa: E402 import kit_faults as faults # noqa: E402 from effects_measure import SAMPLE_RATE # noqa: E402 @@ -573,6 +574,15 @@ def test_deinit_releases_every_node_the_class_built(self): self.assertTrue(getattr(node, "_deinited", True)) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p1 at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(notch_module.Notch, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + notch_module.Notch, "square", db=-3, keys=("p1",))) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_cpython_effects_parametriceq.py b/tests/test_cpython_effects_parametriceq.py index d6150fa7..b1c88c1d 100644 --- a/tests/test_cpython_effects_parametriceq.py +++ b/tests/test_cpython_effects_parametriceq.py @@ -27,6 +27,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) import effects_measure # noqa: E402 +import kit_ceiling # noqa: E402 from effects_measure import SAMPLE_RATE, source # noqa: E402 from audioeffects import _component # noqa: E402 @@ -456,5 +457,14 @@ def __init__(self, sample): self.output = sample +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # dc is where it rails first: p3 at -9 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(parametriceq.ParametricEQ, "dc"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + parametriceq.ParametricEQ, "dc", db=-9, keys=("p3",))) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_cpython_effects_phaser.py b/tests/test_cpython_effects_phaser.py index fa902f56..f02910aa 100644 --- a/tests/test_cpython_effects_phaser.py +++ b/tests/test_cpython_effects_phaser.py @@ -25,6 +25,7 @@ from audioeffects import _component # noqa: E402 from audioeffects import phaser as phaser_module # noqa: E402 from tools import effect_measurements as kit # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults as faults # noqa: E402 VENDOR = "PyDevices" @@ -911,7 +912,19 @@ def test_construction_needs_audiobiquad_and_audioshaper(self): sys.modules[name] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # sq147 is where it rails first: p1 at -6 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(Phaser, "sq147"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + Phaser, "sq147", db=-6, keys=("p1",))) + + CLAIMS = ( + ("Keep the input under -9 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("`Phaser` - four first-order all-pass stages swept the way a Phase 90 " "is.", ("TestPhaserTraits.test_p1_two_notches_at_defaults_six_stages_is_" diff --git a/tests/test_cpython_effects_pingpongdelay.py b/tests/test_cpython_effects_pingpongdelay.py index aae72360..fc864a58 100644 --- a/tests/test_cpython_effects_pingpongdelay.py +++ b/tests/test_cpython_effects_pingpongdelay.py @@ -30,6 +30,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import audiocore # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 @@ -2427,12 +2428,24 @@ def test_construction_needs_audioecho(self): sys.modules["audioecho"] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # dc is where it rails first: p3 at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(PingPongDelay, "dc"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + PingPongDelay, "dc", db=-3, keys=("p3",))) + + # -------------------------------------------------------------------------- # The docstring's claims #: Every claim the class docstring makes, word for word, and the tests that #: assert it ("Class.test_name", in this file). CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("Two delay lines crossed into each other: the repeats bounce between " "the speakers.", ("T1RepeatsAlternate.test_the_three_feedbacks_at_three_rates",)), diff --git a/tests/test_cpython_effects_reverb.py b/tests/test_cpython_effects_reverb.py index 64bf8c51..ce610788 100644 --- a/tests/test_cpython_effects_reverb.py +++ b/tests/test_cpython_effects_reverb.py @@ -24,6 +24,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import audiocore # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 @@ -1804,14 +1805,6 @@ def sine_wet_gain(hz, rms, rate=RATE): class InputCeiling(unittest.TestCase): - def test_a_plucked_phrase_is_clean_to_minus_4_dbfs(self): - """audiocomponents#192: the kit's phrase is clean on every patch - at -4 dBFS, and rails at -3 dBFS on Slow Bloom alone.""" - import kit_levels - self.assertEqual(kit_levels.red_patches(Reverb, -4), []) - self.assertEqual(kit_levels.red_patches(Reverb, -3), [9]) - self.assertEqual(Reverb.PATCHES[9][0], "Slow Bloom") - def test_no_patch_reaches_the_rail_at_4000_lsb_rms(self): # on eight noise seeds, at three rates, each patch's peak stays # under the rail by more than the spread of its peaks @@ -1838,12 +1831,24 @@ def test_the_tanks_lines_compress_from_8000_lsb_rms(self): self.assertLess(g8 - g4, -1.0, (g4, g8)) +class TheInputCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # sq220 is where it rails first: p9 at -9 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(Reverb, "sq220"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + Reverb, "sq220", db=-9, keys=("p9",))) + + # -- the claims --------------------------------------------------------------- #: Every sentence of the docstrings that makes a claim, word for word, and #: the tests that assert it (the trial brief). A sentence that is not here #: may hold no number. CLAIMS = ( + ("Keep the input under -12 dBFS and the defaults and patches stay " + "off the rails.", + ("TheInputCeiling.test_clean_at_the_stated_input_level",)), ("Your dry signal passes untouched and a reverb tail rises behind it.", ("T7LowCut.test_the_dry_is_flat",)), ("Character picks the machine: `plate` is dense from the first " @@ -1909,9 +1914,6 @@ def test_the_tanks_lines_compress_from_8000_lsb_rms(self): "reaches the rail at 48, 44.1 or 22.05 kHz; Bright Chamber at 44.1 kHz " "is not claimed.", ("InputCeiling.test_no_patch_reaches_the_rail_at_4000_lsb_rms",)), - ("A plucked phrase stays off the rails on every patch to -4 dBFS; at " - "-3 dBFS Slow Bloom rails.", - ("InputCeiling.test_a_plucked_phrase_is_clean_to_minus_4_dbfs",)), ("`tail_samples` bounds the frames until the output is exactly zero " "once your input stops: 222 868 frames at the defaults at 48 kHz.", ("Tier1.test_the_tail_reaches_exact_zero_inside_tail_samples", diff --git a/tests/test_cpython_effects_slapbackdelay.py b/tests/test_cpython_effects_slapbackdelay.py index bbd791d5..4a9c88aa 100644 --- a/tests/test_cpython_effects_slapbackdelay.py +++ b/tests/test_cpython_effects_slapbackdelay.py @@ -39,6 +39,7 @@ import audiocore # noqa: E402 import audiomixer # noqa: E402 import audioroute # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 @@ -1900,6 +1901,15 @@ def test_the_stated_ceiling_is_clean_and_just_over_is_not(self): **options), 0, options) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p2 at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(SlapbackDelay, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + SlapbackDelay, "square", db=-3, keys=("p2",))) + + # -------------------------------------------------------------------------- # The docstring's claims, each tied to the test that asserts it (the trial # of the second process, 2026-09-29) @@ -1908,6 +1918,9 @@ def test_the_stated_ceiling_is_clean_and_just_over_is_not(self): #: that asserts it). Every sentence in the docstring that makes a claim is #: here; one that could not be tied to a test was struck. CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("test_clean_at_the_stated_input_level",)), ("By default the repeat comes 134.4 ms after the dry, once.", "test_defaults_three_rates_stereo_and_mono"), ("Time runs from 40 to 250 ms, Level from 0 to 2, Saturation from 0 to " diff --git a/tests/test_cpython_effects_tapedelay.py b/tests/test_cpython_effects_tapedelay.py index 13553998..6fe4c48b 100644 --- a/tests/test_cpython_effects_tapedelay.py +++ b/tests/test_cpython_effects_tapedelay.py @@ -34,6 +34,7 @@ import audiocore # noqa: E402 import lifecycle # noqa: E402 +import kit_ceiling # noqa: E402 import kit_faults # noqa: E402 import kit_probes as probes # noqa: E402 from audioeffects import _component # noqa: E402 @@ -2954,11 +2955,23 @@ def test_every_demonstrated_row_is_red_on_a_wire(self): or null["drift_ok"], null) +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: d at -3 dBFS. + self.assertEqual( + kit_ceiling.stated_level_reds(tape.TapeDelay, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + tape.TapeDelay, "square", db=-3, keys=("d",))) + + # -- the docstring's claims ----------------------------------------------- #: Every claim the module docstring makes, word for word, and the tests that #: assert it ("Class.test_name", in this file). CLAIMS = ( + ("Keep the input under -6 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("`TapeDelay` - a tape echo with two transports: the Roland RE-201's " "motor and the Maestro EP-3's sliding head.", ("TheSurface.test_macros_characters_tier_latency", diff --git a/tests/test_cpython_effects_transientshaper.py b/tests/test_cpython_effects_transientshaper.py index 29eda36a..478e11cc 100644 --- a/tests/test_cpython_effects_transientshaper.py +++ b/tests/test_cpython_effects_transientshaper.py @@ -31,6 +31,7 @@ #: These tests still import the home module so a planted-fault subclass is #: measured against this file, not only `create()`. import effect_measurements as kit # noqa: E402 +from support import kit_ceiling # noqa: E402 from support import kit_probes as probes # noqa: E402 VENDOR = "PyDevices" @@ -219,21 +220,6 @@ def test_t5_the_rms_detector_settles_where_the_peak_detector_does_not( self.assertGreater(steady["peak"], 1.5) -class TheInputCeiling(unittest.TestCase): - """audiocomponents#192: the kit's plucked phrase is clean on every - patch at -10 dBFS, and rails at -3 dBFS on Snap and Kick Punch.""" - - def test_a_plucked_phrase_is_clean_to_minus_10_dbfs(self): - import kit_levels - self.assertEqual(kit_levels.red_patches( - transientshaper.TransientShaper, -10), []) - - def test_at_minus_3_dbfs_snap_and_kick_punch_rail(self): - import kit_levels - cls = transientshaper.TransientShaper - self.assertEqual(kit_levels.red_patches(cls, -3), [1, 5]) - self.assertEqual([cls.PATCHES[k][0] for k in (1, 5)], - ["Snap", "Kick Punch"]) def attack(db): @@ -321,7 +307,19 @@ def test_construction_needs_audiodynamics(self): sys.modules["audiodynamics"] = saved +class TheCeiling(unittest.TestCase): + def test_clean_at_the_stated_input_level(self): + # square is where it rails first: p1 at -9 dBFS. + cls = transientshaper.TransientShaper + self.assertEqual(kit_ceiling.stated_level_reds(cls, "square"), []) + self.assertTrue(kit_ceiling.stated_level_reds( + cls, "square", db=-9, keys=("p1",))) + + CLAIMS = ( + ("Keep the input under -12 dBFS and the defaults and patches stay " + "off the rails.", + ("TheCeiling.test_clean_at_the_stated_input_level",)), ("There is no threshold and no ratio: the gain follows the difference " "between two envelopes of your own signal, so between -20 and -40 dBFS " "a quiet hit is shaped as hard as a loud one, to within 0.5 dB.", @@ -344,10 +342,6 @@ def test_construction_needs_audiodynamics(self): ("On a slow decay Sustain runs out before its end: on a note fading at " "3 dB a second, Sustain -24 reaches about -16 dB.", ("TheClaims.test_sustain_runs_out_on_a_slow_decay",)), - ("A plucked phrase stays off the rails on every patch to -10 dBFS; at " - "-3 dBFS Snap and Kick Punch rail.", - ("TheInputCeiling.test_a_plucked_phrase_is_clean_to_minus_10_dbfs", - "TheInputCeiling.test_at_minus_3_dbfs_snap_and_kick_punch_rail")), ("Latency is zero at every setting, and so is the tail: silence in is " "silence out in the same frame.", ("TheClaims.test_latency_is_zero", diff --git a/tests/test_kit.py b/tests/test_kit.py index 745321ed..ef9dc57c 100644 --- a/tests/test_kit.py +++ b/tests/test_kit.py @@ -541,6 +541,26 @@ def test_a_rail_is_red(self): # A square at full scale sits on the rail because the input does. self.assertEqual(self.v(kp.KitPlain, "square"), "ok") + def test_a_rail_by_design_is_excused_where_declared(self): + self.assertTrue(self.v(kp.ClipAtTop).startswith("RED(rail")) + self.assertTrue(self.v(kp.ClipAtTop, knob=(0, 127)).startswith( + "RED(rail")) + self.assertEqual(self.v(kp.ClipAtTop, knob=(1, 127)), "ok") + self.assertEqual(self.v(kp.ClipAtTop, knob=[(0, 127), (1, 127)]), + "ok") + self.assertEqual(self.v(kp.ClipsAlways), "ok") + # A wrap is still judged. + self.assertTrue(self.v(kp.WrapsAlways).startswith("RED(wrap")) + + def test_a_stated_level_holds_and_the_level_above_does_not(self): + self.assertEqual( + kit_ceiling.stated_level_reds(kp.WrapKitQuiet, "sine"), []) + reds = kit_ceiling.stated_level_reds(kp.WrapKitQuiet, "sine", db=0) + self.assertEqual(len(reds), 3) # the defaults, two patches + self.assertIn("RED(wrap", reds[0]) + self.assertEqual(len(kit_ceiling.stated_level_reds( + kp.WrapKitQuiet, "sine", db=0, keys=("p1",))), 1) + def test_silence_at_a_stop_is_red(self): self.assertTrue(self.v(kp.DeadStop, knob=(1, 0)).startswith( "RED(silent"))