| @@ -30,7 +30,7 @@ struct Engine { | |||||
| /** Advances the engine by `frames` frames. | /** Advances the engine by `frames` frames. | ||||
| Only call this method from the primary module. | Only call this method from the primary module. | ||||
| */ | */ | ||||
| void step(int frames); | |||||
| void stepBlock(int frames); | |||||
| void setPrimaryModule(Module* module); | void setPrimaryModule(Module* module); | ||||
| Module* getPrimaryModule(); | Module* getPrimaryModule(); | ||||
| @@ -41,29 +41,32 @@ struct Engine { | |||||
| */ | */ | ||||
| float getSampleTime(); | float getSampleTime(); | ||||
| /** Causes worker threads to block on a mutex instead of spinlock. | /** Causes worker threads to block on a mutex instead of spinlock. | ||||
| Call this in your Module::step() method to hint that the operation will take more than ~0.1 ms. | |||||
| Call this in your Module::stepBlock() method to hint that the operation will take more than ~0.1 ms. | |||||
| */ | */ | ||||
| void yieldWorkers(); | void yieldWorkers(); | ||||
| /** Returns the number of audio samples since the Engine's first sample. | |||||
| /** Returns the number of stepBlock() calls since the Engine was created. | |||||
| */ | |||||
| int64_t getBlock(); | |||||
| /** Returns the number of audio samples since the Engine was created. | |||||
| */ | */ | ||||
| int64_t getFrame(); | int64_t getFrame(); | ||||
| /** Returns the estimated timestamp corresponding to the current frame, based on the timestamp of when step() was last called. | |||||
| /** Returns the estimated time corresponding to the current frame, based on the time of when stepBlock() was last called. | |||||
| Calculated by `stepTime + framesSinceStep / sampleRate`. | Calculated by `stepTime + framesSinceStep / sampleRate`. | ||||
| */ | */ | ||||
| double getFrameTime(); | double getFrameTime(); | ||||
| /** Returns the frame when step() was last called. | |||||
| /** Returns the frame when stepBlock() was last called. | |||||
| */ | */ | ||||
| int64_t getStepFrame(); | |||||
| /** Returns the timestamp in seconds when step() was last called. | |||||
| int64_t getBlockFrame(); | |||||
| /** Returns the time in seconds when stepBlock() was last called. | |||||
| */ | */ | ||||
| double getStepTime(); | |||||
| /** Returns the total number of frames in the current step() call. | |||||
| double getBlockTime(); | |||||
| /** Returns the total number of frames in the current stepBlock() call. | |||||
| */ | */ | ||||
| int getStepFrames(); | |||||
| /** Returns the total time that step() is advancing, in seconds. | |||||
| int getBlockFrames(); | |||||
| /** Returns the total time that stepBlock() is advancing, in seconds. | |||||
| Calculated by `stepFrames / sampleRate`. | Calculated by `stepFrames / sampleRate`. | ||||
| */ | */ | ||||
| double getStepDuration(); | |||||
| double getBlockDuration(); | |||||
| // Modules | // Modules | ||||
| size_t getNumModules(); | size_t getNumModules(); | ||||
| @@ -348,7 +348,7 @@ struct Module { | |||||
| PRIVATE const float* meterBuffer(); | PRIVATE const float* meterBuffer(); | ||||
| PRIVATE int meterLength(); | PRIVATE int meterLength(); | ||||
| PRIVATE int meterIndex(); | PRIVATE int meterIndex(); | ||||
| PRIVATE void step(const ProcessArgs& args); | |||||
| PRIVATE void doProcess(const ProcessArgs& args); | |||||
| }; | }; | ||||
| @@ -401,40 +401,50 @@ void ModuleWidget::draw(const DrawArgs& args) { | |||||
| int meterLength = module->meterLength(); | int meterLength = module->meterLength(); | ||||
| int meterIndex = module->meterIndex(); | int meterIndex = module->meterIndex(); | ||||
| float meterMax = 0.f; | |||||
| float meterAvg = 0.f; | float meterAvg = 0.f; | ||||
| for (int i = 0; i < meterLength; i++) { | for (int i = 0; i < meterLength; i++) { | ||||
| meterAvg += meterBuffer[i]; | |||||
| float m = meterBuffer[i]; | |||||
| meterAvg += m; | |||||
| meterMax = std::max(meterMax, m); | |||||
| } | } | ||||
| meterAvg /= meterLength; | meterAvg /= meterLength; | ||||
| float percentMax = meterMax * sampleRate; | |||||
| float mult = (percentMax <= 0.1f) ? 10.f : 1.f; | |||||
| // Text background | |||||
| nvgBeginPath(args.vg); | |||||
| nvgRect(args.vg, 0, box.size.y - 20, box.size.x, 20); | |||||
| nvgFillColor(args.vg, nvgRGBAf(0, 0, 0, 0.75)); | |||||
| nvgFill(args.vg); | |||||
| // Text | |||||
| float percent = meterAvg * sampleRate * 100.f; | |||||
| float microseconds = meterAvg * 1e6f; | |||||
| std::string meterText = string::f("%.1f%% %.2f ÎĽs", percent, microseconds); | |||||
| bndLabel(args.vg, 0.0, box.size.y - 20.0, INFINITY, INFINITY, -1, meterText.c_str()); | |||||
| // // Text background | |||||
| // nvgBeginPath(args.vg); | |||||
| // nvgRect(args.vg, 0.0, box.size.y - infoHeight, box.size.x, infoHeight); | |||||
| // nvgFillColor(args.vg, nvgRGBAf(0, 0, 0, 0.75)); | |||||
| // nvgFill(args.vg); | |||||
| // Draw time plot | // Draw time plot | ||||
| nvgBeginPath(args.vg); | nvgBeginPath(args.vg); | ||||
| nvgMoveTo(args.vg, box.size.x, box.size.y); | nvgMoveTo(args.vg, box.size.x, box.size.y); | ||||
| for (int i = 0; i < meterLength; i++) { | for (int i = 0; i < meterLength; i++) { | ||||
| int index = (meterIndex - i + meterLength) % meterLength; | int index = (meterIndex - i + meterLength) % meterLength; | ||||
| float percent = math::clamp(meterBuffer[index] * mult * sampleRate, 0.f, 1.f); | |||||
| math::Vec p; | math::Vec p; | ||||
| p.x = (1.f - (float) i / (meterLength - 1)) * box.size.x; | p.x = (1.f - (float) i / (meterLength - 1)) * box.size.x; | ||||
| p.y = (1.f - meterBuffer[index] * sampleRate * 1.f) * (box.size.y - 20); | |||||
| p.y = (1.f - percent) * (box.size.y); | |||||
| nvgLineTo(args.vg, p.x, p.y); | nvgLineTo(args.vg, p.x, p.y); | ||||
| } | } | ||||
| NVGcolor color = nvgRGBAf(0.6, 0, 0, 0.6); | |||||
| NVGcolor color; | |||||
| if (mult == 1.f) | |||||
| color = nvgRGBAf(0.5, 0, 0, 0.85); | |||||
| else if (mult == 10.f) | |||||
| color = nvgRGBAf(0.85, 0, 0, 0.85); | |||||
| nvgLineTo(args.vg, 0.0, box.size.y); | nvgLineTo(args.vg, 0.0, box.size.y); | ||||
| nvgClosePath(args.vg); | nvgClosePath(args.vg); | ||||
| nvgFillColor(args.vg, color); | nvgFillColor(args.vg, color); | ||||
| nvgFill(args.vg); | nvgFill(args.vg); | ||||
| // Text | |||||
| float percent = meterAvg * sampleRate * 100.f; | |||||
| float microseconds = meterAvg * 1e6f; | |||||
| std::string meterText = string::f("%.0fx\n%.2f ÎĽs\n%.1f%%", mult, microseconds, percent); | |||||
| bndLabel(args.vg, 0.0, box.size.y - 60, INFINITY, INFINITY, -1, meterText.c_str()); | |||||
| // Draw border | // Draw border | ||||
| nvgStrokeColor(args.vg, color); | nvgStrokeColor(args.vg, color); | ||||
| nvgBeginPath(args.vg); | nvgBeginPath(args.vg); | ||||
| @@ -245,7 +245,7 @@ struct AudioInterface : Module, audio::Port { | |||||
| bool isPrimary = (APP->engine->getPrimaryModule() == this); | bool isPrimary = (APP->engine->getPrimaryModule() == this); | ||||
| // Step engine | // Step engine | ||||
| if (isPrimary && requestedEngineFrames > 0) { | if (isPrimary && requestedEngineFrames > 0) { | ||||
| APP->engine->step(requestedEngineFrames); | |||||
| APP->engine->stepBlock(requestedEngineFrames); | |||||
| } | } | ||||
| } | } | ||||
| @@ -73,7 +73,7 @@ struct MIDI_CC : Module { | |||||
| while (!midiInput.queue.empty()) { | while (!midiInput.queue.empty()) { | ||||
| midi::Message& msg = midiInput.queue.front(); | midi::Message& msg = midiInput.queue.front(); | ||||
| // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | ||||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||||
| break; | break; | ||||
| processMessage(msg); | processMessage(msg); | ||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| @@ -124,7 +124,7 @@ struct MIDI_CV : Module { | |||||
| while (!midiInput.queue.empty()) { | while (!midiInput.queue.empty()) { | ||||
| midi::Message& msg = midiInput.queue.front(); | midi::Message& msg = midiInput.queue.front(); | ||||
| // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | ||||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||||
| break; | break; | ||||
| processMessage(msg); | processMessage(msg); | ||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| @@ -639,6 +639,10 @@ struct MIDI_CVWidget : ModuleWidget { | |||||
| panicItem->text = "Panic"; | panicItem->text = "Panic"; | ||||
| panicItem->module = module; | panicItem->module = module; | ||||
| menu->addChild(panicItem); | menu->addChild(panicItem); | ||||
| // Example of using appendMidiMenu() | |||||
| // menu->addChild(new MenuSeparator); | |||||
| // appendMidiMenu(menu, &module->midiInput); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -67,7 +67,7 @@ struct MIDI_Gate : Module { | |||||
| while (!midiInput.queue.empty()) { | while (!midiInput.queue.empty()) { | ||||
| midi::Message& msg = midiInput.queue.front(); | midi::Message& msg = midiInput.queue.front(); | ||||
| // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | ||||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||||
| break; | break; | ||||
| processMessage(msg); | processMessage(msg); | ||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| @@ -85,7 +85,7 @@ struct MIDI_Map : Module { | |||||
| while (!midiInput.queue.empty()) { | while (!midiInput.queue.empty()) { | ||||
| midi::Message& msg = midiInput.queue.front(); | midi::Message& msg = midiInput.queue.front(); | ||||
| // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | // Don't process MIDI message until its timestamp corresponds with the audio frame time when played back in the next block. | ||||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||||
| break; | break; | ||||
| processMessage(msg); | processMessage(msg); | ||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| @@ -196,10 +196,11 @@ struct Engine::Internal { | |||||
| float sampleRate = 0.f; | float sampleRate = 0.f; | ||||
| float sampleTime = 0.f; | float sampleTime = 0.f; | ||||
| int64_t block = 0; | |||||
| int64_t frame = 0; | int64_t frame = 0; | ||||
| int64_t stepFrame = 0; | |||||
| double stepTime = 0.0; | |||||
| int stepFrames = 0; | |||||
| int64_t blockFrame = 0; | |||||
| double blockTime = 0.0; | |||||
| int blockFrames = 0; | |||||
| Module* primaryModule = NULL; | Module* primaryModule = NULL; | ||||
| // Parameter smoothing | // Parameter smoothing | ||||
| @@ -207,15 +208,15 @@ struct Engine::Internal { | |||||
| int smoothParamId = 0; | int smoothParamId = 0; | ||||
| float smoothValue = 0.f; | float smoothValue = 0.f; | ||||
| /** Engine mutex | |||||
| Writers lock when mutating the engine's Modules, Cables, etc. | |||||
| Readers lock when using the engine's Modules, Cables, etc. | |||||
| /** Mutex that guards the Engine state, such as settings, Modules, and Cables. | |||||
| Writers lock when mutating the engine's state. | |||||
| Readers lock when using the engine's state. | |||||
| */ | */ | ||||
| SharedMutex mutex; | SharedMutex mutex; | ||||
| /** Step mutex | |||||
| step() locks to guarantee its exclusivity. | |||||
| /** Mutex that guards the block. | |||||
| stepBlock() locks to guarantee its exclusivity. | |||||
| */ | */ | ||||
| std::mutex stepMutex; | |||||
| std::mutex blockMutex; | |||||
| int threadCount = 0; | int threadCount = 0; | ||||
| std::vector<EngineWorker> workers; | std::vector<EngineWorker> workers; | ||||
| @@ -310,7 +311,7 @@ static void Engine_stepWorker(Engine* that, int threadId) { | |||||
| break; | break; | ||||
| Module* module = internal->modules[i]; | Module* module = internal->modules[i]; | ||||
| module->step(processArgs); | |||||
| module->doProcess(processArgs); | |||||
| } | } | ||||
| } | } | ||||
| @@ -497,16 +498,16 @@ void Engine::clear() { | |||||
| } | } | ||||
| void Engine::step(int frames) { | |||||
| std::lock_guard<std::mutex> stepLock(internal->stepMutex); | |||||
| void Engine::stepBlock(int frames) { | |||||
| std::lock_guard<std::mutex> stepLock(internal->blockMutex); | |||||
| SharedLock lock(internal->mutex); | SharedLock lock(internal->mutex); | ||||
| // Configure thread | // Configure thread | ||||
| initMXCSR(); | initMXCSR(); | ||||
| random::init(); | random::init(); | ||||
| internal->stepFrame = internal->frame; | |||||
| internal->stepTime = system::getTime(); | |||||
| internal->stepFrames = frames; | |||||
| internal->blockFrame = internal->frame; | |||||
| internal->blockTime = system::getTime(); | |||||
| internal->blockFrames = frames; | |||||
| // Set sample rate | // Set sample rate | ||||
| if (internal->sampleRate != settings::sampleRate) { | if (internal->sampleRate != settings::sampleRate) { | ||||
| @@ -538,9 +539,11 @@ void Engine::step(int frames) { | |||||
| yieldWorkers(); | yieldWorkers(); | ||||
| double endTime = system::getTime(); | double endTime = system::getTime(); | ||||
| float duration = endTime - internal->stepTime; | |||||
| float stepDuration = internal->stepFrames * internal->sampleTime; | |||||
| // DEBUG("%d %f / %f = %f%%", internal->stepFrames, duration, stepDuration, duration / stepDuration * 100.f); | |||||
| float duration = endTime - internal->blockTime; | |||||
| float blockDuration = internal->blockFrames * internal->sampleTime; | |||||
| // DEBUG("%d %f / %f = %f%%", internal->blockFrames, duration, blockDuration, duration / blockDuration * 100.f); | |||||
| internal->block++; | |||||
| } | } | ||||
| @@ -577,34 +580,39 @@ void Engine::yieldWorkers() { | |||||
| } | } | ||||
| int64_t Engine::getBlock() { | |||||
| return internal->block; | |||||
| } | |||||
| int64_t Engine::getFrame() { | int64_t Engine::getFrame() { | ||||
| return internal->frame; | return internal->frame; | ||||
| } | } | ||||
| double Engine::getFrameTime() { | double Engine::getFrameTime() { | ||||
| double timeSinceStep = (internal->frame - internal->stepFrame) * internal->sampleTime; | |||||
| return internal->stepTime + timeSinceStep; | |||||
| double timeSinceBlock = (internal->frame - internal->blockFrame) * internal->sampleTime; | |||||
| return internal->blockTime + timeSinceBlock; | |||||
| } | } | ||||
| int64_t Engine::getStepFrame() { | |||||
| return internal->stepFrame; | |||||
| int64_t Engine::getBlockFrame() { | |||||
| return internal->blockFrame; | |||||
| } | } | ||||
| double Engine::getStepTime() { | |||||
| return internal->stepTime; | |||||
| double Engine::getBlockTime() { | |||||
| return internal->blockTime; | |||||
| } | } | ||||
| int Engine::getStepFrames() { | |||||
| return internal->stepFrames; | |||||
| int Engine::getBlockFrames() { | |||||
| return internal->blockFrames; | |||||
| } | } | ||||
| double Engine::getStepDuration() { | |||||
| return internal->stepFrames * internal->sampleTime; | |||||
| double Engine::getBlockDuration() { | |||||
| return internal->blockFrames * internal->sampleTime; | |||||
| } | } | ||||
| @@ -1031,7 +1039,7 @@ json_t* Engine::toJson() { | |||||
| void Engine::fromJson(json_t* rootJ) { | void Engine::fromJson(json_t* rootJ) { | ||||
| // We can't lock here because addModule() and addCable() are called inside. | // We can't lock here because addModule() and addCable() are called inside. | ||||
| // Also, AudioInterface::fromJson() can open the audio device, which can call Engine::step() before this method exits. | |||||
| // Also, AudioInterface::fromJson() can open the audio device, which can call Engine::stepBlock() before this method exits. | |||||
| // ExclusiveSharedLock lock(internal->mutex); | // ExclusiveSharedLock lock(internal->mutex); | ||||
| clear(); | clear(); | ||||
| // modules | // modules | ||||
| @@ -1,8 +1,10 @@ | |||||
| #include <engine/Module.hpp> | #include <engine/Module.hpp> | ||||
| #include <engine/Engine.hpp> | |||||
| #include <plugin.hpp> | #include <plugin.hpp> | ||||
| #include <system.hpp> | #include <system.hpp> | ||||
| #include <settings.hpp> | #include <settings.hpp> | ||||
| #include <asset.hpp> | #include <asset.hpp> | ||||
| #include <context.hpp> | |||||
| namespace rack { | namespace rack { | ||||
| @@ -11,19 +13,19 @@ namespace engine { | |||||
| // Arbitrary prime number so it doesn't over- or under-estimate time of buffered processors. | // Arbitrary prime number so it doesn't over- or under-estimate time of buffered processors. | ||||
| static const int meterDivider = 23; | |||||
| static const int samplesCount = 64; | |||||
| static const int meterDivider = 1; | |||||
| static const int meterBufferLength = 128; | static const int meterBufferLength = 128; | ||||
| struct Module::Internal { | struct Module::Internal { | ||||
| bool bypass = false; | bool bypass = false; | ||||
| float meterTimeTotal = 0.f; | |||||
| int64_t meterLastBlock = 0; | |||||
| int meterSamples = 0; | int meterSamples = 0; | ||||
| int meterIndex = 0; | |||||
| float meterTimeTotal = 0.f; | |||||
| float meterBuffer[meterBufferLength] = {}; | float meterBuffer[meterBufferLength] = {}; | ||||
| int meterIndex = 0; | |||||
| }; | }; | ||||
| @@ -326,7 +328,7 @@ static void Port_step(Port* that, float deltaTime) { | |||||
| } | } | ||||
| void Module::step(const ProcessArgs& args) { | |||||
| void Module::doProcess(const ProcessArgs& args) { | |||||
| // This global setting can change while the function is running, so use a local variable. | // This global setting can change while the function is running, so use a local variable. | ||||
| bool meterEnabled = settings::cpuMeter && (args.frame % meterDivider == 0); | bool meterEnabled = settings::cpuMeter && (args.frame % meterDivider == 0); | ||||
| @@ -347,15 +349,21 @@ void Module::step(const ProcessArgs& args) { | |||||
| double endTime = system::getTime(); | double endTime = system::getTime(); | ||||
| float duration = endTime - startTime; | float duration = endTime - startTime; | ||||
| internal->meterTimeTotal += duration; | |||||
| if (++internal->meterSamples >= samplesCount) { | |||||
| int64_t block = APP->engine->getBlock(); | |||||
| if (block > internal->meterLastBlock) { | |||||
| // Push time to buffer | // Push time to buffer | ||||
| internal->meterBuffer[internal->meterIndex++] = internal->meterTimeTotal / samplesCount; | |||||
| internal->meterIndex %= meterBufferLength; | |||||
| if (internal->meterSamples > 0) { | |||||
| internal->meterBuffer[internal->meterIndex++] = internal->meterTimeTotal / internal->meterSamples; | |||||
| internal->meterIndex %= meterBufferLength; | |||||
| } | |||||
| // Reset total | // Reset total | ||||
| internal->meterSamples = 0; | internal->meterSamples = 0; | ||||
| internal->meterTimeTotal = 0.f; | internal->meterTimeTotal = 0.f; | ||||
| } | } | ||||
| internal->meterLastBlock = block; | |||||
| internal->meterSamples++; | |||||
| internal->meterTimeTotal += duration; | |||||
| } | } | ||||
| // Iterate ports to step plug lights | // Iterate ports to step plug lights | ||||