After installing re-mock, here is the most basic example (aka "hello world" of re-mock) demonstrating how to write a test:
#include <gtest/gtest.h>
#include <re_cmake_build.h>
#include <re/mock/re-mock.h>
#include <Device.h>
using namespace re::mock;
// Device - Init
TEST(Device, Init)
{
auto c = DeviceConfig<Device>::fromJBoxExport(RE_CMAKE_PROJECT_DIR);
auto tester = InstrumentTester<Device>(c);
tester.nextBatch();
}
gtest/gtest.h
is the Google Test framework (and you can replace with your own testing framework if you do not use Google Test).re_cmake_build.h
is a file generated byre-cmake
that contains very useful "defines" likeRE_CMAKE_PROJECT_DIR
pointing to the root of your plugin, thus giving access toinfo.lua
, etc...re/mock/re-mock.h
is the primary include for the framework giving access to the various device testersDevice.h
is whatever header file that contains the main class of your plugin
re_cmake_build.h
is only available if you usere-cmake
. If you do not usere-cmake
theRE_CMAKE_PROJECT_DIR
define will not be available, so you need to provide the path pointing to the root of the plugin.
The "main" class of your plugin is the "instance" that you instantiate in the
JBox_Export_CreateNativeObject
call. For example, in the RE exampleVerySimplySampler
, this would beCVerySimpleSampler
. In this example, the main instance is of typeDevice
.
All classes in re-mock
are under the re::mock
namespace, so we simply use it to avoid repeating the namespace over and over.
- We instantiate a typed
DeviceConfig
for our main classDevice
by providing the location of whereinfo.lua
,motherboard_def.lua
andrelatime_controller.lua
are located. Thanks tore_cmake_build.h
, this is trivial. - We instantiate a tester for our device. Each tester is specialized for the kind of rack extension under test and must match the
device_type
entry frominfo.lua
. Note that the tester is also itself typed (allowing to access the main class directly). - We call
nextBatch
which advances the engine by one batch and as a result will instantiate the device under test and callJBox_Export_RenderRealtime
one time, thus effectively initializing the device.
Although this code is small and seems to test nothing, it actually does a lot of work:
- it parses
info.lua
,motherboard_def.lua
andrelatime_controller.lua
to build an in-memory model of the rack extension - it runs the
rtc_bindings
(lua code), thus effectively callingJBox_Export_CreateNativeObject
(c++ code) and instantiates theDevice
- it calls
JBox_Export_RenderRealtime
with all the proper set ofiPropertyDiffs
as defined in thert_input_setup/notify
section ofrealtime_controller.lua
A lot can go wrong in this initialization and so it is always a good thing to have such a small/concise test run prior to deploying to Recon, to catch some typos for example, some of them triggering a hard crash of Recon!
Recon/Reason has a main event loop, calling JBox_Export_RenderRealtime
over and over as fast as necessary. For example, at a sample rate of 48000, it calls this function 750 times per second which can make logging and/or debugging quite challenging.
re-mock
puts you in charge of the main event loop, so you decide when this API is called. For example:
- invoking
tester.nextBach()
runs through exactly one iteration of this loop. - invoking
tester.nextBatches(time::Duration{1000})
runs through 750 iterations (assuming 48000 sample rate which can be chosen when instantiating the tester).
By controlling precisely the main event loop, it becomes easy to set the device under test in a precise configuration to be able to run some tests.
TEST(Device, test1)
{
auto c = DeviceConfig<Device>::fromJBoxExport(RE_CMAKE_PROJECT_DIR);
auto tester = InstrumentTester<Device>(c, 48000);
// wiring main out
tester.wireMainOut("OutLeft", "OutRight");
// first batch = initialization
tester.nextBatch();
// load a patch to set all the parameters of the device to a known state
tester.device().loadPatch("/Public/Sync Bell.repatch");
// apply patch
tester.nextBatch();
// import a Midi file containing note on/off events
tester.importMidi(resource::File{fmt::path(RE_CMAKE_PROJECT_DIR, "test", "midi", "test1.mid")});
// set the transport position at the start of bar 2
tester.transportPlayPos(sequencer::Time(2,1,1,0));
// play for 2 bars (so bar 2 and 3) and captures the output
auto sample = tester.bounce(sequencer::Duration::k1Beat_4x4 * 2);
// make sure that the sample is what we expect
ASSERT_EQ(*sample, *tester.loadSample(resource::File{fmt::path(RE_CMAKE_PROJECT_DIR, "test", "wav", "test1.wav")));
// tester.saveSample(*sample, resource::File{"/tmp/result.wav"});
}
-
Like the "Hello World" example, we instantiate an instrument tester for the device this time with a sample rate of 48000 (the default being 44100).
-
Instantiating an
InstrumentTester
actually instantiates another device: a "destination" device (MAUDst
) so that the sound generated by the device under test can be captured. Callingtester.wireMainOut("OutLeft", "OutRight")
is wiring the main device under test to this destination device andOutLeft
andOutRight
are the name of the sockets defined inmotherboard_def.lua
-
The test runs 1 batch to initialize everything.
If the device under test defines a
default_patch
ininfo.lua
, all the values of this patch will be part of the firstiPropertyDiffs
exactly like Recon/Reason (if they are defined inrt_input_setup
). -
The test then loads a different patch by calling the
loadPatch
method on the device itself. Note how it uses a built-in patch simply referenced by its path (/Public/Sync Bell.repatch
).It is possible to load a patch that is not built-in by using the overloaded api
tester.device().loadPatch(resource::File const &)
, particularly useful for example, if a user reports some issue, he/she can simply saves the patch and be directly loaded. -
Applying the patch will provide all the values of the patch to the device (either via
iPropertyDiffs
if the device usesrt_input_setup
or viaJBox_LoadMOMProperty
if the device uses this technique instead). -
The test then imports a Midi file (which is located in the source tree). The Midi file can have been generated by any means (like for example "Exporting Midi" in Reason). Each device has a sequencer track and the notes from the Midi file populates the sequencer track.
-
For the sake of this example, we set the transport play position starting at bar 2 (if we don't, it just starts at 1) and we assume that there are either no events prior to bar 2 in the Midi file, or we just simply want to skip them.
-
The test uses the
IntrumentTester::bounce()
convenient api which does a few things:- it runs as many batches as necessary to cover the provided duration
- it starts the transport playback when it starts, and stops it when it ends (so that the events that were imported in the sequencer track from the Midi file are triggered)
- it captures the output of the device and returns it as a
Sample
(MockAudioDevice::Sample
) which can be directly compared with some other "expected" sample.
-
The test compares the output generated with some sample file loaded directly from the file system.
-
Finally, as shown in the commented line, you could also save the sample generated directly in a file so that you can use any external tool to check the result.
This final example in this quick start section is a test written for the Very Simple Sampler example code provided with the RE SDK.
TEST(CVerySimpleSampler, Play)
{
auto c = DeviceConfig<CVerySimpleSampler>::fromJBoxExport(RE_CMAKE_PROJECT_DIR).disable_trace();
auto tester = InstrumentTester<CVerySimpleSampler>(c);
// we wire main out to the proper sockets in the device
tester.wireMainOut("left", "right");
// we load the actual samples that will be played so that we can use them to check playback
auto const bellSample = tester.loadSample("/Public/Samples/Bell.wav");
auto const chipSample = tester.loadSample("/Public/Samples/Chip.wav");
auto const machinaSample = tester.loadSample("/Public/Samples/Machina.wav");
auto const mkivSample = tester.loadSample("/Public/Samples/MkIV.wav");
// initializes the device
tester.nextBatch();
// Make sure we play at max volume (gain = 1.0 / preview_volume_level = 127) and set the proper root_key
tester.device().setNum("/user_samples/0/root_key", Midi::C(3)); tester.device().setNum("/user_samples/0/preview_volume_level", 127);
tester.device().setNum("/user_samples/1/root_key", Midi::D_sharp(3)); tester.device().setNum("/user_samples/1/preview_volume_level", 127);
tester.device().setNum("/user_samples/2/root_key", Midi::F_sharp(3)); tester.device().setNum("/user_samples/2/preview_volume_level", 127);
tester.device().setNum("/user_samples/3/root_key", Midi::A(3)); tester.device().setNum("/user_samples/3/preview_volume_level", 127);
// play C3 (Bell) (gain = 1.0)
ASSERT_EQ(*bellSample,
*tester.bounce(sample::Duration{bellSample.getFrameCount()},
tester.newTimeline()
.note(Midi::C(3), sample::Duration{bellSample.getFrameCount()}, 127)));
// play C3 again (note off/note on) (Bell) (gain = 0.78 (100/127))
ASSERT_EQ(bellSample->clone().applyGain(100/127.0f),
*tester.bounce(sample::Duration{bellSample.getFrameCount()},
tester.newTimeline()
.note(Midi::C(3), sample::Duration{bellSample.getFrameCount()}, 100)));
// play D#3 (Chip) (gain = 1.0) ...
// play F#3 (Machina) (gain = 1.0) ...
// play A3 (MkIV) (gain = 1.0) ...
// Expected result is 4 samples mixed
auto mixedSample = bellSample->clone().mixWith(*chipSample).mixWith(*machinaSample).mixWith(*mkivSample);
ASSERT_EQ(mixedSample,
*tester.bounce(sample::Duration{mixedSample.getFrameCount()},
tester.newTimeline()
.note(Midi::C(3), sample::Duration{bellSample.getFrameCount()}, 127)
.note(Midi::D_sharp(3), sample::Duration{chipSample.getFrameCount()}, 127)
.note(Midi::F_sharp(3), sample::Duration{machinaSample.getFrameCount()}, 127)
.note(Midi::A(3), sample::Duration{mkivSample.getFrameCount()}, 127)));
}
-
Like in the previous example, we initialize the tester. Note how the class is
CVerySimpleSampler
and we calldisable_trace()
because the device generates a lot of traces otherwise (due toJBOX_TRACE("NoteOff")
in theVoicePool
class). -
The output sockets are wired (called
left
andright
for this device). -
The built-in samples are pre-loaded because we will use them to test the output
-
The next section demonstrates how to simulate user input by changing the value of any motherboard property. In this case we change the volume to 127 (unity gain) and set the proper root key for each of the 4 samples.
The device does not adjust the root key (which I think is a bug/mistake): for example in order to play "chip" you have to press D#3, but that does not mean that it should sound like if its root key is C3, and it has been transposed to D#3... This is why we set the proper root key
-
The previous example used a Midi file and the sequencer track to play some notes. This example uses a different technique which is equivalent to loading the plugin in Reason/Recon and simply hitting notes on a Midi keyboard (without entering them on the sequencer track). It uses the concept of a
Timeline
which describes what happens during the variousbounce
,play
ornextBatches
call:// timeline describes holding the C3 note from the beginning of the bounce call for a duration of // "bellSample.getFrameCount()" and a velocity of 127 tester.bounce(sample::Duration{bellSample.getFrameCount()}, tester.newTimeline() .note(Midi::C(3), sample::Duration{bellSample.getFrameCount()}, 127)))
-
The next assert shows how to easily apply gain to a sample (
bellSample->clone().applyGain(100/127.0f)
) -
The next 3 asserts are not shown for brevity since they do the same for the other samples
-
Finally, the last assert shows how to mix multiple samples together and how the timeline describes holding 4 notes with various durations
The Documentation page describes some of those concepts in more details.