HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_signal.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath> // sin()
5#include <cstdint>
6#include <memory>
7#include <random>
8#include <string>
9#include <vector>
10
12#include "dsp/hart_dsp.hpp"
14#include "hart_utils.hpp" // floatsNotEqual(), roundToSizeT()
15
16/// @defgroup Signals Signals
17/// @brief Generate signals
18
19namespace hart {
20
21/// @brief Polymorphic base for all signals
22/// @warning This class exists only for type erasure and polymorphism.
23/// Do NOT inherit custom signals from this class directly.
24/// Inherit from @ref hart::Signal instead.
25/// @ingroup Signals
26template<typename SampleType>
28{
29public:
30 /// @brief Default constructor
31 SignalBase() = default;
32
33 /// @brief Copies other signal
34 SignalBase (const SignalBase& other):
35 m_numChannels(other.m_numChannels),
36 m_startTimestampSeconds (other.m_startTimestampSeconds)
37 {
38 if (other.dspChain.size() == 0)
39 return;
40
41 dspChain.reserve (dspChain.size());
42
43 for (auto& dsp : other.dspChain)
44 dspChain.push_back (dsp->copy());
45 }
46
47 /// @brief Moves from other signal
48 SignalBase (SignalBase&& other) noexcept:
49 m_numChannels (other.m_numChannels),
50 m_startTimestampSeconds (other.m_startTimestampSeconds),
51 dspChain (std::move (other.dspChain))
52 {
53 other.m_numChannels = 0;
54 other.m_startTimestampSeconds = 0.0;
55 }
56
57 /// @brief Destructor
58 virtual ~SignalBase() = default;
59
60 /// @brief Copies from other signal
62 {
63 if (this == &other)
64 return *this;
65
66 m_numChannels = other.m_numChannels;
67 m_startTimestampSeconds = other.m_startTimestampSeconds;
68 dspChain.clear();
69
70 if (other.dspChain.size() == 0)
71 return *this;
72
73 for (auto& dsp : other.dspChain)
74 dspChain.push_back (dsp->copy());
75
76 return *this;
77 }
78
79 /// @brief Moves from other signal
80 SignalBase& operator= (SignalBase&& other) noexcept
81 {
82 if (this == &other)
83 return *this;
84
85 m_numChannels = other.m_numChannels;
86 dspChain = std::move (other.dspChain);
87 m_startTimestampSeconds = other.m_startTimestampSeconds;
88 other.m_numChannels = 0;
89 other.m_startTimestampSeconds = 0.0;
90
91 return *this;
92 }
93
94 /// @brief Tells the host whether this Signal is capable of generating audio for a certain amount of channels
95 /// @details It is guaranteed that the signal will not receive unsupported number of channels in @ref renderNextBlock().
96 /// This method is guaranteed to be called at least once before @ref prepare()
97 /// @note This method should only care about the Signal itself, and not the attached effects in DSP chain - they'll be queried separately
98 /// @param numChannels Number of output channels that will need to be filled
99 /// @return true if signal is capable of filling this many channels with audio, false otherwise
100 virtual bool supportsNumChannels (size_t /* numChannels */) const { return true; };
101
102 /// @brief Tells whether this Signal supports given sample rate
103 /// @details It is guaranteed to be called before @ref prepare()
104 /// @note This method should only care about the Signal itself, and not the attached effects in DSP chain - they'll be queried separately
105 /// @param sampleRateHz sample rate at which the audio should be generated
106 /// @return true if signal is capable of generating audio at a given sample rate, false otherwise
107 virtual bool supportsSampleRate (double /* sampleRateHz */) const { return true; }
108
109 /// @brief Prepare the signal for rendering
110 /// @details This method is guaranteed to be called after @ref supportsNumChannels() and supportsSampleRate(),
111 /// but before @ref renderNextBlock().
112 /// It is guaranteed that ```numChannels``` obeys supportsNumChannels() preferences, same with ```sampleRateHz```
113 /// and @ref supportsSampleRate(). It is guaranteed that all subsequent renderNextBlock() calls will be in line
114 /// with the arguments received in this callback.
115 /// @param sampleRateHz sample rate at which the audio should be generated
116 /// @param numOutputChannels Number of output channels to be filled
117 /// @param maxBlockSizeFrames Maximum block size in frames (samples)
118 virtual void prepare (double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames) = 0;
119
120 /// @brief Renders next block audio for the signal
121 /// @details Depending on circumstances, this callback will either be called once to generate an entire piece of audio from
122 /// start to finish, or called repeatedly, one block at a time.
123 /// This method is guaranteed to be called strictly after @ref prepare(), or not called at all.
124 /// Number of channels and max block size are guaranteed to be in line with the ones set by prepare() callback.
125 /// Assume sample rate to always be equal to the one received in the last @ref prepare() callback.
126 /// All audio blocks except the last one are guaranteed to be equal to ```maxBlockSizeFrames``` set in @ref prepare() callback.
127 /// @warning Remember that the very last block of audio is almost always smaller than the block size set in @ref prepare(), so be
128 /// careful with buffer bounds.
129 /// @note Note that this method does not have to be real-time safe, as all rendering always happens offline.
130 /// Also note that, unlike real-time audio applications, this method is called on the same thread as all others like @ref prepare().
131 /// @param output Output audio block
132 /// @warning Output audio buffer is not guaranteed to be pre-filled with zeros, it may contain junk data.
133 virtual void renderNextBlock (AudioBuffer<SampleType>& output) = 0;
134
135 /// @brief Resets the Signal to initial state
136 /// @details Ideally should be implemented in a way that audio produced after resetting is identical to audio produced after instantiation
137 virtual void reset() = 0;
138
139 /// @brief Returns a smart pointer with a copy of this object
140 virtual std::unique_ptr<SignalBase<SampleType>> copy() const = 0;
141
142 /// @brief Returns a smart pointer with a moved instance of this object
143 virtual std::unique_ptr<SignalBase<SampleType>> move() = 0;
144
145 /// @brief Makes a text representation of this Signal for test failure outputs.
146 /// @details It is strongly encouraged to follow python's
147 /// <a href="https://docs.python.org/3/reference/datamodel.html#object.__repr__" target="_blank">repr()</a>
148 /// conventions for returned text - basically, put something like "MyClass(value1, value2)" (with no quotes)
149 /// into the stream whenever possible, or "<Readable info in angled brackets>" otherwise.
150 /// Also, use built-in stream manipulators like @ref dbPrecision wherever applicable.
151 /// Use @ref HART_DEFINE_GENERIC_REPRESENT() to get a basic implementation for this method.
152 /// @param[out] stream Output stream to write to
153 virtual void represent (std::ostream& stream) const = 0;
154
155 /// @brief Prepares the signal and all attached effects in the DSP chain for rendering
156 /// @details This method is intended to be called by Signal hosts like AudioTestBuilder or Matcher.
157 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
158 /// like a custom Matcher, you must call this method before calling @ref renderNextBlockWithDSPChain().
159 /// You must also call @ref supportsNumChannels() and @ref supportsSampleRate() before calling this method.
160 /// @attention If you're not making a custom host, you probably don't need to call this method.
161 void prepareWithDSPChain (double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)
162 {
163 prepare (sampleRateHz, numOutputChannels, maxBlockSizeFrames);
164 const size_t numInputChannels = numOutputChannels;
165
166 for (auto& dsp : dspChain)
167 {
168 if (! dsp->supportsChannelLayout (numInputChannels, numOutputChannels))
169 HART_THROW_OR_RETURN_VOID (ChannelLayoutError, "Not all DSP in the Signal's DSP chain support its channel layout");
170
171 if (! dsp->supportsSampleRate (sampleRateHz))
172 HART_THROW_OR_RETURN_VOID (hart::SampleRateError, "Not all DSP in the Signal's DSP chain support its sample rate");
173
174 dsp->prepareWithEnvelopes (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
175 }
176
177 // Perform optional fast-forward set by DSP::skipTo()
179 performSkipTo (sampleRateHz, numOutputChannels, maxBlockSizeFrames);
180 }
181
182 /// @brief Renders next block audio for the signal and all the effects in the DSP chain
183 /// @details This method is intended to be called by Signal hosts like AudioTestBuilder or Matcher
184 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
185 /// like a custom Matcher, you must call it after calling @ref prepareWithDSPChain().
186 /// @attention If you're not making a custom host, you probably don't need to call this method.
187 void renderNextBlockWithDSPChain (AudioBuffer<SampleType>& output)
188 {
189 renderNextBlock (output);
190 AudioBuffer<SampleType>& inputReplacing = output;
191
192 for (auto& dsp : dspChain)
193 dsp->processWithEnvelopes (inputReplacing, output);
194 }
195
196 /// @brief Resets to Signal and all the effects attached to its DSP chain to initial state
197 /// @details This method is intended to be called by hosts like AudioTestBuilder or Matcher.
198 /// If you're not making a custom host, you probably don't need this method.
199 virtual void resetWithDSPChain()
200 {
201 reset();
202
203 for (auto& dsp : dspChain)
204 dsp->reset();
205 }
206
207 /// @brief Makes a text representation of this signal and its entire signal chain for test failure outputs.
208 /// @details Used by "<<" operator
209 /// @private
210 /// @param[out] stream Output stream to write to
211 void representWithDSPChain (std::ostream& stream) const
212 {
213 represent (stream);
214
215 for (const auto& dsp : dspChain)
216 stream << " >> " << *dsp;
217 }
218
219 /// @brief Helper for template resolution
220 /// @private
221 using m_SampleType = SampleType;
222
223protected:
224 void setNumChannels (size_t numChannels)
225 {
226 m_numChannels = numChannels;
227 }
228
230 {
231 return m_numChannels;
232 }
233
234 size_t m_numChannels = 1;
236 std::vector<std::unique_ptr<DSPBase<SampleType>>> dspChain;
237
238private:
239 void performSkipTo (double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)
240 {
242
243 size_t fastForwardFramesLeft = roundToSizeT (m_startTimestampSeconds * sampleRateHz);
244
245 while (fastForwardFramesLeft != 0)
246 {
247 const size_t blockSizeFrames = fastForwardFramesLeft >= maxBlockSizeFrames ? maxBlockSizeFrames : fastForwardFramesLeft;
248 AudioBuffer<SampleType> dummyAudioBlock (numOutputChannels, blockSizeFrames);
249 renderNextBlockWithDSPChain (dummyAudioBlock);
250 fastForwardFramesLeft -= blockSizeFrames;
251 }
252
254 }
255};
256
257/// @brief Base class for signals
258/// @ingroup Signals
259/// @tparam SampleType Type of values that will be generated, typically ```float``` or ```double```
260/// @tparam Derived Subclass for CRTP
261template<typename SampleType, typename Derived>
262class Signal:
263 public SignalBase<SampleType>
264{
265public:
266 /// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
267 /// @note If your DSP object does not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
268 /// @param dsp A DSP effect instance
269 Derived& followedBy (const DSPBase<SampleType>& dsp)
270 {
271 this->dspChain.emplace_back (dsp.copy());
272 return static_cast<Derived&> (*this);
273 }
274
275 /// @brief Adds a DSP effect to the end of signal's DSP chain by transfering a smart pointer
276 /// @note If your DSP object does not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
277 /// @param dsp A DSP effect instance
278 Signal& followedBy (std::unique_ptr<DSPBase<SampleType>> dsp)
279 {
280 this->dspChain.emplace_back (std::move (dsp));
281 return static_cast<Derived&> (*this);
282 }
283
284 // TODO: Add check if rvalue
285 // TODO: Check if if this template ever gets picked
286 /// @brief Adds a DSP effect to the end of signal's DSP chain by moving it
287 /// @note If your DSP object does not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
288 /// @param dsp A DSP effect instance
289 template <
290 typename DerivedDSP,
291 typename = typename std::enable_if<
292 std::is_base_of<
293 DSPBase<SampleType>,
294 typename std::decay<DerivedDSP>::type
295 >::value
296 >::type
297 >
298 Signal& followedBy (DerivedDSP&& dsp)
299 {
300 this->dspChain.emplace_back (dsp.move());
301 return static_cast<Derived&> (*this);
302 }
303
304
305 // TODO: Add followedBy() for smart pointer to DSO
306
307 std::unique_ptr<SignalBase<SampleType>> copy() const override
308 {
309 return hart::make_unique<Derived> (static_cast<const Derived&> (*this));
310 }
311
312 std::unique_ptr<SignalBase<SampleType>> move() override
313 {
314 return hart::make_unique<Derived> (std::move (static_cast<const Derived&> (*this)));
315 }
316
317 /// @brief Skips the signal to a specific timestamp
318 /// @details Fast-forwards the signal, with all attaches DSP effects and their automation
319 /// envelopes. Calling it multiple times on one instance will stack the skip times.
320 /// @note Keep in mind that the skip is accurate within one audio frame tolerance
321 /// @param startTimestampSeconds How much time to skip from the start of the signal
322 Derived& skipTo (double startTimestampSeconds)
323 {
324 if (startTimestampSeconds < 0)
325 HART_THROW_OR_RETURN (hart::ValueError, "Can't skip to a negative timestamp", static_cast<Derived&> (*this));
326
327 this->m_startTimestampSeconds += startTimestampSeconds;
328 return static_cast<Derived&> (*this);
329 }
330
331 /// @brief Returns a copy of this signal, but with flipped phase
332 Derived operator-() const
333 {
334 auto newSignal = static_cast<const Derived&> (*this);
335 newSignal.dspChain.emplace_back (hart::make_unique<GainLinear<SampleType>> (SampleType (-1)));
336 return newSignal;
337 }
338
339 /// @brief Returns a copy of this signal, but with flipped phase
340 Derived operator~() const
341 {
342 return -(*this);
343 }
344};
345
346/// @brief Prints readable text representation of the Signal object into the I/O stream
347/// @relates Signal
348/// @ingroup Signals
349template<typename SampleType>
350std::ostream& operator<< (std::ostream& stream, const SignalBase<SampleType>& signal)
351{
352 signal.representWithDSPChain (stream);
353 return stream;
354}
355
356// TODO: Drop some of the >> overloads - too many of those!
357
358/// @brief Adds a DSP effect to the end of signal's DSP chain by moving it
359/// @relates Signal
360/// @ingroup Signals
361template<
362 typename SampleType,
363 typename DerivedSignal,
364 typename DerivedDSP, typename std::enable_if<std::is_base_of<DSPBase<SampleType>, typename std::decay<DerivedDSP>::type>::value>::type>
365Signal<SampleType, DerivedSignal>& operator>> (Signal<SampleType, DerivedSignal>& signal, DerivedDSP&& dsp)
366{
367 return signal.followedBy (std::move (dsp));
368}
369
370/// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
371/// @relates Signal
372/// @ingroup Signals
373template<typename SampleType, typename DerivedSignal>
374Signal<SampleType, DerivedSignal>& operator>> (Signal<SampleType, DerivedSignal>& signal, const DSPBase<SampleType>& dsp)
375{
376 return signal.followedBy (dsp);
377}
378
379/// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
380/// @relates Signal
381/// @ingroup Signals
382template<typename SampleType, typename DerivedSignal>
383Signal<SampleType, DerivedSignal>&& operator>> (Signal<SampleType, DerivedSignal>&& signal, const DSPBase<SampleType>& dsp)
384{
385 return std::move (signal.followedBy (dsp));
386}
387
388/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
389/// @details This is for smart pointers to abstract DSP type
390/// @relates Signal
391/// @ingroup Signals
392template<typename SampleType, typename DerivedSignal>
393Signal<SampleType, DerivedSignal>& operator>> (Signal<SampleType, DerivedSignal>& signal, std::unique_ptr<DSPBase<SampleType>>&& dsp)
394{
395 signal.followedBy (std::move (dsp));
396 return signal;
397}
398
399/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
400/// @details This is for smart pointers to abstract DSP type
401/// @relates Signal
402/// @ingroup Signals
403template<typename SampleType, typename DerivedSignal>
404Signal<SampleType, DerivedSignal>&& operator>> (Signal<SampleType, DerivedSignal>&& signal, std::unique_ptr<DSPBase<SampleType>>&& dsp)
405{
406 signal.followedBy (std::move (dsp));
407 return std::move (signal);
408}
409
410/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
411/// @details This is for smart pointers to actual (derived) DSP type
412/// @relates Signal
413/// @ingroup Signals
414template<
415 typename SampleType,
416 typename DerivedSignal,
417 typename DerivedDSP, typename = typename std::enable_if<std::is_base_of<DSPBase<SampleType>, DerivedDSP>::value>::type>
418Signal<SampleType, DerivedSignal>& operator>>(Signal<SampleType, DerivedSignal>& signal, std::unique_ptr<DerivedDSP>&& dsp)
419{
420 signal.followedBy (std::move (dsp));
421 return signal;
422}
423
424/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
425/// @details This is for smart pointers to actual (derived) DSP type
426/// @relates Signal
427/// @ingroup Signals
428template<
429 typename SampleType,
430 typename DerivedSignal,
431 typename DerivedDSP, typename = typename std::enable_if<std::is_base_of<DSPBase<SampleType>, DerivedDSP>::value>::type>
432Signal<SampleType, DerivedSignal>&& operator>>(Signal<SampleType, DerivedSignal>&& signal, std::unique_ptr<DerivedDSP>&& dsp)
433{
434 signal.followedBy (std::move (dsp));
435 return std::move (signal);
436}
437
438} // namespace hart
439
440/// @brief Forbids @ref hart::Signal::copy() and @ref hart::Signal::move() methods
441/// @details Put this into your class body's ```public``` section if either is true:
442/// - Your class is not trivially copyable and movable
443/// - You don't want to trouble yourself with implementing move and copy semantics for your class
444///
445/// Otherwise, use @ref HART_SIGNAL_DEFINE_COPY_AND_MOVE() instead.
446/// Obviously, you won't be able to pass your class to the host
447/// by reference, copy or explicit move, but you still can pass
448/// it wrapped into a smart pointer like so:
449/// ```cpp
450/// processAudioWith (MyDSP())
451/// .withInputSignal(hart::make_unique<MyDspType>()) // As input signal
452/// .expectTrue (EqualsTo (hart::make_unique<MyDspType>())) // As reference signal
453/// .process();
454/// ```
455/// @ingroup Signals
456#define HART_SIGNAL_FORBID_COPY_AND_MOVE
457 std::unique_ptr<Signal<SampleType>> copy() const override {
458 static_assert(false, "This Signal cannot be copied");
459 return nullptr;
460 }
461 std::unique_ptr<Signal<SampleType>> move() override {
462 static_assert(false, "This Signal cannot be moved");
463 return nullptr;
464 }
465
466/// @private
467#define HART_SIGNAL_DECLARE_ALIASES_FOR(ClassName)
468 namespace aliases_float{
469 using ClassName = hart::ClassName<float>;
470 }
471 namespace aliases_double{
472 using ClassName = hart::ClassName<double>;
473 }
Polymorphic base for all DSP.
Definition hart_dsp.hpp:32
Polymorphic base for all signals.
virtual void reset()=0
Resets the Signal to initial state.
virtual ~SignalBase()=default
Destructor.
virtual void represent(std::ostream &stream) const =0
Makes a text representation of this Signal for test failure outputs.
double m_startTimestampSeconds
SignalBase(const SignalBase &other)
Copies other signal.
virtual std::unique_ptr< SignalBase< SampleType > > copy() const =0
Returns a smart pointer with a copy of this object.
void renderNextBlockWithDSPChain(AudioBuffer< SampleType > &output)
Renders next block audio for the signal and all the effects in the DSP chain.
virtual void resetWithDSPChain()
Resets to Signal and all the effects attached to its DSP chain to initial state.
SignalBase & operator=(const SignalBase &other)
Copies from other signal.
SignalBase()=default
Default constructor.
SignalBase & operator=(SignalBase &&other) noexcept
Moves from other signal.
virtual void renderNextBlock(AudioBuffer< SampleType > &output)=0
Renders next block audio for the signal.
void prepareWithDSPChain(double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)
Prepares the signal and all attached effects in the DSP chain for rendering.
void setNumChannels(size_t numChannels)
SignalBase(SignalBase &&other) noexcept
Moves from other signal.
virtual std::unique_ptr< SignalBase< SampleType > > move()=0
Returns a smart pointer with a moved instance of this object.
virtual bool supportsNumChannels(size_t) const
Tells the host whether this Signal is capable of generating audio for a certain amount of channels.
virtual bool supportsSampleRate(double) const
Tells whether this Signal supports given sample rate.
size_t getNumChannels()
std::vector< std::unique_ptr< DSPBase< SampleType > > > dspChain
virtual void prepare(double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)=0
Prepare the signal for rendering.
Base class for signals.
Signal & followedBy(std::unique_ptr< DSPBase< SampleType > > dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering a smart pointer.
Derived & skipTo(double startTimestampSeconds)
Skips the signal to a specific timestamp.
Derived operator-() const
Returns a copy of this signal, but with flipped phase.
std::unique_ptr< SignalBase< SampleType > > move() override
Returns a smart pointer with a moved instance of this object.
Derived & followedBy(const DSPBase< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Derived operator~() const
Returns a copy of this signal, but with flipped phase.
std::unique_ptr< SignalBase< SampleType > > copy() const override
Returns a smart pointer with a copy of this object.
Signal & followedBy(DerivedDSP &&dsp)
Adds a DSP effect to the end of signal's DSP chain by moving it.
Signal< SampleType, DerivedSignal > && operator>>(Signal< SampleType, DerivedSignal > &&signal, std::unique_ptr< DSPBase< SampleType > > &&dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
Signal< SampleType, DerivedSignal > & operator>>(Signal< SampleType, DerivedSignal > &signal, std::unique_ptr< DSPBase< SampleType > > &&dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
Signal< SampleType, DerivedSignal > && operator>>(Signal< SampleType, DerivedSignal > &&signal, const DSPBase< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Signal< SampleType, DerivedSignal > & operator>>(Signal< SampleType, DerivedSignal > &signal, DerivedDSP &&dsp)
Adds a DSP effect to the end of signal's DSP chain by moving it.
Signal< SampleType, DerivedSignal > && operator>>(Signal< SampleType, DerivedSignal > &&signal, std::unique_ptr< DerivedDSP > &&dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
Signal< SampleType, DerivedSignal > & operator>>(Signal< SampleType, DerivedSignal > &signal, const DSPBase< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Signal< SampleType, DerivedSignal > & operator>>(Signal< SampleType, DerivedSignal > &signal, std::unique_ptr< DerivedDSP > &&dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
static size_t roundToSizeT(SampleType x)
Rounds a floating point value to a size_t value.
static SampleType floatsNotEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
#define hassert(condition)
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)