diff --git a/firmware/application/external/breakout/main.cpp b/firmware/application/external/breakout/main.cpp index a4e31c86..eaa27bf6 100644 --- a/firmware/application/external/breakout/main.cpp +++ b/firmware/application/external/breakout/main.cpp @@ -20,13 +20,13 @@ void initialize_app(ui::NavigationView& nav) { extern "C" { __attribute__((section(".external_app.app_breakout.application_information"), used)) application_information_t _application_information_breakout = { - (uint8_t*)0x00000000, - ui::external_app::breakout::initialize_app, - CURRENT_HEADER_VERSION, - VERSION_MD5, + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::breakout::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, - "Breakout", - { + /*.app_name = */ "Breakout", + /*.bitmap_data = */ { 0x00, 0x00, 0x7F, @@ -60,11 +60,11 @@ __attribute__((section(".external_app.app_breakout.application_information"), us 0xF7, 0xF7, }, - ui::Color::green().v, - app_location_t::GAMES, - -1, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::GAMES, + /*.desired_menu_position = */ -1, - {0, 0, 0, 0}, - 0x00000000, + /*.m4_app_tag = */ {'P', 'A', 'B', 'P'}, // This is the audio beep baseband tag! + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time }; -} // namespace ui::external_app::breakout \ No newline at end of file +} \ No newline at end of file diff --git a/firmware/application/external/breakout/ui_breakout.cpp b/firmware/application/external/breakout/ui_breakout.cpp index 3e872e50..d894e767 100644 --- a/firmware/application/external/breakout/ui_breakout.cpp +++ b/firmware/application/external/breakout/ui_breakout.cpp @@ -7,6 +7,12 @@ */ #include "ui_breakout.hpp" +#include "baseband_api.hpp" +#include "audio.hpp" +#include "portapack.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; namespace ui::external_app::breakout { @@ -92,6 +98,37 @@ void BreakoutView::init_game() { game_state = STATE_MENU; menu_blink_state = true; menu_blink_counter = 0; + + // Play startup sound + if (sound_enabled) { + baseband::request_audio_beep(262, 24000, 150); // C4 - + chThdSleepMilliseconds(200); + baseband::request_audio_beep(262, 24000, 150); // C4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(392, 24000, 150); // G4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(392, 24000, 150); // G4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(440, 24000, 150); // A4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(440, 24000, 150); // A4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(392, 24000, 300); // G4 + chThdSleepMilliseconds(350); + baseband::request_audio_beep(349, 24000, 150); // F4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(349, 24000, 150); // F4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(330, 24000, 150); // E4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(330, 24000, 150); // E4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(294, 24000, 150); // D4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(294, 24000, 150); // D4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(262, 24000, 400); // C4 + } } void BreakoutView::init_level() { @@ -126,6 +163,34 @@ void BreakoutView::draw_screen() { draw_level(); } +void BreakoutView::draw_menu_volume_bar() { + // Clear the entire volume area first to handle bar shrinking + painter.fill_rectangle({40, 259, 220, 40}, Color::black()); + + // Draw volume label + auto style = *ui::Theme::getInstance()->fg_light; + painter.draw_string({40, 260}, style, "Volume:"); + + // Draw volume bar background + painter.draw_rectangle({100, 259, 102, 10}, Color::grey()); + + // Draw volume level + int bar_width = (volume_level * 100) / 99; + if (bar_width > 0) { + painter.fill_rectangle({101, 260, bar_width, 8}, Color::green()); + } + + // Draw percentage + painter.draw_string({210, 260}, style, std::to_string(volume_level) + "%"); + + // Draw sound status + if (sound_enabled) { + painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_green, "Sound: ON"); + } else { + painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_red, "Sound: OFF"); + } +} + void BreakoutView::draw_borders() { rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER); } @@ -166,6 +231,22 @@ void BreakoutView::draw_level() { painter.draw_string({80, 30}, style, "Level: " + std::to_string(level)); } +void BreakoutView::adjust_volume(int delta) { + volume_level += delta; + if (volume_level < 0) volume_level = 0; + if (volume_level > 99) volume_level = 99; + + // Update the actual audio volume using decibel conversion + // Map 0-99 to roughly -60dB to 0dB range + int db = (volume_level - 99) * 60 / 99; // Maps 0->-60dB, 99->0dB + audio::headphone::set_volume(volume_t::decibel(db)); + + // If we're in the menu, redraw the volume bar + if (game_state == STATE_MENU) { + draw_menu_volume_bar(); + } +} + void BreakoutView::move_paddle_left() { if (paddle_x > 0) { fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND); @@ -231,19 +312,23 @@ void BreakoutView::update_game() { if (ball_x < 0) { ball_x = 0; ball_dx = -ball_dx; + play_wall_hit(); } else if (ball_x > SCREEN_WIDTH - BALL_SIZE) { ball_x = SCREEN_WIDTH - BALL_SIZE; ball_dx = -ball_dx; + play_wall_hit(); } if (ball_y < GAME_AREA_TOP) { ball_y = GAME_AREA_TOP; ball_dy = -ball_dy; + play_wall_hit(); } // Bottom edge - lose life if (ball_y > GAME_AREA_BOTTOM) { lives--; + play_death_sound(); // Play death sound draw_lives(); if (lives <= 0) { handle_game_over(); @@ -262,6 +347,8 @@ void BreakoutView::update_game() { ball_y = PADDLE_Y - BALL_SIZE; ball_dy = -ball_dy; + play_paddle_hit(); + // Add some angle based on hit position float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x; float angle = (hit_position / PADDLE_WIDTH) - 0.5f; @@ -301,6 +388,8 @@ bool BreakoutView::check_brick_collision(int row, int col) { score += (5 - row) * 10; draw_score(); + play_brick_hit(); + ball_dy = -ball_dy; return true; } @@ -314,6 +403,8 @@ bool BreakoutView::check_level_complete() { void BreakoutView::next_level() { level++; + play_level_complete(); + chThdSleepMilliseconds(500); // Give time for sound to play init_level(); draw_screen(); } @@ -322,9 +413,11 @@ void BreakoutView::handle_game_over() { game_state = STATE_GAME_OVER; gameover_blink_state = true; gameover_blink_counter = 0; + play_game_over(); } void BreakoutView::show_menu() { + // Only draw the static parts once if (menu_blink_counter == 0) { cls(); background(COLOR_BACKGROUND); @@ -338,23 +431,30 @@ void BreakoutView::show_menu() { painter.draw_string({UI_POS_X_CENTER(20), 130}, style_cyan, "ROTARY: MOVE PADDLE"); painter.draw_string({UI_POS_X_CENTER(21), 160}, style_cyan, "SELECT: START/LAUNCH"); painter.draw_string({UI_POS_X_CENTER(25), 190}, style_blue, "========================"); + + // Volume adjustment instructions + painter.draw_string({UI_POS_X_CENTER(24), 220}, style_cyan, "Press UP/DOWN for Volume"); + + // Draw volume control at bottom + draw_menu_volume_bar(); } - auto style_red = *ui::Theme::getInstance()->fg_red; - + // Only flash the "PRESS SELECT" text if (++menu_blink_counter >= 30) { - menu_blink_counter = 0; + menu_blink_counter = 1; // Keep it at 1 so we don't redraw everything menu_blink_state = !menu_blink_state; - painter.fill_rectangle({UI_POS_X_CENTER(17), 228, 128, 20}, Color::black()); + auto style_red = *ui::Theme::getInstance()->fg_red; + painter.fill_rectangle({UI_POS_X_CENTER(17), 238, 128, 20}, Color::black()); if (menu_blink_state) { - painter.draw_string({UI_POS_X_CENTER(17), 230}, style_red, "* PRESS SELECT *"); + painter.draw_string({UI_POS_X_CENTER(17), 240}, style_red, "* PRESS SELECT *"); } } } void BreakoutView::show_game_over() { + // Only draw the static parts once if (gameover_blink_counter == 0) { cls(); background(COLOR_BACKGROUND); @@ -366,12 +466,12 @@ void BreakoutView::show_game_over() { painter.draw_string({UI_POS_X_CENTER(11), 150}, style_yellow, "SCORE: " + std::to_string(score)); } - auto style_green = *ui::Theme::getInstance()->fg_green; - + // Only flash the "PRESS SELECT" text if (++gameover_blink_counter >= 30) { - gameover_blink_counter = 0; + gameover_blink_counter = 1; // Keep it at 1 so we don't redraw everything gameover_blink_state = !gameover_blink_state; + auto style_green = *ui::Theme::getInstance()->fg_green; painter.fill_rectangle({UI_POS_X_CENTER(13), 198, 96, 20}, Color::black()); if (gameover_blink_state) { @@ -385,17 +485,169 @@ void BreakoutView::reset_game() { score = 0; lives = 3; game_state = STATE_PLAYING; + + dummy.focus(); + init_level(); draw_screen(); } +// Sound effect implementations with timing control +void BreakoutView::play_paddle_hit() { + if (sound_enabled) { + uint32_t current_time = chTimeNow(); + if (current_time - last_sound_time >= min_sound_interval) { + baseband::request_beep_stop(); // Stop any previous sound + chThdSleepMilliseconds(5); // Small delay + baseband::request_audio_beep(440, 24000, 40); // 440Hz A4, 40ms + last_sound_time = current_time; + } + } +} + +void BreakoutView::play_brick_hit() { + if (sound_enabled) { + uint32_t current_time = chTimeNow(); + if (current_time - last_sound_time >= min_sound_interval) { + baseband::request_beep_stop(); // Stop any previous sound + chThdSleepMilliseconds(5); // Small delay + // Higher pitch that varies with remaining bricks + uint32_t freq = 880 + ((25 - brick_count) * 20); // Gets higher as bricks are destroyed + baseband::request_audio_beep(freq, 24000, 30); + last_sound_time = current_time; + } + } +} + +void BreakoutView::play_wall_hit() { + if (sound_enabled) { + uint32_t current_time = chTimeNow(); + if (current_time - last_sound_time >= min_sound_interval) { + baseband::request_beep_stop(); // Stop any previous sound + chThdSleepMilliseconds(5); // Small delay + baseband::request_audio_beep(220, 24000, 120); // 220Hz A3, 120ms + last_sound_time = current_time; + } + } +} + +void BreakoutView::play_death_sound() { + if (sound_enabled) { + baseband::request_audio_beep(493, 24000, 150); // B4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(466, 24000, 150); // Bb4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(440, 24000, 150); // A4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(415, 24000, 150); // Ab4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(392, 24000, 150); // G4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(369, 24000, 150); // Gb4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(349, 24000, 150); // F4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(329, 24000, 150); // E4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(293, 24000, 150); // D4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(261, 24000, 150); // C4 + chThdSleepMilliseconds(180); + baseband::request_audio_beep(196, 24000, 200); // G3 + chThdSleepMilliseconds(230); + baseband::request_audio_beep(130, 24000, 300); // C3 + } +} + +void BreakoutView::play_game_over() { + if (sound_enabled) { + // First phrase + baseband::request_audio_beep(392, 24000, 300); // G4 + chThdSleepMilliseconds(350); + baseband::request_audio_beep(392, 24000, 150); // G4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(392, 24000, 150); // G4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(311, 24000, 500); // Eb4 - long + chThdSleepMilliseconds(600); + + // Second phrase + baseband::request_audio_beep(349, 24000, 300); // F4 + chThdSleepMilliseconds(350); + baseband::request_audio_beep(349, 24000, 150); // F4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(349, 24000, 150); // F4 + chThdSleepMilliseconds(200); + baseband::request_audio_beep(294, 24000, 500); // D4 - long + chThdSleepMilliseconds(600); + + // Final phrase - slower and more dramatic + baseband::request_audio_beep(262, 24000, 200); // C4 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(247, 24000, 200); // B3 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(233, 24000, 200); // Bb3 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(220, 24000, 200); // A3 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(196, 24000, 200); // G3 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(175, 24000, 200); // F3 + chThdSleepMilliseconds(250); + baseband::request_audio_beep(131, 24000, 600); // C3 + } +} + +void BreakoutView::play_level_complete() { + if (sound_enabled) { + baseband::request_beep_stop(); // Stop any previous sound + chThdSleepMilliseconds(10); + baseband::request_audio_beep(1047, 24000, 300); // 1047Hz C6, 300ms + } +} + BreakoutView::BreakoutView(NavigationView& nav) - : nav_{nav}, bricks{} { // Add bricks{} here + : nav_{nav}, bricks{} { + // Initialize audio system for beeps FIRST + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); // Load the audio beep baseband + add_children({&dummy}); + + // Initialize audio + audio::set_rate(audio::Rate::Hz_24000); + audio::output::start(); + + // Set initial volume to 80% and ensure sound is ON + volume_level = 80; + sound_enabled = true; // Make sure this is explicitly set to ON + + // Set the actual volume + int db = (volume_level - 99) * 60 / 99; // Maps to roughly -12dB for 80% + audio::headphone::set_volume(volume_t::decibel(db)); + attach(1.0 / 60.0); } +BreakoutView::~BreakoutView() { + // Clean shutdown + baseband::request_beep_stop(); + receiver_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + void BreakoutView::on_show() { + // Nothing needed here, initialization done in constructor +} + +void BreakoutView::on_hide() { + // Additional cleanup + baseband::request_beep_stop(); + audio::output::stop(); +} + +void BreakoutView::focus() { + dummy.focus(); } void BreakoutView::paint(Painter& painter) { @@ -433,16 +685,33 @@ bool BreakoutView::on_key(const KeyEvent key) { } else if (game_state == STATE_PLAYING && ball_attached) { launch_ball(); } else if (game_state == STATE_GAME_OVER) { - reset_game(); + // Return to menu after game over + game_state = STATE_MENU; + menu_blink_counter = 0; + init_game(); } } else if (key == KeyEvent::Left && game_state == STATE_PLAYING) { move_paddle_left(); } else if (key == KeyEvent::Right && game_state == STATE_PLAYING) { move_paddle_right(); + } else if (key == KeyEvent::Up && game_state == STATE_MENU) { + // Only adjust volume in menu + adjust_volume(5); + if (!sound_enabled) sound_enabled = true; + set_dirty(); + } else if (key == KeyEvent::Down && game_state == STATE_MENU) { + // Only adjust volume in menu + adjust_volume(-5); + set_dirty(); } set_dirty(); return true; } +bool BreakoutView::on_touch(const TouchEvent event) { + (void)event; // Intentionally unused - touch disabled to avoid nav bar issues + return true; +} + } // namespace ui::external_app::breakout \ No newline at end of file diff --git a/firmware/application/external/breakout/ui_breakout.hpp b/firmware/application/external/breakout/ui_breakout.hpp index 022fe779..fcd277c8 100644 --- a/firmware/application/external/breakout/ui_breakout.hpp +++ b/firmware/application/external/breakout/ui_breakout.hpp @@ -11,6 +11,7 @@ #include "ui.hpp" #include "ui_navigation.hpp" +#include "ui_receiver.hpp" #include "event_m0.hpp" #include "message.hpp" #include "irq_controls.hpp" @@ -56,15 +57,18 @@ enum { class BreakoutView : public View { public: BreakoutView(NavigationView& nav); + ~BreakoutView(); void on_show() override; + void on_hide() override; std::string title() const override { return "Breakout"; } - void focus() override { dummy.focus(); } + void focus() override; void paint(Painter& painter) override; void frame_sync(); bool on_encoder(const EncoderEvent event) override; bool on_key(KeyEvent key) override; + bool on_touch(const TouchEvent event) override; void cls(); void background(int color); @@ -81,6 +85,7 @@ class BreakoutView : public View { void draw_lives(); void draw_level(); void draw_borders(); + void draw_menu_volume_bar(); void move_paddle_left(); void move_paddle_right(); void launch_ball(); @@ -96,6 +101,15 @@ class BreakoutView : public View { void check_game_timer(); void attach(double delay_sec); void detach(); + void adjust_volume(int delta); + + // Sound effect methods + void play_paddle_hit(); + void play_brick_hit(); + void play_wall_hit(); + void play_death_sound(); + void play_game_over(); + void play_level_complete(); private: const Color pp_colors[9] = { @@ -147,6 +161,12 @@ class BreakoutView : public View { double game_update_timeout = 0; uint32_t game_update_counter = 0; + // Audio control + bool sound_enabled = true; + int volume_level = 80; // 0-99 + uint32_t last_sound_time = 0; + uint32_t min_sound_interval = 20; // Minimum ms between sounds + Button dummy{ {screen_width, 0, 0, 0}, ""};