Any good strategies for this before I start exploring:
I want to record a stream of audio and at the same time display it and allow random playback of portions of the recording, while it’s still recording.
The recording might be very long.
Any good strategies for this before I start exploring:
I want to record a stream of audio and at the same time display it and allow random playback of portions of the recording, while it’s still recording.
The recording might be very long.
It turns out my stupid first naive implementation actually works. I was expecting problems maybe with the WAV headers or with input and output stream on the same file.
So the idiot implementation just has an AudioFormatWriter and AudioFormatReader on the same WAV file. The writer keeps appending and flushing and the reader, well reads and displays stuff.
Problems I can think of already:
Here’s the code, minus the bits that draw the waveform.
Any thoughts … greatly appreciated
/**
* Write to an audio file.
*/
class InfiniteRecorder
{
public:
InfiniteRecorder(jcf::DataViewController & controller_, const File & destination_)
:
destination(destination_), controller(controller_)
{
destination_.deleteFile();
auto stream = destination_.createOutputStream();
if (stream)
{
WavAudioFormat wav;
writer = wav.createWriterFor(stream, 44100.0, 2, 16, {}, 0);
}
}
void writeBlock(const AudioBuffer<float> & data)
{
if (!writer)
return;
writer->writeFromAudioSampleBuffer(data, 0, data.getNumSamples());
writer->flush();
lengthInSeconds += data.getNumSamples() * sampleRate;
notifyLengthChanged();
}
void notifyLengthChanged() const
{
controller.setMaximumPosition(lengthInSeconds, true);
}
private:
File destination;
ScopedPointer<AudioFormatWriter> writer;
double lengthInSeconds{ 0.0 };
double sampleRate{ 44100.0 };
jcf::DataViewController& controller;
};
class ReaderDataSource : public jcf::DataViewSource
{
public:
ReaderDataSource(const File & source_): source(source_) {}
/** Load a chunk of the audio file and return min/max pairs so we can display it. */
void getValues (std::vector<std::pair<float, float>>& result, double startPositionInSeconds, double stepDelta, int numSteps) override
{
auto inputStream = source.createInputStream();
WavAudioFormat wav;
ScopedPointer<AudioFormatReader> reader = wav.createReaderFor(inputStream, true);
jassert(reader);
if (reader)
{
auto stepDeltaSamples = stepDelta * reader->sampleRate;
auto numSamples = roundToInt(stepDeltaSamples * double(numSteps));
auto readerStartSample = startPositionInSeconds * reader->sampleRate;
audio.setSize(reader->numChannels, numSamples, false, false, true /* avoid reallocation */);
reader->read(&audio, 0, numSamples, readerStartSample, true, true);
int pos{ 0 };
for (int i = 0; i < numSteps; ++i)
{
double min, max;
getMinMaxForPosition(pos, pos + stepDeltaSamples + 1, min, max);
result.push_back({ min, max });
pos += stepDeltaSamples;
}
}
}
void getMinMaxForPosition(int st, int ed, double& min, double& max) const
{
auto chanData = audio.getReadPointer(0);
auto numSamples = audio.getNumSamples();
min = std::numeric_limits<double>::max();
max = std::numeric_limits<double>::lowest();
st = jlimit(0, numSamples - 1, st);
ed = jlimit(0, numSamples - 1, ed);
if (st == ed)
{
min = chanData[st];
max = chanData[st];
}
else
{
for (int i = st; i < ed; i++)
{
if (chanData[i] < min) min = chanData[i];
if (chanData[i] > max) max = chanData[i];
}
}
}
double getMaxY () override { return 1.0; }
double getMinY () override { return -1.0; }
private:
AudioBuffer<float> audio;
File source;
};
class InfiniteRecorderView
:
public Component
{
public:
InfiniteRecorderView(jcf::DataViewController & controller_, const File& sourceFile_)
:
sourceFile(sourceFile_), controller(controller_)
{
addAndMakeVisible(ruler);
addAndMakeVisible(channel);
}
void resized() override
{
auto bounds = getLocalBounds();
ruler.setBounds(bounds.removeFromTop(30));
channel.setBounds(bounds);
}
File sourceFile;
jcf::DataViewController& controller;
ReaderDataSource source{ sourceFile };
jcf::DataViewRulerTime ruler{ controller };
jcf::DataChannelView channel{ controller, 0, &source };
};
class NewProjectAudioProcessorEditor : public AudioProcessorEditor
{
public:
NewProjectAudioProcessorEditor (NewProjectAudioProcessor&);
~NewProjectAudioProcessorEditor();
void paint (Graphics&) override;
void resized() override;
private:
File destination{ File::getSpecialLocation(File::userHomeDirectory).getChildFile("test_writer.wav") };
jcf::DataViewController controller;
InfiniteRecorderView view{ controller, destination };
InfiniteRecorder recorder{ controller, destination };
NewProjectAudioProcessor& processor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NewProjectAudioProcessorEditor)
};
Right, so the main trick is to have an in-memory summary that updates as we go along. And then only resort to opening the file for reading if we want to play a portion back or we zoom in a long way. There’s a small challenge if we are zoomed in a long way and following the latest audio, but I a small circular buffer for the latest audio will make this super efficient.
The only thing left that I really don’t like is how often I’m calling flush. But I can stop doing this unless we enter the very-zoomed-in mode…