Skip to content
1 change: 1 addition & 0 deletions cmake/pffft.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ include(FetchContent)
FetchContent_Declare(
pffft
GIT_REPOSITORY "https://github.com/marton78/pffft.git"
GIT_TAG a9786ad2e709dd2b8024f0da1ced26b083c1ffc9
SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/pffft-src
)

Expand Down
50 changes: 17 additions & 33 deletions common/data_structures/src/TimeFormatConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,6 @@ juce::String TimeFormatConverter::secondsToBarsBeats(
juce::String(ticks).paddedLeft('0', 3);
}

juce::String TimeFormatConverter::secondsToTimecode(
int timeInSeconds, const juce::AudioPlayHead::FrameRate& frameRate) {
int hours = timeInSeconds / 3600;
int minutes = (timeInSeconds % 3600) / 60;
int seconds = timeInSeconds % 60;
int frames = 0; // For integer seconds, frames = 0

// Format: HH:MM:SS:FF
juce::String hourStr = juce::String(hours).paddedLeft('0', 2);
juce::String minStr = juce::String(minutes).paddedLeft('0', 2);
juce::String secStr = juce::String(seconds).paddedLeft('0', 2);
juce::String frameStr = juce::String(frames).paddedLeft('0', 2);

return hourStr + ":" + minStr + ":" + secStr + ":" + frameStr;
}

int TimeFormatConverter::hmsToSeconds(const juce::String& val) {
auto parts = juce::StringArray::fromTokens(val, ":", "");
if (parts.size() != 3) {
Expand Down Expand Up @@ -130,33 +114,33 @@ int TimeFormatConverter::barsBeatsToSeconds(
return static_cast<int>(totalBeats * secondsPerBeat);
}

int TimeFormatConverter::timecodeToSeconds(const juce::String& val) {
int TimeFormatConverter::timecodeToMs(const juce::String& val, double fps) {
auto parts = juce::StringArray::fromTokens(val, ":", "");
if (parts.size() != 4) {
return -1;
}
if (!parts[0].containsOnly("0123456789") ||
!parts[1].containsOnly("0123456789") ||
!parts[2].containsOnly("0123456789") ||
!parts[3].containsOnly("0123456789")) {
return -1;
}
if (parts.size() != 4) return -1;

for (auto& p : parts)
if (!p.containsOnly("0123456789")) return -1;

int hours = parts[0].getIntValue();
int minutes = parts[1].getIntValue();
int seconds = parts[2].getIntValue();
int frames = parts[3].getIntValue();

// Validate ranges (frames validation would require frame rate)
if (minutes > 59 || seconds > 59 || hours < 0 || minutes < 0 || seconds < 0 ||
frames < 0) {
frames < 0 || frames >= (int)fps)
return -1;
}

// Note: Frames are ignored for now as we work with integer seconds
// This means 00:00:05:00 and 00:00:05:29 both become 5 seconds
// This is acceptable for the current use case but creates lossy conversion
return (hours * 3600 + minutes * 60 + seconds);
double totalSeconds = hours * 3600 + minutes * 60 + seconds + (frames / fps);
return static_cast<int>(std::round(totalSeconds * 1000.0));
}

juce::String TimeFormatConverter::msToTimecode(const int ms, const double fps) {
double totalSeconds = ms / 1000.0;
int hh = (int)(totalSeconds / 3600);
int mm = (int)(std::fmod(totalSeconds, 3600.0) / 60.0);
int ss = (int)(std::fmod(totalSeconds, 60.0));
int ff = (int)(std::fmod(totalSeconds, 1.0) * fps);
return juce::String::formatted("%02d:%02d:%02d:%02d", hh, mm, ss, ff);
}

juce::String TimeFormatConverter::getFormatDescription(TimeFormat format) {
Expand Down
5 changes: 2 additions & 3 deletions common/data_structures/src/TimeFormatConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ class TimeFormatConverter {
static juce::String secondsToBarsBeats(
int timeInSeconds, double bpm,
const juce::AudioPlayHead::TimeSignature& timeSig);
static juce::String secondsToTimecode(
int timeInSeconds, const juce::AudioPlayHead::FrameRate& frameRate);
static juce::String msToTimecode(int ms, double fps);

static int hmsToSeconds(const juce::String& val);
static int barsBeatsToSeconds(
const juce::String& val, double bpm,
const juce::AudioPlayHead::TimeSignature& timeSig);
static int timecodeToSeconds(const juce::String& val);
static int timecodeToMs(const juce::String& val, double fps);

static juce::String getFormatDescription(TimeFormat format);
};
7 changes: 7 additions & 0 deletions common/logger/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ class Logger {

bool isInitialized() const { return initialized; }

void resetForTesting() {
std::lock_guard<std::mutex> lock(initMutex);
boost::log::core::get()->remove_all_sinks();
initialized = false;
logFilePattern = "";
}

private:
Logger() = default;
~Logger() = default;
Expand Down
6 changes: 6 additions & 0 deletions common/logger/tests/Logger_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ TEST(LoggerTest, InitializeLogger) {

// Test Logging Different Severity Levels
TEST(LoggerTest, LogMessages) {
// Reset singleton so this test can re-initialize with debug severity
Logger::getInstance().resetForTesting();
// Clean up log files before the test
Logger::getInstance().init("testlog", 1, boost::log::trivial::debug);
std::vector<std::string> existingLogFiles =
Expand Down Expand Up @@ -235,6 +237,8 @@ TEST(LoggerTest, LoggerInitMultipleCalls) {

// Test File Retention Policy
TEST(LoggerTest, FileRetentionPolicy) {
// Reset singleton so this test can re-initialize with 1MB file size
Logger::getInstance().resetForTesting();
Logger::getInstance().init("testlog", 1, boost::log::trivial::info);

// Clean up existing log files before the test
Expand Down Expand Up @@ -310,6 +314,8 @@ TEST(LoggerTest, FileRetentionPolicy) {

// Test File Retention During Active Logging
TEST(LoggerTest, FileRetentionDuringActiveLogging) {
// Reset singleton so this test can re-initialize with 1MB file size
Logger::getInstance().resetForTesting();
Logger::getInstance().init("testlog", 1, boost::log::trivial::info);

// Clean up existing log files before the test
Expand Down
43 changes: 25 additions & 18 deletions common/processors/file_output/FileOutputProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ FileOutputProcessor::FileOutputProcessor(
mixPresentationRepository_(mixPresentationRepository),
mixPresentationLoudnessRepository_(mixPresentationLoudnessRepository) {}

FileOutputProcessor::~FileOutputProcessor() {}
FileOutputProcessor::~FileOutputProcessor() = default;

//==============================================================================
const juce::String FileOutputProcessor::getName() const {
return {"FileOutput"};
}
//==============================================================================
void FileOutputProcessor::prepareToPlay(double sampleRate,
int samplesPerBlock) {
void FileOutputProcessor::prepareToPlay(const double sampleRate,
const int samplesPerBlock) {
FileExport configParams = fileExportRepository_.get();
if (configParams.getSampleRate() != sampleRate) {
LOG_ANALYTICS(0, "FileOutputProcessor sample rate changed to " +
Expand All @@ -61,7 +61,8 @@ void FileOutputProcessor::prepareToPlay(double sampleRate,
sampleRate_ = sampleRate;
}

void FileOutputProcessor::setNonRealtime(bool isNonRealtime) noexcept {
void FileOutputProcessor::setNonRealtime(const bool isNonRealtime) noexcept {
// Bouncing in DAW and currently rendering || not bouncing and not rendering
if (isNonRealtime == performingRender_) {
return;
}
Expand Down Expand Up @@ -92,10 +93,12 @@ void FileOutputProcessor::processBlock(juce::AudioBuffer<float>& buffer,
return;
}

// Process audio elements individually as Wav files
for (int i = 0; i < iamfWavFileWriters_.size(); ++i) {
iamfWavFileWriters_[i]->write(buffer);
}

// Process IAMF File
if (iamfFileWriter_) {
iamfFileWriter_->writeFrame(buffer);
}
Expand All @@ -105,8 +108,8 @@ void FileOutputProcessor::processBlock(juce::AudioBuffer<float>& buffer,
void FileOutputProcessor::initializeFileExport(FileExport& config) {
LOG_ANALYTICS(0, "Beginning .iamf file export");
performingRender_ = true;
startTime_ = config.getStartTime();
endTime_ = config.getEndTime();
startTime_ = config.getStartTime() / 1000.0;
endTime_ = config.getEndTime() / 1000.0;
std::string exportFile = config.getExportFile().toStdString();

// To create the IAMF file, create a list of all the audio element wav
Expand Down Expand Up @@ -159,10 +162,10 @@ void FileOutputProcessor::initializeFileExport(FileExport& config) {
}
}

void FileOutputProcessor::closeFileExport(FileExport& config) {
void FileOutputProcessor::closeFileExport(const FileExport& config) {
LOG_ANALYTICS(0, "closing writers and exporting IAMF file");
// close the output file, since rendering is completed
for (auto& writer : iamfWavFileWriters_) {
for (const auto& writer : iamfWavFileWriters_) {
writer->close();
}

Expand Down Expand Up @@ -209,20 +212,24 @@ bool FileOutputProcessor::shouldBufferBeWritten(

// Calculate the current time with the existing number of samples that have
// been processed
long currentTime = sampleTally_ / sampleRate_;
const double currentTime = static_cast<double>(sampleTally_) / sampleRate_;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rather then casting here, should we just make sampleTally_ a double?

In general, this math seems a bit confused since sampleRate_ is already a long cast from a double. So feels like they could both become doubles here and it will be more straightforward?

// update the sample tally
sampleTally_ += buffer.getNumSamples();
// with the updated sample tally, calculate the next time
long nextTime = sampleTally_ / sampleRate_;

if (startTime_ != 0 || endTime_ != 0) {
// Handle the case where startTime and endTime are set, implying we
// are only bouncing a subset of the mix
// No range specified — write everything
if (startTime_ <= 0 && endTime_ <= 0) {
return true;
}

// do not render
if (currentTime < startTime_ || nextTime > endTime_) {
return false;
}
// Skip if buffer starts before the requested start time
if (startTime_ > 0 && currentTime < startTime_) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need to check is startTime > 0 or endTime is > 0. Looks like above we are returning if startTime or endTime are <= 0 already?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you make the suggested change, I would combine these into one statement

return false;
}

// Skip if buffer starts at or past the requested end time
if (endTime_ > 0 && currentTime >= endTime_) {
return false;
}

return true;
}
8 changes: 4 additions & 4 deletions common/processors/file_output/FileOutputProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class FileOutputProcessor : public ProcessorBase {

void initializeFileExport(FileExport& config);

void closeFileExport(FileExport& config);
void closeFileExport(const FileExport& config);

bool shouldBufferBeWritten(const juce::AudioBuffer<float>& buffer);

Expand All @@ -92,9 +92,9 @@ class FileOutputProcessor : public ProcessorBase {
MixPresentationLoudnessRepository& mixPresentationLoudnessRepository_;
std::vector<std::unique_ptr<AudioElementFileWriter>> iamfWavFileWriters_;
int numSamples_;
long sampleRate_;
int startTime_;
int endTime_;
double sampleRate_;
double startTime_;
double endTime_;
long sampleTally_;
std::unique_ptr<IAMFFileWriter> iamfFileWriter_;
//==============================================================================
Expand Down
4 changes: 2 additions & 2 deletions common/processors/file_output/WavFileOutputProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ void WavFileOutputProcessor::setNonRealtime(bool isNonRealtime) noexcept {
// Start Rendering
FileExport configParams = fileExportRepository_.get();
RoomSetup roomSetup = roomSetupRepository_.get();
startTime_ = configParams.getStartTime();
endTime_ = configParams.getEndTime();
startTime_ = configParams.getStartTime() / 1000.0;
endTime_ = configParams.getEndTime() / 1000.0;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like every time we fetch these values we're dividing them by 1000.

Does it make more sense to divide them when we store them instead, since that will be one place instead of on each fetch? I'm genuinely asking, there might be a good reason to divide the value this way instead.

if ((configParams.getAudioFileFormat() == AudioFileFormat::WAV) &&
configParams.getExportAudio()) {
fileWriter_ = new FileWriter(
Expand Down
6 changes: 3 additions & 3 deletions common/processors/file_output/WavFileOutputProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ class WavFileOutputProcessor final : public ProcessorBase,
RoomSetupRepository& roomSetupRepository_;
FileWriter* fileWriter_;
int numSamples_;
int sampleRate_;
int startTime_;
int endTime_;
double sampleRate_;
double startTime_;
double endTime_;
juce::SpinLock lock_;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WavFileOutputProcessor)
Expand Down
4 changes: 2 additions & 2 deletions common/processors/loudness_export/LoudnessExportProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ void LoudnessExportProcessor::initializeLoudnessExport(FileExport& config) {

sampleRate_ = config.getSampleRate();
sampleTally_ = 0;
startTime_ = config.getStartTime();
endTime_ = config.getEndTime();
startTime_ = config.getStartTime() / 1000;
endTime_ = config.getEndTime() / 1000;

intializeExportContainers();
}
Expand Down
Loading
Loading