Recording and playing back the same file at the same time


#1

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.


#2

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:

  • Performance
    • Having to call flush after every write. However I want to display the data to the user immediately - and the only way around that I can think of is to have a RAM buffer which is also displayed to the user pre-write. Maybe I’m assuming the file system code is slow whereas actually it’s massively efficient though and does all my caching for me!
    • Constantly opening the file and rewriting or rereading the header.
    • Handling the case where the user is zoomed out - I need some low resolution overview cache I guess
  • Thread safety
    • Presumably the OS handles the case where two different threads are reading and writing to the same file.
  • Blocking the audio thread on writing
    • I’ll put a queue in.
  • Blocking the UI thread on reading
    • I’ll need to think about this. Up till now the DataView classes used for the waveform drawing and scrolling have always been referring to data that’s in memory.

Here’s the code, minus the bits that draw the waveform.

Any thoughts … greatly appreciated :slight_smile:

 /**
 * 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)
};

#4

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…

image