Skip to content

Conversation

@steven-jaro
Copy link

@steven-jaro steven-jaro commented May 23, 2025

This is the first stage of the implementation of the ring buffer for real time safety when recording as this is needed to solve issue number 4 in #7786. Issue 4 says that recording is not safe with the current method. Here are the changes made to the code in this PR:

  1. AudioEngine.h:
  • Included "#include "LocklessRingBuffer.h"
  • Added needed ringbuffer variables.
  • Added new function "processBufferedInputFrames".
  1. AudioEngine.cpp:
  • Included:
    #include
    #include
    #include
    #include
    #include "LocklessRingBuffer.h"
  • Created static constant for the buffer size:
    static const f_cnt_t FIXED_INPUT_BUFFER_CAPACITY = DEFAULT_BUFFER_SIZE * 100;
  • In the constructor, we make use of the constant "DEFAULT_BUFFER_SIZE". Changed the name of "wt" to "workerThread". Added the initialization in the constructor for the 3 ringbuffer needed variables.
  • Added code to the destructor to safely delete the worker threads.
  • Changed "pushInputFrames" function to write in the ringbuffer.
  • Created new function "void AudioEngine::processBufferedInputFrames()". This function runs continuously. First checks if there is data in the ringbuffer, if not, it returns. Then reads the data read in the ringbuffer into two halfs so that data can be written continously. Then it writes the data of the two halfs from the ringbuffer to m_inputBufferFrames.
  1. AudioDevie.h:
  • Added function "isProcessing()" to check the value of m_inProcess. This is so that the destructor can check if "m_inProcess" is true so that it closes the processing correctly.

How to test this PR?"
This PR just makes that recording is memmory safe, so one should look for memmory leaks, segfaults or possible reallocatio in the code.

@firiox
Copy link

firiox commented May 23, 2025

No downloable .exe

@regulus79
Copy link
Member

Oh yeah that's because the workflows weren't running. They're going now, so an exe should be available in the next 15-20 minutes.

@steven-jaro steven-jaro changed the title Reltime safe recording with ring buffer stange one Real time safe recording with ring buffer stange one May 23, 2025
@steven-jaro steven-jaro changed the title Real time safe recording with ring buffer stange one Real time safe recording with ring buffer stane one May 23, 2025
@steven-jaro steven-jaro changed the title Real time safe recording with ring buffer stane one Real time safe recording with ring buffer stage one May 23, 2025
@firiox
Copy link

firiox commented May 23, 2025

Ok how I test It dude? . .

@tresf
Copy link
Member

tresf commented May 24, 2025

Ok how I test It dude? . .

https://lmms.io/download/pull-request/7903

@AW1534
Copy link
Member

AW1534 commented May 24, 2025

Works well but i get these occasional crackles and the audio peaks.
image

edit: Audacity doesn't have this issue

@steven-jaro
Copy link
Author

Works well but i get these occasional crackles and the audio peaks. image

edit: Audacity doesn't have this issue

Hey, thanks for the feedback.

@steven-jaro
Copy link
Author

steven-jaro commented May 25, 2025

I changed the approach as it was pretty unstable. Please feel free to test the code and review it. My new approach also is supposed to fix the random segfaults and clipping audio. I also tried to fix all the naming issues with the variables for the camelCase, if some are still remaining, I will fix it in the next days.

delete [] buf;
f_cnt_t newSize = std::max(currentWriteBufSize * 2, requiredSize + DEFAULT_BUFFER_SIZE);

auto newBuffer = new SampleFrame[newSize];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this function is run in realtime context, then this line would cause an allocation, which is not realtime-safe.

m_inputBufferFrames[i] = 0;
m_inputBufferSize[i] = initialBufferSize;

delete[] m_inputBuffer[i];
Copy link
Contributor

@JohannesLorenz JohannesLorenz May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete[], new[] and reserve are - sadly - not realtime-safe either.

(In initialization code it is fine though)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will solve any problem.

@AW1534
Copy link
Member

AW1534 commented May 25, 2025

image
You fixed it, great job! Still seems to run smoothly too, haven't had any crashes or bugs

@steven-jaro
Copy link
Author

image You fixed it, great job! Still seems run smoothly too, haven't had any crashes or bugs

Thank you for the feedback and testing.

@steven-jaro
Copy link
Author

My most recent push has the intent to remove all the allocation I could think of to get more real time safety. I think there is still some points with non real time safety, but at least I would like to get feedback as I consider this a progress and not the final deal.

@regulus79
Copy link
Member

Help me understand this code...

Before this PR, were we using a single buffer m_inputBuffer[m_inputBufferWrite] for recording audio which would just be scaled up 2x every time it got filled up?

And this PR replaces that with a ring buffer.... which is slowly filled up and copied over to the other buffer ?

@steven-jaro
Copy link
Author

Help me understand this code...

Before this PR, were we using a single buffer m_inputBuffer[m_inputBufferWrite] for recording audio which would just be scaled up 2x every time it got filled up?

And this PR replaces that with a ring buffer.... which is slowly filled up and copied over to the other buffer ?

Hello, this is the explaination of the code for this PR.
First I will answer your questions:

Before this PR, were we using a single buffer "m_inputBuffer[m_inputBufferWrite]" for recording audio which would just be scaled up 2x every time it got filled up?
R/ Basically yes. Here is how old recording used to work:
a. The function "AudioEngine::pushInputFrames()" was called directly by the os audio driver's callback.
b. It would directly write incoming audio frames to "m_inputBuffer[m_inputBufferWrite]".
c. If the space "m_inputBuffer[m_inputBufferWrite]" wasn't enough anymore, "pushInputFrames" would: Lock a mutex using "requestChangeInModel()", ALLOCATE a new larger buffer, copy data from the old buffer to the new one and delete the old buffer and the lock the mutex again.

All of that is not real time safe as we were allocating with "new" and deallocating with "delete[]".

"And this PR replaces that with a ring buffer.... which is slowly filled up and copied over to the other buffer ?"
R/ Yes. My most recent approach does this:
a. "AudioEngine::pushInputFrames()" is called by the audio driver.

  • But now it would only write incoming audio to the lockless ring buffer "m_inputAudioRingBuffer".
  • The functions lightweight as it doesn't perform a mutex locking or allocation. If the ring buffer is full, it drops the frames which is better than allocating.

b. I added the function "AudioEngine::processBufferedInputFrames()".

  • It is called from "AudioEngine::renderStageNoteSetup()" which is called continously.
  • It first checks if "m_inputAudioRingBuffer" has new data.
  • Then it copies the ring buffer into "m_inputBuffer[m_inputBufferWrite]".
  • It is safe as it doesn't run in a less critical context than the OS calling it directly. So now it can use "requestChangeInModel()" safely.
  • "m_inputBuffer[0]" and "m_inputBuffer[1]" are now allocanted one time by the constructor to a fixed capacity. So now "processBufferedInputFrames" doesn't do reallocation.
  • "std::vector m_tempInputProcessingBuffer" is used to consolidate the data from the ring buffer. The vector is reserver in the constructor to a capacity that is based on the ring buffer size. In "processBufferedInputFrames" I use "resize()" but to the actual number of frames that are being processed. And this is withing the reserved capacity so we are not doing heal reallocations.

@szeli1 szeli1 self-requested a review May 29, 2025 21:23
@steven-jaro
Copy link
Author

I solved the problems with the resize and reserve. Now it is supposed to be completely real time safe.

@JohannesLorenz
Copy link
Contributor

I solved the problems with the resize and reserve. Now it is supposed to be completely real time safe.

OK, I will look later at it. But please, can you fix the indentation issues first? Thanks!

Copy link
Contributor

@JohannesLorenz JohannesLorenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial code review finished, will need another one after the rework.

What I checked:

  1. Functionality - partially
  2. Style - partially

@steven-jaro
Copy link
Author

All changes requested will be applied. But I just have that doubt with the and statement.

@steven-jaro
Copy link
Author

All the resquests were applied.

Copy link
Contributor

@szeli1 szeli1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not test this PR. I believe I found a size_t casting bug which might cause a crash. It is similar to old AudioFileProcessor code, where size_t values were subtracted from carelessly resulting in out of index reads / writes. I may be incorrect about my finding tho.

m_inputBuffer[i] = new SampleFrame[ DEFAULT_BUFFER_SIZE * 100 ];
zeroSampleFrames(m_inputBuffer[i], m_inputBufferSize[i]);
m_inputBufferSize[i] = FIXED_INPUT_BUFFER_CAPACITY;
m_inputBuffer[i] = new SampleFrame[ FIXED_INPUT_BUFFER_CAPACITY ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m_inputBuffer[i] = new SampleFrame[ FIXED_INPUT_BUFFER_CAPACITY ];
m_inputBuffer[i] = new SampleFrame[FIXED_INPUT_BUFFER_CAPACITY];

Maybe replace this with an std::vector?
Allocation, size is handled by default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or 2 std vectors

Copy link
Contributor

@szeli1 szeli1 May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You closed this without expressing your opinion about using std::vector

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit of using an std::vector when we just need a fixed size simple array? Is the benefit on the exception safety?

Copy link
Contributor

@szeli1 szeli1 Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefit is that std::vector does everything we already do, but it can destruct itself. (And it incorporates the size data, meaning we don't have to store it)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using std::array would be the replacement, not std::vector.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using std::array would be the replacement, not std::vector.

This is correct, I thought m_inputBuffer was still increased in void AudioEngine::pushInputFrames( SampleFrame* _ab, const f_cnt_t _frames )

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the recent refactor.

I've replaced the C-style arrays with std::array<std::vector<SampleFrame>, 2>.

The vectors are pre-allocated with .reserve(FIXED_INPUT_BUFFER_CAPACITY) and grow
dynamically with .resize() as frames are added (up to the reserved capacity), so:

  • No manual memory management (new/delete)
  • Size is tracked automatically (.size() instead of m_inputBufferFrames)
  • Automatic cleanup (RAII)
  • Still real-time safe (no allocations during recording since we stay within reserved capacity)

The reason I used std::vector instead of std::array is because m_inputBuffer needs
to grow dynamically as frames are added in processBufferedInputFrames(), then gets
cleared in swapBuffers(). A fixed-size std::array wouldn't work for this use case.

@Veratil - Good point about std::array being typical for fixed-size, but in this case
the variable size behavior is needed (though bounded by the reserved capacity).

@steven-jaro steven-jaro requested a review from szeli1 November 23, 2025 00:33
Copy link
Contributor

@szeli1 szeli1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not test this PR. What steps to follow to test this PR?


if (framesInSequence == 0) return;

requestChangeInModel();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really real time safe when it uses mutexes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is real-time safe. The mutex in requestChangeInModel() is NOT in the real-time
audio callback path. Here's why:

Normal playback mode:

  • Uses fifoWriter (needsFifo = true by default in startProcessing())
  • Audio callback only calls nextBuffer()m_fifo->read() (lockless, no mutex)
  • The fifoWriter thread (separate, non-RT thread) calls renderNextBuffer() which includes
    the mutex, but this is outside the RT path

Export/rendering mode:

  • Doesn't use fifoWriter (needsFifo = false in ProjectRenderer.cpp:171)
  • But there's no real-time audio callback in this mode, just writing to a file
  • No RT constraints, so mutex is perfectly fine

The architecture ensures that pushInputFrames() (called from audio device callback)
only writes to the lockless ring buffer, while processBufferedInputFrames() (which has
the mutex) runs either in the fifoWriter thread or in rendering mode where RT-safety
isn't required.

delete m_midiClient; m_midiClient = nullptr;
delete m_audioDev; m_audioDev = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe do = nullptr on new lines?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it will be fixed.


std::unique_ptr<LocklessRingBuffer<SampleFrame>> m_inputAudioRingBuffer;
std::unique_ptr<LocklessRingBufferReader<SampleFrame>> m_inputAudioRingBufferReader;
std::vector<SampleFrame> m_tempInputProcessingBuffer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could you leave a comment why a temporary vector is needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. After reviewing the code, I realized this temporary vector is not actually
being used. The current implementation copies directly from the ring buffer sequence
(handling the two potential halves with first_half_ptr() and second_half_ptr()) to the
destination buffer without needing an intermediate vector.

I've removed it to keep the code clean.

Comment on lines 351 to 352
SampleFrame* currentWriteBufPtr = m_inputBuffer[m_inputBufferWrite];
f_cnt_t currentWriteBufCapacity = m_inputBufferSize[m_inputBufferWrite];
f_cnt_t currentFramesInWriteBuf = m_inputBufferFrames[m_inputBufferWrite];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use vectors instead of C arrays. That way the size is also known. I reviewed the code and to me, it seems like m_inputBuffer is used in about 3 functions, it shouldn't be hard to replace it with std::vector. (and also it would get rid of m_inputBufferFrames)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a reasonable request to ask you to refactor m_inputBuffer. Please would you mind doing that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've refactored m_inputBuffer to use std::vector as you suggested.

Changes made:

Header (AudioEngine.h):

  • Replaced SampleFrame* m_inputBuffer[2], f_cnt_t m_inputBufferFrames[2], and
    f_cnt_t m_inputBufferSize[2] with std::array<std::vector<SampleFrame>, 2> m_inputBuffer
  • Updated inputBuffer() to return m_inputBuffer[m_inputBufferRead].data()
  • Updated inputBufferFrames() to return m_inputBuffer[m_inputBufferRead].size()

Implementation (AudioEngine.cpp):

  • Constructor: Changed from new SampleFrame[FIXED_INPUT_BUFFER_CAPACITY] to
    m_inputBuffer[i].reserve(FIXED_INPUT_BUFFER_CAPACITY)
  • Destructor: Removed manual delete[] - vectors clean up automatically
  • processBufferedInputFrames():
    • Use vector references instead of raw pointers
    • Call .resize() before memcpy() to accommodate new frames
    • Removed manual size tracking (now uses .size(), .capacity())
  • swapBuffers(): Changed from m_inputBufferFrames[m_inputBufferWrite] = 0 to
    m_inputBuffer[m_inputBufferWrite].clear()

Real-time safety maintained:

  • Vectors are pre-allocated with .reserve(FIXED_INPUT_BUFFER_CAPACITY) in constructor
  • .resize() only increases size up to the reserved capacity, so no heap allocations
    occur during recording (capacity never exceeded)
  • All operations remain real-time safe

Testing:

Compiled successfully and tested recording for several minutes without issues.

Thanks for the suggestion. The code is cleaner and more maintainable now.

steven-jaro and others added 20 commits November 23, 2025 09:46
@steven-jaro steven-jaro force-pushed the recording-with-ringbuffer branch from e492d48 to 52868fd Compare November 23, 2025 15:48
@steven-jaro steven-jaro requested review from Veratil and szeli1 December 4, 2025 14:22
@messmerd
Copy link
Member

messmerd commented Dec 7, 2025

@steven-jaro Please refrain from rebasing feature branches after you open a PR and people have started reviewing it. It rewrites the git history and makes it harder for reviewers to see what recent changes you made. If you need to fix merge conflicts, you can merge master (or in this case feature/recording-stage-one) into your feature branch instead.

@steven-jaro
Copy link
Author

@steven-jaro Please refrain from rebasing feature branches after you open a PR and people have started reviewing it. It rewrites the git history and makes it harder for reviewers to see what recent changes you made. If you need to fix merge conflicts, you can merge master (or in this case feature/recording-stage-one) into your feature branch instead.

I am really sorry, I wasn't aware of this. Won't happen again.

@Veratil
Copy link
Contributor

Veratil commented Dec 8, 2025

You can rebase at the end before merging though.

@steven-jaro steven-jaro marked this pull request as ready for review December 9, 2025 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants