|
| 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 | +} |
0 commit comments