Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ target_sources(OpenStudio PRIVATE
Source/MainComponent.cpp
Source/MixerWindowManager.cpp
Source/AppUpdater.cpp
Source/CrashDiagnostics.cpp
Source/AudioEngine.cpp
Source/AudioRecorder.cpp
Source/PlaybackEngine.cpp
Expand Down Expand Up @@ -370,6 +371,7 @@ if(WIN32)
if(MSVC)
target_link_libraries(OpenStudio PRIVATE
"${webview2_SOURCE_DIR}/build/native/x64/WebView2LoaderStatic.lib"
dbghelp
dwmapi
)
endif()
Expand Down
222 changes: 176 additions & 46 deletions Source/AudioEngine.cpp

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Source/AudioEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,9 @@ class AudioEngine : public juce::AudioIODeviceCallback,
std::atomic<int> midiLateEventCount { 0 };
std::atomic<int> midiMaxEventsPerBlock { 0 };
std::atomic<int> midiLastComputedSampleOffset { 0 };
std::atomic<int> midiLastInputFanoutCount { 0 };
std::atomic<int> midiMaxInputFanoutCount { 0 };
std::atomic<int> midiMappedParameterUpdateCount { 0 };
ProcessingPrecisionMode processingPrecisionMode { ProcessingPrecisionMode::Float32 };

// Master FX (Phase 5)
Expand Down Expand Up @@ -840,6 +843,7 @@ class AudioEngine : public juce::AudioIODeviceCallback,
float spectrumInputBuffer[FFT_SIZE * 2] = {}; // ring buffer for FFT input
float spectrumOutputBuffer[FFT_SIZE] = {}; // magnitude spectrum
int spectrumWritePos { 0 };
int spectrumFftDecimationCounter { 0 };
bool spectrumReady { false };
juce::CriticalSection spectrumLock;
std::atomic<uint64> spectrumFftPublishCount { 0 };
Expand Down
109 changes: 109 additions & 0 deletions Source/CrashDiagnostics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include "CrashDiagnostics.h"

#include <mutex>

#if JUCE_WINDOWS
#include <windows.h>
#include <dbghelp.h>
#endif

namespace
{
std::once_flag installOnce;

juce::File getDiagnosticsDirectory()
{
auto dir = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory)
.getChildFile("OpenStudio");
dir.createDirectory();
return dir;
}

void rotateIfLarge(const juce::File& file)
{
constexpr juce::int64 maxBytes = 512 * 1024;
if (!file.existsAsFile() || file.getSize() <= maxBytes)
return;

const auto archived = file.getSiblingFile(file.getFileNameWithoutExtension() + ".1." + file.getFileExtension());
archived.deleteFile();
file.moveFileTo(archived);
}

#if JUCE_WINDOWS
LONG WINAPI openStudioUnhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo)
{
const auto dumpFile = OpenStudioCrashDiagnostics::getLastCrashDumpFile();
HANDLE dumpHandle = CreateFileW(dumpFile.getFullPathName().toWideCharPointer(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);

if (dumpHandle != INVALID_HANDLE_VALUE)
{
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo {};
dumpExceptionInfo.ThreadId = GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = exceptionInfo;
dumpExceptionInfo.ClientPointers = FALSE;

MiniDumpWriteDump(GetCurrentProcess(),
GetCurrentProcessId(),
dumpHandle,
MiniDumpNormal,
exceptionInfo != nullptr ? &dumpExceptionInfo : nullptr,
nullptr,
nullptr);
CloseHandle(dumpHandle);
}

juce::String detail = "dump=" + dumpFile.getFullPathName();
if (exceptionInfo != nullptr && exceptionInfo->ExceptionRecord != nullptr)
{
detail += " code=0x" + juce::String::toHexString(
static_cast<juce::int64>(exceptionInfo->ExceptionRecord->ExceptionCode));
}
OpenStudioCrashDiagnostics::recordBreadcrumb("unhandled_exception", detail);
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
}

namespace OpenStudioCrashDiagnostics
{
juce::File getBreadcrumbLogFile()
{
return getDiagnosticsDirectory().getChildFile("crash_breadcrumbs.log");
}

juce::File getLastCrashDumpFile()
{
return getDiagnosticsDirectory().getChildFile("last_crash.dmp");
}

void recordBreadcrumb(const juce::String& stage, const juce::String& detail)
{
const auto logFile = getBreadcrumbLogFile();
rotateIfLarge(logFile);

juce::String line = juce::Time::getCurrentTime().toString(true, true)
+ " | " + stage;
if (detail.isNotEmpty())
line += " | " + detail;

logFile.appendText(line + "\n");
}

void installCrashHandlers()
{
std::call_once(installOnce, []
{
#if JUCE_WINDOWS
SetUnhandledExceptionFilter(openStudioUnhandledExceptionFilter);
#endif
recordBreadcrumb("crash_handlers_installed");
});
}
}
11 changes: 11 additions & 0 deletions Source/CrashDiagnostics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <JuceHeader.h>

namespace OpenStudioCrashDiagnostics
{
void installCrashHandlers();
void recordBreadcrumb(const juce::String& stage, const juce::String& detail = {});
juce::File getBreadcrumbLogFile();
juce::File getLastCrashDumpFile();
}
132 changes: 60 additions & 72 deletions Source/PeakCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,95 +64,83 @@ juce::var PeakCache::getPeaks(const juce::File& audioFile,
int startSample,
int numPixels) const
{
juce::Array<juce::var> peakData;

CacheEntry entry;
bool loaded = false;

// Try memory cache
{
const juce::ScopedLock sl(cacheLock);
auto it = memoryCache.find(audioFile.getFullPathName());
if (it != memoryCache.end())
{
entry = it->second;
loaded = true;
}
}

// Try loading from disk
if (!loaded)
auto buildResult = [samplesPerPixel, startSample, numPixels](const CacheEntry& entry)
{
auto peakFile = getPeakFilePath(audioFile);
if (!peakFile.existsAsFile())
peakFile = getLegacyPeakFilePath(audioFile);
juce::Array<juce::var> peakData;
if (entry.levels.empty())
return juce::var(peakData);

if (peakFile.existsAsFile() && loadFromFile(peakFile, audioFile, entry))
// Find the best mipmap level (closest stride <= samplesPerPixel)
const MipmapLevel* bestLevel = &entry.levels[0];
for (const auto& level : entry.levels)
{
loaded = true;
// Store in memory cache for next time
const juce::ScopedLock sl(cacheLock);
memoryCache[audioFile.getFullPathName()] = entry;
if (level.stride <= samplesPerPixel)
bestLevel = &level;
}
}

if (!loaded || entry.levels.empty())
return peakData;

// Find the best mipmap level (closest stride <= samplesPerPixel)
const MipmapLevel* bestLevel = &entry.levels[0];
for (const auto& level : entry.levels)
{
if (level.stride <= samplesPerPixel)
bestLevel = &level;
}

// Calculate how many source peaks we need per output pixel
int ratio = samplesPerPixel / bestLevel->stride;
if (ratio < 1) ratio = 1;
int ratio = samplesPerPixel / bestLevel->stride;
if (ratio < 1) ratio = 1;

int numChannels = bestLevel->numChannels;
const int numChannels = bestLevel->numChannels;
const int startPeak = (startSample > 0) ? std::min(startSample / bestLevel->stride, bestLevel->numPeaks - 1) : 0;
const int remainingMipmapPeaks = std::max(0, bestLevel->numPeaks - startPeak);
const int actualPeaks = std::min(numPixels, remainingMipmapPeaks / std::max(1, ratio));

// Convert startSample to an index in this mipmap level, clamped to valid range
int startPeak = (startSample > 0) ? std::min(startSample / bestLevel->stride, bestLevel->numPeaks - 1) : 0;
// remainingPeaks: mipmap-level peaks left after startPeak.
// Divide by ratio to get output pixels. (startPeak is in mipmap units, NOT output-pixel units.)
int remainingMipmapPeaks = std::max(0, bestLevel->numPeaks - startPeak);
int actualPeaks = std::min(numPixels, remainingMipmapPeaks / std::max(1, ratio));
peakData.ensureStorageAllocated(1 + actualPeaks * numChannels * 2);
peakData.add(juce::var(numChannels));

peakData.ensureStorageAllocated(1 + actualPeaks * numChannels * 2);
peakData.add(juce::var(numChannels)); // Header

int stride2 = numChannels * 2; // Floats per peak entry in the data array

for (int pixel = 0; pixel < actualPeaks; ++pixel)
{
int srcStart = startPeak + pixel * ratio;
int srcEnd = std::min(srcStart + ratio, bestLevel->numPeaks);

for (int ch = 0; ch < numChannels; ++ch)
const int stride2 = numChannels * 2;
for (int pixel = 0; pixel < actualPeaks; ++pixel)
{
float minVal = 0.0f;
float maxVal = 0.0f;
const int srcStart = startPeak + pixel * ratio;
const int srcEnd = std::min(srcStart + ratio, bestLevel->numPeaks);

for (int s = srcStart; s < srcEnd; ++s)
for (int ch = 0; ch < numChannels; ++ch)
{
int dataIdx = s * stride2 + ch * 2;
if (dataIdx + 1 < (int)bestLevel->data.size())
float minVal = 0.0f;
float maxVal = 0.0f;

for (int s = srcStart; s < srcEnd; ++s)
{
float mn = bestLevel->data[static_cast<size_t>(dataIdx)];
float mx = bestLevel->data[static_cast<size_t>(dataIdx) + 1];
if (s == srcStart || mn < minVal) minVal = mn;
if (s == srcStart || mx > maxVal) maxVal = mx;
const int dataIdx = s * stride2 + ch * 2;
if (dataIdx + 1 < static_cast<int>(bestLevel->data.size()))
{
const float mn = bestLevel->data[static_cast<size_t>(dataIdx)];
const float mx = bestLevel->data[static_cast<size_t>(dataIdx) + 1];
if (s == srcStart || mn < minVal) minVal = mn;
if (s == srcStart || mx > maxVal) maxVal = mx;
}
}
}

peakData.add(juce::var(minVal));
peakData.add(juce::var(maxVal));
peakData.add(juce::var(minVal));
peakData.add(juce::var(maxVal));
}
}

return juce::var(peakData);
};

{
const juce::ScopedLock sl(cacheLock);
auto it = memoryCache.find(audioFile.getFullPathName());
if (it != memoryCache.end())
return buildResult(it->second);
}

return peakData;
CacheEntry entry;
auto peakFile = getPeakFilePath(audioFile);
if (!peakFile.existsAsFile())
peakFile = getLegacyPeakFilePath(audioFile);

if (!peakFile.existsAsFile() || !loadFromFile(peakFile, audioFile, entry))
return juce::var(juce::Array<juce::var>());

auto result = buildResult(entry);
{
const juce::ScopedLock sl(cacheLock);
memoryCache[audioFile.getFullPathName()] = std::move(entry);
}
return result;
}

bool PeakCache::loadFromFile(const juce::File& peakFile, const juce::File& audioFile, CacheEntry& entry) const
Expand Down
5 changes: 3 additions & 2 deletions Source/PeakCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ class PeakCache
std::set<juce::String> pendingGenerations;
juce::CriticalSection pendingLock;

// Background thread pool for concurrent peak generation
juce::ThreadPool backgroundPool { juce::jmax(2, juce::SystemStats::getNumCpus() / 2) };
// Keep waveform generation serialized and low-impact; concurrent full-file
// peak scans can steal disk/CPU from 32/64-sample monitoring callbacks.
juce::ThreadPool backgroundPool { 1 };

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PeakCache)
};
Loading
Loading