Parcourir la source

Added splash screen will customize more later, added sound/vibro, added logging, still crashes after team selection and not sure why

RocketGod il y a 1 an
Parent
commit
afde2eec5e
7 fichiers modifiés avec 768 ajouts et 180 suppressions
  1. 13 1
      game_state.c
  2. 1 1
      game_state.h
  3. 105 138
      infrared_controller.c
  4. 350 0
      infrared_signal.c
  5. 216 0
      infrared_signal.h
  6. 55 40
      laser_tag_app.c
  7. 28 0
      sound/sound.h

+ 13 - 1
game_state.c

@@ -1,4 +1,3 @@
-
 #include "game_state.h"
 #include <furi.h>
 #include <stdlib.h>
@@ -15,6 +14,7 @@ struct GameState {
 GameState* game_state_alloc() {
     GameState* state = malloc(sizeof(GameState));
     if(!state) {
+        FURI_LOG_E("GameState", "Failed to allocate GameState");
         return NULL;
     }
     state->team = TeamRed;
@@ -23,6 +23,7 @@ GameState* game_state_alloc() {
     state->ammo = INITIAL_AMMO;
     state->game_time = 0;
     state->game_over = false;
+    FURI_LOG_I("GameState", "GameState allocated successfully");
     return state;
 }
 
@@ -33,11 +34,13 @@ void game_state_reset(GameState* state) {
     state->ammo = INITIAL_AMMO;
     state->game_time = 0;
     state->game_over = false;
+    FURI_LOG_I("GameState", "GameState reset");
 }
 
 void game_state_set_team(GameState* state, LaserTagTeam team) {
     furi_assert(state);
     state->team = team;
+    FURI_LOG_I("GameState", "Team set to %s", (team == TeamRed) ? "Red" : "Blue");
 }
 
 LaserTagTeam game_state_get_team(GameState* state) {
@@ -52,12 +55,15 @@ void game_state_decrease_health(GameState* state, uint8_t amount) {
     } else {
         state->health = 0;
         state->game_over = true;
+        FURI_LOG_W("GameState", "Health depleted, game over");
     }
+    FURI_LOG_I("GameState", "Health decreased to %d", state->health);
 }
 
 void game_state_increase_health(GameState* state, uint8_t amount) {
     furi_assert(state);
     state->health = (state->health + amount > MAX_HEALTH) ? MAX_HEALTH : state->health + amount;
+    FURI_LOG_I("GameState", "Health increased to %d", state->health);
 }
 
 uint8_t game_state_get_health(GameState* state) {
@@ -68,6 +74,7 @@ uint8_t game_state_get_health(GameState* state) {
 void game_state_increase_score(GameState* state, uint16_t points) {
     furi_assert(state);
     state->score += points;
+    FURI_LOG_I("GameState", "Score increased to %d", state->score);
 }
 
 uint16_t game_state_get_score(GameState* state) {
@@ -81,12 +88,15 @@ void game_state_decrease_ammo(GameState* state, uint16_t amount) {
         state->ammo -= amount;
     } else {
         state->ammo = 0;
+        FURI_LOG_W("GameState", "Ammo depleted");
     }
+    FURI_LOG_I("GameState", "Ammo decreased to %d", state->ammo);
 }
 
 void game_state_increase_ammo(GameState* state, uint16_t amount) {
     furi_assert(state);
     state->ammo += amount;
+    FURI_LOG_I("GameState", "Ammo increased to %d", state->ammo);
 }
 
 uint16_t game_state_get_ammo(GameState* state) {
@@ -97,6 +107,7 @@ uint16_t game_state_get_ammo(GameState* state) {
 void game_state_update_time(GameState* state, uint32_t delta_time) {
     furi_assert(state);
     state->game_time += delta_time;
+    FURI_LOG_I("GameState", "Game time updated to %ld seconds", state->game_time);
 }
 
 uint32_t game_state_get_time(GameState* state) {
@@ -112,4 +123,5 @@ bool game_state_is_game_over(GameState* state) {
 void game_state_set_game_over(GameState* state, bool game_over) {
     furi_assert(state);
     state->game_over = game_over;
+    FURI_LOG_I("GameState", "Game over status set to %s", game_over ? "true" : "false");
 }

+ 1 - 1
game_state.h

@@ -1,4 +1,3 @@
-
 #pragma once
 
 #include <stdint.h>
@@ -10,6 +9,7 @@ typedef enum {
 } LaserTagTeam;
 
 typedef enum {
+    LaserTagStateSplashScreen,
     LaserTagStateTeamSelect,
     LaserTagStateGame,
 } LaserTagState;

+ 105 - 138
infrared_controller.c

@@ -1,208 +1,175 @@
 #include "infrared_controller.h"
 #include <furi.h>
-#include <furi_hal.h>
-#include <infrared.h>
 #include <infrared_worker.h>
-#include <stdlib.h>
+#include <infrared_signal.h>
+#include <notification/notification_messages.h>
 
 #define TAG "InfraredController"
 
+const NotificationSequence sequence_hit = {
+    &message_vibro_on,
+    &message_note_d4,
+    &message_delay_1000,
+    &message_vibro_off,
+    &message_sound_off,
+    NULL,
+};
+
 struct InfraredController {
     LaserTagTeam team;
     InfraredWorker* worker;
-    FuriThread* rx_thread;
-    volatile bool rx_running;
-    volatile bool hit_received;
-    FuriMutex* mutex;
+    InfraredSignal* signal;
+    NotificationApp* notification;
+    bool hit_received;
+    bool processing_signal;
 };
 
 static void infrared_rx_callback(void* context, InfraredWorkerSignal* received_signal) {
-    FURI_LOG_D(TAG, "RX callback triggered");
-    furi_assert(context);
-    furi_assert(received_signal);
+    FURI_LOG_I(TAG, "RX callback triggered");
 
     InfraredController* controller = (InfraredController*)context;
-    furi_mutex_acquire(controller->mutex, FuriWaitForever);
+    if(controller->processing_signal) {
+        FURI_LOG_W(TAG, "Already processing a signal, skipping callback");
+        return;
+    }
 
-    FURI_LOG_D(TAG, "Context and received signal validated");
+    controller->processing_signal = true;
 
-    const InfraredMessage* message = infrared_worker_get_decoded_signal(received_signal);
-    if(message != NULL) {
-        uint32_t received_command = message->address;
-        FURI_LOG_D(TAG, "Received command: 0x%lx", (unsigned long)received_command);
+    if(!received_signal) {
+        FURI_LOG_E(TAG, "Received signal is NULL");
+        controller->processing_signal = false;
+        return;
+    }
 
-        if((controller->team == TeamRed && received_command == IR_COMMAND_BLUE_TEAM) ||
-           (controller->team == TeamBlue && received_command == IR_COMMAND_RED_TEAM)) {
+    const InfraredMessage* message = infrared_worker_get_decoded_signal(received_signal);
+    FURI_LOG_I(TAG, "Received signal - signal address: %p", (void*)received_signal);
+
+    if(message) {
+        FURI_LOG_I(
+            TAG,
+            "Received message: protocol=%d, address=0x%lx, command=0x%lx",
+            message->protocol,
+            (unsigned long)message->address,
+            (unsigned long)message->command);
+
+        if((controller->team == TeamRed && message->command == IR_COMMAND_BLUE_TEAM) ||
+           (controller->team == TeamBlue && message->command == IR_COMMAND_RED_TEAM)) {
             controller->hit_received = true;
-            FURI_LOG_D(
+            FURI_LOG_I(
                 TAG, "Hit detected for team: %s", controller->team == TeamRed ? "Red" : "Blue");
+            notification_message_block(controller->notification, &sequence_hit);
         }
     } else {
-        FURI_LOG_E(TAG, "Received NULL message");
-    }
-
-    furi_mutex_release(controller->mutex);
-}
-
-static int32_t infrared_rx_thread(void* context) {
-    FURI_LOG_D(TAG, "RX thread started");
-    furi_assert(context);
-
-    InfraredController* controller = (InfraredController*)context;
-
-    while(controller->rx_running) {
-        furi_mutex_acquire(controller->mutex, FuriWaitForever);
-        FURI_LOG_D(TAG, "Starting infrared_worker_rx_start");
-
-        // Check for worker validity before starting
-        if(controller->worker) {
-            infrared_worker_rx_start(controller->worker);
-            FURI_LOG_D(TAG, "infrared_worker_rx_start succeeded");
-        } else {
-            FURI_LOG_E(TAG, "InfraredWorker is NULL");
-            furi_mutex_release(controller->mutex);
-            continue;
-        }
-
-        furi_mutex_release(controller->mutex);
-
-        FURI_LOG_D(TAG, "Waiting for thread flags");
-        FuriStatus status = furi_thread_flags_wait(0, FuriFlagWaitAny, 10);
-
-        if(status == FuriStatusErrorTimeout) {
-            FURI_LOG_D(TAG, "RX loop timeout, continuing");
-        } else {
-            FURI_LOG_D(TAG, "RX loop received flag: %d", status);
-        }
+        FURI_LOG_W(TAG, "RX callback received NULL message");
     }
 
-    FURI_LOG_D(TAG, "RX thread stopping");
-    return 0;
+    FURI_LOG_I(TAG, "RX callback completed");
+    controller->processing_signal = false;
 }
 
 InfraredController* infrared_controller_alloc() {
-    FURI_LOG_D(TAG, "Allocating InfraredController");
+    FURI_LOG_I(TAG, "Allocating InfraredController");
 
     InfraredController* controller = malloc(sizeof(InfraredController));
     if(!controller) {
-        FURI_LOG_E(TAG, "Failed to allocate InfraredController struct");
+        FURI_LOG_E(TAG, "Failed to allocate InfraredController");
         return NULL;
     }
-    FURI_LOG_D(TAG, "InfraredController struct allocated");
 
     controller->team = TeamRed;
-    FURI_LOG_D(TAG, "Team initialized to Red");
-
-    FURI_LOG_D(TAG, "Allocating InfraredWorker");
     controller->worker = infrared_worker_alloc();
-    if(!controller->worker) {
-        FURI_LOG_E(TAG, "Failed to allocate InfraredWorker");
-        free(controller);
-        return NULL;
-    }
-    FURI_LOG_D(TAG, "InfraredWorker allocated");
-
-    controller->rx_running = true;
+    controller->signal = infrared_signal_alloc();
+    controller->notification = furi_record_open(RECORD_NOTIFICATION);
     controller->hit_received = false;
+    controller->processing_signal = false;
 
-    FURI_LOG_D(TAG, "Creating mutex");
-    controller->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
-    if(!controller->mutex) {
-        FURI_LOG_E(TAG, "Failed to create mutex");
-        infrared_worker_free(controller->worker);
+    if(controller->worker && controller->signal && controller->notification) {
+        FURI_LOG_I(
+            TAG, "InfraredWorker, InfraredSignal, and NotificationApp allocated successfully");
+    } else {
+        FURI_LOG_E(TAG, "Failed to allocate resources");
         free(controller);
         return NULL;
     }
 
-    FURI_LOG_D(TAG, "Setting up RX callback");
+    FURI_LOG_I(TAG, "Setting up RX callback");
     infrared_worker_rx_set_received_signal_callback(
         controller->worker, infrared_rx_callback, controller);
 
-    FURI_LOG_D(TAG, "Allocating RX thread");
-    controller->rx_thread = furi_thread_alloc();
-    if(!controller->rx_thread) {
-        FURI_LOG_E(TAG, "Failed to allocate RX thread");
-        furi_mutex_free(controller->mutex);
-        infrared_worker_free(controller->worker);
-        free(controller);
-        return NULL;
-    }
-
-    furi_thread_set_name(controller->rx_thread, "IR_Rx_Thread");
-    furi_thread_set_stack_size(controller->rx_thread, 1024);
-    furi_thread_set_context(controller->rx_thread, controller);
-    furi_thread_set_callback(controller->rx_thread, infrared_rx_thread);
-
-    FURI_LOG_D(TAG, "Starting RX thread");
-    furi_thread_start(controller->rx_thread);
-
-    FURI_LOG_D(TAG, "Starting InfraredWorker RX");
-    infrared_worker_rx_start(controller->worker);
-
-    FURI_LOG_D(TAG, "InfraredController allocated successfully");
+    FURI_LOG_I(TAG, "InfraredController allocated successfully");
     return controller;
 }
 
 void infrared_controller_free(InfraredController* controller) {
-    FURI_LOG_D(TAG, "Freeing InfraredController");
-    furi_assert(controller);
+    FURI_LOG_I(TAG, "Freeing InfraredController");
 
-    controller->rx_running = false;
-    FURI_LOG_D(TAG, "Stopping RX thread");
-    furi_thread_join(controller->rx_thread);
-    furi_thread_free(controller->rx_thread);
+    if(controller) {
+        FURI_LOG_I(TAG, "Stopping InfraredWorker RX");
+        infrared_worker_rx_stop(controller->worker);
 
-    FURI_LOG_D(TAG, "Stopping InfraredWorker RX");
-    furi_mutex_acquire(controller->mutex, FuriWaitForever);
-    infrared_worker_rx_stop(controller->worker);
-    infrared_worker_free(controller->worker);
-    furi_mutex_release(controller->mutex);
+        FURI_LOG_I(TAG, "Freeing InfraredWorker and InfraredSignal");
+        infrared_worker_free(controller->worker);
+        infrared_signal_free(controller->signal);
+
+        FURI_LOG_I(TAG, "Closing NotificationApp");
+        furi_record_close(RECORD_NOTIFICATION);
+
+        free(controller);
 
-    furi_mutex_free(controller->mutex);
-    free(controller);
-    FURI_LOG_D(TAG, "InfraredController freed");
+        FURI_LOG_I(TAG, "InfraredController freed successfully");
+    } else {
+        FURI_LOG_W(TAG, "Attempted to free NULL InfraredController");
+    }
 }
 
 void infrared_controller_set_team(InfraredController* controller, LaserTagTeam team) {
-    furi_assert(controller);
-    furi_mutex_acquire(controller->mutex, FuriWaitForever);
+    FURI_LOG_I(TAG, "Setting team to %s", team == TeamRed ? "Red" : "Blue");
     controller->team = team;
-    furi_mutex_release(controller->mutex);
-    FURI_LOG_D(TAG, "Team set to %s", (team == TeamRed) ? "Red" : "Blue");
 }
 
 void infrared_controller_send(InfraredController* controller) {
-    FURI_LOG_D(TAG, "Sending IR signal");
-    furi_assert(controller);
-
-    furi_mutex_acquire(controller->mutex, FuriWaitForever);
+    FURI_LOG_I(TAG, "Preparing to send infrared signal");
 
-    uint32_t command = (controller->team == TeamRed) ? IR_COMMAND_RED_TEAM : IR_COMMAND_BLUE_TEAM;
     InfraredMessage message = {
-        .protocol = InfraredProtocolNEC, .address = 0x00, .command = command, .repeat = false};
+        .protocol = InfraredProtocolNEC,
+        .address = 0x00,
+        .command = (controller->team == TeamRed) ? IR_COMMAND_RED_TEAM : IR_COMMAND_BLUE_TEAM};
 
-    infrared_worker_set_decoded_signal(controller->worker, &message);
+    FURI_LOG_I(
+        TAG,
+        "Prepared message: protocol=%d, address=0x%lx, command=0x%lx",
+        message.protocol,
+        (unsigned long)message.address,
+        (unsigned long)message.command);
 
-    FURI_LOG_D(TAG, "Starting IR transmission");
-    infrared_worker_tx_set_get_signal_callback(
-        controller->worker, infrared_worker_tx_get_signal_steady_callback, NULL);
+    FURI_LOG_I(TAG, "Setting message for infrared signal");
+    infrared_signal_set_message(controller->signal, &message);
 
-    infrared_worker_tx_start(controller->worker);
+    FURI_LOG_I(TAG, "Starting infrared signal transmission");
+    infrared_signal_transmit(controller->signal);
 
-    furi_delay_ms(250);
-
-    infrared_worker_tx_stop(controller->worker);
-    FURI_LOG_D(TAG, "IR signal sent");
-
-    furi_mutex_release(controller->mutex);
+    FURI_LOG_I(TAG, "Infrared signal transmission completed");
 }
 
 bool infrared_controller_receive(InfraredController* controller) {
-    furi_assert(controller);
-    furi_mutex_acquire(controller->mutex, FuriWaitForever);
+    FURI_LOG_I(TAG, "Starting infrared signal reception");
+
+    if(controller->processing_signal) {
+        FURI_LOG_W(TAG, "Cannot start reception, another signal is still being processed");
+        return false;
+    }
+
+    infrared_worker_rx_start(controller->worker);
+
+    furi_delay_ms(50);
+
+    infrared_worker_rx_stop(controller->worker);
+
     bool hit = controller->hit_received;
+
+    FURI_LOG_I(TAG, "Signal reception complete, hit received: %s", hit ? "true" : "false");
+
     controller->hit_received = false;
-    furi_mutex_release(controller->mutex);
-    FURI_LOG_D(TAG, "Checking for hit: %s", hit ? "Hit received" : "No hit");
+
     return hit;
 }

+ 350 - 0
infrared_signal.c

@@ -0,0 +1,350 @@
+#include "infrared_signal.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <core/check.h>
+#include <infrared_worker.h>
+#include <infrared_transmit.h>
+
+#define TAG "InfraredSignal"
+
+// Common keys
+#define INFRARED_SIGNAL_NAME_KEY "name"
+#define INFRARED_SIGNAL_TYPE_KEY "type"
+
+// Type key values
+#define INFRARED_SIGNAL_TYPE_RAW    "raw"
+#define INFRARED_SIGNAL_TYPE_PARSED "parsed"
+
+// Raw signal keys
+#define INFRARED_SIGNAL_DATA_KEY       "data"
+#define INFRARED_SIGNAL_FREQUENCY_KEY  "frequency"
+#define INFRARED_SIGNAL_DUTY_CYCLE_KEY "duty_cycle"
+
+// Parsed signal keys
+#define INFRARED_SIGNAL_PROTOCOL_KEY "protocol"
+#define INFRARED_SIGNAL_ADDRESS_KEY  "address"
+#define INFRARED_SIGNAL_COMMAND_KEY  "command"
+
+struct InfraredSignal {
+    bool is_raw;
+    union {
+        InfraredMessage message;
+        InfraredRawSignal raw;
+    } payload;
+};
+
+static void infrared_signal_clear_timings(InfraredSignal* signal) {
+    if(signal->is_raw) {
+        free(signal->payload.raw.timings);
+        signal->payload.raw.timings_size = 0;
+        signal->payload.raw.timings = NULL;
+    }
+}
+
+static bool infrared_signal_is_message_valid(const InfraredMessage* message) {
+    if(!infrared_is_protocol_valid(message->protocol)) {
+        FURI_LOG_E(TAG, "Unknown protocol");
+        return false;
+    }
+
+    uint32_t address_length = infrared_get_protocol_address_length(message->protocol);
+    uint32_t address_mask = (1UL << address_length) - 1;
+
+    if(message->address != (message->address & address_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Address is out of range (mask 0x%08lX): 0x%lX\r\n",
+            address_mask,
+            message->address);
+        return false;
+    }
+
+    uint32_t command_length = infrared_get_protocol_command_length(message->protocol);
+    uint32_t command_mask = (1UL << command_length) - 1;
+
+    if(message->command != (message->command & command_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Command is out of range (mask 0x%08lX): 0x%lX\r\n",
+            command_mask,
+            message->command);
+        return false;
+    }
+
+    return true;
+}
+
+static bool infrared_signal_is_raw_valid(const InfraredRawSignal* raw) {
+    if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) {
+        FURI_LOG_E(
+            TAG,
+            "Frequency is out of range (%X - %X): %lX",
+            INFRARED_MIN_FREQUENCY,
+            INFRARED_MAX_FREQUENCY,
+            raw->frequency);
+        return false;
+
+    } else if((raw->duty_cycle <= 0) || (raw->duty_cycle > 1)) {
+        FURI_LOG_E(TAG, "Duty cycle is out of range (0 - 1): %f", (double)raw->duty_cycle);
+        return false;
+
+    } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
+        FURI_LOG_E(
+            TAG,
+            "Timings amount is out of range (0 - %X): %zX",
+            MAX_TIMINGS_AMOUNT,
+            raw->timings_size);
+        return false;
+    }
+
+    return true;
+}
+
+static inline bool
+    infrared_signal_save_message(const InfraredMessage* message, FlipperFormat* ff) {
+    const char* protocol_name = infrared_get_protocol_name(message->protocol);
+    return flipper_format_write_string_cstr(
+               ff, INFRARED_SIGNAL_TYPE_KEY, INFRARED_SIGNAL_TYPE_PARSED) &&
+           flipper_format_write_string_cstr(ff, INFRARED_SIGNAL_PROTOCOL_KEY, protocol_name) &&
+           flipper_format_write_hex(
+               ff, INFRARED_SIGNAL_ADDRESS_KEY, (uint8_t*)&message->address, 4) &&
+           flipper_format_write_hex(
+               ff, INFRARED_SIGNAL_COMMAND_KEY, (uint8_t*)&message->command, 4);
+}
+
+static inline bool infrared_signal_save_raw(const InfraredRawSignal* raw, FlipperFormat* ff) {
+    furi_assert(raw->timings_size <= MAX_TIMINGS_AMOUNT);
+    return flipper_format_write_string_cstr(
+               ff, INFRARED_SIGNAL_TYPE_KEY, INFRARED_SIGNAL_TYPE_RAW) &&
+           flipper_format_write_uint32(ff, INFRARED_SIGNAL_FREQUENCY_KEY, &raw->frequency, 1) &&
+           flipper_format_write_float(ff, INFRARED_SIGNAL_DUTY_CYCLE_KEY, &raw->duty_cycle, 1) &&
+           flipper_format_write_uint32(
+               ff, INFRARED_SIGNAL_DATA_KEY, raw->timings, raw->timings_size);
+}
+
+static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* buf;
+    buf = furi_string_alloc();
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, INFRARED_SIGNAL_PROTOCOL_KEY, buf)) break;
+
+        InfraredMessage message;
+        message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
+
+        if(!flipper_format_read_hex(ff, INFRARED_SIGNAL_ADDRESS_KEY, (uint8_t*)&message.address, 4))
+            break;
+        if(!flipper_format_read_hex(ff, INFRARED_SIGNAL_COMMAND_KEY, (uint8_t*)&message.command, 4))
+            break;
+        if(!infrared_signal_is_message_valid(&message)) break;
+
+        infrared_signal_set_message(signal, &message);
+        success = true;
+    } while(false);
+
+    furi_string_free(buf);
+    return success;
+}
+
+static inline bool infrared_signal_read_raw(InfraredSignal* signal, FlipperFormat* ff) {
+    bool success = false;
+
+    do {
+        uint32_t frequency;
+        if(!flipper_format_read_uint32(ff, INFRARED_SIGNAL_FREQUENCY_KEY, &frequency, 1)) break;
+
+        float duty_cycle;
+        if(!flipper_format_read_float(ff, INFRARED_SIGNAL_DUTY_CYCLE_KEY, &duty_cycle, 1)) break;
+
+        uint32_t timings_size;
+        if(!flipper_format_get_value_count(ff, INFRARED_SIGNAL_DATA_KEY, &timings_size)) break;
+
+        if(timings_size > MAX_TIMINGS_AMOUNT) break;
+
+        uint32_t* timings = malloc(sizeof(uint32_t) * timings_size);
+        if(!flipper_format_read_uint32(ff, INFRARED_SIGNAL_DATA_KEY, timings, timings_size)) {
+            free(timings);
+            break;
+        }
+        infrared_signal_set_raw_signal(signal, timings, timings_size, frequency, duty_cycle);
+        free(timings);
+
+        success = true;
+    } while(false);
+
+    return success;
+}
+
+bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, INFRARED_SIGNAL_TYPE_KEY, tmp)) break;
+
+        if(furi_string_equal(tmp, INFRARED_SIGNAL_TYPE_RAW)) {
+            if(!infrared_signal_read_raw(signal, ff)) break;
+        } else if(furi_string_equal(tmp, INFRARED_SIGNAL_TYPE_PARSED)) {
+            if(!infrared_signal_read_message(signal, ff)) break;
+        } else {
+            FURI_LOG_E(TAG, "Unknown signal type: %s", furi_string_get_cstr(tmp));
+            break;
+        }
+
+        success = true;
+    } while(false);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+InfraredSignal* infrared_signal_alloc() {
+    InfraredSignal* signal = malloc(sizeof(InfraredSignal));
+
+    signal->is_raw = false;
+    signal->payload.message.protocol = InfraredProtocolUnknown;
+
+    return signal;
+}
+
+void infrared_signal_free(InfraredSignal* signal) {
+    infrared_signal_clear_timings(signal);
+    free(signal);
+}
+
+bool infrared_signal_is_raw(const InfraredSignal* signal) {
+    return signal->is_raw;
+}
+
+bool infrared_signal_is_valid(const InfraredSignal* signal) {
+    return signal->is_raw ? infrared_signal_is_raw_valid(&signal->payload.raw) :
+                            infrared_signal_is_message_valid(&signal->payload.message);
+}
+
+void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other) {
+    if(other->is_raw) {
+        const InfraredRawSignal* raw = &other->payload.raw;
+        infrared_signal_set_raw_signal(
+            signal, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle);
+    } else {
+        const InfraredMessage* message = &other->payload.message;
+        infrared_signal_set_message(signal, message);
+    }
+}
+
+void infrared_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle) {
+    infrared_signal_clear_timings(signal);
+
+    signal->is_raw = true;
+
+    signal->payload.raw.timings_size = timings_size;
+    signal->payload.raw.frequency = frequency;
+    signal->payload.raw.duty_cycle = duty_cycle;
+
+    signal->payload.raw.timings = malloc(timings_size * sizeof(uint32_t));
+    memcpy(signal->payload.raw.timings, timings, timings_size * sizeof(uint32_t));
+}
+
+const InfraredRawSignal* infrared_signal_get_raw_signal(const InfraredSignal* signal) {
+    furi_assert(signal->is_raw);
+    return &signal->payload.raw;
+}
+
+void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message) {
+    infrared_signal_clear_timings(signal);
+
+    signal->is_raw = false;
+    signal->payload.message = *message;
+}
+
+const InfraredMessage* infrared_signal_get_message(const InfraredSignal* signal) {
+    furi_assert(!signal->is_raw);
+    return &signal->payload.message;
+}
+
+bool infrared_signal_save(const InfraredSignal* signal, FlipperFormat* ff, const char* name) {
+    if(!flipper_format_write_comment_cstr(ff, "") ||
+       !flipper_format_write_string_cstr(ff, INFRARED_SIGNAL_NAME_KEY, name)) {
+        return false;
+    } else if(signal->is_raw) {
+        return infrared_signal_save_raw(&signal->payload.raw, ff);
+    } else {
+        return infrared_signal_save_message(&signal->payload.message, ff);
+    }
+}
+
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) {
+    bool success = false;
+
+    do {
+        if(!infrared_signal_read_name(ff, name)) break;
+        if(!infrared_signal_read_body(signal, ff)) break;
+
+        success = true; //-V779
+    } while(false);
+
+    return success;
+}
+
+bool infrared_signal_read_name(FlipperFormat* ff, FuriString* name) {
+    return flipper_format_read_string(ff, INFRARED_SIGNAL_NAME_KEY, name);
+}
+
+bool infrared_signal_search_by_name_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const char* name) {
+    bool success = false;
+    FuriString* tmp = furi_string_alloc();
+
+    while(infrared_signal_read_name(ff, tmp)) {
+        if(furi_string_equal(tmp, name)) {
+            success = infrared_signal_read_body(signal, ff);
+            break;
+        }
+    }
+
+    furi_string_free(tmp);
+    return success;
+}
+
+bool infrared_signal_search_by_index_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    size_t index) {
+    bool success = false;
+    FuriString* tmp = furi_string_alloc();
+
+    for(uint32_t i = 0; infrared_signal_read_name(ff, tmp); ++i) {
+        if(i == index) {
+            success = infrared_signal_read_body(signal, ff);
+            break;
+        }
+    }
+
+    furi_string_free(tmp);
+    return success;
+}
+
+void infrared_signal_transmit(const InfraredSignal* signal) {
+    if(signal->is_raw) {
+        const InfraredRawSignal* raw_signal = &signal->payload.raw;
+        infrared_send_raw_ext(
+            raw_signal->timings,
+            raw_signal->timings_size,
+            true,
+            raw_signal->frequency,
+            raw_signal->duty_cycle);
+    } else {
+        const InfraredMessage* message = &signal->payload.message;
+        infrared_send(message, 1);
+    }
+}

+ 216 - 0
infrared_signal.h

@@ -0,0 +1,216 @@
+/**
+ * @file infrared_signal.h
+ * @brief Infrared signal library.
+ *
+ * Infrared signals may be of two types:
+ * - known to the infrared signal decoder, or *parsed* signals
+ * - the rest, or *raw* signals, which are treated merely as a set of timings.
+ */
+#pragma once
+
+#include <flipper_format/flipper_format.h>
+#include <infrared/encoder_decoder/infrared.h>
+
+/**
+ * @brief InfraredSignal opaque type declaration.
+ */
+typedef struct InfraredSignal InfraredSignal;
+
+/**
+ * @brief Raw signal type definition.
+ *
+ * Measurement units used:
+ * - time: microseconds (uS)
+ * - frequency: Hertz (Hz)
+ * - duty_cycle: no units, fraction between 0 and 1.
+ */
+typedef struct {
+    size_t timings_size; /**< Number of elements in the timings array. */
+    uint32_t* timings; /**< Pointer to an array of timings describing the signal. */
+    uint32_t frequency; /**< Carrier frequency of the signal. */
+    float duty_cycle; /**< Duty cycle of the signal. */
+} InfraredRawSignal;
+
+/**
+ * @brief Create a new InfraredSignal instance.
+ *
+ * @returns pointer to the instance created.
+ */
+InfraredSignal* infrared_signal_alloc();
+
+/**
+ * @brief Delete an InfraredSignal instance.
+ *
+ * @param[in,out] signal pointer to the instance to be deleted.
+ */
+void infrared_signal_free(InfraredSignal* signal);
+
+/**
+ * @brief Test whether an InfraredSignal instance holds a raw signal.
+ *
+ * @param[in] signal pointer to the instance to be tested.
+ * @returns true if the instance holds a raw signal, false otherwise.
+ */
+bool infrared_signal_is_raw(const InfraredSignal* signal);
+
+/**
+ * @brief Test whether an InfraredSignal instance holds any signal.
+ *
+ * @param[in] signal pointer to the instance to be tested.
+ * @returns true if the instance holds raw signal, false otherwise.
+ */
+bool infrared_signal_is_valid(const InfraredSignal* signal);
+
+/**
+ * @brief Set an InfraredInstance to hold the signal from another one.
+ *
+ * Any instance's previous contents will be automatically deleted before
+ * copying the source instance's contents.
+ *
+ * @param[in,out] signal pointer to the destination instance.
+ * @param[in] other pointer to the source instance.
+ */
+void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other);
+
+/**
+ * @brief Set an InfraredInstance to hold a raw signal.
+ *
+ * Any instance's previous contents will be automatically deleted before
+ * copying the raw signal.
+ *
+ * After this call, infrared_signal_is_raw() will return true.
+ *
+ * @param[in,out] signal pointer to the destination instance.
+ * @param[in] timings pointer to an array containing the raw signal timings.
+ * @param[in] timings_size number of elements in the timings array.
+ * @param[in] frequency signal carrier frequency, in Hertz.
+ * @param[in] duty_cycle signal duty cycle, fraction between 0 and 1.
+ */
+void infrared_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle);
+
+/**
+ * @brief Get the raw signal held by an InfraredSignal instance.
+ *
+ * @warning the instance MUST hold a *raw* signal, otherwise undefined behaviour will occur.
+ *
+ * @param[in] signal pointer to the instance to be queried.
+ * @returns pointer to the raw signal structure held by the instance.
+ */
+const InfraredRawSignal* infrared_signal_get_raw_signal(const InfraredSignal* signal);
+
+/**
+ * @brief Set an InfraredInstance to hold a parsed signal.
+ *
+ * Any instance's previous contents will be automatically deleted before
+ * copying the raw signal.
+ *
+ * After this call, infrared_signal_is_raw() will return false.
+ *
+ * @param[in,out] signal pointer to the destination instance.
+ * @param[in] message pointer to the message containing the parsed signal.
+ */
+void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message);
+
+/**
+ * @brief Get the parsed signal held by an InfraredSignal instance.
+ *
+ * @warning the instance MUST hold a *parsed* signal, otherwise undefined behaviour will occur.
+ *
+ * @param[in] signal pointer to the instance to be queried.
+ * @returns pointer to the parsed signal structure held by the instance.
+ */
+const InfraredMessage* infrared_signal_get_message(const InfraredSignal* signal);
+
+/**
+ * @brief Read a signal and its name from a FlipperFormat file into an InfraredSignal instance.
+ *
+ * The file must be allocated and open prior to this call. The seek position determines
+ * which signal will be read (if there is more than one in the file). Calling this function
+ * repeatedly will result in all signals in the file to be read until no more are left.
+ *
+ * @param[in,out] signal pointer to the instance to be read into.
+ * @param[in,out] ff pointer to the FlipperFormat file instance to read from.
+ * @param[out] name pointer to the string to hold the signal name. Must be properly allocated.
+ * @returns true if a signal was successfully read, false otherwise (e.g. no more signals to read).
+ */
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name);
+
+/**
+ * @brief Read a signal name from a FlipperFormat file.
+ *
+ * Same behaviour as infrared_signal_read(), but only the name is read.
+ *
+ * @param[in,out] ff pointer to the FlipperFormat file instance to read from.
+ * @param[out] name pointer to the string to hold the signal name. Must be properly allocated.
+ * @returns true if a signal name was successfully read, false otherwise (e.g. no more signals to read).
+ */
+bool infrared_signal_read_name(FlipperFormat* ff, FuriString* name);
+
+/**
+ * @brief Read a signal from a FlipperFormat file.
+ *
+ * Same behaviour as infrared_signal_read(), but only the body is read.
+ *
+ * @param[in,out] ff pointer to the FlipperFormat file instance to read from.
+ * @param[out] body pointer to the InfraredSignal instance to hold the signal body. Must be properly allocated.
+ * @returns true if a signal body was successfully read, false otherwise (e.g. syntax error).
+ */
+bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff);
+
+/**
+ * @brief Read a signal with a particular name from a FlipperFormat file into an InfraredSignal instance.
+ *
+ * This function will look for a signal with the given name and if found, attempt to read it.
+ * Same considerations apply as to infrared_signal_read().
+ *
+ * @param[in,out] signal pointer to the instance to be read into.
+ * @param[in,out] ff pointer to the FlipperFormat file instance to read from.
+ * @param[in] name pointer to a zero-terminated string containing the requested signal name.
+ * @returns true if a signal was found and successfully read, false otherwise (e.g. the signal was not found).
+ */
+bool infrared_signal_search_by_name_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const char* name);
+
+/**
+ * @brief Read a signal with a particular index from a FlipperFormat file into an InfraredSignal instance.
+ *
+ * This function will look for a signal with the given index and if found, attempt to read it.
+ * Same considerations apply as to infrared_signal_read().
+ *
+ * @param[in,out] signal pointer to the instance to be read into.
+ * @param[in,out] ff pointer to the FlipperFormat file instance to read from.
+ * @param[in] index the requested signal index.
+ * @returns true if a signal was found and successfully read, false otherwise (e.g. the signal was not found).
+ */
+bool infrared_signal_search_by_index_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    size_t index);
+
+/**
+ * @brief Save a signal contained in an InfraredSignal instance to a FlipperFormat file.
+ *
+ * The file must be allocated and open prior to this call. Additionally, an appropriate header
+ * must be already written into the file.
+ *
+ * @param[in] signal pointer to the instance holding the signal to be saved.
+ * @param[in,out] ff pointer to the FlipperFormat file instance to write to.
+ * @param[in] name pointer to a zero-terminated string contating the name of the signal.
+ */
+bool infrared_signal_save(const InfraredSignal* signal, FlipperFormat* ff, const char* name);
+
+/**
+ * @brief Transmit a signal contained in an InfraredSignal instance.
+ *
+ * The transmission happens once per call using the built-in hardware (via HAL calls).
+ *
+ * @param[in] signal pointer to the instance holding the signal to be transmitted.
+ */
+void infrared_signal_transmit(const InfraredSignal* signal);

+ 55 - 40
laser_tag_app.c

@@ -28,22 +28,33 @@ static void laser_tag_app_timer_callback(void* context) {
     furi_assert(context);
     LaserTagApp* app = context;
     FURI_LOG_D(TAG, "Timer callback triggered");
-    if(app->game_state) {
-        game_state_update_time(app->game_state, 1);
-        FURI_LOG_D(TAG, "Updated game time by 1");
-        if(app->view) {
-            FURI_LOG_D(TAG, "Updating view with the latest game state");
-            laser_tag_view_update(app->view, app->game_state);
-            app->need_redraw = true;
+
+    if(app->state == LaserTagStateSplashScreen) {
+        if(game_state_get_time(app->game_state) >= 2) {
+            FURI_LOG_I(TAG, "Splash screen time over, switching to TeamSelect");
+            app->state = LaserTagStateTeamSelect;
+            game_state_reset(app->game_state);
+            FURI_LOG_D(TAG, "Game state reset after splash screen");
+        } else {
+            FURI_LOG_D(TAG, "Updating splash screen time");
+            game_state_update_time(app->game_state, 1);
         }
+    } else if(app->state == LaserTagStateGame) {
+        FURI_LOG_D(TAG, "Updating game time by 1 second");
+        game_state_update_time(app->game_state, 1);
+    }
+
+    if(app->view) {
+        FURI_LOG_D(TAG, "Updating view with the latest game state");
+        laser_tag_view_update(app->view, app->game_state);
+        app->need_redraw = true;
     }
 }
 
 static void laser_tag_app_input_callback(InputEvent* input_event, void* context) {
     furi_assert(context);
     LaserTagApp* app = context;
-    FURI_LOG_D(
-        TAG, "Input callback triggered: type=%d, key=%d", input_event->type, input_event->key);
+    FURI_LOG_D(TAG, "Input event received: type=%d, key=%d", input_event->type, input_event->key);
     furi_message_queue_put(app->event_queue, input_event, 0);
     FURI_LOG_D(TAG, "Input event queued successfully");
 }
@@ -53,7 +64,12 @@ static void laser_tag_app_draw_callback(Canvas* canvas, void* context) {
     LaserTagApp* app = context;
     FURI_LOG_D(TAG, "Entering draw callback");
 
-    if(app->state == LaserTagStateTeamSelect) {
+    if(app->state == LaserTagStateSplashScreen) {
+        FURI_LOG_D(TAG, "Drawing splash screen");
+        canvas_clear(canvas);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 32, 32, "Laser Tag!");
+    } else if(app->state == LaserTagStateTeamSelect) {
         FURI_LOG_D(TAG, "Drawing team selection screen");
         canvas_clear(canvas);
         canvas_set_font(canvas, FontPrimary);
@@ -74,7 +90,7 @@ LaserTagApp* laser_tag_app_alloc() {
         FURI_LOG_E(TAG, "Failed to allocate LaserTagApp");
         return NULL;
     }
-    FURI_LOG_D(TAG, "LaserTagApp struct allocated");
+    FURI_LOG_I(TAG, "LaserTagApp allocated successfully");
 
     memset(app, 0, sizeof(LaserTagApp));
 
@@ -87,14 +103,14 @@ LaserTagApp* laser_tag_app_alloc() {
 
     if(!app->gui || !app->view_port || !app->view || !app->notifications || !app->game_state ||
        !app->event_queue) {
-        FURI_LOG_E(TAG, "Failed to allocate resources");
+        FURI_LOG_E(TAG, "Failed to allocate resources for LaserTagApp");
         laser_tag_app_free(app);
         return NULL;
     }
 
-    app->state = LaserTagStateTeamSelect;
+    app->state = LaserTagStateSplashScreen;
     app->need_redraw = true;
-    FURI_LOG_D(TAG, "Initial state set");
+    FURI_LOG_I(TAG, "Initial state set to SplashScreen");
 
     view_port_draw_callback_set(app->view_port, laser_tag_app_draw_callback, app);
     view_port_input_callback_set(app->view_port, laser_tag_app_input_callback, app);
@@ -107,12 +123,11 @@ LaserTagApp* laser_tag_app_alloc() {
         laser_tag_app_free(app);
         return NULL;
     }
-    FURI_LOG_D(TAG, "Timer allocated");
+    FURI_LOG_I(TAG, "Timer allocated");
 
     furi_timer_start(app->timer, furi_kernel_get_tick_frequency());
     FURI_LOG_D(TAG, "Timer started");
 
-    FURI_LOG_D(TAG, "Laser Tag App allocated successfully");
     return app;
 }
 
@@ -134,7 +149,7 @@ void laser_tag_app_free(LaserTagApp* app) {
     furi_record_close(RECORD_NOTIFICATION);
 
     free(app);
-    FURI_LOG_D(TAG, "Laser Tag App freed");
+    FURI_LOG_I(TAG, "Laser Tag App freed successfully");
 }
 
 void laser_tag_app_fire(LaserTagApp* app) {
@@ -146,46 +161,44 @@ void laser_tag_app_fire(LaserTagApp* app) {
         return;
     }
 
-    FURI_LOG_D(TAG, "Sending infrared signal");
     infrared_controller_send(app->ir_controller);
-    FURI_LOG_D(TAG, "Decreasing ammo by 1");
+    FURI_LOG_D(TAG, "Laser fired, decreasing ammo by 1");
     game_state_decrease_ammo(app->game_state, 1);
-    FURI_LOG_D(TAG, "Notifying with blink blue");
     notification_message(app->notifications, &sequence_blink_blue_100);
+    FURI_LOG_I(TAG, "Notifying user with blink blue");
     app->need_redraw = true;
 }
 
 void laser_tag_app_handle_hit(LaserTagApp* app) {
     furi_assert(app);
-    FURI_LOG_D(TAG, "Handling hit");
+    FURI_LOG_D(TAG, "Handling hit, decreasing health by 10");
 
-    FURI_LOG_D(TAG, "Decreasing health by 10");
     game_state_decrease_health(app->game_state, 10);
-    FURI_LOG_D(TAG, "Notifying with vibration");
     notification_message(app->notifications, &sequence_vibro_1);
+    FURI_LOG_I(TAG, "Notifying user with vibration");
     app->need_redraw = true;
 }
 
 static bool laser_tag_app_enter_game_state(LaserTagApp* app) {
     furi_assert(app);
-    FURI_LOG_D(TAG, "Entering game state");
+    FURI_LOG_I(TAG, "Entering game state");
 
     app->state = LaserTagStateGame;
-    FURI_LOG_D(TAG, "Resetting game state");
     game_state_reset(app->game_state);
-    FURI_LOG_D(TAG, "Updating view with new game state");
+    FURI_LOG_D(TAG, "Game state reset");
+
     laser_tag_view_update(app->view, app->game_state);
+    FURI_LOG_D(TAG, "View updated with new game state");
 
-    FURI_LOG_D(TAG, "Allocating IR controller");
     app->ir_controller = infrared_controller_alloc();
     if(!app->ir_controller) {
         FURI_LOG_E(TAG, "Failed to allocate IR controller");
         return false;
     }
-    FURI_LOG_D(TAG, "IR controller allocated");
+    FURI_LOG_I(TAG, "IR controller allocated");
 
-    FURI_LOG_D(TAG, "Setting IR controller team");
     infrared_controller_set_team(app->ir_controller, game_state_get_team(app->game_state));
+    FURI_LOG_D(TAG, "IR controller team set");
     app->need_redraw = true;
     return true;
 }
@@ -201,7 +214,6 @@ int32_t laser_tag_app(void* p) {
     }
     FURI_LOG_D(TAG, "LaserTagApp allocated successfully");
 
-    FURI_LOG_D(TAG, "Entering main loop");
     InputEvent event;
     bool running = true;
     while(running) {
@@ -211,34 +223,38 @@ int32_t laser_tag_app(void* p) {
         if(status == FuriStatusOk) {
             FURI_LOG_D(TAG, "Received input event: type=%d, key=%d", event.type, event.key);
             if(event.type == InputTypePress || event.type == InputTypeRepeat) {
-                FURI_LOG_D(TAG, "Processing input event");
-                if(app->state == LaserTagStateTeamSelect) {
+                if(app->state == LaserTagStateSplashScreen ||
+                   app->state == LaserTagStateTeamSelect) {
                     switch(event.key) {
                     case InputKeyLeft:
-                        FURI_LOG_D(TAG, "Selected Red Team");
+                        FURI_LOG_I(TAG, "Red team selected");
                         game_state_set_team(app->game_state, TeamRed);
                         if(!laser_tag_app_enter_game_state(app)) {
                             running = false;
                         }
                         break;
                     case InputKeyRight:
-                        FURI_LOG_D(TAG, "Selected Blue Team");
+                        FURI_LOG_I(TAG, "Blue team selected");
                         game_state_set_team(app->game_state, TeamBlue);
                         if(!laser_tag_app_enter_game_state(app)) {
                             running = false;
                         }
                         break;
+                    case InputKeyBack:
+                        FURI_LOG_I(TAG, "Back key pressed, exiting");
+                        running = false;
+                        break;
                     default:
                         break;
                     }
                 } else {
                     switch(event.key) {
                     case InputKeyBack:
-                        FURI_LOG_D(TAG, "Exiting game");
+                        FURI_LOG_I(TAG, "Back key pressed, exiting");
                         running = false;
                         break;
                     case InputKeyOk:
-                        FURI_LOG_D(TAG, "Firing laser");
+                        FURI_LOG_I(TAG, "OK key pressed, firing laser");
                         laser_tag_app_fire(app);
                         break;
                     default:
@@ -253,21 +269,20 @@ int32_t laser_tag_app(void* p) {
         }
 
         if(app->state == LaserTagStateGame && app->ir_controller) {
-            FURI_LOG_D(TAG, "Game is active");
             if(infrared_controller_receive(app->ir_controller)) {
-                FURI_LOG_D(TAG, "Hit received");
+                FURI_LOG_D(TAG, "Hit received, processing");
                 laser_tag_app_handle_hit(app);
             }
 
             if(game_state_is_game_over(app->game_state)) {
-                FURI_LOG_D(TAG, "Game over");
+                FURI_LOG_I(TAG, "Game over, notifying user with error sequence");
                 notification_message(app->notifications, &sequence_error);
                 running = false;
             }
         }
 
         if(app->need_redraw) {
-            FURI_LOG_D(TAG, "Updating view port");
+            FURI_LOG_D(TAG, "Updating viewport");
             view_port_update(app->view_port);
             app->need_redraw = false;
         }

+ 28 - 0
sound/sound.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef enum {
+    SoundGeneratorModePlay,
+    SoundGeneratorModeStop,
+} SoundGeneratorMode;
+
+typedef enum {
+    SoundGeneratorSoundBeep,
+    SoundGeneratorSoundTone,
+    SoundGeneratorSoundNoise,
+} SoundGeneratorSound;
+
+typedef struct SoundGenerator SoundGenerator;
+
+SoundGenerator* sound_generator_alloc();
+void sound_generator_free(SoundGenerator* sound_generator);
+
+void sound_generator_set_mode(SoundGenerator* sound_generator, SoundGeneratorMode mode);
+void sound_generator_start(SoundGenerator* sound_generator);
+void sound_generator_stop(SoundGenerator* sound_generator);
+
+void sound_generator_set_sound(SoundGenerator* sound_generator, SoundGeneratorSound sound);
+void sound_generator_set_frequency(SoundGenerator* sound_generator, uint16_t frequency);
+void sound_generator_set_volume(SoundGenerator* sound_generator, uint8_t volume);