Manguebyte

Beyond JUCE

Where it already goes beyond JUCE

JUCE is the reference for what a complete audio framework contains, and Manguebyte’s first goal is to match it. This page is about the second goal: the places where Manguebyte already does something JUCE does not, and how each claim can be checked.

JUCE is far ahead in breadth, and the last section says where. Nothing in Manguebyte contains JUCE code: see ADR 0004.

Three rules keep this page honest

  1. Every claim points at a test. If it cannot be verified by running something in the repository, it is not listed.
  2. Every statement about JUCE has a source, and describes JUCE’s master branch at version 9.0.4 (October 2026). JUCE keeps moving; the JUCE team has announced an “AudioProcessor v2” that targets several of these points, so some of them may change in later releases.
  3. Limits are stated next to the claim, not left for the reader to find.

Summary

The fifteen claims. Each row links to its section below.
Claim JUCE 9.0.4 Manguebyte Checked by
1. Automation timing Per block in AudioProcessor On the exact sample plugin automation_lands_on_its_exact_frame_and_updates_the_parameterclap automation_lands_on_its_frame_when_processing_in_place
2. Block size and sound Left to each plugin Bit-identical across block sizes, tested manguebyte the_song_is_the_same_whatever_the_block_sizepluck sounds_the_same_whatever_the_block_size
3. Changes from a UI or a restored state Seen by polling, or by listeners that may run on the audio thread Delivered as events in the same stream as automation plugin changes_made_outside_the_host_reach_the_plugin_as_events
4. Real-time safety A convention A heap call while processing fails CI dsp processors_do_not_use_the_heapdsp a_synth_handles_a_flood_of_events_without_using_the_heap
5. Per-note control Through MPE channels Per-note expression and modulation events dsp expression_and_modulation_reach_one_note_and_parameters_reach_alldsp notes_with_identifiers_overlap_on_one_key_and_end_separately
6. CLAP Not available natively Native, passes clap-validator ci CI job: CLAP validationduck plugins/duck/src/lib.rs
7. Testing a plugin Needs a host or a hand-built rig In a unit test, with Harness pluck plays_the_pitch_it_is_asked_forduck automation_lands_on_its_frame_with_audio_running_through
8. Thread safety of parameters and state Synchronised by each plugin Atomics; nothing to lock params state_from_another_version_loads_as_far_as_it_canparams params_are_shareable_across_threads
9. Licence AGPLv3 or commercial MIT or Apache-2.0 file LICENSE-MITfile LICENSE-APACHE
10. Slow-downs in silence A guard each plugin places in its own callback Set by the framework around every plugin and every device callback clap numbers_too_small_to_hear_are_zero_while_the_plugin_runs_and_only_thenvst3 numbers_too_small_to_hear_are_zero_while_the_plugin_runs_and_only_then
11. Hosting No CLAP hosting; one object called from every thread; parameter changes carry no frame CLAP and VST 3; thread rules in the types; every event on its frame host the_hosted_plugin_sounds_exactly_as_the_plugin_does_alonehost the_hosted_plugin_sounds_exactly_as_the_plugin_does_alone
12. Loudness and broadcast meters None documented BS.1770 / R 128 loudness, true peak, VU, PPM, correlation, checked against the EBU’s test signals analysis cases_1_and_2_a_steady_tone_reads_its_level_on_all_three_scalesanalysis cases_15_to_19_a_sine_is_read_at_its_crest_wherever_the_samples_fall
13. OSC timing Listeners on the message or network thread Time-tagged bundles land on their frame; every OSC 1.0 type; TCP osc a_time_tag_lands_on_its_exact_frameosc every_frame_of_a_block_can_be_hit
14. Pitch and band analysis None documented YIN and McLeod pitch; IEC octave bands and weighting analysis every_test_tone_is_found_within_a_centanalysis a_full_scale_sine_reads_0_db_in_its_band
15. Keyboard state Listeners that may run on the audio thread Atomics an interface polls; no listener, no lock midi the_keyboard_state_does_not_use_the_heapmidi a_key_that_goes_on_sounding_is_never_read_as_silent

Automation on the exact sample

The problem

A host records automation with sample positions. If a plugin only learns the value once per block, a change that should happen on frame 300 of a 512-frame block is applied on frame 0 or frame 512 instead. Fast automation turns into steps, and the size of the steps depends on the host’s buffer setting.

In JUCE

AudioProcessor::processBlock receives audio and a MIDI buffer. Parameter changes are not part of the call; a processor reads each parameter’s current value, which the host updates between blocks. In May 2026 a member of the JUCE team said sample-accurate automation would not be ready for the first release of JUCE 9 (forum thread).

In Manguebyte

The process call carries an EventBuffer in which every parameter change has the frame it falls on. split_at_events renders up to each event, applies it, and continues, using AudioBuffer::windowed to narrow the buffer in place. Nothing is copied or allocated.

Check it

automation_lands_on_its_exact_frame_and_updates_the_parameter schedules changes on frames 100, 101 and 250 and asserts the output sample by sample. automation_lands_on_its_frame_when_processing_in_place does the same through the CLAP C interface, and a test of the same name does it through VST 3, where a host sends a queue of points per parameter and every point is delivered. (JUCE’s VST3 wrapper applies the last point of each queue: processParameterChanges in juce_audio_plugin_client_VST3.cpp.)

VST 3 adds a trap of its own. A host reports each change twice: to the processor with a frame number, and to the controller on the main thread so that an editor can follow. If the controller’s copy is stored into the parameter, the plugin jumps to a value that is slightly early or late until the next point corrects it. While blocks are running, Manguebyte’s wrapper keeps the controller’s copy for display only; what_the_controller_is_told_does_not_disturb_running_automation checks it.

Show the test
Rust
#[test]
fn automation_lands_on_its_exact_frame_and_updates_the_parameter() {
    let mut harness = Harness::<Level>::new(48_000.0, 64);
    harness.automate(100, "level", 2.0);
    harness.automate(101, "level", 3.0);
    harness.automate(250, "level", 0.5);

    let output = harness.process(&ones(2, 300));
    let left = output.channel(0);
    assert_eq!(&left[..100], &[1.0; 100]);
    assert_eq!(left[100], 2.0);
    assert_eq!(&left[101..250], &[3.0; 149]);
    assert_eq!(&left[250..], &[0.5; 50]);
    assert_eq!(output.channel(1), left);

    // Anything polling the parameter sees where automation left it.
    assert_eq!(harness.params().level.value(), 0.5);
    assert_eq!(harness.position(), 300);
}
From crates/manguebyte-plugin/src/harness.rs at commit c5835bf, as it is in the file.

The sound does not depend on the host’s block size

The problem

Hosts hand plugins blocks of different lengths, and some change the length on every call. If anything in a plugin is tied to block boundaries, the same project sounds slightly different at 64 and at 1024 samples, and an offline bounce differs from what was heard while mixing.

In JUCE

Nothing in the framework prevents or tests this. It is up to each plugin author to split blocks at events by hand and to notice when something depends on the block length.

In Manguebyte

Because events carry their frame and the synth resets a voice before every fresh note, the output is a function of the timeline and not of how it was cut up. This is tested rather than assumed.

Check it

the_song_is_the_same_whatever_the_block_size renders a polyphonic song with effects and automation six ways: 64, 256, 997 and 1024 frames, an irregular pattern of 1, 511, 7, 300, 64, 2, 129, and blocks larger than the instrument’s own maximum. The results must be equal bit for bit. Both example plugins have the same test: the instrument and the effect.

The first time this test ran, it failed. A voice that had finished in the middle of a block kept advancing until the end of it, so the next note started from a state that depended on the block length. The test exists to find exactly that, and the fix is part of the Voice contract.

Show the test
Rust
#[test]
fn the_song_is_the_same_whatever_the_block_size() {
    let reference = render(256, &[256]);
    assert!(reference.peak() > 0.1, "the reference render is silent");

    for (max_block_size, pattern) in [
        (64, &[64][..]),
        (1024, &[1024]),
        // Odd lengths that never line up with a note boundary.
        (997, &[997]),
        // A host that changes its mind on every call.
        (512, &[1, 511, 7, 300, 64, 2, 129]),
        // Blocks larger than the player's own, which it must split itself.
        (48, &[5_000]),
    ] {
        let other = render(max_block_size, pattern);
        assert!(
            other == reference,
            "blocks of {pattern:?} (max {max_block_size}) changed the output"
        );
    }
}
From crates/manguebyte/tests/offline.rs at commit c5835bf, as it is in the file.

Every parameter change reaches the plugin as an event

The problem

Host automation is not the only thing that changes a parameter. A user turns a knob in the plugin’s own window, or the host restores a saved state. Those writes do not arrive through the host’s event list, so a plugin that tracks a value from events alone never hears about them.

In JUCE

A processor finds out by polling the parameter, or through a listener. Parameter listeners are called synchronously on whichever thread made the change, which during host automation is the audio thread, and the framework’s attachment classes respond by posting to the operating system’s message queue, which takes a lock (forum thread, bug report).

In Manguebyte

There are no listeners. Before each block, ParamWatcher compares every parameter with the value the plugin last saw and adds an event on frame 0 for each one that differs, ahead of the host’s own events. The event stream is therefore complete: a plugin that follows it never needs to poll, and no user code runs as a side effect of a write.

Check it

changes_made_outside_the_host_reach_the_plugin_as_events uses a test plugin that never reads its parameter, only events. It passes for a direct write, for a restored state, and for host automation on the same frame, which takes precedence.

Real-time safety is tested, not promised

The problem

Code on the audio thread must not allocate or free memory: the allocator can take a lock or ask the operating system for pages, and either can outlast the few milliseconds a block has. One stray allocation is an audible dropout that appears only under load.

In JUCE

Real-time safety is a rule in the documentation. The framework does not check user code against it, and its own parameter attachments break it, as described in the previous section.

In Manguebyte

The test suite replaces the global allocator with one that counts. Any allocation, reallocation or free made while a processor is processing fails the test, and so fails CI. The same guard wraps the CLAP wrapper’s process call, measured from the host’s side of the C interface.

Check it

processors_do_not_use_the_heap and its neighbours cover every processor, the FFT, a synth handling more notes than it has voices, and parameter changes made while running. A test of the guard itself makes sure it would notice. Through the CLAP interface, heap_calls must be zero for automation, notes, changes reported to the host and the block after a state restore.

The guard has already earned its place. On macOS it reported one heap call inside process: a test plugin was recording a value through a standard mutex, which on that platform allocates the first time it is locked.

Show the test
Rust
/// Runs `work` and panics if it used the heap in any way.
fn assert_no_heap_use(what: &str, work: impl FnOnce()) {
    HEAP_CALLS.with(|calls| calls.set(0));
    WATCHING.with(|watching| watching.set(true));
    work();
    WATCHING.with(|watching| watching.set(false));
    let calls = HEAP_CALLS.with(Cell::get);
    assert_eq!(
        calls, 0,
        "{what} used the heap {calls} time(s) while processing"
    );
}

/// Prepares a processor, then runs it over blocks of several sizes with the
/// allocator watched.
fn check<P: Processor<f32>>(what: &str, mut processor: P) {
    processor.prepare(&SPEC);
    let mut buffer = AudioBuffer::<f32>::new(SPEC.num_channels, SPEC.max_block_size);
    assert_no_heap_use(what, || {
        for frames in [256, 1, 64, 255, 256] {
            buffer.set_num_frames(frames);
            for channel in buffer.iter_channels_mut() {
                channel.fill(0.25);
            }
            processor.process(&mut buffer);
        }
        processor.reset();
        buffer.set_num_frames(256);
        processor.process(&mut buffer);
    });
}
Excerpt from crates/manguebyte-dsp/tests/realtime.rs at commit c5835bf.

Per-note expression and modulation

The problem

Bending, swelling or filtering one note of a chord needs a way to address that note. So does a host’s modulation system that wants to move a parameter for one voice without overwriting the value the user set.

In JUCE

Per-note control is available through MPE, which gives each note its own MIDI channel. The processor model has no note expression events and no modulation distinct from changing a parameter’s value, which is why the third-party CLAP wrapper adds its own extension interfaces for them (clap-juce-extensions).

In Manguebyte

Notes have an identity (NoteRef: channel, key and an optional id), so two notes on the same key can overlap. NoteExpression shapes one note. ParamModulation adds an offset on top of a parameter, optionally for a single note, and never changes the stored value, so it cannot fight automation or end up in a saved preset. Synth routes each to the voice that is playing the note.

Check it

expression_and_modulation_reach_one_note_and_parameters_reach_all and notes_with_identifiers_overlap_on_one_key_and_end_separately in the synth; an_instrument_takes_notes_expression_modulation_and_a_sidechain does it through CLAP.

Native CLAP

In JUCE

Building CLAP plugins is not supported natively. The plugin client module covers VST, VST3, AU, AUv3, LV2 and AAX. CLAP was listed for JUCE 9 in the 2024 roadmap and is not in the 9.0.x releases; the route today is the unofficial clap-juce-extensions.

In Manguebyte

export_clap!(MyPlugin) is the whole integration. CLAP’s event model is the one this framework is built on, so nothing is approximated on the way in.

Check it

The CI job builds both example plugins and runs the CLAP project’s own clap-validator on them, failing on any failed test. At the commit this site was written against, the validator ran 44 tests on each: the instrument passed 35 and skipped 9, the effect passed 33 and skipped 11, and neither failed any. The skipped tests are for things the plugins do not have yet: preset discovery (3 tests), 64-bit audio (2) and the extensions that let a host reconfigure the audio ports (3). The instrument also skips a test that feeds it audio, because it has no audio input, and the effect skips the three note tests, because it takes no notes.

cargo mangue bundle writes the .clap beside the .vst3 in each system’s layout, and cargo mangue validate runs the CLAP project’s validator on it as well as Steinberg’s on the .vst3; a CI job runs the tool end to end on Windows, Linux and macOS. JUCE’s juce_add_plugin accepts no CLAP format.

A plugin can be tested without a host

In JUCE

Exercising a plugin’s audio path needs a host, or a test rig the author builds around AudioProcessor.

In Manguebyte

Harness is that rig. It prepares the plugin, schedules notes and automation on a timeline, cuts time into any pattern of block sizes, keeps a transport moving and saves and restores state. The plugin’s sound becomes something an ordinary unit test can assert, on a CI machine with no sound card.

Check it

The example instrument’s tests check that it is silent until played, that it plays the pitch it is asked for (by FFT), that automation and per-note modulation change the sound, that state round-trips, and that the output is independent of block size. The example effect’s tests feed its sidechain the same way.

Show the test
Rust
#[test]
fn plays_the_pitch_it_is_asked_for() {
    use manguebyte::dsp::fft::{Complex, RealFft};

    let mut harness = Harness::<Pluck>::new(RATE, 512);
    harness.params().reverb.set(0.0);
    // No resonant peak, so the fundamental is the loudest partial.
    harness.params().resonance.set(0.7);
    harness.note(0, 40_000, 69, 1.0); // A4, 440 Hz
    let sound = harness.render(32_768);

    let mut fft = RealFft::<f32>::new(32_768);
    let mut spectrum = vec![Complex::ZERO; fft.spectrum_len()];
    fft.forward(sound.channel(0), &mut spectrum);
    let loudest = (1..spectrum.len())
        .max_by(|&a, &b| spectrum[a].norm_sqr().total_cmp(&spectrum[b].norm_sqr()))
        .unwrap();
    let hertz = loudest as f64 * RATE / 32_768.0;
    assert!((hertz - 440.0).abs() < 2.0, "loudest partial at {hertz} Hz");
}
From plugins/pluck/src/lib.rs at commit c5835bf, as it is in the file.

Parameters and state cannot race

In JUCE

getStateInformation and setStateInformation can be called from any non-audio thread while processBlock is running, and the framework does not serialise them; each plugin has to (forum thread). Between JUCE 5.0 and 5.2 the tree that state was saved from could lag behind the real parameter values (forum thread).

In Manguebyte

A parameter is an atomic, and every method takes &self. Saving reads the atomics; loading writes them. There is one copy of each value, so state cannot go stale, and the compiler rejects code that would share anything else between threads without synchronisation. Saved state holds real values keyed by identifier, so a range can be reshaped in a later version without changing how an old session sounds.

Check it

state_from_another_version_loads_as_far_as_it_can and params_are_shareable_across_threads in the parameters crate.

Licence

In JUCE

JUCE is offered under the AGPLv3 or a commercial licence.

In Manguebyte

Manguebyte is under MIT or Apache-2.0, at your option: no copyleft, no per-seat fee and no splash screen, for open and closed products alike.

Check it

The two licence texts in the repository: MIT and Apache-2.0.

A fading sound cannot slow the audio thread down

The problem

A fading sound does not reach zero. Filter memory and reverb tails shrink until their values are subnormal: still not zero, and tens of times slower for a processor to compute with. The plugin then costs the most in the silence after a note. Processors have a per-thread setting that rounds such numbers to zero, and using it is standard practice.

In JUCE

The setting is available as ScopedNoDenormals, a guard that the author of each plugin places at the top of processBlock. The plugin template includes it. Code that did not start from the template, or a callback written later, is unprotected until someone notices.

In Manguebyte

The framework holds the guard itself: the CLAP and VST 3 wrappers and the standalone engine around Plugin::process, and the device streams around every callback. There is nothing for a plugin author to remember. The setting is put back on the way out, so the host’s own code runs as it expects.

Check it

A test in both wrappers (CLAP, VST 3) has a plugin report what the processor does with a subnormal number from inside process, then checks that the host’s thread is back to its own setting afterwards, including after the plugin panics. Measured on the example effect, 4,000 blocks of subnormal input took 442 ms without the guard and 31 ms with it.

Hosting a plugin: thread rules in the types, every event on its frame

The problem

A host calls a plugin from two kinds of thread, and the formats say which calls belong where. Getting it wrong crashes plugins in the field, and nothing stops a host from getting it wrong. A host that wants to pass automation to a plugin precisely also needs a way to say on which frame each change happens.

In JUCE

JUCE 9.0.4 does not host CLAP plugins. For the formats it hosts, a plugin is one AudioPluginInstance, an AudioProcessor that the host calls from the message thread and the audio thread; which method may run where is set out in the documentation, and the compiler checks none of it. The hosting interface is processBlock(AudioBuffer, MidiBuffer): MIDI events carry a sample position, but a parameter change, made through AudioProcessorParameter::setValue, carries none, so a host can place a change on an exact frame only by cutting the block there. JUCE has announced sample-accurate automation for a later JUCE 9 update.

In Manguebyte

manguebyte-host loads CLAP and VST 3 plugins in two halves. The main-thread half (parameters, state, activation) is not Send, so code that would load state from the audio thread does not compile; the audio half is Send, is the only thing that can process, and processes through &mut self. Every event of the framework’s event buffer reaches the plugin on its frame: CLAP events with their offsets, and for VST 3 one queue per parameter with every point on its frame, notes with ids, per-note pressure and note expression.

Check it

Compile-fail doc-tests on ClapPlugin and Vst3Plugin show that the main-thread half cannot be sent to another thread. the_hosted_plugin_sounds_exactly_as_the_plugin_does_alone, for CLAP and for VST 3, loads the example instrument through its real entry point and requires its output to be identical bit for bit to the same plugin run in process by Harness, for four block layouts, with automation of five parameters, notes with ids, expression and pitch bend; removing any one kind of event changes the output, so the identity is not two deaf paths agreeing. automation_lands_on_its_frame_through_the_host sends 59 VST 3 points 61 frames apart.

Loudness, true peak and broadcast meters, ready to use

The problem

Anything that delivers audio for broadcast or streaming has to measure loudness to ITU-R BS.1770 and EBU R 128, and true peak. Getting the gating, the windows and the K-weighting right at every sample rate is easy to get subtly wrong.

In JUCE

JUCE 9.0.4’s documented API has no loudness, LUFS, K-weighting or true-peak class or function, and no VU, PPM, correlation or balance meter (class list). Its level tools are AudioBuffer::getRMSLevel and getMagnitude, AudioDeviceManager::LevelMeter and dsp::BallisticsFilter. Users on the forum write their own (loudness, true peak).

In Manguebyte

manguebyte-analysis has a LoudnessMeter (momentary, short-term, integrated, loudness range, maxima), a TruePeakMeter (BS.1770-4 Annex 2, 4x by default), a VU meter, a PPM with IEC 60268-10 Type I timing, and a correlation and balance meter. They run on the audio thread in memory fixed when they are built, and any thread reads them through atomics without waiting.

Check it

ebu_tech_3341.rs synthesises the EBU Tech 3341 test signals and requires cases 1 to 6 and 9 to 14 within 0.1 LU, and the true-peak cases 15 to 23 within +0.2/−0.4 dB; ebu_tech_3342.rs does the same for loudness range, cases 1 to 4. The VU needle reaches 99% in 300 ms ±3 ms with 1.0 to 1.5% overshoot at 44.1 to 192 kHz. Readings are identical to the bit whatever the block size, and measuring makes no heap call.

Open Sound Control that lands on the sample

The problem

OSC bundles carry a time tag so that a controller or a live-coding program can say when something should happen. An audio engine that applies messages when they arrive, or once per block, throws that timing away.

In JUCE

OSCReceiver calls its listeners on the message thread or directly on the network thread; its documentation describes no delivery into the audio callback and no scheduling by time tag. OSCArgument documents five argument types (int32, float32, string, blob, colour). OSCAddressPattern documents the OSC 1.0 wildcards, and OSCSender and OSCReceiver document UDP only.

In Manguebyte

manguebyte-osc decodes on a network thread and hands fixed events to the audio thread through wait-free queues, and a message in a time-tagged bundle lands on the frame nearest its time tag. Every argument type the OSC 1.0 specification lists is supported, nested arrays included; the // wildcard proposed for OSC 1.1 (Freed and Schmeder, 2009) is matched; and TCP with the OSC 1.1 SLIP framing sits beside UDP.

Check it

a_time_tag_lands_on_its_exact_frame, every_frame_of_a_block_can_be_hit and where_an_event_lands_does_not_depend_on_the_block_size (400 events, seven block patterns, identical frames); reading on the audio thread makes no heap call. codec.rs encodes the specification’s examples byte for byte and round-trips every type.

Pitch detection and fractional-octave analysis

In JUCE

JUCE 9.0.4’s documented class list has no pitch detection and no fractional-octave band analyser or A- and C-weighting class (classes); it provides dsp::FFT and dsp::WindowingFunction as building blocks, and spectrum display appears in its tutorials as code the reader writes.

In Manguebyte

PitchDetector offers YIN and the McLeod pitch method with a confidence value and the nearest note named, refined between samples by band-limited interpolation. SpectrumAnalyzer gives octave to twenty-fourth-octave bands on the exact frequencies of IEC 61260-1 and A and C weighting from IEC 61672-1, with ballistics and peak hold, handing whole frames to an interface through a wait-free triple buffer.

Check it

pitch.rs: sines, sawtooths, square waves, tones without their fundamental and tones with a strong second harmonic from 55 Hz to 2 kHz read within 2 cents at 44.1 and 48 kHz, with no octave errors. spectrum.rs: a full-scale sine reads 0 dB in its band within 0.01 dB, and the third-octave bands of white and pink noise add up within 0.05 dB.

A keyboard an interface can read without listening

In JUCE

MidiKeyboardState reports key changes to its listeners synchronously, on whichever thread updates it, and its documentation warns that the callback could happen from an audio callback thread (listener). An interface can poll isNoteOn() instead, but the documentation gives no guarantee that updates or reads are free of locks or allocation, and the class tracks keys, not pedals.

In Manguebyte

midi::keyboard::KeyboardState keeps keys, velocities and the sustain pedal in atomics. The audio thread updates it with no listener code, no lock and no heap call; an interface polls it. A reader never sees a key that was not struck, and never sees a sustained key go silent while it passes from the hand to the pedal.

Check it

the_keyboard_state_does_not_use_the_heap (20,000 random messages, reads interleaved), and a_key_that_goes_on_sounding_is_never_read_as_silent and an_interface_can_read_while_another_thread_writes.

Where JUCE is ahead

JUCE has been in production for more than twenty years, and it shows. Today it has, and Manguebyte does not:

  • a complete GUI toolkit, with plugin editor windows on every system and in every format (Manguebyte’s editors have windows on Win32 only, and none has been opened in a host by hand yet);
  • Audio Unit and AAX plugin formats;
  • hosting of AU and LV2 plugins, plugin scanning, and the editors of hosted plugins (Manguebyte hosts CLAP and VST 3 plugins, without their editors);
  • more DSP building blocks, and more file formats;
  • iOS and Android;
  • documentation, tutorials and a large community.

JUCE_PARITY.md tracks that gap module by module, and the roadmap says in what order it is being closed.

This page follows docs/BEYOND_JUCE.md in the repository at commit c5835bf. Where the README is newer, the page follows the README: the limit of section 6, on editors and LV2, and the list of where JUCE is ahead. The limit of section 2 also cites what testing in REAPER found.