diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 908dfc45..ea51c060 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -134,11 +134,14 @@ set(EXTCPPSRC #mcu_temperature external/mcu_temperature/main.cpp external/mcu_temperature/mcu_temperature.cpp - #fmradio external/fmradio/main.cpp external/fmradio/ui_fmradio.cpp + + #tuner + external/tuner/main.cpp + external/tuner/ui_tuner.cpp ) set(EXTAPPLIST @@ -172,7 +175,8 @@ set(EXTAPPLIST ook_editor shoppingcart_lock flippertx - remote + remote mcu_temperature fmradio + tuner ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 9177dc07..28a568ac 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -55,7 +55,9 @@ MEMORY ram_external_app_ook_editor(rwx) : org = 0xADCE0000, len = 32k ram_external_app_remote(rwx) : org = 0xADCF0000, len = 32k ram_external_app_mcu_temperature(rwx) : org = 0xADD00000, len = 32k - ram_external_app_fmradio(rwx) : org = 0xADE00000, len = 32k + ram_external_app_fmradio(rwx) : org = 0xADD10000, len = 32k + ram_external_app_tuner(rwx) : org = 0xADD20000, len = 32k + } SECTIONS @@ -252,11 +254,16 @@ SECTIONS KEEP(*(.external_app.app_mcu_temperature.application_information)); *(*ui*external_app*mcu_temperature*); } > ram_external_app_mcu_temperature - .external_app_fmradio : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_fmradio.application_information)); *(*ui*external_app*fmradio*); } > ram_external_app_fmradio + + .external_app_tuner : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_tuner.application_information)); + *(*ui*external_app*tuner*); + } > ram_external_app_tuner } diff --git a/firmware/application/external/tuner/main.cpp b/firmware/application/external/tuner/main.cpp new file mode 100644 index 00000000..4df2414e --- /dev/null +++ b/firmware/application/external/tuner/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 Bernd + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_tuner.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::tuner { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::tuner + +extern "C" { + +__attribute__((section(".external_app.app_tuner.application_information"), used)) application_information_t _application_information_tuner = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::tuner::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Tuner", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0x22, + 0x44, + 0x21, + 0x84, + 0x2D, + 0xB4, + 0x25, + 0xA4, + 0x25, + 0xA4, + 0x2D, + 0xB4, + 0x61, + 0x86, + 0xC2, + 0x43, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::cyan().v, + /*.menu_location = */ app_location_t::UTILITIES, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'A', 'B', 'P'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/tuner/ui_tuner.cpp b/firmware/application/external/tuner/ui_tuner.cpp new file mode 100644 index 00000000..5aa9e1c0 --- /dev/null +++ b/firmware/application/external/tuner/ui_tuner.cpp @@ -0,0 +1,256 @@ +/* + * copyleft 2024 sommermorgentraum + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_tuner.hpp" +#include "baseband_api.hpp" +#include "audio.hpp" +#include "portapack.hpp" + +using namespace portapack; + +namespace ui::external_app::tuner { + +TunerView::TunerView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); // proc_audio_beep baseband is external too + + add_children({ + &labels, + &field_volume, + &options_instrument, + &options_note, + &button_play_stop, + &text_note_frequency, + &text_note_octave_shift, + }); + + audio::set_rate(audio::Rate::Hz_24000); + + options_instrument.on_change = [this](size_t, int32_t value) { + const Instrument* selected_instrument = nullptr; + + switch (value) { + case 0: + selected_instrument = &guitar; + break; + case 1: + selected_instrument = &violin; + break; + case 2: + selected_instrument = &pitch_fork; + break; + } + + if (selected_instrument) { + update_notes_for_instrument(*selected_instrument); + } + + update_current_note(); + }; + + options_note.on_change = [this](size_t, int32_t index) { + const Instrument* current_instrument = nullptr; + switch (options_instrument.selected_index_value()) { + case 0: + current_instrument = &guitar; + break; + case 1: + current_instrument = &violin; + break; + case 2: + current_instrument = &pitch_fork; + break; + } + + if (current_instrument) { + auto it = current_instrument->notes.begin(); + std::advance(it, index); + if (it != current_instrument->notes.end()) { + current_note_frequency = it->second.frequency; + current_note_sample_rate = it->second.sample_rate; + current_note_octave_shift = it->second.octave_shift; + } + } + + update_current_note(); + }; + + button_play_stop.on_select = [this]() { + if (current_note_playing) { + stop_play(); + } else { + play_change_note(); + } + }; + + options_instrument.set_selected_index(0); + update_notes_for_instrument(guitar); + update_current_note(); + + field_volume.set_value(0); // seems that a change is required to force update, so setting to 0 first + field_volume.set_value(99); + + audio::set_rate(audio::Rate::Hz_24000); + audio::output::start(); +} + +TunerView::~TunerView() { + receiver_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + +void TunerView::focus() { + options_instrument.focus(); +} + +void TunerView::update_notes_for_instrument(const Instrument& instrument) { + std::vector note_options; + size_t index = 0; + + for (const auto& note_pair : instrument.notes) { + note_options.emplace_back(OptionsField::option_t{ + note_pair.first, + index++}); + } + + options_note.set_options(note_options); + if (!note_options.empty()) { + options_note.set_selected_index(0); + } +} + +void TunerView::update_current_note() { + const Instrument* current_instrument = nullptr; + + switch (options_instrument.selected_index_value()) { + case 0: + current_instrument = &guitar; + break; + case 1: + current_instrument = &violin; + break; + case 2: + current_instrument = &pitch_fork; + break; + } + + if (current_instrument) { + std::string note_name = options_note.selected_index_name(); + + // map + auto note_it = current_instrument->notes.find(note_name); + if (note_it != current_instrument->notes.end()) { + current_note_frequency = note_it->second.frequency; + current_note_sample_rate = note_it->second.sample_rate; + current_note_octave_shift = note_it->second.octave_shift; + + text_note_frequency.set(std::to_string(current_note_frequency)); + text_note_octave_shift.set(std::to_string(current_note_octave_shift)); + + set_dirty(); + + if (current_note_playing) { + play_change_note(); + } + } + } +} + +void TunerView::stop_play() { + baseband::request_beep_stop(); + current_note_playing = false; + button_play_stop.set_bitmap(&bitmap_icon_replay); +} + +void TunerView::play_change_note() { + if (current_note_playing) { + baseband::request_beep_stop(); + audio::set_rate(current_note_sample_rate); + baseband::request_audio_beep(current_note_frequency, protected_sample_rate(current_note_sample_rate), 0); + } else { + audio::set_rate(current_note_sample_rate); + baseband::request_audio_beep(current_note_frequency, protected_sample_rate(current_note_sample_rate), 0); + } + current_note_playing = true; + button_play_stop.set_bitmap(&bitmap_icon_sleep); + set_dirty(); +} + +uint32_t TunerView::protected_sample_rate(audio::Rate r) { + switch (r) { + case audio::Rate::Hz_12000: + return 12000; + case audio::Rate::Hz_24000: + return 24000; + case audio::Rate::Hz_48000: + return 48000; + default: + return 24000; + } +} + +void TunerView::paint(Painter& painter) { + View::paint(painter); + + painter.fill_rectangle( + {screen_width / 4, 8 * 16, screen_width / 2, 6 * 16}, + Theme::getInstance()->bg_darkest->background); + + if (!current_note_playing) return; + + painter.fill_rectangle( + {screen_width / 4, 10 * 16, 2, 2 * 16}, + Theme::getInstance()->fg_light->foreground); + + painter.fill_rectangle( + {screen_width / 4, 12 * 16, screen_width / 4 * 2 + 2, 2}, + Theme::getInstance()->fg_light->foreground); + + painter.fill_rectangle( + {(screen_width / 4) * 3, 10 * 16, 2, 2 * 16}, + Theme::getInstance()->fg_light->foreground); + + painter.draw_string( + {screen_width / 4 - 2 * 8, 8 * 16}, + (current_note_octave_shift == 0) ? *Theme::getInstance()->bg_darkest : *Theme::getInstance()->fg_red, + std::to_string(current_note_frequency)); + + int16_t real_frequency = current_note_frequency; + if (current_note_octave_shift > 0) { + for (int i = 0; i < current_note_octave_shift; i++) { + real_frequency *= 2; + } + } else if (current_note_octave_shift < 0) { + for (int i = 0; i > current_note_octave_shift; i--) { + real_frequency /= 2; + } + } + painter.draw_string({(screen_width / 4) * 3 - 2 * 8, 8 * 16}, + (current_note_octave_shift == 0) ? *Theme::getInstance()->bg_darkest : *Theme::getInstance()->fg_red, + std::to_string(real_frequency)); + + painter.draw_string({screen_width / 2 - 3 * 16, 13 * 16}, + (current_note_octave_shift == 0) ? *Theme::getInstance()->bg_darkest : *Theme::getInstance()->fg_red, + std::to_string(current_note_octave_shift) + " * 8ev"); +} + +} // namespace ui::external_app::tuner diff --git a/firmware/application/external/tuner/ui_tuner.hpp b/firmware/application/external/tuner/ui_tuner.hpp new file mode 100644 index 00000000..7b76d040 --- /dev/null +++ b/firmware/application/external/tuner/ui_tuner.hpp @@ -0,0 +1,145 @@ +/* + * copyleft 2024 sommermorgentraum + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_TUNER_H__ +#define __UI_TUNER_H__ + +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "audio.hpp" + +namespace ui::external_app::tuner { + +struct Instrument { + std::string name; + struct NoteInfo { + uint16_t frequency; + audio::Rate sample_rate; // tune samplerate to allow for freqs + int8_t octave_shift; + // PP hardware can't handle some extremely low/high frequencies, this indicates how much the freq that were played is shifted up/down, + // for example, if shift is -2 and note is A3, it means the real string should play A5 but PP's speaker plays A3 + }; + std::map notes; // this is for fast looking : O(log(n)) , usage `auto note = guitar.notes["A4"];` +}; + +class TunerView : public View { + public: + TunerView(NavigationView& nav); + ~TunerView(); + TunerView(const TunerView& other) = delete; + TunerView& operator=(const TunerView& other) = delete; + + void focus() override; + void update_audio_beep(); + + std::string title() const override { return "Tuner"; }; + + private: + NavigationView& nav_; + bool beep{false}; + + void update_notes_for_instrument(const Instrument& instrument); + void play_change_note(); + void stop_play(); + void update_current_note(); + uint32_t protected_sample_rate(audio::Rate r); + void paint(Painter& painter) override; + + uint16_t current_note_frequency{440}; + audio::Rate current_note_sample_rate{audio::Rate::Hz_12000}; + int8_t current_note_octave_shift{0}; + bool current_note_playing{false}; + + Labels labels{ + {{0 * 8, 1 * 16}, "Instrument:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "Note:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "Note Frequency:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 4 * 16}, "Note Octave Shift:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 5 * 16}, "Volume:", Theme::getInstance()->fg_light->foreground}}; + + Text text_note_frequency{ + {(sizeof("Note Frequency:") + 1) * 8, 3 * 16, screen_width - (sizeof("Note Frequency:") + 1) * 8, 16}, + "", + }; + + Text text_note_octave_shift{ + {(sizeof("Note Octave Shift:") + 1) * 8, 4 * 16, screen_width - (sizeof("Note Octave Shift:") + 1) * 8, 16}, + "", + }; + + AudioVolumeField field_volume{ + {(sizeof("Volume:") + 1) * 8, 5 * 16}}; + + // TODO: load list runtime + OptionsField options_instrument{ + {(sizeof("Instrument:") + 1) * 8, 1 * 16}, + screen_width - (sizeof("Instrument:") + 1) * 8, + {{"Guitar", 0}, + {"Violin", 1}, + {"Pitch Fork", 2}}}; + + OptionsField options_note{ + {(sizeof("Note:") + 1) * 8, 2 * 16}, + screen_width - (sizeof("Note:") + 1) * 8, + {}}; + + NewButton button_play_stop{ + {0 * 8, 16 * 16, screen_width, screen_height - 16 * 16}, + {}, + &bitmap_icon_replay, + Theme::getInstance()->fg_red->foreground}; + + // TODO: load from file DB + const Instrument guitar = { + .name = "Folk Guitar", + .notes = { + {"E2", {165, audio::Rate::Hz_12000, -1}}, // actual: E2, PP speaker: E3 + {"A2", {110, audio::Rate::Hz_12000, 0}}, + {"D3", {147, audio::Rate::Hz_12000, 0}}, + {"G3", {196, audio::Rate::Hz_24000, 0}}, + {"B3", {247, audio::Rate::Hz_24000, 0}}, + {"E4", {330, audio::Rate::Hz_24000, 0}}}}; + + const Instrument violin = { + .name = "Violin 440 Standard, 12ET", + .notes = { + {"G3", {196, audio::Rate::Hz_12000, 0}}, + {"D4", {294, audio::Rate::Hz_24000, 0}}, + {"A4", {440, audio::Rate::Hz_48000, 0}}, + {"E5", {659, audio::Rate::Hz_48000, 0}}}}; + + const Instrument pitch_fork = {// freq copied from flipper app + .name = "Pitch Fork", + .notes = { + {"12ET A4", {440, audio::Rate::Hz_48000, 0}}, + {"Sarti's A4", {436, audio::Rate::Hz_48000, 0}}, + {"1858 A4", {435, audio::Rate::Hz_48000, 0}}, + {"Verdi's A4", {432, audio::Rate::Hz_48000, 0}}}}; + + std::map instruments{ + {"Guitar", guitar}, + {"Violin", violin}, + {"Pitch Fork", pitch_fork}}; +}; + +} // namespace ui::external_app::tuner + +#endif /*__UI_TUNER_H__*/ diff --git a/firmware/graphics/icon_tune_fork.png b/firmware/graphics/icon_tune_fork.png new file mode 100644 index 00000000..a542e128 Binary files /dev/null and b/firmware/graphics/icon_tune_fork.png differ