WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 3ee10c7

Browse files
authored
feat: add session recorder
1 parent d137386 commit 3ee10c7

File tree

7 files changed

+391
-15
lines changed

7 files changed

+391
-15
lines changed

clients/oooooooo/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ set(SRC
115115
src/DisplayRing.cpp
116116
src/DrawFunctions.cpp
117117
src/BufDiskWorker.cpp
118+
src/SessionRecorder.cpp
118119
src/Window.cpp
119120
)
120121

clients/oooooooo/src/Display.cpp

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -534,19 +534,21 @@ void Display::renderLoop() {
534534
SDL_DestroyTexture(cpuTextTexture);
535535
SDL_FreeSurface(cpuTextSurface);
536536

537-
// show the help text in the bottom left corner
538-
std::string helpText = "Tab: toggle menu, h: toggle help";
539-
int helpTextWidth, helpTextHeight;
540-
TTF_SizeText(font, helpText.c_str(), &helpTextWidth, &helpTextHeight);
541-
SDL_Surface* helpTextSurface = TTF_RenderText_Solid(
542-
font, helpText.c_str(), {40, 40, 40, 255}); // Dark gray text
543-
SDL_Texture* helpTextTexture =
544-
SDL_CreateTextureFromSurface(renderer_, helpTextSurface);
545-
SDL_Rect helpTextRect = {10, height_ - helpTextHeight - 10, helpTextWidth,
546-
helpTextHeight};
547-
SDL_RenderCopy(renderer_, helpTextTexture, nullptr, &helpTextRect);
548-
SDL_DestroyTexture(helpTextTexture);
549-
SDL_FreeSurface(helpTextSurface);
537+
// show recording indicator if session recording is active
538+
if (softCutClient_ && softCutClient_->isSessionRecording()) {
539+
std::string recText = "REC";
540+
int recTextWidth, recTextHeight;
541+
TTF_SizeText(font, recText.c_str(), &recTextWidth, &recTextHeight);
542+
SDL_Surface* recTextSurface = TTF_RenderText_Solid(
543+
font, recText.c_str(), {255, 0, 0, 255}); // Red text
544+
SDL_Texture* recTextTexture =
545+
SDL_CreateTextureFromSurface(renderer_, recTextSurface);
546+
SDL_Rect recTextRect = {width_ - cpuTextWidth - recTextWidth - 20, 10,
547+
recTextWidth, recTextHeight};
548+
SDL_RenderCopy(renderer_, recTextTexture, nullptr, &recTextRect);
549+
SDL_DestroyTexture(recTextTexture);
550+
SDL_FreeSurface(recTextSurface);
551+
}
550552

551553
// Update screen
552554
SDL_RenderPresent(renderer_);

clients/oooooooo/src/KeyboardHandler.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,16 @@ void KeyboardHandler::handleKeyDown(SDL_Keycode key, bool isRepeat,
244244
params_[i].ToggleView();
245245
}
246246
break;
247+
case SDLK_y:
248+
if (!isRepeat) {
249+
softcut_->toggleSessionRecording();
250+
if (softcut_->isSessionRecording()) {
251+
display_->SetMessage("Session recording started", 2);
252+
} else {
253+
display_->SetMessage("Session recording stopped", 2);
254+
}
255+
}
256+
break;
247257
case SDLK_UP:
248258
params_[0].SelectedDelta(isRepeat ? -2 : -1);
249259
break;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
#include "SessionRecorder.h"
2+
3+
#include <chrono>
4+
#include <ctime>
5+
#include <filesystem>
6+
#include <iomanip>
7+
#include <iostream>
8+
#include <sstream>
9+
10+
SessionRecorder::SessionRecorder() : recording_(false), writerRunning_(false) {}
11+
12+
SessionRecorder::~SessionRecorder() {
13+
if (recording_.load()) {
14+
stopRecording();
15+
}
16+
}
17+
18+
std::string SessionRecorder::generateTimestamp() {
19+
auto now = std::chrono::system_clock::now();
20+
auto time_t_now = std::chrono::system_clock::to_time_t(now);
21+
std::tm* tm_now = std::localtime(&time_t_now);
22+
23+
std::ostringstream oss;
24+
oss << std::put_time(tm_now, "%Y%m%d-%H%M");
25+
return oss.str();
26+
}
27+
28+
std::string SessionRecorder::generateFilename(const std::string& voiceId) {
29+
std::string folder = "oooooooo";
30+
if (!std::filesystem::exists(folder)) {
31+
std::filesystem::create_directory(folder);
32+
}
33+
return folder + "/oooooooo_" + sessionTimestamp_ + "_loop_" + voiceId +
34+
".wav";
35+
}
36+
37+
void SessionRecorder::initializeBuffer(VoiceBuffer& buffer, int channels) {
38+
buffer.buffer.resize(RING_BUFFER_SIZE * channels);
39+
buffer.writePos.store(0);
40+
buffer.readPos.store(0);
41+
buffer.hasAudio.store(false);
42+
}
43+
44+
void SessionRecorder::startRecording(int numVoices, float sampleRate) {
45+
if (recording_.load()) {
46+
return; // Already recording
47+
}
48+
49+
numVoices_ = numVoices;
50+
sampleRate_ = sampleRate;
51+
sessionTimestamp_ = generateTimestamp();
52+
53+
std::cout << "Starting session recording: " << sessionTimestamp_ << std::endl;
54+
55+
// Initialize main mix buffer (stereo)
56+
initializeBuffer(mainMixBuffer_, 2);
57+
std::string mainFilename = generateFilename("all");
58+
mainMixBuffer_.file = std::make_unique<SndfileHandle>(
59+
mainFilename, SFM_WRITE, SF_FORMAT_WAV | SF_FORMAT_PCM_16, 2,
60+
static_cast<int>(sampleRate_));
61+
62+
if (mainMixBuffer_.file->error()) {
63+
std::cerr << "Error creating main mix file: "
64+
<< mainMixBuffer_.file->strError() << std::endl;
65+
return;
66+
}
67+
68+
// Initialize voice buffers (stereo)
69+
voiceBuffers_.resize(numVoices_);
70+
for (int i = 0; i < numVoices_; i++) {
71+
initializeBuffer(voiceBuffers_[i], 2);
72+
std::string voiceFilename = generateFilename(std::to_string(i));
73+
voiceBuffers_[i].file = std::make_unique<SndfileHandle>(
74+
voiceFilename, SFM_WRITE, SF_FORMAT_WAV | SF_FORMAT_PCM_16, 2,
75+
static_cast<int>(sampleRate_));
76+
77+
if (voiceBuffers_[i].file->error()) {
78+
std::cerr << "Error creating voice " << i
79+
<< " file: " << voiceBuffers_[i].file->strError() << std::endl;
80+
}
81+
}
82+
83+
recording_.store(true);
84+
writerRunning_.store(true);
85+
86+
// Start writer thread
87+
writerThread_ =
88+
std::make_unique<std::thread>(&SessionRecorder::writerThreadFunc, this);
89+
90+
std::cout << "Session recording started" << std::endl;
91+
}
92+
93+
void SessionRecorder::stopRecording() {
94+
if (!recording_.load()) {
95+
return;
96+
}
97+
98+
std::cout << "Stopping session recording..." << std::endl;
99+
100+
recording_.store(false);
101+
writerRunning_.store(false);
102+
103+
// Wait for writer thread to finish
104+
if (writerThread_ && writerThread_->joinable()) {
105+
writerThread_->join();
106+
}
107+
108+
// Flush remaining data and close files
109+
writeAvailableData(mainMixBuffer_);
110+
mainMixBuffer_.file.reset();
111+
112+
for (auto& voiceBuffer : voiceBuffers_) {
113+
if (voiceBuffer.hasAudio.load()) {
114+
writeAvailableData(voiceBuffer);
115+
}
116+
voiceBuffer.file.reset();
117+
}
118+
119+
// Delete files for voices that had no audio
120+
for (int i = 0; i < numVoices_; i++) {
121+
if (!voiceBuffers_[i].hasAudio.load()) {
122+
std::string filename = generateFilename(std::to_string(i));
123+
std::filesystem::remove(filename);
124+
}
125+
}
126+
127+
voiceBuffers_.clear();
128+
129+
std::cout << "Session recording stopped" << std::endl;
130+
}
131+
132+
void SessionRecorder::captureMainMix(const sample_t* left, const sample_t* right,
133+
size_t numFrames) {
134+
if (!recording_.load()) {
135+
return;
136+
}
137+
138+
size_t writePos = mainMixBuffer_.writePos.load(std::memory_order_relaxed);
139+
140+
for (size_t i = 0; i < numFrames; i++) {
141+
size_t idx = (writePos * 2) % mainMixBuffer_.buffer.size();
142+
mainMixBuffer_.buffer[idx] = static_cast<float>(left[i]);
143+
mainMixBuffer_.buffer[idx + 1] = static_cast<float>(right[i]);
144+
writePos++;
145+
}
146+
147+
mainMixBuffer_.writePos.store(writePos, std::memory_order_release);
148+
mainMixBuffer_.hasAudio.store(true);
149+
}
150+
151+
void SessionRecorder::captureVoice(int voice, const sample_t* left,
152+
const sample_t* right, size_t numFrames) {
153+
if (!recording_.load() || voice < 0 || voice >= numVoices_) {
154+
return;
155+
}
156+
157+
auto& voiceBuffer = voiceBuffers_[voice];
158+
size_t writePos = voiceBuffer.writePos.load(std::memory_order_relaxed);
159+
160+
// Check if there's actual audio (not just silence)
161+
bool hasSignal = false;
162+
for (size_t i = 0; i < numFrames; i++) {
163+
float leftSample = static_cast<float>(left[i]);
164+
float rightSample = static_cast<float>(right[i]);
165+
if (std::abs(leftSample) > 0.0001f || std::abs(rightSample) > 0.0001f) {
166+
hasSignal = true;
167+
}
168+
size_t idx = (writePos * 2) % voiceBuffer.buffer.size();
169+
voiceBuffer.buffer[idx] = leftSample;
170+
voiceBuffer.buffer[idx + 1] = rightSample;
171+
writePos++;
172+
}
173+
174+
voiceBuffer.writePos.store(writePos, std::memory_order_release);
175+
if (hasSignal) {
176+
voiceBuffer.hasAudio.store(true);
177+
}
178+
}
179+
180+
size_t SessionRecorder::writeAvailableData(VoiceBuffer& buffer) {
181+
size_t writePos = buffer.writePos.load(std::memory_order_acquire);
182+
size_t readPos = buffer.readPos.load(std::memory_order_relaxed);
183+
184+
if (writePos == readPos) {
185+
return 0; // No data to write
186+
}
187+
188+
size_t available = writePos - readPos;
189+
size_t bufferSize = buffer.buffer.size();
190+
size_t channels = buffer.file->channels();
191+
192+
// Write in chunks to handle ring buffer wraparound
193+
size_t totalWritten = 0;
194+
while (available > 0) {
195+
size_t startIdx = (readPos * channels) % bufferSize;
196+
size_t endIdx = bufferSize;
197+
size_t chunkFrames = std::min(available, (endIdx - startIdx) / channels);
198+
199+
sf_count_t written =
200+
buffer.file->write(&buffer.buffer[startIdx], chunkFrames * channels);
201+
202+
if (written <= 0) {
203+
std::cerr << "Error writing to file: " << buffer.file->strError()
204+
<< std::endl;
205+
break;
206+
}
207+
208+
size_t framesWritten = static_cast<size_t>(written) / channels;
209+
readPos += framesWritten;
210+
available -= framesWritten;
211+
totalWritten += framesWritten;
212+
}
213+
214+
buffer.readPos.store(readPos, std::memory_order_release);
215+
216+
if (totalWritten > 0) {
217+
std::cout << "Wrote " << totalWritten << " frames" << std::endl;
218+
}
219+
220+
return totalWritten;
221+
}
222+
223+
void SessionRecorder::writerThreadFunc() {
224+
std::cout << "Writer thread started" << std::endl;
225+
226+
while (writerRunning_.load()) {
227+
// Write main mix
228+
writeAvailableData(mainMixBuffer_);
229+
230+
// Write voices
231+
for (auto& voiceBuffer : voiceBuffers_) {
232+
if (voiceBuffer.hasAudio.load()) {
233+
writeAvailableData(voiceBuffer);
234+
}
235+
}
236+
237+
// Sleep to avoid busy-waiting
238+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
239+
}
240+
241+
std::cout << "Writer thread stopped" << std::endl;
242+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#ifndef SESSION_RECORDER_H
2+
#define SESSION_RECORDER_H
3+
4+
#include <sndfile.hh>
5+
6+
#include <atomic>
7+
#include <memory>
8+
#include <string>
9+
#include <thread>
10+
#include <vector>
11+
12+
// Use double as sample type to match softcut
13+
using sample_t = double;
14+
15+
class SessionRecorder {
16+
public:
17+
SessionRecorder();
18+
~SessionRecorder();
19+
20+
// Start recording with timestamp-based filenames
21+
void startRecording(int numVoices, float sampleRate);
22+
23+
// Stop recording and close files
24+
void stopRecording();
25+
26+
// Check if currently recording
27+
bool isRecording() const { return recording_.load(); }
28+
29+
// Called from audio thread - captures audio frames
30+
void captureMainMix(const sample_t* left, const sample_t* right,
31+
size_t numFrames);
32+
void captureVoice(int voice, const sample_t* left, const sample_t* right,
33+
size_t numFrames);
34+
35+
private:
36+
std::atomic<bool> recording_;
37+
std::string sessionTimestamp_;
38+
int numVoices_;
39+
float sampleRate_;
40+
41+
// Ring buffers for thread-safe audio capture
42+
static const size_t RING_BUFFER_SIZE = 48000 * 10; // 10 seconds at 48kHz
43+
struct VoiceBuffer {
44+
std::vector<float> buffer;
45+
std::atomic<size_t> writePos;
46+
std::atomic<size_t> readPos;
47+
std::unique_ptr<SndfileHandle> file;
48+
std::atomic<bool> hasAudio;
49+
50+
VoiceBuffer() : writePos(0), readPos(0), hasAudio(false) {}
51+
VoiceBuffer(VoiceBuffer&& other) noexcept
52+
: buffer(std::move(other.buffer)),
53+
writePos(other.writePos.load()),
54+
readPos(other.readPos.load()),
55+
file(std::move(other.file)),
56+
hasAudio(other.hasAudio.load()) {}
57+
VoiceBuffer& operator=(VoiceBuffer&& other) noexcept {
58+
if (this != &other) {
59+
buffer = std::move(other.buffer);
60+
writePos.store(other.writePos.load());
61+
readPos.store(other.readPos.load());
62+
file = std::move(other.file);
63+
hasAudio.store(other.hasAudio.load());
64+
}
65+
return *this;
66+
}
67+
// Delete copy constructor and assignment
68+
VoiceBuffer(const VoiceBuffer&) = delete;
69+
VoiceBuffer& operator=(const VoiceBuffer&) = delete;
70+
};
71+
72+
VoiceBuffer mainMixBuffer_;
73+
std::vector<VoiceBuffer> voiceBuffers_;
74+
75+
// Worker thread for writing to disk
76+
std::unique_ptr<std::thread> writerThread_;
77+
std::atomic<bool> writerRunning_;
78+
79+
void writerThreadFunc();
80+
std::string generateTimestamp();
81+
std::string generateFilename(const std::string& voiceId);
82+
void initializeBuffer(VoiceBuffer& buffer, int channels);
83+
size_t writeAvailableData(VoiceBuffer& buffer);
84+
};
85+
86+
#endif // SESSION_RECORDER_H

0 commit comments

Comments
 (0)