+ All Categories
Home > Engineering > Designing and implementing embedded synthesizer UIs with JUCE (Geert Bevin, Amos Gaynes)

Designing and implementing embedded synthesizer UIs with JUCE (Geert Bevin, Amos Gaynes)

Date post: 29-Jan-2018
Category:
Upload: geert-bevin
View: 118 times
Download: 5 times
Share this document with a friend
78
EMBEDDED SYNTHESIZER UIS WITH JUCE
Transcript

EMBEDDED SYNTHESIZER UIS WITH JUCE

AMOS GAYNESGEERT BEVIN

ENGINEERS ATMOOG MUSIC INC.

WHAT IS THIS ABOUT?

WHAT IS THIS ABOUT?

> Product with a central LCD UI> Physical front-panel with knobs and switches

> Embedded Linux at its core> JUCE as cross-platform framework

(develop on macOS, run on Linux)> We learned a few things we'd like to share

AGENDA

> Hardware GUI design> Embrace the ProJucer

> UI navigation> Instantiate early

> Virtual Panel> Automated tests

DESIGN

HARDWARE GUI DESIGN

HARDWARE GUI DESIGN

> Create a state of flow> Target display hardware

> Hard versus soft controls

CREATE A STATE OF FLOW

CREATE A STATE OF FLOW

> Immediacy> Immersion

> Support user intuition

TARGET DISPLAY HARDWARE

TARGET DISPLAY HARDWARE

> Colours, gamma, contrast> Font rendering (OSX v. Linux)

> Refresh rate> Glare and environment

HARD VERSUS SOFT CONTROLS

HARD VERSUS SOFT CONTROLS

> Hard as in hardware (fixed-purpose)High frequencyHigh immediacy

> Soft as in software-defined (changeable)Embedded GUI provides soft control and feedback

IMPLEMENT

EMBRACE THE PROJUCER

EMBRACE THE PROJUCER

> Project management done automatically> WYSIWYG UI and graphics editing

> Component-level re-use> Full code-level tweakability

> Embed external binary resources

PROJECT MANAGEMENT DONE AUTOMATICALLY

PROJECT MANAGEMENT DONE AUTOMATICALLY

PROJECT MANAGEMENT DONE AUTOMATICALLY

PROJECT MANAGEMENT DONE AUTOMATICALLY

WYSIWYG UI AND GRAPHICS EDITING

WYSIWYG UI AND GRAPHICS EDITING

WYSIWYG UI AND GRAPHICS EDITING

COMPONENT-LEVELRE-USE

COMPONENT-LEVEL RE-USE

COMPONENT-LEVEL RE-USE

FULL CODE-LEVEL TWEAKABILITY

FULL CODE-LEVEL TWEAKABILITY

FULL CODE-LEVEL TWEAKABILITY

OscillatorGraph::OscillatorGraph() { //[Constructor_pre] You can add your own custom stuff here.. mix_ = 0.f; pulseWidth_ = 0.f; mode_ = "F"; //[/Constructor_pre]

//[UserPreSize] //[/UserPreSize]

setSize (380, 280);

//[Constructor] You can add your own custom stuff here.. //[/Constructor]}

FULL CODE-LEVEL TWEAKABILITY

FULL CODE-LEVEL TWEAKABILITYvoid OscillatorGraph::resized() { //[UserPreResize] Add your own custom resize code here.. //[/UserPreResize]

internalPath1.clear(); internalPath1.startNewSubPath (19.0f, 257.0f); internalPath1.lineTo (76.0f, 23.0f); internalPath1.lineTo (133.0f, 257.0f); internalPath1.lineTo (190.0f, 23.0f); internalPath1.lineTo (247.0f, 257.0f); internalPath1.lineTo (304.0f, 23.0f); internalPath1.lineTo (361.0f, 257.0f);

//[UserResized] Add your own custom resize handling here.. internalPath1.clear(); internalPath1 = drawOscillator(Rectangle<float>(19.f, 23.f, 342.f, 234.f), mix_, pulseWidth_, mode_); //[/UserResized]}

EMBED EXTERNAL BINARY RESOURCES

EMBED EXTERNAL BINARY RESOURCES

EMBED EXTERNAL BINARY RESOURCESstruct LASLookAndFeel::Pimpl { Pimpl() : futuraPtBook_(Typeface::createSystemTypefaceFor(BinaryData::FTN45_Futura_PT_Book_otf, BinaryData::FTN45_Futura_PT_Book_otfSize)), futuraPtMedium_(Typeface::createSystemTypefaceFor(BinaryData::FTN55_Futura_PT_Medium_otf, BinaryData::FTN55_Futura_PT_Medium_otfSize)) {}

Typeface::Ptr getTypefaceForFont(const Font& font) { if (font.getTypefaceName() == "Futura PT") { if (font.getTypefaceStyle() == "Heavy") { return futuraPtHeavy_; } else { return futuraPtMedium_; } }

return nullptr; }

Typeface::Ptr futuraPtBook_; Typeface::Ptr futuraPtMedium_;};

EMBED EXTERNAL BINARY RESOURCESstruct LASLookAndFeel::Pimpl { Pimpl() : futuraPtBook_(Typeface::createSystemTypefaceFor(BinaryData::FTN45_Futura_PT_Book_otf, BinaryData::FTN45_Futura_PT_Book_otfSize)), futuraPtMedium_(Typeface::createSystemTypefaceFor(BinaryData::FTN55_Futura_PT_Medium_otf, BinaryData::FTN55_Futura_PT_Medium_otfSize)) {}

Typeface::Ptr getTypefaceForFont(const Font& font) { if (font.getTypefaceName() == "Futura PT") { if (font.getTypefaceStyle() == "Heavy") { return futuraPtHeavy_; } else { return futuraPtMedium_; } }

return nullptr; }

Typeface::Ptr futuraPtBook_; Typeface::Ptr futuraPtMedium_;};

DESIGN

UI NAVIGATION

> Consistent grammar> Navigational widgets

> Examples

CONSISTENT GRAMMAR

CONSISTENT GRAMMAR

> Predictability> Muscle memory> Fluid navigation

NAVIGATIONAL WIDGETS

NAVIGATIONAL WIDGETS

> Limitations can be a strength> Keep UI organisation as shallow as possible

> Dedicated buttons are quicker than lists> Minimise long lists - group by related functions

UI NAVIGATION EXAMPLES

UI NAVIGATION EXAMPLES

> Rotate encoder to scroll or adjust value> Shift-rotate encoder to move selected item

> Press encoder to change UI focus> Shift-press encoder to confirm action

IMPLEMENT

INSTANTIATE EARLY

INSTANTIATE EARLY

> Plenty of memory, limited CPU> Rapid dispatch and propagation> Whole UI is always up-to-date> Update data without redraws

PLENTY OF MEMORY, LIMITED CPU

PLENTY OF MEMORY, LIMITED CPU

MainComponent::MainComponent() { //[UserPreSize] // create all UI panels lfo1_ = new LFOPanel(0); lfo1_->setComponentID(LFO1); lfo2_ = new LFOPanel(1); lfo2_->setComponentID(LFO2); oscillator1_ = new OscillatorPanel(0); oscillator1_->setComponentID(OSCILLATOR1); oscillator2_ = new OscillatorPanel(1); oscillator2_->setComponentID(OSCILLATOR2); // ... addChildComponent(lfo1_); addChildComponent(lfo2_); addChildComponent(oscillator1_); addChildComponent(oscillator2_); // ... for (int c = 0; c < getNumChildComponents(); ++c) { getChildComponent(c)->setVisible(false); } //[/UserPreSize]

PLENTY OF MEMORY, LIMITED CPU

void MainComponent::showPanel(Component* panel) { if (panel) { Component* parent = panel->getParentComponent(); if (parent) { for (int i = 0; i < parent->getNumChildComponents(); ++i) { Component* child = parent->getChildComponent(i); if (child && child != panel) { child->setVisible(false); } } } panel->setVisible(true); }}

PLENTY OF MEMORY, LIMITED CPU

LFOPanel::LFOPanel(int ordinal) : ordinal_(ordinal) { addAndMakeVisible(rate_ = new Label(String(), TRANS("0.01ms")));

//[UserPreSize] rate_->setComponentID(RATE); //[/UserPreSize] setSize (760, 460);

//[Constructor] You can add your own custom stuff here.. LApp->registerComponentUpdater(lfo1Rate, new LFORateUpdater(this)); //[/Constructor]}

PLENTY OF MEMORY, LIMITED CPU

struct LFORateUpdater : ComponentUpdater {

LFORateUpdater(LFOPanel* panel) : panel_(panel) { };

void update(var value, var previous) override { panel_->updateRateLabel(value); }

LFOPanel* const panel_;};

RAPID DISPATCH AND PROPAGATION

RAPID DISPATCH AND PROPAGATION

void registerComponentUpdater(UpdaterIndex i, ComponentUpdater* c) { if (componentUpdaterMap_.contains(i)) { ComponentUpdater* updater = componentUpdaterMap_[i]; componentUpdaterMap_.remove(i); delete updater; } if (c != nullptr) componentUpdaterMap_.set(i, c);}void updateComponent(UpdaterIndex i, var value) { if (!lastComponentValues_.contains(i) || lastComponentValues_[i] != value) { var previous = lastComponentValues_[i];

lastComponentValues_.set(i, value);

componentUpdaterMap_[i]->update(value, previous); }}HashMap<UpdaterIndex, ComponentUpdater*> componentUpdaterMap_;

UPDATE DATA WITHOUT REDRAWS

UPDATE DATA WITHOUT REDRAWS

void LFOPanel::updateRateLabel(var rate) { rate_->setText(String(int(rate)) + " Hz", dontSendNotification);}

void LFOPanel::setVisible(bool shouldBeVisible) { rate_->setVisible(shouldBeVisible);

Component::setVisible(shouldBeVisible);}

DESIGN

VIRTUAL PANEL

VIRTUAL PANEL

> Simulated hardware UI> Sends same messages to GUI application

> Ready when your hardware isn’t

IMPLEMENT

AUTOMATED TESTS

AUTOMATED TESTS

> Semantic UI tests> Functional tests and unit tests

> Useful test runner tricks> Hook into Continuous Integration

SEMANTIC UI TESTS

SEMANTIC UI TESTS

class LFOPanelTests : public GUITest {public: LFOPanelTests() : GUITest("LFOPanelTests") {} void runTest() override { beginGUITest("LFO 1 Visibility"); { // simulate hardware front panel input // ...

expect(getComponentVisibility(IDPATH(LFO1)), "lfo1 not visible"); expect(!getComponentVisibility(IDPATH(LFO2)), "lfo2 visible");

screenshot("lfo1Panel"); } }}

SEMANTIC UI TESTS

class GUITest : public UnitTest { public: Component* findComponent(const StringArray& idPath) const { Component* result = LASFrontEndApplication::getAppInstance()->getMainComponent(); for (String id : idPath) { result = result->findChildWithID(id); if (result == nullptr) return nullptr; } return result; }

bool getComponentVisibility(const StringArray& idPath) const { Component* component = findComponent(idPath); if (nullptr == component) return false;

return component->isVisible(); }

SEMANTIC UI TESTS

beginGUITest("Rate Labels"); { expect(clearLabelText(IDPATH(LFO1,RATE))); expect(clearLabelText(IDPATH(LFO2,RATE)));

// simulate hardware front panel input // ...

expectEquals(getLabelText(IDPATH(LFO1,RATE)), String("0.9 Hz")); expectEquals(getLabelText(IDPATH(LFO2,RATE)), String("100 Hz"));

screenshot("rates"); }

FUNCTIONAL TESTS AND UNIT TESTS

FUNCTIONAL TESTS AND UNIT TESTSclass BitPackedTests : public UnitTest {public: BitPackedTests() : UnitTest("BitPackedTests") {} void runTest() override { beginTest("Copy constructor"); { BitPacked p1; p1.add32Bit(0b10101010001010101010010101010010); p1.add32Bit(0b11110001001000101010101101001011); BitPacked p2(p1);

expectEquals(p2.count16Bit(), 4);

expectEquals(p2.get16Bit(0), (uint16_t)0b1010010101010010); expectEquals(p2.get16Bit(1), (uint16_t)0b1010101000101010); expectEquals(p2.get16Bit(2), (uint16_t)0b1010101101001011); expectEquals(p2.get16Bit(3), (uint16_t)0b1111000100100010); } }}static BitPackedTests bitPackedTests;

USEFUL TEST RUNNER TRICKS

USEFUL TEST RUNNER TRICKS

> Ship your executable with all tests includedvoid LASFrontEndApplication::initialise(const String& commandLine){ // Fully setup and initialise the application // ...

// Run tests if necessary if (!testRunner_.scheduleTests()) // If no tests are going to run, populate with example data if that was requested // ... }}

LASTestRunner testRunner_;

USEFUL TEST RUNNER TRICKS

> Ship your executable with all tests includedbool LASTestRunner::Pimpl::scheduleTests() { bool runTests = false; #if JUCE_DEBUG if (!JUCEApplication::getInstance()->getCommandLineParameters().contains("--disable-tests")) { runTests = true; } #endif if (JUCEApplication::getInstance()->getCommandLineParameters().contains("--enable-tests")) { runTests = true; }

if (runTests) { (new ScheduleTestsCallback(this))->post(); }}

USEFUL TEST RUNNER TRICKS

> Run your tests asynchronouslystruct ScheduleTestsCallback : public CallbackMessage { ScheduleTestsCallback(LASTestRunner::Pimpl* runner) : runner_(runner) {} void messageCallback(); LASTestRunner::Pimpl* runner_;};

struct LASTestRunner::Pimpl : public Thread { Pimpl() : Thread("LASTestRunner thread") {} void run() { UnitTestRunner unitTestRunner; // ... }}

void ScheduleTestsCallback::messageCallback() { runner_->startThread();}

USEFUL TEST RUNNER TRICKS

> Easily focus on specific testsUnitTestRunner unitTestRunner;

Array<UnitTest*> focusedTests;Array<UnitTest*>& allTests = UnitTest::getAllTests();for (UnitTest* test : allTests) { if (test->getName().isEmpty { focusedTests.add(test); }}

if (focusedTests.isEmpty()) unitTestRunner.runTests(allTests);else unitTestRunner.runTests(focusedTests);

HOOK INTO CONTINUOUS INTEGRATION (GITLAB)

HOOK INTO CONTINUOUS INTEGRATION (GITLAB).test_template: &test_definition stage: test variables: GIT_STRATEGY: none image: gbevin/raspberry-ci script: - LAS_TEST=gitlab DISPLAY=:99 xvfb-run -n 99 \ -s "-ac -screen 0 800x480x16" Builds/LinuxMakefile/build/las-frontend --enable-tests artifacts: when: always paths: - las-frontend.log - las-frontend.db - las-screenshot*.png

test:pi2: <<: *test_definition tags: - raspberrypi2

Thank you!

www.moogmusic.com


Recommended