Browse Source

Add tone_gen from https://github.com/GEMISIS/tone_gen

git-subtree-dir: tone_gen
git-subtree-mainline: 024ba3e2b07b512a5a82ea0432cc1d4e41747ad8
git-subtree-split: 6dc1a236fe0c395261b9edbfb594d61352355c16
Willy-JL 1 năm trước cách đây
mục cha
commit
798311b9b7
35 tập tin đã thay đổi với 960 bổ sung0 xóa
  1. 41 0
      tone_gen/.github/workflows/build.yml
  2. 6 0
      tone_gen/.gitignore
  3. 1 0
      tone_gen/.gitsubtree
  4. 21 0
      tone_gen/LICENSE
  5. 23 0
      tone_gen/README.MD
  6. 17 0
      tone_gen/application.fam
  7. 0 0
      tone_gen/images/.gitkeep
  8. BIN
      tone_gen/images/play_button.gif
  9. BIN
      tone_gen/images/play_button/frame_01.png
  10. BIN
      tone_gen/images/play_button/frame_02.png
  11. BIN
      tone_gen/images/play_button/frame_03.png
  12. BIN
      tone_gen/images/play_button/frame_04.png
  13. 1 0
      tone_gen/images/play_button/frame_rate
  14. BIN
      tone_gen/images/settings_button.gif
  15. BIN
      tone_gen/images/settings_button/frame_01.png
  16. BIN
      tone_gen/images/settings_button/frame_02.png
  17. 1 0
      tone_gen/images/settings_button/frame_rate
  18. BIN
      tone_gen/screenshots/menu.png
  19. BIN
      tone_gen/screenshots/playback.png
  20. BIN
      tone_gen/screenshots/settings.png
  21. 141 0
      tone_gen/src/app_context.c
  22. 62 0
      tone_gen/src/app_context.h
  23. 88 0
      tone_gen/src/scenes/playback_scene.c
  24. 11 0
      tone_gen/src/scenes/playback_scene.h
  25. 119 0
      tone_gen/src/scenes/settings_scene.c
  26. 11 0
      tone_gen/src/scenes/settings_scene.h
  27. 92 0
      tone_gen/src/scenes/starting_scene.c
  28. 11 0
      tone_gen/src/scenes/starting_scene.h
  29. 28 0
      tone_gen/src/system/audio_helper.c
  30. 16 0
      tone_gen/src/system/audio_helper.h
  31. 140 0
      tone_gen/src/tone_gen.c
  32. 36 0
      tone_gen/src/tone_gen.h
  33. 71 0
      tone_gen/src/utils/linked_list.c
  34. 23 0
      tone_gen/src/utils/linked_list.h
  35. BIN
      tone_gen/tone_gen.png

+ 41 - 0
tone_gen/.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "branches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 6 - 0
tone_gen/.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 1 - 0
tone_gen/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/GEMISIS/tone_gen main /

+ 21 - 0
tone_gen/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Gerald McAlister
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 23 - 0
tone_gen/README.MD

@@ -0,0 +1,23 @@
+# Tone Generator
+
+This repo contains a simple application for the Flipper Zero that can generate a steady tone with a variety of user chosen settings. It's designed to be a simple application showcasing how to develop apps for the Flipper Zero, and get my hands dirty with doing so. It is written entirely in C, and is very simple in nature by design.
+
+## Dependencies
+
+This project depends on [uFBT](https://github.com/flipperdevices/flipperzero-ufbt) for building. It is also recommended to install VSCode for ease of development, as well as [minicom](https://en.wikipedia.org/wiki/Minicom) for debugging (`brew install minicom`).
+
+## App Source File Structure
+
+The application is structured such that each scene can be self contained, with the sharing of data between scenes done via the app context. All of the scenes can be found with their corresponding source and header files in the `src/scenes` directory. The scenes do as follows:
+
+- **Starting Scene**: The scene where the application starts. This has the main menu options that users see when they start the app.
+- **Playback Scene**: The scene where the sound is played. Animates a waveform with the *approximate* shape of the sound being played.
+- **Settings Scene**: The scene where users can configure the tone's properties.
+
+Note as well that the app context file is generic, and designed in such as way that it should not need to be updated for things specific to the application. This allows for an easier time to allow scenes to self manage, insteaed of having somewhere else that centrally manages everything.
+
+## Helpful Commands
+
+- `ufbt`: Builds the project
+- `ufbt launch`: Launches the project on a device. Make sure no other applications (including qFlipper) are connected to the device.
+- `minicom -D /dev/tty.X`: Replace `X` with the name of your flipper device when connected and then use this to start a command line interface to your flipper device. From there, you can run `log debug` to see debug logs from the app while it is running.

+ 17 - 0
tone_gen/application.fam

@@ -0,0 +1,17 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="tone_gen",  # Must be unique
+    name="Tone Generator",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="tone_gen_app",
+    stack_size=2 * 1024,
+    fap_category="Tools",
+    fap_version="1.0",
+    fap_icon="tone_gen.png",  # 10x10 1-bit PNG
+    fap_description="A simple app to generate sound tones.",
+    fap_author="Gerald McAlister",
+    fap_weburl="https://github.com/GEMISIS/tone_gen",
+    fap_icon_assets="images",  # Image assets to compile for this application
+    sources=["src/*.c", "src/scenes/*.c", "src/system/*.c", "src/utils/*.c"],
+)

+ 0 - 0
tone_gen/images/.gitkeep


BIN
tone_gen/images/play_button.gif


BIN
tone_gen/images/play_button/frame_01.png


BIN
tone_gen/images/play_button/frame_02.png


BIN
tone_gen/images/play_button/frame_03.png


BIN
tone_gen/images/play_button/frame_04.png


+ 1 - 0
tone_gen/images/play_button/frame_rate

@@ -0,0 +1 @@
+2

BIN
tone_gen/images/settings_button.gif


BIN
tone_gen/images/settings_button/frame_01.png


BIN
tone_gen/images/settings_button/frame_02.png


+ 1 - 0
tone_gen/images/settings_button/frame_rate

@@ -0,0 +1 @@
+2

BIN
tone_gen/screenshots/menu.png


BIN
tone_gen/screenshots/playback.png


BIN
tone_gen/screenshots/settings.png


+ 141 - 0
tone_gen/src/app_context.c

@@ -0,0 +1,141 @@
+#include <gui/modules/menu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+
+#include "tone_gen.h"
+#include "app_context.h"
+
+/** custom event handler - passes the event to the scene manager */
+bool viewDispatcherCustomCallback(void* context, uint32_t custom_event) {
+    furi_assert(context);
+    struct AppContext_t* appContext = context;
+    return scene_manager_handle_custom_event(appContext->scene_manager, custom_event);
+}
+
+/** navigation event handler - passes the event to the scene manager */
+bool viewDispatcherNavigationCallback(void* context) {
+    furi_assert(context);
+    struct AppContext_t* appContext = context;
+    return scene_manager_handle_back_event(appContext->scene_manager);
+}
+
+AppContextStatus initializeAppContext(
+    struct AppContext_t** context,
+    int viewsCount,
+    const SceneManagerHandlers* sceneManagerHandlers) {
+    FURI_LOG_I(TAG, "Allocating memory for app context");
+
+    *context = malloc(sizeof(struct AppContext_t));
+    if(*context == NULL) {
+        FURI_LOG_E(TAG, "Failed to allocate memory for app context");
+        return APP_CONTEXT_CANT_ALLOCATE;
+    }
+
+    // Allocate our scene manager with the handlers provided
+    FURI_LOG_I(TAG, "Setting up the scene manager");
+    (*context)->scene_manager = scene_manager_alloc(sceneManagerHandlers, *context);
+
+    // Now setup our view dispatchers
+    FURI_LOG_I(TAG, "Setting up the view dispatcher");
+    (*context)->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue((*context)->view_dispatcher);
+
+    FURI_LOG_I(TAG, "Setting view dispatcher callbacks");
+    view_dispatcher_set_event_callback_context((*context)->view_dispatcher, (*context));
+    FURI_LOG_I(TAG, "Setting view dispatcher custom event callbacks");
+    view_dispatcher_set_custom_event_callback(
+        (*context)->view_dispatcher, viewDispatcherCustomCallback);
+    FURI_LOG_I(TAG, "Setting view dispatcher navigation event callbacks");
+    view_dispatcher_set_navigation_event_callback(
+        (*context)->view_dispatcher, viewDispatcherNavigationCallback);
+    FURI_LOG_I(TAG, "Setting view dispatcher callbacks done");
+
+    (*context)->activeViews = malloc(sizeof(struct View_t) * viewsCount);
+    if((*context)->activeViews == NULL) {
+        return APP_CONTEXT_CANT_ALLOCATE;
+    }
+    (*context)->activeViewsCount = viewsCount;
+
+    return APP_CONTEXT_OK;
+}
+
+AppContextStatus addViewToAppContext(struct AppContext_t** context, struct View_t* view) {
+    if(view->viewId > (*context)->activeViewsCount || view->viewId < 0) {
+        FURI_LOG_I(TAG, "Not enough views!");
+        return APP_CONTEXT_NOT_ENOUGH_VIEWS;
+    }
+    (*context)->activeViews[view->viewId] = view;
+    switch(view->type) {
+    case MENU:
+        view_dispatcher_add_view(
+            (*context)->view_dispatcher, view->viewId, menu_get_view(view->viewData));
+        break;
+    case SUBMENU:
+        view_dispatcher_add_view(
+            (*context)->view_dispatcher, view->viewId, submenu_get_view(view->viewData));
+        break;
+    case VIEW:
+        view_dispatcher_add_view((*context)->view_dispatcher, view->viewId, view->viewData);
+        break;
+    case VARIABLE_ITEM_LIST:
+        view_dispatcher_add_view(
+            (*context)->view_dispatcher,
+            view->viewId,
+            variable_item_list_get_view(view->viewData));
+        break;
+    case POPUP:
+        view_dispatcher_add_view(
+            (*context)->view_dispatcher, view->viewId, popup_get_view(view->viewData));
+        break;
+    }
+    return APP_CONTEXT_OK;
+}
+
+AppContextStatus freeAppContextViews(struct AppContext_t** context) {
+    FURI_LOG_I(TAG, "Freeing views");
+    for(int i = 0; i < (*context)->activeViewsCount; i++) {
+        struct View_t* view = (*context)->activeViews[i];
+        if(view != NULL) {
+            view_dispatcher_remove_view((*context)->view_dispatcher, view->viewId);
+
+            switch(view->type) {
+            case MENU:
+                menu_free(view->viewData);
+                break;
+            case SUBMENU:
+                submenu_free(view->viewData);
+                break;
+            case VIEW:
+                view_free(view->viewData);
+                break;
+            case VARIABLE_ITEM_LIST:
+                variable_item_list_free(view->viewData);
+                break;
+            case POPUP:
+                popup_free(view->viewData);
+                break;
+            }
+            free(view);
+        }
+    }
+    FURI_LOG_I(TAG, "Removing all views from list");
+    free((*context)->activeViews);
+    (*context)->activeViewsCount = 0;
+    return APP_CONTEXT_OK;
+}
+
+AppContextStatus freeAppContext(struct AppContext_t** context) {
+    FURI_LOG_I(TAG, "Ensuring views are free'd");
+    AppContextStatus result = freeAppContextViews(context);
+    if(result != APP_CONTEXT_OK) {
+        return APP_CONTEXT_CANT_FREE_VIEWS;
+    }
+    FURI_LOG_I(TAG, "Freeing the scene");
+    scene_manager_free((*context)->scene_manager);
+    FURI_LOG_I(TAG, "Freeing the view dispatcher");
+    view_dispatcher_free((*context)->view_dispatcher);
+    FURI_LOG_I(TAG, "Freeing the app context");
+    free((*context));
+    return APP_CONTEXT_OK;
+}

+ 62 - 0
tone_gen/src/app_context.h

@@ -0,0 +1,62 @@
+#ifndef _APP_CONTEXT_H_
+
+#define _APP_CONTEXT_H_
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+
+#include "utils/linked_list.h"
+
+typedef enum {
+    MENU,
+    SUBMENU,
+    VIEW,
+    VARIABLE_ITEM_LIST,
+    POPUP,
+} ViewType;
+
+struct View_t {
+    ViewType type;
+    int viewId;
+    void* viewData;
+};
+
+typedef enum {
+    APP_CONTEXT_OK = 0,
+    APP_CONTEXT_CANT_ALLOCATE = -1,
+    APP_CONTEXT_CANT_FREE_VIEWS = -2,
+    APP_CONTEXT_NOT_ENOUGH_VIEWS = -3,
+    APP_CONTEXT_UNKNOWN_ERROR = -4,
+} AppContextStatus;
+
+struct AppContext_t {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+    struct View_t** activeViews;
+    int activeViewsCount;
+    void* additionalData;
+};
+
+/// @brief Creates an app context with the desired scene handlers.
+/// @param context The app context to populate. Will be passed through to the supplied scene handlers.
+/// @param viewsCount The number of views that to be added to this scene.
+/// @param sceneManagerHandlers The scene handlers to use for each scene in your app.
+/// @return Returns APP_CONTEXT_OK on success, APP_CONTEXT_CANT_ALLOCATE if there is an error.
+AppContextStatus initializeAppContext(
+    struct AppContext_t** context,
+    int viewsCount,
+    const SceneManagerHandlers* sceneManagerHandlers);
+
+/// @brief Adds a view to the given app context.
+/// @param context The app context to add the view to.
+/// @param view The view to add to the app context.
+/// @return Returns APP_CONTEXT_OK on success, APP_CONTEXT_NOT_ENOUGH_VIEWS if the ID of
+//  the view exceeds the number of available views in the app context.
+AppContextStatus addViewToAppContext(struct AppContext_t** context, struct View_t* view);
+
+/// @brief Frees the app context entirely, cleaning it up from usage.
+/// @param context The app context to clean up.
+/// @return Returns APP_CONTEXT_OK on success. Should not error.
+AppContextStatus freeAppContext(struct AppContext_t** context);
+
+#endif

+ 88 - 0
tone_gen/src/scenes/playback_scene.c

@@ -0,0 +1,88 @@
+#include <furi_hal.h>
+
+#include "playback_scene.h"
+#include "../app_context.h"
+#include "../tone_gen.h"
+#include "../system/audio_helper.h"
+
+#define SINE_WAVE(x, toneModelData) \
+    (sin((x + toneDataModel->animationOffset) * 50) * 20 + (64 / 2))
+
+#define SQUARE_WAVE(x, toneModelData) \
+    ((sin((x + toneDataModel->animationOffset) * 50) > 0 ? 1 : -1) * 20 + (64 / 2))
+
+// Renders the waveform
+static void playback_view_draw_callback(Canvas* canvas, void* model) {
+    UNUSED(model);
+
+    struct ToneData_t* toneDataModel = (struct ToneData_t*)model;
+    for(int x = 1; x < 128; x++) {
+        int x1 = x - 1;
+        int x2 = x;
+        int y1 = 0;
+        int y2 = 0;
+        switch(toneDataModel->waveType) {
+        case SINE:
+            y1 = SINE_WAVE(x1, toneDataModel);
+            y2 = SINE_WAVE(x2, toneDataModel);
+            break;
+        case SQUARE:
+            y1 = SQUARE_WAVE(x1, toneDataModel);
+            y2 = SQUARE_WAVE(x2, toneDataModel);
+            break;
+        default:
+            y1 = 64 / 2;
+            y2 = 64 / 2;
+            break;
+        }
+        // Draw lines to connect the pieces of the wave.
+        canvas_draw_line(canvas, x1, y1, x2, y2);
+    }
+    if(toneDataModel->animationOffset < 128) {
+        toneDataModel->animationOffset += 2;
+    } else {
+        toneDataModel->animationOffset = 0;
+    }
+}
+
+// Sets up the waveform to be displayed
+void scene_on_enter_playback_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_enter_playback_scene");
+    struct AppContext_t* app = (struct AppContext_t*)context;
+    struct View_t* playbackView = app->activeViews[ToneGenAppView_PlaybackView];
+
+    // Configure the custom view
+    view_set_draw_callback(playbackView->viewData, playback_view_draw_callback);
+    view_set_context(playbackView->viewData, context);
+
+    FURI_LOG_I(TAG, "setting view model");
+    struct ToneData_t* toneDataModel = (struct ToneData_t*)view_get_model(playbackView->viewData);
+    toneDataModel->waveType = ((struct ToneData_t*)app->additionalData)->waveType;
+    toneDataModel->frequency = ((struct ToneData_t*)app->additionalData)->frequency;
+    toneDataModel->volume = ((struct ToneData_t*)app->additionalData)->volume;
+
+    // Set the currently active view
+    FURI_LOG_I(TAG, "setting active view");
+    view_dispatcher_switch_to_view(app->view_dispatcher, ToneGenAppView_PlaybackView);
+
+    if(initializeSpeaker()) {
+        FURI_LOG_I(TAG, "Starting sound");
+        startSound(toneDataModel);
+    }
+}
+
+// Not actively used in this instance.
+bool scene_on_event_playback_scene(void* context, SceneManagerEvent event) {
+    FURI_LOG_I(TAG, "scene_on_event_playback_scene");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+// Not actively used in this instance.
+void scene_on_exit_playback_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_exit_playback_scene");
+    UNUSED(context);
+    stopSound();
+    deinitializeSpeaker();
+}

+ 11 - 0
tone_gen/src/scenes/playback_scene.h

@@ -0,0 +1,11 @@
+#ifndef _PLAYBACK_SCENE_H_
+
+#define _PLAYBACK_SCENE_H_
+
+#include <gui/scene_manager.h>
+
+void scene_on_enter_playback_scene(void* context);
+bool scene_on_event_playback_scene(void* context, SceneManagerEvent event);
+void scene_on_exit_playback_scene(void* context);
+
+#endif

+ 119 - 0
tone_gen/src/scenes/settings_scene.c

@@ -0,0 +1,119 @@
+#include <gui/modules/variable_item_list.h>
+
+#include "settings_scene.h"
+#include "../app_context.h"
+#include "../tone_gen.h"
+#include "../utils/linked_list.h"
+
+// Not actively used in this instance.
+void menu_callback_settings_scene(void* context, uint32_t index) {
+    UNUSED(context);
+    UNUSED(index);
+}
+
+static uint8_t wave_option_values[] = {SINE, SQUARE};
+static char* wave_option_names[] = {"Sine", "Square"};
+static void wave_type_option_change(VariableItem* item) {
+    struct AppContext_t* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, wave_option_names[index]);
+    ((struct ToneData_t*)app->additionalData)->waveType = index;
+}
+
+// Since the max number of options for variable item lists is
+// the size of an 8-bit integer, we need to limit the max
+// number of steps. In this case, we limit it to 241 total
+// steps available, incrementing in steps of 10.
+#define MIN_FREQ 100
+#define MAX_FREQ 2500
+#define FREQ_STEPS 10
+#define INDEX_TO_FREQ(index) (uint16_t)((index * FREQ_STEPS) + MIN_FREQ)
+#define FREQ_TO_INDEX(freq) (uint8_t)((freq - MIN_FREQ) / FREQ_STEPS)
+char* frequencyStr;
+static void frequency_option_change(VariableItem* item) {
+    struct AppContext_t* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    ((struct ToneData_t*)app->additionalData)->frequency = INDEX_TO_FREQ(index);
+    snprintf(frequencyStr, 8, "%dhz", ((struct ToneData_t*)app->additionalData)->frequency);
+    variable_item_set_current_value_text(item, frequencyStr);
+}
+
+char* volumeStr;
+static void volume_option_change(VariableItem* item) {
+    struct AppContext_t* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    ((struct ToneData_t*)app->additionalData)->volume = ((float)(index)) / 10.0f;
+    snprintf(volumeStr, 5, "%d", (index * 10));
+    variable_item_set_current_value_text(item, volumeStr);
+}
+
+/** resets the menu, gives it content, callbacks and selection enums */
+void scene_on_enter_settings_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_enter_settings_scene");
+    struct AppContext_t* app = (struct AppContext_t*)context;
+
+    // Setup our menu
+    FURI_LOG_D(TAG, "Adding view menu");
+    struct View_t* variableItemListView = app->activeViews[ToneGenAppView_VariableItemList];
+
+    // Set the currently active view
+    variable_item_list_reset(variableItemListView->viewData);
+
+    FURI_LOG_D(TAG, "Adding options for settings");
+    // Wave type setting
+    VariableItem* item = variable_item_list_add(
+        variableItemListView->viewData,
+        "Wave Display",
+        COUNT_OF(wave_option_values),
+        wave_type_option_change,
+        app);
+    variable_item_set_current_value_index(
+        item, ((struct ToneData_t*)app->additionalData)->waveType);
+    variable_item_set_current_value_text(
+        item, wave_option_names[((struct ToneData_t*)app->additionalData)->waveType]);
+
+    // Frequency setting
+    item = variable_item_list_add(
+        variableItemListView->viewData,
+        "Frequency",
+        FREQ_TO_INDEX(MAX_FREQ) + 1,
+        frequency_option_change,
+        app);
+    variable_item_set_current_value_index(
+        item, FREQ_TO_INDEX(((struct ToneData_t*)app->additionalData)->frequency));
+
+    frequencyStr = calloc(8, sizeof(char));
+    snprintf(frequencyStr, 8, "%dhz", ((struct ToneData_t*)app->additionalData)->frequency);
+    variable_item_set_current_value_text(item, frequencyStr);
+
+    // Volume setting
+    item = variable_item_list_add(
+        variableItemListView->viewData, "Volume", 11, volume_option_change, app);
+    variable_item_set_current_value_index(
+        item, (uint8_t)(((struct ToneData_t*)app->additionalData)->volume * 10.0f));
+
+    volumeStr = calloc(5, sizeof(char));
+    snprintf(
+        volumeStr,
+        5,
+        "%d",
+        ((uint8_t)(((struct ToneData_t*)app->additionalData)->volume * 100.0f)));
+    variable_item_set_current_value_text(item, volumeStr);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, ToneGenAppView_VariableItemList);
+}
+
+// Not actively used in this instance.
+bool scene_on_event_settings_scene(void* context, SceneManagerEvent event) {
+    FURI_LOG_I(TAG, "scene_on_event_settings_scene");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void scene_on_exit_settings_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_exit_settings_scene");
+    UNUSED(context);
+    free(frequencyStr);
+    free(volumeStr);
+}

+ 11 - 0
tone_gen/src/scenes/settings_scene.h

@@ -0,0 +1,11 @@
+#ifndef _SETTINGS_SCENE_H_
+
+#define _SETTINGS_SCENE_H_
+
+#include <gui/scene_manager.h>
+
+void scene_on_enter_settings_scene(void* context);
+bool scene_on_event_settings_scene(void* context, SceneManagerEvent event);
+void scene_on_exit_settings_scene(void* context);
+
+#endif

+ 92 - 0
tone_gen/src/scenes/starting_scene.c

@@ -0,0 +1,92 @@
+#include <gui/modules/menu.h>
+#include <gui/modules/popup.h>
+
+#include "starting_scene.h"
+#include "../app_context.h"
+#include "../tone_gen.h"
+#include "../utils/linked_list.h"
+
+// Icons to include
+#include "tone_gen_icons.h"
+
+/** indices for menu items */
+typedef enum {
+    ToneGenAppMenuSelection_Play,
+    ToneGenAppMenuSelection_Adjust
+} ToneGenAppMenuSelection;
+
+/** main menu callback - sends a custom event to the scene manager based on the menu selection */
+void menu_callback_starting_scene(void* context, uint32_t index) {
+    FURI_LOG_I(TAG, "menu_callback_starting_scene");
+    UNUSED(context);
+    struct AppContext_t* app = context;
+    switch(index) {
+    case ToneGenAppMenuSelection_Play:
+        scene_manager_handle_custom_event(app->scene_manager, ToneGenAppMenuSelection_Play);
+        break;
+    case ToneGenAppMenuSelection_Adjust:
+        scene_manager_handle_custom_event(app->scene_manager, ToneGenAppMenuSelection_Adjust);
+        break;
+    }
+}
+
+/** resets the menu, gives it content, callbacks and selection enums */
+void scene_on_enter_starting_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_enter_starting_scene");
+    struct AppContext_t* app = (struct AppContext_t*)context;
+    struct View_t* menuView = app->activeViews[ToneGenAppView_SharedMenu];
+
+    // Set the currently active view
+    menu_reset(menuView->viewData);
+
+    // NB. icons are specified as NULL below, because:
+    // * icons are _always_ animated by the menu
+    // * the icons provided (&I_one, &I_two) are generated by the build process
+    // * these icons do not have a framerate (resulting in a division by zero)
+    menu_add_item(
+        menuView->viewData,
+        "Play Tone",
+        &A_play_button,
+        ToneGenAppMenuSelection_Play,
+        menu_callback_starting_scene,
+        app);
+    menu_add_item(
+        menuView->viewData,
+        "Adjust Tone",
+        &A_settings_button,
+        ToneGenAppMenuSelection_Adjust,
+        menu_callback_starting_scene,
+        app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, ToneGenAppView_SharedMenu);
+}
+
+/** main menu event handler - switches scene based on the event */
+bool scene_on_event_starting_scene(void* context, SceneManagerEvent event) {
+    FURI_LOG_I(TAG, "scene_on_event_starting_scene");
+    UNUSED(context);
+    struct AppContext_t* app = context;
+    bool consumed = false;
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        switch(event.event) {
+        case ToneGenAppMenuSelection_Play:
+            scene_manager_next_scene(app->scene_manager, ToneGenAppScene_Playback);
+            consumed = true;
+            break;
+        case ToneGenAppMenuSelection_Adjust:
+            scene_manager_next_scene(app->scene_manager, ToneGenAppScene_Settings);
+            consumed = true;
+            break;
+        }
+        break;
+    default: // eg. SceneManagerEventTypeBack, SceneManagerEventTypeTick
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+void scene_on_exit_starting_scene(void* context) {
+    FURI_LOG_I(TAG, "scene_on_exit_starting_scene");
+    UNUSED(context);
+}

+ 11 - 0
tone_gen/src/scenes/starting_scene.h

@@ -0,0 +1,11 @@
+#ifndef _STARTING_SCENE_H_
+
+#define _STARTING_SCENE_H_
+
+#include <gui/scene_manager.h>
+
+void scene_on_enter_starting_scene(void* context);
+bool scene_on_event_starting_scene(void* context, SceneManagerEvent event);
+void scene_on_exit_starting_scene(void* context);
+
+#endif

+ 28 - 0
tone_gen/src/system/audio_helper.c

@@ -0,0 +1,28 @@
+#include "audio_helper.h"
+
+bool initializeSpeaker() {
+    return furi_hal_speaker_acquire(SPEAKER_TIMEOUT);
+}
+
+bool startSound(struct ToneData_t* toneData) {
+    bool hasSpeaker = furi_hal_speaker_is_mine();
+    if(!hasSpeaker) {
+        hasSpeaker = furi_hal_speaker_acquire(SPEAKER_TIMEOUT);
+    }
+
+    if(hasSpeaker) {
+        FURI_LOG_I(TAG, "Sound is beginning to play");
+        furi_hal_speaker_start(toneData->frequency, toneData->volume);
+    } else {
+        FURI_LOG_E(TAG, "Error acquiring speaker!");
+    }
+    return hasSpeaker;
+}
+
+void stopSound() {
+    furi_hal_speaker_stop();
+}
+
+void deinitializeSpeaker() {
+    furi_hal_speaker_release();
+}

+ 16 - 0
tone_gen/src/system/audio_helper.h

@@ -0,0 +1,16 @@
+#ifndef _AUDIO_HELPER_H_
+
+#define _AUDIO_HELPER_H_
+
+#include <furi_hal.h>
+
+#include "../tone_gen.h"
+
+#define SPEAKER_TIMEOUT 10
+
+bool initializeSpeaker();
+bool startSound(struct ToneData_t* toneData);
+void stopSound();
+void deinitializeSpeaker();
+
+#endif

+ 140 - 0
tone_gen/src/tone_gen.c

@@ -0,0 +1,140 @@
+#include <gui/canvas.h>
+#include <gui/modules/menu.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+
+/* generated by fbt from .png files in images folder */
+#include <tone_gen_icons.h>
+
+#include "app_context.h"
+#include "tone_gen.h"
+
+#include "scenes/starting_scene.h"
+#include "scenes/playback_scene.h"
+#include "scenes/settings_scene.h"
+
+/** collection of all scene on_enter handlers - in the same order as their enum */
+void (*const scene_on_enter_handlers[])(void*) = {
+    scene_on_enter_starting_scene,
+    scene_on_enter_playback_scene,
+    scene_on_enter_settings_scene,
+};
+
+/** collection of all scene on event handlers - in the same order as their enum */
+bool (*const scene_on_event_handlers[])(void*, SceneManagerEvent) = {
+    scene_on_event_starting_scene,
+    scene_on_event_playback_scene,
+    scene_on_event_settings_scene,
+};
+
+/** collection of all scene on exit handlers - in the same order as their enum */
+void (*const scene_on_exit_handlers[])(void*) = {
+    scene_on_exit_starting_scene,
+    scene_on_exit_playback_scene,
+    scene_on_exit_settings_scene,
+};
+
+const SceneManagerHandlers scene_event_handlers = {
+    .on_enter_handlers = scene_on_enter_handlers,
+    .on_event_handlers = scene_on_event_handlers,
+    .on_exit_handlers = scene_on_exit_handlers,
+    .scene_num = ToneGenAppScene_count};
+
+int setupViews(struct AppContext_t** appContext) {
+    // Create views
+    FURI_LOG_I(TAG, "Creating views");
+    struct View_t* sharedMenuView = malloc(sizeof(struct View_t));
+    sharedMenuView->viewData = menu_alloc();
+    sharedMenuView->viewId = ToneGenAppView_SharedMenu;
+    sharedMenuView->type = MENU;
+
+    struct View_t* submenuView = malloc(sizeof(struct View_t));
+    submenuView->viewData = submenu_alloc();
+    submenuView->viewId = ToneGenAppView_Submenu;
+    submenuView->type = SUBMENU;
+
+    struct View_t* playbackView = malloc(sizeof(struct View_t));
+    playbackView->viewData = view_alloc();
+    playbackView->viewId = ToneGenAppView_PlaybackView;
+    playbackView->type = VIEW;
+
+    FURI_LOG_I(TAG, "creating var-item-list view");
+    struct View_t* variableItemListView = malloc(sizeof(struct View_t));
+    FURI_LOG_I(TAG, "allocating view data");
+    variableItemListView->viewData = variable_item_list_alloc();
+    FURI_LOG_I(TAG, "setting view id");
+    variableItemListView->viewId = ToneGenAppView_VariableItemList;
+    FURI_LOG_I(TAG, "setting view type");
+    variableItemListView->type = VARIABLE_ITEM_LIST;
+    FURI_LOG_I(TAG, "moving on");
+
+    // Add views to the app context to be managed there
+    FURI_LOG_I(TAG, "Adding views to app context");
+    AppContextStatus result = addViewToAppContext(appContext, sharedMenuView);
+    if(result != APP_CONTEXT_OK) {
+        FURI_LOG_E(TAG, "There was a problem adding the view %d!", sharedMenuView->viewId);
+        return -1;
+    }
+
+    result = addViewToAppContext(appContext, submenuView);
+    if(result != APP_CONTEXT_OK) {
+        FURI_LOG_E(TAG, "There was a problem adding the view %d!", submenuView->viewId);
+        return -1;
+    }
+
+    result = addViewToAppContext(appContext, playbackView);
+    if(result != APP_CONTEXT_OK) {
+        FURI_LOG_E(TAG, "There was a problem adding the view %d!", playbackView->viewId);
+        return -1;
+    }
+
+    FURI_LOG_I(TAG, "Adding variable item list view");
+    result = addViewToAppContext(appContext, variableItemListView);
+    if(result != APP_CONTEXT_OK) {
+        FURI_LOG_E(TAG, "There was a problem adding the view %d!", variableItemListView->viewId);
+        return -1;
+    }
+
+    // On the playback view, ensure we only allocate for the model once
+    FURI_LOG_I(TAG, "allocating view model for playback");
+    view_allocate_model(playbackView->viewData, ViewModelTypeLockFree, sizeof(struct ToneData_t));
+
+    return 0;
+}
+
+int32_t tone_gen_app(void* p) {
+    UNUSED(p);
+
+    FURI_LOG_I(TAG, "Tone gen app starting...");
+
+    struct AppContext_t* appContext;
+    AppContextStatus result =
+        initializeAppContext(&appContext, ToneGenAppView_count, &scene_event_handlers);
+
+    if(result == APP_CONTEXT_OK) {
+        appContext->additionalData = malloc(sizeof(struct ToneData_t));
+        ((struct ToneData_t*)appContext->additionalData)->animationOffset = 0;
+        ((struct ToneData_t*)appContext->additionalData)->frequency = 440;
+        ((struct ToneData_t*)appContext->additionalData)->waveType = SINE;
+        ((struct ToneData_t*)appContext->additionalData)->volume = 1.0f;
+
+        result = setupViews(&appContext);
+        if(result == 0) {
+            // set the scene and launch the main loop
+            FURI_LOG_D(TAG, "Setting the scene");
+            Gui* gui = furi_record_open(RECORD_GUI);
+            view_dispatcher_attach_to_gui(
+                appContext->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+            scene_manager_next_scene(appContext->scene_manager, ToneGenAppScene_Starting);
+            FURI_LOG_D(TAG, "Starting the view dispatcher");
+            view_dispatcher_run(appContext->view_dispatcher);
+        }
+
+        // free all memory
+        FURI_LOG_D(TAG, "Ending the app");
+        furi_record_close(RECORD_GUI);
+        freeAppContext(&appContext);
+        return 0;
+    }
+    return -1;
+}

+ 36 - 0
tone_gen/src/tone_gen.h

@@ -0,0 +1,36 @@
+#ifndef _TONE_GEN_H_
+
+#define _TONE_GEN_H_
+
+#define TAG "tone-gen"
+
+#include <furi.h>
+
+// ids for all scenes used by the app
+typedef enum {
+    ToneGenAppScene_Starting,
+    ToneGenAppScene_Playback,
+    ToneGenAppScene_Settings,
+    ToneGenAppScene_count
+} ToneGenAppScene;
+
+// ids for the 2 types of view used by the app
+typedef enum {
+    ToneGenAppView_SharedMenu,
+    ToneGenAppView_Submenu,
+    ToneGenAppView_VariableItemList,
+    ToneGenAppView_PlaybackView,
+    ToneGenAppView_Popup,
+    ToneGenAppView_count
+} ToneGenAppView;
+
+typedef enum { SINE, SQUARE } ToneWaveType;
+
+struct ToneData_t {
+    int animationOffset;
+    ToneWaveType waveType;
+    uint16_t frequency;
+    float volume;
+};
+
+#endif

+ 71 - 0
tone_gen/src/utils/linked_list.c

@@ -0,0 +1,71 @@
+#include <stdlib.h>
+
+#include "linked_list.h"
+
+int getLength(const struct ListNode_t* root) {
+    int count = 0;
+    while(root) {
+        root = root->next;
+        count += 1;
+    }
+
+    return count;
+}
+
+LinkedListStatus
+    createNode(struct ListNode_t** emptyNode, struct ListNode_t* previousNode, void* data) {
+    *emptyNode = malloc(sizeof(struct ListNode_t));
+    if(*emptyNode == 0) {
+        return LIST_CANT_ALLOCATE;
+    }
+    (*emptyNode)->data = data;
+    (*emptyNode)->previous = previousNode;
+    return LIST_OK;
+}
+
+LinkedListStatus addNode(struct ListNode_t** root, void* data) {
+    // If there is no root node, add that first
+    if(*root == 0) {
+        return createNode(root, 0, data);
+    }
+
+    // Iterate until we find an empty node
+    struct ListNode_t* base = *root;
+    while(base->next) {
+        base = base->next;
+    }
+    return createNode(&base->next, base, data);
+}
+
+LinkedListStatus removeNode(struct ListNode_t** root, const void* data) {
+    if(*root == 0) {
+        return LIST_NO_NODE;
+    }
+
+    struct ListNode_t* base = *root;
+    while(base->data != data && base->next) {
+        base = base->next;
+    }
+
+    // Delete node if data is matching
+    if(base->data == data) {
+        (base->previous)->next = base->next;
+        (base->next)->previous = base->previous;
+        free(base);
+        return LIST_OK;
+    }
+    return LIST_NO_NODE;
+}
+
+LinkedListStatus removeAllNodes(struct ListNode_t** root) {
+    struct ListNode_t* base = *root;
+    struct ListNode_t* temp;
+    while(base) {
+        temp = base;
+        base = base->next;
+        free(temp);
+    }
+    (*root) = NULL;
+
+    return LIST_OK;
+}

+ 23 - 0
tone_gen/src/utils/linked_list.h

@@ -0,0 +1,23 @@
+#ifndef _LINKED_LIST_H_
+
+#define _LINKED_LIST_H_
+
+typedef enum {
+    LIST_OK = 0,
+    LIST_CANT_ALLOCATE = -1,
+    LIST_NO_NODE = -2,
+} LinkedListStatus;
+
+struct ListNode_t {
+    struct ListNode_t* previous;
+    struct ListNode_t* next;
+
+    void* data;
+};
+
+int getLength(const struct ListNode_t* root);
+LinkedListStatus addNode(struct ListNode_t** root, void* data);
+LinkedListStatus removeNode(struct ListNode_t** root, const void* data);
+LinkedListStatus removeAllNodes(struct ListNode_t** root);
+
+#endif

BIN
tone_gen/tone_gen.png