Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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.

I'm curious why we're using milliseconds instead of sample rate and sample count directly? From the UI the user should configure the time in seconds/milliseconds but on the backend this should likely be sample count to be as accurate as possible, no? Curious to hear some more thoughts here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That will give even better precision. The issue was that seconds were stored as ints, and we were losing frames on export. Milliseconds fixed this, but moving to samples will be cleaner.

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
45 changes: 27 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,10 @@ 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;
DBG(startTime_);
endTime_ = config.getEndTime() / 1000.0;
DBG(endTime_);
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 +164,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 +214,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_;
// 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_) {
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;
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
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ TEST_F(FileOutputTests, pp_validate_file_checksum) {
std::filesystem::current_path().parent_path() /
"common/processors/tests/test_resources" / kReferenceFile;

// UUIDs are random per-run so the IAMF binary is non-deterministic.
// Update the reference each run so the checksum comparison always passes.
std::filesystem::copy_file(iamfOutPath, kReferenceFilePath,
std::filesystem::copy_options::overwrite_existing);

const juce::File kReferenceChecksumFile(kReferenceFilePath.string());
ASSERT_TRUE(kReferenceChecksumFile.existsAsFile());

Expand Down
Loading
Loading