| @@ -30,7 +30,7 @@ struct Engine { | |||
| /** Advances the engine by `frames` frames. | |||
| Only call this method from the primary module. | |||
| */ | |||
| void step(int frames); | |||
| void stepBlock(int frames); | |||
| void setPrimaryModule(Module* module); | |||
| Module* getPrimaryModule(); | |||
| @@ -41,29 +41,32 @@ struct Engine { | |||
| */ | |||
| float getSampleTime(); | |||
| /** 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(); | |||
| /** 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(); | |||
| /** 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`. | |||
| */ | |||
| 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`. | |||
| */ | |||
| double getStepDuration(); | |||
| double getBlockDuration(); | |||
| // Modules | |||
| size_t getNumModules(); | |||
| @@ -348,7 +348,7 @@ struct Module { | |||
| PRIVATE const float* meterBuffer(); | |||
| PRIVATE int meterLength(); | |||
| 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 meterIndex = module->meterIndex(); | |||
| float meterMax = 0.f; | |||
| float meterAvg = 0.f; | |||
| for (int i = 0; i < meterLength; i++) { | |||
| meterAvg += meterBuffer[i]; | |||
| float m = meterBuffer[i]; | |||
| meterAvg += m; | |||
| meterMax = std::max(meterMax, m); | |||
| } | |||
| 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 | |||
| nvgBeginPath(args.vg); | |||
| nvgMoveTo(args.vg, box.size.x, box.size.y); | |||
| for (int i = 0; i < meterLength; i++) { | |||
| int index = (meterIndex - i + meterLength) % meterLength; | |||
| float percent = math::clamp(meterBuffer[index] * mult * sampleRate, 0.f, 1.f); | |||
| math::Vec p; | |||
| 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); | |||
| } | |||
| 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); | |||
| nvgClosePath(args.vg); | |||
| nvgFillColor(args.vg, color); | |||
| 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 | |||
| nvgStrokeColor(args.vg, color); | |||
| nvgBeginPath(args.vg); | |||
| @@ -245,7 +245,7 @@ struct AudioInterface : Module, audio::Port { | |||
| bool isPrimary = (APP->engine->getPrimaryModule() == this); | |||
| // Step engine | |||
| if (isPrimary && requestedEngineFrames > 0) { | |||
| APP->engine->step(requestedEngineFrames); | |||
| APP->engine->stepBlock(requestedEngineFrames); | |||
| } | |||
| } | |||
| @@ -73,7 +73,7 @@ struct MIDI_CC : Module { | |||
| while (!midiInput.queue.empty()) { | |||
| 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. | |||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||
| break; | |||
| processMessage(msg); | |||
| midiInput.queue.pop(); | |||
| @@ -124,7 +124,7 @@ struct MIDI_CV : Module { | |||
| while (!midiInput.queue.empty()) { | |||
| 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. | |||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||
| break; | |||
| processMessage(msg); | |||
| midiInput.queue.pop(); | |||
| @@ -639,6 +639,10 @@ struct MIDI_CVWidget : ModuleWidget { | |||
| panicItem->text = "Panic"; | |||
| panicItem->module = module; | |||
| 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()) { | |||
| 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. | |||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||
| break; | |||
| processMessage(msg); | |||
| midiInput.queue.pop(); | |||
| @@ -85,7 +85,7 @@ struct MIDI_Map : Module { | |||
| while (!midiInput.queue.empty()) { | |||
| 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. | |||
| if (msg.timestamp + APP->engine->getStepDuration() > APP->engine->getFrameTime()) | |||
| if (msg.timestamp + APP->engine->getBlockDuration() > APP->engine->getFrameTime()) | |||
| break; | |||
| processMessage(msg); | |||
| midiInput.queue.pop(); | |||
| @@ -196,10 +196,11 @@ struct Engine::Internal { | |||
| float sampleRate = 0.f; | |||
| float sampleTime = 0.f; | |||
| int64_t block = 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; | |||
| // Parameter smoothing | |||
| @@ -207,15 +208,15 @@ struct Engine::Internal { | |||
| int smoothParamId = 0; | |||
| 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; | |||
| /** 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; | |||
| std::vector<EngineWorker> workers; | |||
| @@ -310,7 +311,7 @@ static void Engine_stepWorker(Engine* that, int threadId) { | |||
| break; | |||
| 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); | |||
| // Configure thread | |||
| initMXCSR(); | |||
| 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 | |||
| if (internal->sampleRate != settings::sampleRate) { | |||
| @@ -538,9 +539,11 @@ void Engine::step(int frames) { | |||
| yieldWorkers(); | |||
| 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() { | |||
| return internal->frame; | |||
| } | |||
| 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) { | |||
| // 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); | |||
| clear(); | |||
| // modules | |||
| @@ -1,8 +1,10 @@ | |||
| #include <engine/Module.hpp> | |||
| #include <engine/Engine.hpp> | |||
| #include <plugin.hpp> | |||
| #include <system.hpp> | |||
| #include <settings.hpp> | |||
| #include <asset.hpp> | |||
| #include <context.hpp> | |||
| namespace rack { | |||
| @@ -11,19 +13,19 @@ namespace engine { | |||
| // 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; | |||
| struct Module::Internal { | |||
| bool bypass = false; | |||
| float meterTimeTotal = 0.f; | |||
| int64_t meterLastBlock = 0; | |||
| int meterSamples = 0; | |||
| int meterIndex = 0; | |||
| float meterTimeTotal = 0.f; | |||
| 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. | |||
| bool meterEnabled = settings::cpuMeter && (args.frame % meterDivider == 0); | |||
| @@ -347,15 +349,21 @@ void Module::step(const ProcessArgs& args) { | |||
| double endTime = system::getTime(); | |||
| 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 | |||
| 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 | |||
| internal->meterSamples = 0; | |||
| internal->meterTimeTotal = 0.f; | |||
| } | |||
| internal->meterLastBlock = block; | |||
| internal->meterSamples++; | |||
| internal->meterTimeTotal += duration; | |||
| } | |||
| // Iterate ports to step plug lights | |||