SlideShare a Scribd company logo
1 of 78
Download to read offline
EMBEDDED
SYNTHESIZER
UIS WITH JUCE
AMOS GAYNES
GEERT BEVIN
ENGINEERS AT
MOOG 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 frequency
High 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-LEVEL
RE-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 TWEAKABILITY
void 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 RESOURCES
struct 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 RESOURCES
struct 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 TESTS
class 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 included
void 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 included
bool 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 asynchronously
struct 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 tests
UnitTestRunner 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

More Related Content

What's hot

Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...
Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...
Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...dantleech
 
Меняем javascript с помощью javascript
Меняем javascript с помощью javascriptМеняем javascript с помощью javascript
Меняем javascript с помощью javascriptPavel Volokitin
 
#PDR15 - Developing for Round
#PDR15 - Developing for Round#PDR15 - Developing for Round
#PDR15 - Developing for RoundPebble Technology
 
Formal Verification of Transactional Interaction Contract
Formal Verification of Transactional Interaction ContractFormal Verification of Transactional Interaction Contract
Formal Verification of Transactional Interaction ContractGera Shegalov
 
Kotlin for android developers whats new
Kotlin for android developers whats newKotlin for android developers whats new
Kotlin for android developers whats newSerghii Chaban
 
JavaScript Event Loop
JavaScript Event LoopJavaScript Event Loop
JavaScript Event LoopDesignveloper
 
Powershell Tech Ed2009
Powershell Tech Ed2009Powershell Tech Ed2009
Powershell Tech Ed2009rsnarayanan
 
Currying and Partial Function Application (PFA)
Currying and Partial Function Application (PFA)Currying and Partial Function Application (PFA)
Currying and Partial Function Application (PFA)Dhaval Dalal
 
JavaScript ∩ WebAssembly
JavaScript ∩ WebAssemblyJavaScript ∩ WebAssembly
JavaScript ∩ WebAssemblyTadeu Zagallo
 
The Ring programming language version 1.5.2 book - Part 9 of 181
The Ring programming language version 1.5.2 book - Part 9 of 181The Ring programming language version 1.5.2 book - Part 9 of 181
The Ring programming language version 1.5.2 book - Part 9 of 181Mahmoud Samir Fayed
 
The Ring programming language version 1.3 book - Part 59 of 88
The Ring programming language version 1.3 book - Part 59 of 88The Ring programming language version 1.3 book - Part 59 of 88
The Ring programming language version 1.3 book - Part 59 of 88Mahmoud Samir Fayed
 
Sequencing Audio Using React and the Web Audio API
Sequencing Audio Using React and the Web Audio APISequencing Audio Using React and the Web Audio API
Sequencing Audio Using React and the Web Audio APIVincent Riemer
 
Creating Lazy stream in CSharp
Creating Lazy stream in CSharpCreating Lazy stream in CSharp
Creating Lazy stream in CSharpDhaval Dalal
 
Understaing Android EGL
Understaing Android EGLUnderstaing Android EGL
Understaing Android EGLSuhan Lee
 

What's hot (20)

Android workshop
Android workshopAndroid workshop
Android workshop
 
Sneaking inside Kotlin features
Sneaking inside Kotlin featuresSneaking inside Kotlin features
Sneaking inside Kotlin features
 
Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...
Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...
Building and Incredible Machine with Pipelines and Generators in PHP (IPC Ber...
 
Меняем javascript с помощью javascript
Меняем javascript с помощью javascriptМеняем javascript с помощью javascript
Меняем javascript с помощью javascript
 
#PDR15 - Developing for Round
#PDR15 - Developing for Round#PDR15 - Developing for Round
#PDR15 - Developing for Round
 
Formal Verification of Transactional Interaction Contract
Formal Verification of Transactional Interaction ContractFormal Verification of Transactional Interaction Contract
Formal Verification of Transactional Interaction Contract
 
Kotlin for android developers whats new
Kotlin for android developers whats newKotlin for android developers whats new
Kotlin for android developers whats new
 
JavaScript Event Loop
JavaScript Event LoopJavaScript Event Loop
JavaScript Event Loop
 
C++ Programs
C++ ProgramsC++ Programs
C++ Programs
 
Powershell Tech Ed2009
Powershell Tech Ed2009Powershell Tech Ed2009
Powershell Tech Ed2009
 
JavaScript Event Loop
JavaScript Event LoopJavaScript Event Loop
JavaScript Event Loop
 
Currying and Partial Function Application (PFA)
Currying and Partial Function Application (PFA)Currying and Partial Function Application (PFA)
Currying and Partial Function Application (PFA)
 
9.1 Mystery Tour
9.1 Mystery Tour9.1 Mystery Tour
9.1 Mystery Tour
 
JavaScript ∩ WebAssembly
JavaScript ∩ WebAssemblyJavaScript ∩ WebAssembly
JavaScript ∩ WebAssembly
 
The Ring programming language version 1.5.2 book - Part 9 of 181
The Ring programming language version 1.5.2 book - Part 9 of 181The Ring programming language version 1.5.2 book - Part 9 of 181
The Ring programming language version 1.5.2 book - Part 9 of 181
 
The Ring programming language version 1.3 book - Part 59 of 88
The Ring programming language version 1.3 book - Part 59 of 88The Ring programming language version 1.3 book - Part 59 of 88
The Ring programming language version 1.3 book - Part 59 of 88
 
Sequencing Audio Using React and the Web Audio API
Sequencing Audio Using React and the Web Audio APISequencing Audio Using React and the Web Audio API
Sequencing Audio Using React and the Web Audio API
 
Workshop 10: ECMAScript 6
Workshop 10: ECMAScript 6Workshop 10: ECMAScript 6
Workshop 10: ECMAScript 6
 
Creating Lazy stream in CSharp
Creating Lazy stream in CSharpCreating Lazy stream in CSharp
Creating Lazy stream in CSharp
 
Understaing Android EGL
Understaing Android EGLUnderstaing Android EGL
Understaing Android EGL
 

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

Tamir Dresher - What’s new in ASP.NET Core 6
Tamir Dresher - What’s new in ASP.NET Core 6Tamir Dresher - What’s new in ASP.NET Core 6
Tamir Dresher - What’s new in ASP.NET Core 6Tamir Dresher
 
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfCreating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfShaiAlmog1
 
Sahana Eden - Introduction to the Code
Sahana Eden - Introduction to the CodeSahana Eden - Introduction to the Code
Sahana Eden - Introduction to the CodeAidIQ
 
Rntb20200805
Rntb20200805Rntb20200805
Rntb20200805t k
 
Adopting 3D Touch in your apps
Adopting 3D Touch in your appsAdopting 3D Touch in your apps
Adopting 3D Touch in your appsJuan C Catalan
 
MBLTDev15: Egor Tolstoy, Rambler&Co
MBLTDev15: Egor Tolstoy, Rambler&CoMBLTDev15: Egor Tolstoy, Rambler&Co
MBLTDev15: Egor Tolstoy, Rambler&Coe-Legion
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modelingMario Fusco
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modelingCodemotion
 
Category theory, Monads, and Duality in the world of (BIG) Data
Category theory, Monads, and Duality in the world of (BIG) DataCategory theory, Monads, and Duality in the world of (BIG) Data
Category theory, Monads, and Duality in the world of (BIG) Datagreenwop
 
22Flutter.pdf
22Flutter.pdf22Flutter.pdf
22Flutter.pdfdbaman
 
オープンデータを使ったモバイルアプリ開発(応用編)
オープンデータを使ったモバイルアプリ開発(応用編)オープンデータを使ったモバイルアプリ開発(応用編)
オープンデータを使ったモバイルアプリ開発(応用編)Takayuki Goto
 

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

Tamir Dresher - What’s new in ASP.NET Core 6
Tamir Dresher - What’s new in ASP.NET Core 6Tamir Dresher - What’s new in ASP.NET Core 6
Tamir Dresher - What’s new in ASP.NET Core 6
 
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfCreating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdf
 
Sahana Eden - Introduction to the Code
Sahana Eden - Introduction to the CodeSahana Eden - Introduction to the Code
Sahana Eden - Introduction to the Code
 
Qe Reference
Qe ReferenceQe Reference
Qe Reference
 
shiny.pdf
shiny.pdfshiny.pdf
shiny.pdf
 
Rntb20200805
Rntb20200805Rntb20200805
Rntb20200805
 
Advanced redux
Advanced reduxAdvanced redux
Advanced redux
 
Adopting 3D Touch in your apps
Adopting 3D Touch in your appsAdopting 3D Touch in your apps
Adopting 3D Touch in your apps
 
Refactoring
RefactoringRefactoring
Refactoring
 
Backendless apps
Backendless appsBackendless apps
Backendless apps
 
MBLTDev15: Egor Tolstoy, Rambler&Co
MBLTDev15: Egor Tolstoy, Rambler&CoMBLTDev15: Egor Tolstoy, Rambler&Co
MBLTDev15: Egor Tolstoy, Rambler&Co
 
mobl
moblmobl
mobl
 
Managing console
Managing consoleManaging console
Managing console
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modeling
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modeling
 
To-Do App With Flutter: Step By Step Guide
To-Do App With Flutter: Step By Step GuideTo-Do App With Flutter: Step By Step Guide
To-Do App With Flutter: Step By Step Guide
 
Category theory, Monads, and Duality in the world of (BIG) Data
Category theory, Monads, and Duality in the world of (BIG) DataCategory theory, Monads, and Duality in the world of (BIG) Data
Category theory, Monads, and Duality in the world of (BIG) Data
 
Day 5
Day 5Day 5
Day 5
 
22Flutter.pdf
22Flutter.pdf22Flutter.pdf
22Flutter.pdf
 
オープンデータを使ったモバイルアプリ開発(応用編)
オープンデータを使ったモバイルアプリ開発(応用編)オープンデータを使ったモバイルアプリ開発(応用編)
オープンデータを使ったモバイルアプリ開発(応用編)
 

More from Geert Bevin

Intuitive User Interface Design for Modern Synthesizers
Intuitive User Interface Design for Modern SynthesizersIntuitive User Interface Design for Modern Synthesizers
Intuitive User Interface Design for Modern SynthesizersGeert Bevin
 
Mobile Music Making with iOS
Mobile Music Making with iOSMobile Music Making with iOS
Mobile Music Making with iOSGeert Bevin
 
From Arduino to LinnStrument
From Arduino to LinnStrumentFrom Arduino to LinnStrument
From Arduino to LinnStrumentGeert Bevin
 
LinnStrument : the ultimate open-source hacker instrument
LinnStrument : the ultimate open-source hacker instrumentLinnStrument : the ultimate open-source hacker instrument
LinnStrument : the ultimate open-source hacker instrumentGeert Bevin
 
The Death of a Mouse
The Death of a MouseThe Death of a Mouse
The Death of a MouseGeert Bevin
 
10 Reasons Why Java Now Rocks More Than Ever
10 Reasons Why Java Now Rocks More Than Ever10 Reasons Why Java Now Rocks More Than Ever
10 Reasons Why Java Now Rocks More Than EverGeert Bevin
 

More from Geert Bevin (6)

Intuitive User Interface Design for Modern Synthesizers
Intuitive User Interface Design for Modern SynthesizersIntuitive User Interface Design for Modern Synthesizers
Intuitive User Interface Design for Modern Synthesizers
 
Mobile Music Making with iOS
Mobile Music Making with iOSMobile Music Making with iOS
Mobile Music Making with iOS
 
From Arduino to LinnStrument
From Arduino to LinnStrumentFrom Arduino to LinnStrument
From Arduino to LinnStrument
 
LinnStrument : the ultimate open-source hacker instrument
LinnStrument : the ultimate open-source hacker instrumentLinnStrument : the ultimate open-source hacker instrument
LinnStrument : the ultimate open-source hacker instrument
 
The Death of a Mouse
The Death of a MouseThe Death of a Mouse
The Death of a Mouse
 
10 Reasons Why Java Now Rocks More Than Ever
10 Reasons Why Java Now Rocks More Than Ever10 Reasons Why Java Now Rocks More Than Ever
10 Reasons Why Java Now Rocks More Than Ever
 

Recently uploaded

Work Experience-Dalton Park.pptxfvvvvvvv
Work Experience-Dalton Park.pptxfvvvvvvvWork Experience-Dalton Park.pptxfvvvvvvv
Work Experience-Dalton Park.pptxfvvvvvvvLewisJB
 
Class 1 | NFPA 72 | Overview Fire Alarm System
Class 1 | NFPA 72 | Overview Fire Alarm SystemClass 1 | NFPA 72 | Overview Fire Alarm System
Class 1 | NFPA 72 | Overview Fire Alarm Systemirfanmechengr
 
Engineering Drawing section of solid
Engineering Drawing     section of solidEngineering Drawing     section of solid
Engineering Drawing section of solidnamansinghjarodiya
 
Indian Dairy Industry Present Status and.ppt
Indian Dairy Industry Present Status and.pptIndian Dairy Industry Present Status and.ppt
Indian Dairy Industry Present Status and.pptMadan Karki
 
Main Memory Management in Operating System
Main Memory Management in Operating SystemMain Memory Management in Operating System
Main Memory Management in Operating SystemRashmi Bhat
 
Sachpazis Costas: Geotechnical Engineering: A student's Perspective Introduction
Sachpazis Costas: Geotechnical Engineering: A student's Perspective IntroductionSachpazis Costas: Geotechnical Engineering: A student's Perspective Introduction
Sachpazis Costas: Geotechnical Engineering: A student's Perspective IntroductionDr.Costas Sachpazis
 
Risk Management in Engineering Construction Project
Risk Management in Engineering Construction ProjectRisk Management in Engineering Construction Project
Risk Management in Engineering Construction ProjectErbil Polytechnic University
 
Research Methodology for Engineering pdf
Research Methodology for Engineering pdfResearch Methodology for Engineering pdf
Research Methodology for Engineering pdfCaalaaAbdulkerim
 
11. Properties of Liquid Fuels in Energy Engineering.pdf
11. Properties of Liquid Fuels in Energy Engineering.pdf11. Properties of Liquid Fuels in Energy Engineering.pdf
11. Properties of Liquid Fuels in Energy Engineering.pdfHafizMudaserAhmad
 
Transport layer issues and challenges - Guide
Transport layer issues and challenges - GuideTransport layer issues and challenges - Guide
Transport layer issues and challenges - GuideGOPINATHS437943
 
Input Output Management in Operating System
Input Output Management in Operating SystemInput Output Management in Operating System
Input Output Management in Operating SystemRashmi Bhat
 
Past, Present and Future of Generative AI
Past, Present and Future of Generative AIPast, Present and Future of Generative AI
Past, Present and Future of Generative AIabhishek36461
 
Autonomous emergency braking system (aeb) ppt.ppt
Autonomous emergency braking system (aeb) ppt.pptAutonomous emergency braking system (aeb) ppt.ppt
Autonomous emergency braking system (aeb) ppt.pptbibisarnayak0
 
Earthing details of Electrical Substation
Earthing details of Electrical SubstationEarthing details of Electrical Substation
Earthing details of Electrical Substationstephanwindworld
 
Internet of things -Arshdeep Bahga .pptx
Internet of things -Arshdeep Bahga .pptxInternet of things -Arshdeep Bahga .pptx
Internet of things -Arshdeep Bahga .pptxVelmuruganTECE
 
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catchers
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor CatchersTechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catchers
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catcherssdickerson1
 
welding defects observed during the welding
welding defects observed during the weldingwelding defects observed during the welding
welding defects observed during the weldingMuhammadUzairLiaqat
 
Software and Systems Engineering Standards: Verification and Validation of Sy...
Software and Systems Engineering Standards: Verification and Validation of Sy...Software and Systems Engineering Standards: Verification and Validation of Sy...
Software and Systems Engineering Standards: Verification and Validation of Sy...VICTOR MAESTRE RAMIREZ
 
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTION
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTIONTHE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTION
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTIONjhunlian
 

Recently uploaded (20)

Work Experience-Dalton Park.pptxfvvvvvvv
Work Experience-Dalton Park.pptxfvvvvvvvWork Experience-Dalton Park.pptxfvvvvvvv
Work Experience-Dalton Park.pptxfvvvvvvv
 
Class 1 | NFPA 72 | Overview Fire Alarm System
Class 1 | NFPA 72 | Overview Fire Alarm SystemClass 1 | NFPA 72 | Overview Fire Alarm System
Class 1 | NFPA 72 | Overview Fire Alarm System
 
Engineering Drawing section of solid
Engineering Drawing     section of solidEngineering Drawing     section of solid
Engineering Drawing section of solid
 
Indian Dairy Industry Present Status and.ppt
Indian Dairy Industry Present Status and.pptIndian Dairy Industry Present Status and.ppt
Indian Dairy Industry Present Status and.ppt
 
Main Memory Management in Operating System
Main Memory Management in Operating SystemMain Memory Management in Operating System
Main Memory Management in Operating System
 
Sachpazis Costas: Geotechnical Engineering: A student's Perspective Introduction
Sachpazis Costas: Geotechnical Engineering: A student's Perspective IntroductionSachpazis Costas: Geotechnical Engineering: A student's Perspective Introduction
Sachpazis Costas: Geotechnical Engineering: A student's Perspective Introduction
 
Risk Management in Engineering Construction Project
Risk Management in Engineering Construction ProjectRisk Management in Engineering Construction Project
Risk Management in Engineering Construction Project
 
Research Methodology for Engineering pdf
Research Methodology for Engineering pdfResearch Methodology for Engineering pdf
Research Methodology for Engineering pdf
 
11. Properties of Liquid Fuels in Energy Engineering.pdf
11. Properties of Liquid Fuels in Energy Engineering.pdf11. Properties of Liquid Fuels in Energy Engineering.pdf
11. Properties of Liquid Fuels in Energy Engineering.pdf
 
Transport layer issues and challenges - Guide
Transport layer issues and challenges - GuideTransport layer issues and challenges - Guide
Transport layer issues and challenges - Guide
 
Input Output Management in Operating System
Input Output Management in Operating SystemInput Output Management in Operating System
Input Output Management in Operating System
 
Past, Present and Future of Generative AI
Past, Present and Future of Generative AIPast, Present and Future of Generative AI
Past, Present and Future of Generative AI
 
Autonomous emergency braking system (aeb) ppt.ppt
Autonomous emergency braking system (aeb) ppt.pptAutonomous emergency braking system (aeb) ppt.ppt
Autonomous emergency braking system (aeb) ppt.ppt
 
Earthing details of Electrical Substation
Earthing details of Electrical SubstationEarthing details of Electrical Substation
Earthing details of Electrical Substation
 
young call girls in Green Park🔝 9953056974 🔝 escort Service
young call girls in Green Park🔝 9953056974 🔝 escort Serviceyoung call girls in Green Park🔝 9953056974 🔝 escort Service
young call girls in Green Park🔝 9953056974 🔝 escort Service
 
Internet of things -Arshdeep Bahga .pptx
Internet of things -Arshdeep Bahga .pptxInternet of things -Arshdeep Bahga .pptx
Internet of things -Arshdeep Bahga .pptx
 
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catchers
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor CatchersTechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catchers
TechTAC® CFD Report Summary: A Comparison of Two Types of Tubing Anchor Catchers
 
welding defects observed during the welding
welding defects observed during the weldingwelding defects observed during the welding
welding defects observed during the welding
 
Software and Systems Engineering Standards: Verification and Validation of Sy...
Software and Systems Engineering Standards: Verification and Validation of Sy...Software and Systems Engineering Standards: Verification and Validation of Sy...
Software and Systems Engineering Standards: Verification and Validation of Sy...
 
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTION
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTIONTHE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTION
THE SENDAI FRAMEWORK FOR DISASTER RISK REDUCTION
 

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

  • 4. 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
  • 5. AGENDA > Hardware GUI design > Embrace the ProJucer > UI navigation > Instantiate early > Virtual Panel > Automated tests
  • 8. HARDWARE GUI DESIGN > Create a state of flow > Target display hardware > Hard versus soft controls
  • 9. CREATE A STATE OF FLOW
  • 10. CREATE A STATE OF FLOW > Immediacy > Immersion > Support user intuition
  • 12. TARGET DISPLAY HARDWARE > Colours, gamma, contrast > Font rendering (OSX v. Linux) > Refresh rate > Glare and environment
  • 14. HARD VERSUS SOFT CONTROLS > Hard as in hardware (fixed-purpose) High frequency High immediacy > Soft as in software-defined (changeable) Embedded GUI provides soft control and feedback
  • 15.
  • 18. EMBRACE THE PROJUCER > Project management done automatically > WYSIWYG UI and graphics editing > Component-level re-use > Full code-level tweakability > Embed external binary resources
  • 20. PROJECT MANAGEMENT DONE AUTOMATICALLY
  • 21. PROJECT MANAGEMENT DONE AUTOMATICALLY
  • 22. PROJECT MANAGEMENT DONE AUTOMATICALLY
  • 24. WYSIWYG UI AND GRAPHICS EDITING
  • 25. WYSIWYG UI AND GRAPHICS EDITING
  • 31. 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] }
  • 33. FULL CODE-LEVEL TWEAKABILITY void 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] }
  • 36. EMBED EXTERNAL BINARY RESOURCES struct 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_; };
  • 37. EMBED EXTERNAL BINARY RESOURCES struct 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_; };
  • 39. UI NAVIGATION > Consistent grammar > Navigational widgets > Examples
  • 41. CONSISTENT GRAMMAR > Predictability > Muscle memory > Fluid navigation
  • 43. 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
  • 44.
  • 46. 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
  • 49. INSTANTIATE EARLY > Plenty of memory, limited CPU > Rapid dispatch and propagation > Whole UI is always up-to-date > Update data without redraws
  • 51. 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]
  • 52. 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); } }
  • 53. 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] }
  • 54. 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_; };
  • 56. 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_;
  • 58. 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); }
  • 61. VIRTUAL PANEL > Simulated hardware UI > Sends same messages to GUI application > Ready when your hardware isn’t
  • 64. AUTOMATED TESTS > Semantic UI tests > Functional tests and unit tests > Useful test runner tricks > Hook into Continuous Integration
  • 66. 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"); } } }
  • 67. 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(); }
  • 68. 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"); }
  • 70. FUNCTIONAL TESTS AND UNIT TESTS class 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;
  • 72. USEFUL TEST RUNNER TRICKS > Ship your executable with all tests included void 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_;
  • 73. USEFUL TEST RUNNER TRICKS > Ship your executable with all tests included bool 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(); } }
  • 74. USEFUL TEST RUNNER TRICKS > Run your tests asynchronously struct 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(); }
  • 75. USEFUL TEST RUNNER TRICKS > Easily focus on specific tests UnitTestRunner 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);
  • 77. 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