HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_wavfile.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <memory>
4#include <string>
5
6#include "dependencies/choc/platform/choc_DisableAllWarnings.h"
7#include "dependencies/dr_libs/dr_wav.h"
8#include "dependencies/choc/platform/choc_ReenableAllWarnings.h"
9
11#include "signals/hart_signal.hpp"
12#include "hart_utils.hpp" // toAbsolutePath(), floatsNotEqual(), fileExistsAndReadable(), Loop
13
14namespace hart
15{
16
17// TODO: Add "normalize" option?
18// TODO: Add an entity that reuses wav data if a WavFile for a previously opened file gets instantiated
19
20/// @brief Produces audio from a wav file
21/// @details Original levels from the wav file are preserved. If sample rate requested
22/// by the test runner is different from original waf file's SR, it will quietly re-sample,
23/// so that any sample rate is supported.
24/// @ingroup Signals
25template<typename SampleType>
26class WavFile:
27 public Signal<SampleType, WavFile<SampleType>>
28{
29public:
30 /// @brief Instructs WavFile whether to resample audio, if requested Sample Rate doesn't match original SR of the wav file
31 enum class Resample
32 {
33 no, ///< If sample of test runner doesn't match the wav file's SR, it will refuse to render audio
34 yes ///< If sample of test runner doesn't match the wav file's SR, it will quietly re-sample
35 };
36
37 /// @brief Creates a Signal that produces audio from a wav file
38 /// @param filePath Path to a wav file
39 /// Can be absolute or relative. If a relative path is used, it will resolve
40 /// as relative to a data root path provided via respective CLI argument.
41 /// @see HART_REQUIRES_DATA_PATH_ARG
42 /// @param loop Indicates whether the signal should loop the audio or produce
43 /// @param resample Indicates whether the audio should be re-sampled if test
44 /// runner's Sample Rate doesn't match the wav's SR, see `Resample`.
45 /// silence after wav file runs out of frames.
46 WavFile (const std::string& filePath, Loop loop = Loop::no, Resample resample = Resample::yes):
47 m_filePath (filePath),
48 m_loop (loop),
49 m_resample (resample)
50 {
51 const std::string fileAbsolutePath = toAbsolutePath (filePath);
52
53 if (! fileExistsAndReadable (fileAbsolutePath))
54 HART_THROW_OR_RETURN_VOID (hart::IOError, "Wav file does not exist, or not readable");
55
56 drwav_uint64 numFrames;
57 unsigned int numChannels;
58 unsigned int wavSampleRateHz;
59
60 float* pcmFrames = drwav_open_file_and_read_pcm_frames_f32 (
61 fileAbsolutePath.c_str(),
62 &numChannels,
63 &wavSampleRateHz,
64 &numFrames,
65 nullptr
66 );
67
68 if (pcmFrames == nullptr)
69 HART_THROW_OR_RETURN_VOID (hart::IOError, std::string ("Could not read frames from the wav file"));
70
71 m_wavFramesOriginal = std::make_shared<AudioBuffer<SampleType>> (
72 static_cast<size_t> (numChannels),
73 static_cast<size_t> (numFrames),
74 static_cast<double> (wavSampleRateHz)
75 );
76
77 for (size_t frame = 0; frame < numFrames; ++frame)
78 for (size_t channel = 0; channel < numChannels; ++channel)
79 (*m_wavFramesOriginal)[channel][frame] = static_cast<SampleType> (pcmFrames[frame * numChannels + channel]);
80
81 drwav_free (pcmFrames, nullptr);
82 }
83
84 /// @copydoc Signal::supportsNumChannels()
85 /// @note WavFile can only fill as much channels as there are in the wav file, or less.
86 /// For instance, if the wav file is stereo, it can generate two channels (as they are),
87 /// one channel (left, discarding right), but not three channels.
88 bool supportsNumChannels (size_t numChannels) const override
89 {
90 if (m_wavFramesOriginal == nullptr)
91 {
93 return false;
94 }
95
96 return numChannels <= m_wavFramesOriginal->getNumChannels();
97 };
98
99 bool supportsSampleRate (double sampleRateHz) const override
100 {
101 if (m_resample == Resample::yes)
102 return sampleRateHz > 0.0;
103
104 // If Resample::no was selected instead:
105
106 if (m_wavFramesOriginal == nullptr)
107 {
109 return false;
110 }
111
112 return floatsEqual (sampleRateHz, m_wavFramesOriginal->getSampleRateHz(), m_sampleRateToleranceHz);
113 }
114
115 void prepare (double sampleRateHz, size_t numOutputChannels, size_t /*maxBlockSizeFrames*/) override
116 {
117 if (m_wavFramesOriginal == nullptr)
118 {
120 return;
121 }
122
123 // There are a few obvious cases where channel number mismatch can be gracefully resolved - perhaps in the future
124 if (numOutputChannels != m_wavFramesOriginal->getNumChannels())
125 HART_THROW_OR_RETURN_VOID (hart::ChannelLayoutError, std::string ("Unexpected channel number"));
126
127 hassert (m_wavFramesOriginal->hasSampleRate()); // Sample rate should be assigned to the buffer in the ctor
128 const bool needsResampling = floatsNotEqual (sampleRateHz, m_wavFramesOriginal->getSampleRateHz(), m_sampleRateToleranceHz);
129
130 if (needsResampling)
131 {
132 hassert (m_resample == Resample::yes); // Should have rejected mismatched SR if Resample::no was selected
133 hassert (sampleRateHz > 0.0); // Test runner should catch invalid sample rate values
134
135 if (m_wavFramesResampled == nullptr || floatsNotEqual (m_wavFramesResampled->getSampleRateHz(), sampleRateHz, m_sampleRateToleranceHz))
136 m_wavFramesResampled = std::make_shared<AudioBuffer<SampleType>> (m_wavFramesOriginal->resample (sampleRateHz));
137
138 m_wavFramesSource = m_wavFramesResampled;
139 }
140 else
141 {
142 m_wavFramesSource = m_wavFramesOriginal;
143 }
144
145 hassert (m_wavFramesSource != nullptr);
146 hassert (floatsEqual (m_wavFramesSource->getSampleRateHz(), sampleRateHz, m_sampleRateToleranceHz));
147 }
148
149 void renderNextBlock (AudioBuffer<SampleType>& output) override
150 {
151 // TODO: Add support for number of channels different from the wav file
152
153 if (m_wavFramesSource == nullptr)
154 {
155 // Source should have been assigned during the prepare() call
157 return;
158 }
159
160 hassert (output.getNumChannels() == m_wavFramesSource->getNumChannels());
161 hassert (output.hasSampleRate());
162 hassert (m_wavFramesSource->hasSampleRate());
163 hassert (floatsEqual (output.getSampleRateHz(), m_wavFramesSource->getSampleRateHz(), m_sampleRateToleranceHz));
164
165 const size_t numFrames = output.getNumFrames();
166 const size_t numChannels = output.getNumChannels();
167 size_t frameInOutputBuffer = 0;
168 size_t frameInWavBuffer = m_wavOffsetFrames;
169
170 while (m_wavOffsetFrames < m_wavFramesSource->getNumFrames() && frameInOutputBuffer < numFrames)
171 {
172 for (size_t channel = 0; channel < m_wavFramesSource->getNumChannels(); ++channel)
173 output[channel][frameInOutputBuffer] = (*m_wavFramesSource)[channel][frameInWavBuffer];
174
175 ++frameInOutputBuffer;
176 ++frameInWavBuffer;
177 ++m_wavOffsetFrames;
178
179 if (m_loop == Loop::yes)
180 m_wavOffsetFrames %= m_wavFramesSource->getNumFrames();
181 }
182
183 while (frameInOutputBuffer < numFrames)
184 {
185 hassert (m_loop == Loop::no);
186
187 for (size_t channel = 0; channel < m_wavFramesSource->getNumChannels(); ++channel)
188 output[channel][frameInOutputBuffer] = (SampleType) 0;
189
190 ++frameInOutputBuffer;
191 }
192 }
193
194 void reset() override
195 {
196 m_wavOffsetFrames = 0;
197 }
198
199 void represent (std::ostream& stream) const override
200 {
201 stream << "WavFile (\"" << m_filePath << (m_loop == Loop::yes ? "\", Loop::yes)" : "\", Loop::no)");
202 }
203
204private:
205 static constexpr double m_sampleRateToleranceHz = 1e-3;
206 const std::string m_filePath;
207 const Loop m_loop;
208 const Resample m_resample;
209 size_t m_wavOffsetFrames = 0;
210 std::shared_ptr<AudioBuffer<SampleType>> m_wavFramesOriginal;
211 std::shared_ptr<AudioBuffer<SampleType>> m_wavFramesResampled;
212 std::shared_ptr<AudioBuffer<SampleType>> m_wavFramesSource;
213};
214
216
217} // namespace hart
Thrown when a numbers of channels is mismatched.
Thrown when some I/O operation fails.
Base class for signals.
Produces audio from a wav file.
void renderNextBlock(AudioBuffer< SampleType > &output) override
Renders next block audio for the signal.
bool supportsSampleRate(double sampleRateHz) const override
Tells whether this Signal supports given sample rate.
Resample
Instructs WavFile whether to resample audio, if requested Sample Rate doesn't match original SR of th...
@ no
If sample of test runner doesn't match the wav file's SR, it will refuse to render audio.
@ yes
If sample of test runner doesn't match the wav file's SR, it will quietly re-sample.
void prepare(double sampleRateHz, size_t numOutputChannels, size_t) override
Prepare the signal for rendering.
void represent(std::ostream &stream) const override
Makes a text representation of this Signal for test failure outputs.
WavFile(const std::string &filePath, Loop loop=Loop::no, Resample resample=Resample::yes)
Creates a Signal that produces audio from a wav file.
bool supportsNumChannels(size_t numChannels) const override
Tells the host whether this Signal is capable of generating audio for a certain amount of channels.
void reset() override
Resets the Signal to initial state.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns otherwise.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
#define hassertfalse
Triggers a HartAssertException
static bool fileExistsAndReadable(const std::string &path)
static std::string toAbsolutePath(const std::string &path)
Converts path to absolute, if it's relative.
Loop
Helper values for something that could loop, like a Signal.
#define HART_SIGNAL_DECLARE_ALIASES_FOR(ClassName)