Add core functionality for audio buffer producers and consumers

pull/170/head
James Ball 2023-07-08 13:25:35 +01:00
rodzic 93c58d8a48
commit 8e61d6f280
8 zmienionych plików z 209 dodań i 5 usunięć

Wyświetl plik

@ -87,7 +87,6 @@ ObjComponent::ObjComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessor
Util::changeSvgColour(doc.get(), "white");
fixedRotateWhite = juce::Drawable::createFromSVG(*doc);
Util::changeSvgColour(doc.get(), "red");
DBG(doc->toString());
fixedRotateRed = juce::Drawable::createFromSVG(*doc);
// TODO: any way of removing this duplication?

Wyświetl plik

@ -42,6 +42,9 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr
path.addTriangle(0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.0f);
collapseButton.setShape(path, false, true, true);
audioProcessor.audioProducer.registerConsumer(audioConsumer);
dummyConsumer.startThread();
juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock);
for (int i = 0; i < audioProcessor.numFiles(); i++) {
addCodeEditor(i);
@ -52,7 +55,10 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr
setResizable(true, true);
}
OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() {}
OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() {
audioProcessor.audioProducer.unregisterConsumer(audioConsumer);
dummyConsumer.stopThread(1000);
}
//==============================================================================
void OscirenderAudioProcessorEditor::paint (juce::Graphics& g)

Wyświetl plik

@ -44,6 +44,9 @@ private:
juce::XmlTokeniser xmlTokeniser;
juce::ShapeButton collapseButton;
std::shared_ptr<BufferConsumer> audioConsumer = std::make_shared<BufferConsumer>(50000);
DummyConsumer dummyConsumer{audioConsumer};
void codeDocumentTextInserted(const juce::String& newText, int insertIndex) override;
void codeDocumentTextDeleted(int startIndex, int endIndex) override;
void updateCodeDocument();

Wyświetl plik

@ -118,8 +118,7 @@ void OscirenderAudioProcessor::changeProgramName (int index, const juce::String&
}
//==============================================================================
void OscirenderAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
void OscirenderAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) {
currentSampleRate = sampleRate;
updateAngleDelta();
}
@ -449,7 +448,6 @@ void OscirenderAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, j
x = std::max(-1.0, std::min(1.0, x));
y = std::max(-1.0, std::min(1.0, y));
if (totalNumOutputChannels >= 2) {
channelData[0][sample] = x;
channelData[1][sample] = y;
@ -457,6 +455,8 @@ void OscirenderAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, j
channelData[0][sample] = x;
}
audioProducer.write(x, y);
actualTraceMax = std::max(actualTraceMin + MIN_TRACE, std::min(traceMax->getValue(), 1.0));
actualTraceMin = std::max(MIN_TRACE, std::min(traceMin->getValue(), actualTraceMax - MIN_TRACE));

Wyświetl plik

@ -15,6 +15,7 @@
#include "parser/FrameConsumer.h"
#include "audio/Effect.h"
#include <numbers>
#include "concurrency/BufferProducer.h"
//==============================================================================
/**
@ -167,6 +168,8 @@ public:
std::unique_ptr<FrameProducer> producer;
BufferProducer audioProducer;
void addLuaSlider();
void updateAngleDelta();
void addFrame(std::vector<std::unique_ptr<Shape>> frame, int fileIndex) override;

Wyświetl plik

@ -0,0 +1,124 @@
#pragma once
#include <JuceHeader.h>
// This is a helper class for the producer and consumer threads.
//
// ORDER OF OPERATIONS:
// 1. Consumer is created.
// 2. The thread that owns the consumers calls registerConsumer() on the producer, which acquires the lock on the first buffer to be written to by calling getBuffer().
// LOOP:
// 3. The consumer calls startProcessing() to signal that they want to start processing the current buffer.
// 4. The producer calls finishedWriting() to signal that they have finished writing to the current buffer, which gives the lock to the consumer.
// 5. The consumer calls finishedProcessing() to signal that they have finished processing the current buffer.
// 6. The producer calls getBuffer() to acquire the lock on the next buffer to be written to.
// GOTO LOOP
// 7. The thread that owns the consumer calls unregisterConsumer() on the producer at some point during the loop, which releases the lock on the current buffer.
//
class BufferConsumer {
public:
// bufferSize MUST be a multiple of 2.
BufferConsumer(int bufferSize) {
firstBuffer->resize(bufferSize, 0.0);
secondBuffer->resize(bufferSize, 0.0);
}
~BufferConsumer() {}
// Returns the buffer that is ready to be written to.
// This is only called by the producer thread.
// force forces the lock to be acquired.
// Returns nullptr if the lock can't be acquired when force is false.
// It is only called when the global producer lock is held.
std::shared_ptr<std::vector<float>> getBuffer(bool force) {
auto buffer = firstBufferWriting ? firstBuffer : secondBuffer;
if (lockHeldForWriting) {
return buffer;
}
auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock;
if (force) {
bufferLock->enter();
lockHeldForWriting = true;
return buffer;
} else if (bufferLock->tryEnter()) {
lockHeldForWriting = true;
return buffer;
} else {
return nullptr;
}
}
// This is only called by the producer thread. It is only called when the global
// producer lock is held.
void finishedWriting() {
auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock;
lockHeldForWriting = false;
firstBufferWriting = !firstBufferWriting;
// Try locking before we unlock the current buffer so that
// the consumer doesn't start processing before we
// unlock the buffer. Ignore if we can't get the lock
// because the consumer is still processing.
getBuffer(false);
bufferLock->exit();
}
void releaseLock() {
if (lockHeldForWriting) {
auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock;
bufferLock->exit();
}
}
// Returns the buffer that has been written to fully and is ready to be processed.
// This will lock the buffer so that the producer can't write to it while we're processing.
std::shared_ptr<std::vector<float>> startProcessing() {
auto buffer = firstBufferProcessing ? firstBuffer : secondBuffer;
auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock;
bufferLock->enter();
return buffer;
}
// This should be called after processing has finished.
// It releases the lock on the buffer so that the producer can write to it again.
void finishedProcessing() {
auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock;
firstBufferProcessing = !firstBufferProcessing;
bufferLock->exit();
}
std::shared_ptr<std::vector<float>> firstBuffer = std::make_shared<std::vector<float>>();
std::shared_ptr<std::vector<float>> secondBuffer = std::make_shared<std::vector<float>>();
std::shared_ptr<juce::SpinLock> firstBufferLock = std::make_shared<juce::SpinLock>();
std::shared_ptr<juce::SpinLock> secondBufferLock = std::make_shared<juce::SpinLock>();
private:
// Indirectly used by the producer to signal whether it holds the lock on the buffer.
// This is accurate if the global producer lock is held as the buffer lock is acquired
// and this is set to true before the global producer lock is released.
bool lockHeldForWriting = false;
bool firstBufferWriting = true;
bool firstBufferProcessing = true;
};
class DummyConsumer : public juce::Thread {
public:
DummyConsumer(std::shared_ptr<BufferConsumer> consumer) : juce::Thread("DummyConsumer"), consumer(consumer) {}
~DummyConsumer() {}
void run() override {
while (!threadShouldExit()) {
auto buffer = consumer->startProcessing();
float total = 0.0;
for (int i = 0; i < buffer->size(); i++) {
total += (*buffer)[i];
}
DBG(total);
consumer->finishedProcessing();
}
}
private:
std::shared_ptr<BufferConsumer> consumer;
};

Wyświetl plik

@ -0,0 +1,63 @@
#pragma once
#include <JuceHeader.h>
#include "BufferConsumer.h"
class BufferProducer {
public:
BufferProducer() {}
~BufferProducer() {}
// This should add the buffers and locks to the vectors
// and then lock the first buffer lock so it can start
// being written to.
// This is only called by the thread that owns the consumer thread.
void registerConsumer(std::shared_ptr<BufferConsumer> consumer) {
juce::SpinLock::ScopedLockType scope(lock);
consumers.push_back(consumer);
bufferPositions.push_back(0);
consumer->getBuffer(true);
}
// This is only called by the thread that owns the consumer thread.
// This can't happen at the same time as write() it locks the producer lock.
void unregisterConsumer(std::shared_ptr<BufferConsumer> consumer) {
juce::SpinLock::ScopedLockType scope(lock);
for (int i = 0; i < consumers.size(); i++) {
if (consumers[i] == consumer) {
consumer->releaseLock();
consumers.erase(consumers.begin() + i);
bufferPositions.erase(bufferPositions.begin() + i);
break;
}
}
}
// Writes a sample to the current buffer for all consumers.
void write(float left, float right) {
juce::SpinLock::ScopedLockType scope(lock);
for (int i = 0; i < consumers.size(); i++) {
std::shared_ptr<std::vector<float>> buffer = consumers[i]->getBuffer(false);
if (buffer == nullptr) {
continue;
}
(*buffer)[bufferPositions[i]] = left;
(*buffer)[bufferPositions[i] + 1] = right;
bufferPositions[i] += 2;
// If we've reached the end of the buffer, switch
// to the other buffer and unlock it. This signals
// to the consumer that it can start processing!
if (bufferPositions[i] >= buffer->size()) {
bufferPositions[i] = 0;
consumers[i]->finishedWriting();
}
}
}
private:
juce::SpinLock lock;
std::vector<std::shared_ptr<BufferConsumer>> consumers;
std::vector<int> bufferPositions;
};

Wyświetl plik

@ -24,6 +24,12 @@
</GROUP>
</GROUP>
<GROUP id="{75439074-E50C-362F-1EDF-8B4BE9011259}" name="Source">
<GROUP id="{9F5970A9-8094-E7F3-7AC1-812AE5589B9F}" name="concurrency">
<FILE id="WQ2W15" name="BufferConsumer.h" compile="0" resource="0"
file="Source/concurrency/BufferConsumer.h"/>
<FILE id="yWTiQQ" name="BufferProducer.h" compile="0" resource="0"
file="Source/concurrency/BufferProducer.h"/>
</GROUP>
<FILE id="JceyXh" name="Util.h" compile="0" resource="0" file="Source/Util.h"/>
<FILE id="RHHuXP" name="ObjComponent.cpp" compile="1" resource="0"
file="Source/ObjComponent.cpp"/>