|
|
@@ -1,912 +0,0 @@
|
|
|
-#include <furi.h>
|
|
|
-#include <furi_hal.h>
|
|
|
-#include <gui/gui.h>
|
|
|
-#include <gui/view.h>
|
|
|
-#include <gui/view_dispatcher.h>
|
|
|
-#include <gui/modules/submenu.h>
|
|
|
-#include <gui/modules/text_input.h>
|
|
|
-#include <gui/modules/widget.h>
|
|
|
-#include <gui/modules/variable_item_list.h>
|
|
|
-#include <dialogs/dialogs.h>
|
|
|
-#include <storage/storage.h>
|
|
|
-#include <flipper_format.h>
|
|
|
-#include "fmf_to_sub_icons.h"
|
|
|
-
|
|
|
-#define TAG "FMF to SUB"
|
|
|
-
|
|
|
-#define FMF_FILE_EXTENSION ".fmf"
|
|
|
-
|
|
|
-#define FMF_LOAD_PATH \
|
|
|
- EXT_PATH("apps_data") \
|
|
|
- "/" \
|
|
|
- "music_player"
|
|
|
-
|
|
|
-// Our application menu.
|
|
|
-typedef enum {
|
|
|
- Fmf2SubSubmenuIndexConfigure,
|
|
|
- Fmf2SubSubmenuIndexConvert,
|
|
|
- Fmf2SubSubmenuIndexAbout,
|
|
|
-} Fmf2SubSubmenuIndex;
|
|
|
-
|
|
|
-// Each view is a screen we show the user.
|
|
|
-typedef enum {
|
|
|
- Fmf2SubViewSubmenu, // The menu when the app starts
|
|
|
- Fmf2SubViewTextInput, // Input for configuring text settings
|
|
|
- Fmf2SubViewConfigure, // The configuration screen
|
|
|
- Fmf2SubViewConvert, // The main screen
|
|
|
- Fmf2SubViewAbout, // The about screen with directions, link to social channel, etc.
|
|
|
-} Fmf2SubView;
|
|
|
-
|
|
|
-typedef enum {
|
|
|
- Fmf2SubEventIdRedrawScreen = 0, // Custom event to redraw the screen
|
|
|
- Fmf2SubEventIdOkPressed = 1, // Custom event to process OK button getting pressed down
|
|
|
- Fmf2SubEventIdLoadFile = 2, // Custom event to load the file
|
|
|
- Fmf2SubEventIdCreateSub = 3, // Custom event to create the sub file
|
|
|
-} Fmf2SubEventId;
|
|
|
-
|
|
|
-typedef struct {
|
|
|
- ViewDispatcher* view_dispatcher; // Switches between our views
|
|
|
- DialogsApp* dialogs; // Shows dialogs like file browser
|
|
|
- Submenu* submenu; // The application menu
|
|
|
- TextInput* text_input; // The text input screen
|
|
|
- VariableItemList* variable_item_list_config; // The configuration screen
|
|
|
- VariableItem* variable_item_button; // The button on FlipBoard
|
|
|
- View* view_convert; // The main screen
|
|
|
- Widget* widget_about; // The about screen
|
|
|
- FuriString* file_path; // The path to the file
|
|
|
- char* temp_buffer; // Temporary buffer for text input
|
|
|
- uint32_t temp_buffer_size; // Size of temporary buffer
|
|
|
- FuriTimer* timer; // Timer for redrawing the screen
|
|
|
-} Fmf2SubApp;
|
|
|
-
|
|
|
-typedef enum {
|
|
|
- Fmf2SubStateIdle,
|
|
|
- Fmf2SubStateLoading,
|
|
|
- Fmf2SubStateConverting,
|
|
|
- Fmf2SubStateConverted,
|
|
|
- Fmf2SubStateError,
|
|
|
-} Fmf2SubState;
|
|
|
-
|
|
|
-typedef struct {
|
|
|
- uint32_t bpm;
|
|
|
- uint32_t duration;
|
|
|
- uint32_t octave;
|
|
|
- FuriString* notes;
|
|
|
-} Fmf2SubData;
|
|
|
-
|
|
|
-typedef struct {
|
|
|
- uint8_t setting_frequency_index; // The frequency
|
|
|
- uint8_t setting_modulation_index; // The modulation
|
|
|
- uint8_t setting_button_index; // The button on FlipBoard
|
|
|
- Fmf2SubState state; // The state of the application
|
|
|
- Fmf2SubData data; // The data from the file
|
|
|
-} Fmf2SubConvertModel;
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback for exiting the application.
|
|
|
- * @details This function is called when user press back button. We return VIEW_NONE to
|
|
|
- * indicate that we want to exit the application.
|
|
|
- * @param _context The context - unused
|
|
|
- * @return next view id (VIEW_NONE)
|
|
|
-*/
|
|
|
-static uint32_t fmf2sub_navigation_exit_callback(void* _context) {
|
|
|
- UNUSED(_context);
|
|
|
- return VIEW_NONE;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback for returning to submenu.
|
|
|
- * @details This function is called when user press back button. We return ViewSubmenu to
|
|
|
- * indicate that we want to navigate to the submenu.
|
|
|
- * @param _context The context - unused
|
|
|
- * @return next view id (ViewSubmenu)
|
|
|
-*/
|
|
|
-static uint32_t fmf2sub_navigation_submenu_callback(void* _context) {
|
|
|
- UNUSED(_context);
|
|
|
- return Fmf2SubViewSubmenu;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Handle submenu item selection.
|
|
|
- * @details This function is called when user selects an item from the submenu.
|
|
|
- * @param context The context - Fmf2SubApp object.
|
|
|
- * @param index The Fmf2SubSubmenuIndex item that was clicked.
|
|
|
-*/
|
|
|
-static void fmf2sub_submenu_callback(void* context, uint32_t index) {
|
|
|
- Fmf2SubApp* app = (Fmf2SubApp*)context;
|
|
|
- switch(index) {
|
|
|
- case Fmf2SubSubmenuIndexConfigure:
|
|
|
- view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewConfigure);
|
|
|
- break;
|
|
|
- case Fmf2SubSubmenuIndexConvert:
|
|
|
- view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewConvert);
|
|
|
- break;
|
|
|
- case Fmf2SubSubmenuIndexAbout:
|
|
|
- view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewAbout);
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Frequency settings and values.
|
|
|
-*/
|
|
|
-static const char* setting_frequency_label = "Frequency";
|
|
|
-static char* setting_frequency_values[] = {
|
|
|
- "300000000", "302757000", "303875000", "303900000", "304250000", "307000000", "307500000",
|
|
|
- "307800000", "309000000", "310000000", "312000000", "312100000", "313000000", "313850000",
|
|
|
- "314000000", "314350000", "314980000", "315000000", "318000000", "330000000", "345000000",
|
|
|
- "348000000", "387000000", "390000000", "418000000", "430000000", "431000000", "431500000",
|
|
|
- "433075000", "433220000", "433420000", "433657070", "433889000", "433920000", "434075000",
|
|
|
- "434176948", "434390000", "434420000", "434775000", "438900000", "440175000", "464000000",
|
|
|
- "779000000", "868350000", "868400000", "868800000", "868950000", "906400000", "915000000",
|
|
|
- "925000000", "928000000"};
|
|
|
-static char* setting_frequency_names[] = {
|
|
|
- "300.00", "302.75", "303.88", "303.90", "304.25", "307.00", "307.50", "307.80", "309.00",
|
|
|
- "310.00", "312.00", "312.10", "313.00", "313.85", "314.00", "314.35", "314.98", "315.00",
|
|
|
- "318.00", "330.00", "345.00", "348.00", //
|
|
|
- "387.00", "390.00", "418.00", "430.00", "431.00", "431.50", "433.07", "433.22", "433.42",
|
|
|
- "433.66", "433.89", "433.92", "434.07", "434.18", "434.39", "434.42", "434.78", "438.90",
|
|
|
- "440.18", "464.00", //
|
|
|
- "779.00", "868.35", "868.40", "868.80", "868.95", "906.40", "915.00", "925.00", "928.00"};
|
|
|
-static void fmf2sub_setting_frequency_change(VariableItem* item) {
|
|
|
- Fmf2SubApp* app = variable_item_get_context(item);
|
|
|
- uint8_t index = variable_item_get_current_value_index(item);
|
|
|
- variable_item_set_current_value_text(item, setting_frequency_names[index]);
|
|
|
- Fmf2SubConvertModel* model = view_get_model(app->view_convert);
|
|
|
- model->setting_frequency_index = index;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Modulation
|
|
|
-*/
|
|
|
-static const char* setting_modulation_label = "Modulation";
|
|
|
-static char* setting_modulation_values[] = {
|
|
|
- "FuriHalSubGhzPresetOok270Async",
|
|
|
- "FuriHalSubGhzPresetOok650Async",
|
|
|
- "FuriHalSubGhzPreset2FSKDev238Async",
|
|
|
- "FuriHalSubGhzPreset2FSKDev476Async"};
|
|
|
-static char* setting_modulation_names[] = {"AM270", "AM650", "FM238", "FM476"};
|
|
|
-static void fmf2sub_setting_modulation_change(VariableItem* item) {
|
|
|
- Fmf2SubApp* app = variable_item_get_context(item);
|
|
|
- uint8_t index = variable_item_get_current_value_index(item);
|
|
|
- variable_item_set_current_value_text(item, setting_modulation_names[index]);
|
|
|
- Fmf2SubConvertModel* model = view_get_model(app->view_convert);
|
|
|
- model->setting_modulation_index = index;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Flipboard button
|
|
|
-*/
|
|
|
-static const char* setting_button_label = "FlipButtons";
|
|
|
-static char* setting_button_values[] = {
|
|
|
- "Flip1.sub",
|
|
|
- "Flip2.sub",
|
|
|
- "Flip3.sub",
|
|
|
- "Flip4.sub",
|
|
|
- "Flip5.sub",
|
|
|
- "Flip6.sub",
|
|
|
- "Flip7.sub",
|
|
|
- "Flip8.sub",
|
|
|
- "Flip9.sub",
|
|
|
- "Flip10.sub",
|
|
|
- "Flip11.sub",
|
|
|
- "Flip12.sub",
|
|
|
- "Flip13.sub",
|
|
|
- "Flip14.sub",
|
|
|
- "Flip15.sub"};
|
|
|
-static char* setting_button_names[] = {
|
|
|
- "1",
|
|
|
- "2",
|
|
|
- "1+2",
|
|
|
- "3",
|
|
|
- "1+3",
|
|
|
- "2+3",
|
|
|
- "1+2+3",
|
|
|
- "4",
|
|
|
- "1+4",
|
|
|
- "2+4",
|
|
|
- "1+2+4",
|
|
|
- "3+4",
|
|
|
- "1+3+4",
|
|
|
- "2+3+4",
|
|
|
- "All"};
|
|
|
-static void fmf2sub_setting_button_change(VariableItem* item) {
|
|
|
- Fmf2SubApp* app = variable_item_get_context(item);
|
|
|
- uint8_t index = variable_item_get_current_value_index(item);
|
|
|
- variable_item_set_current_value_text(item, setting_button_names[index]);
|
|
|
- Fmf2SubConvertModel* model = view_get_model(app->view_convert);
|
|
|
- model->setting_button_index = index;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback for drawing the convert screen.
|
|
|
- * @details This function is called when the screen needs to be redrawn, like when the model gets updated.
|
|
|
- * @param canvas The canvas to draw on.
|
|
|
- * @param model The model - MyModel object.
|
|
|
-*/
|
|
|
-static void fmf2sub_view_convert_draw_callback(Canvas* canvas, void* model) {
|
|
|
- Fmf2SubConvertModel* my_model = (Fmf2SubConvertModel*)model;
|
|
|
- canvas_set_font(canvas, FontSecondary);
|
|
|
- canvas_draw_str(canvas, 1, 10, "Press OK to select Flipper");
|
|
|
- canvas_draw_str(canvas, 1, 20, "Music File (.FMF) to convert");
|
|
|
- canvas_draw_str(canvas, 1, 30, "to Sub-GHz format (.SUB).");
|
|
|
- canvas_draw_str(canvas, 10, 40, "FlipBoard buttons:");
|
|
|
- canvas_set_font(canvas, FontPrimary);
|
|
|
- canvas_draw_str(canvas, 90, 40, setting_button_names[my_model->setting_button_index]);
|
|
|
-
|
|
|
- if(my_model->data.notes && my_model->state == Fmf2SubStateConverting) {
|
|
|
- canvas_draw_str(canvas, 1, 50, furi_string_get_cstr(my_model->data.notes));
|
|
|
- } else {
|
|
|
- canvas_draw_str(canvas, 40, 50, setting_button_values[my_model->setting_button_index]);
|
|
|
- }
|
|
|
-
|
|
|
- canvas_set_font(canvas, FontPrimary);
|
|
|
- if(my_model->state == Fmf2SubStateLoading) {
|
|
|
- canvas_draw_str(canvas, 1, 60, "Loading...");
|
|
|
- } else if(my_model->state == Fmf2SubStateError) {
|
|
|
- canvas_draw_str(canvas, 1, 60, "Error!");
|
|
|
- } else if(my_model->state == Fmf2SubStateConverting) {
|
|
|
- canvas_draw_str(canvas, 1, 60, "Converting...");
|
|
|
- } else if(my_model->state == Fmf2SubStateConverted) {
|
|
|
- canvas_draw_str(canvas, 1, 60, "Saved in Sub-GHz folder");
|
|
|
- } else {
|
|
|
- canvas_draw_str(canvas, 1, 60, "Press OK to choose file");
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback when the user starts the convert screen.
|
|
|
- * @details This function is called when the user enters the convert screen.
|
|
|
- * @param context The context - Fmf2SubApp object.
|
|
|
-*/
|
|
|
-static void fmf2sub_view_convert_enter_callback(void* context) {
|
|
|
- UNUSED(context);
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback when the user exits the convert screen.
|
|
|
- * @details This function is called when the user exits the convert screen.
|
|
|
- * @param context The context - Fmf2SubApp object.
|
|
|
-*/
|
|
|
-static void fmf2sub_view_convert_exit_callback(void* context) {
|
|
|
- Fmf2SubApp* app = (Fmf2SubApp*)context;
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- {
|
|
|
- model->state = Fmf2SubStateIdle;
|
|
|
- if(model->data.notes) {
|
|
|
- furi_string_free(model->data.notes);
|
|
|
- model->data.notes = NULL;
|
|
|
- }
|
|
|
- },
|
|
|
- false);
|
|
|
-}
|
|
|
-
|
|
|
-static uint32_t
|
|
|
- fmf2sub_extract_param(FuriString* song_settings, char param, uint32_t default_value) {
|
|
|
- uint16_t value = 0;
|
|
|
- char param_equal[3] = {param, '=', 0};
|
|
|
- size_t index = furi_string_search_str(song_settings, param_equal);
|
|
|
- if(index != FURI_STRING_FAILURE) {
|
|
|
- index += 2;
|
|
|
- do {
|
|
|
- char ch = furi_string_get_char(song_settings, index++);
|
|
|
- if(ch < '0' || ch > '9') {
|
|
|
- break;
|
|
|
- }
|
|
|
- value *= 10;
|
|
|
- value += ch - '0';
|
|
|
- } while(true);
|
|
|
- } else {
|
|
|
- value = default_value;
|
|
|
- }
|
|
|
- return value;
|
|
|
-}
|
|
|
-
|
|
|
-static void fmf2sub_load_txt_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
|
|
|
- UNUSED(app);
|
|
|
- UNUSED(model);
|
|
|
-
|
|
|
- bool error = false;
|
|
|
-
|
|
|
- Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
|
- File* file = storage_file_alloc(storage);
|
|
|
- do {
|
|
|
- if(storage_file_open(
|
|
|
- file, furi_string_get_cstr(app->file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
|
|
|
- char ch;
|
|
|
- while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
|
|
|
- if(ch == ':') {
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if(storage_file_eof(file)) {
|
|
|
- FURI_LOG_E(TAG, "Failed to find first delimiter.");
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- FuriString* song_settings = furi_string_alloc();
|
|
|
- while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
|
|
|
- if(ch == ':') {
|
|
|
- break;
|
|
|
- }
|
|
|
- furi_string_push_back(song_settings, ch);
|
|
|
- }
|
|
|
- model->data.duration = fmf2sub_extract_param(song_settings, 'd', 4);
|
|
|
- model->data.octave = fmf2sub_extract_param(song_settings, 'o', 5);
|
|
|
- model->data.bpm = fmf2sub_extract_param(song_settings, 'b', 120);
|
|
|
- furi_string_free(song_settings);
|
|
|
-
|
|
|
- if(storage_file_eof(file)) {
|
|
|
- FURI_LOG_E(TAG, "Failed to find second delimiter.");
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- model->data.notes = furi_string_alloc();
|
|
|
- while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
|
|
|
- furi_string_push_back(model->data.notes, ch);
|
|
|
- }
|
|
|
- }
|
|
|
- } while(false);
|
|
|
- storage_file_close(file);
|
|
|
- storage_file_free(file);
|
|
|
- furi_record_close(RECORD_STORAGE);
|
|
|
-
|
|
|
- if(error) {
|
|
|
- model->state = Fmf2SubStateError;
|
|
|
- } else {
|
|
|
- view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdCreateSub);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-static void fmf2sub_load_fmf_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
|
|
|
- UNUSED(model);
|
|
|
- FlipperFormat* ff;
|
|
|
- Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
|
- FuriString* buf = furi_string_alloc();
|
|
|
-
|
|
|
- bool error = false;
|
|
|
-
|
|
|
- if(model->data.notes) {
|
|
|
- furi_string_free(model->data.notes);
|
|
|
- }
|
|
|
- model->data.notes = NULL;
|
|
|
-
|
|
|
- ff = flipper_format_buffered_file_alloc(storage);
|
|
|
- do {
|
|
|
- uint32_t format_version;
|
|
|
- if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(app->file_path))) {
|
|
|
- FURI_LOG_E(TAG, "Failed to open file: %s", furi_string_get_cstr(app->file_path));
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- if(!flipper_format_read_header(ff, buf, &format_version)) {
|
|
|
- FURI_LOG_E(TAG, "Failed to read settings header.");
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- flipper_format_read_uint32(ff, "BPM", &(model->data.bpm), 120);
|
|
|
- flipper_format_read_uint32(ff, "Duration", &(model->data.duration), 4);
|
|
|
- flipper_format_read_uint32(ff, "Octave", &(model->data.octave), 5);
|
|
|
- model->data.notes = furi_string_alloc();
|
|
|
- if(!flipper_format_read_string(ff, "Notes", model->data.notes)) {
|
|
|
- FURI_LOG_E(TAG, "Failed to read notes.");
|
|
|
- furi_string_free(model->data.notes);
|
|
|
- model->data.notes = NULL;
|
|
|
- error = true;
|
|
|
- }
|
|
|
- } while(false);
|
|
|
-
|
|
|
- flipper_format_buffered_file_close(ff);
|
|
|
- flipper_format_free(ff);
|
|
|
- furi_record_close(RECORD_STORAGE);
|
|
|
- furi_string_free(buf);
|
|
|
-
|
|
|
- if(error) {
|
|
|
- fmf2sub_load_txt_file(app, model);
|
|
|
- } else {
|
|
|
- view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdCreateSub);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-static void fmf2sub_file_write(File* file, const char* str) {
|
|
|
- storage_file_write(file, str, strlen(str));
|
|
|
-}
|
|
|
-
|
|
|
-void fmf2sub_save_sub_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
|
|
|
- UNUSED(app);
|
|
|
-
|
|
|
- Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
|
- File* file = storage_file_alloc(storage);
|
|
|
- FuriString* file_path = furi_string_alloc();
|
|
|
- furi_string_cat_printf(
|
|
|
- file_path,
|
|
|
- "%s/%s",
|
|
|
- EXT_PATH("/subghz/"),
|
|
|
- setting_button_values[model->setting_button_index]);
|
|
|
- if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_OPEN_ALWAYS)) {
|
|
|
- storage_file_truncate(file);
|
|
|
- FuriString* tmp_string = furi_string_alloc();
|
|
|
- fmf2sub_file_write(file, "Filetype: Flipper SubGhz RAW File\n");
|
|
|
- fmf2sub_file_write(file, "Version: 1\n");
|
|
|
- fmf2sub_file_write(file, "Frequency: ");
|
|
|
- fmf2sub_file_write(file, setting_frequency_values[model->setting_frequency_index]);
|
|
|
- fmf2sub_file_write(file, "\nPreset: ");
|
|
|
- fmf2sub_file_write(file, setting_modulation_values[model->setting_modulation_index]);
|
|
|
- fmf2sub_file_write(file, "\nProtocol: RAW");
|
|
|
-
|
|
|
- // process the notes
|
|
|
- FuriString* notes = model->data.notes;
|
|
|
- int16_t duration = -1;
|
|
|
- int16_t octave = -1;
|
|
|
- bool dot = false;
|
|
|
-
|
|
|
- for(size_t i = 0; i < furi_string_size(notes); i++) {
|
|
|
- char ch = furi_string_get_char(notes, i);
|
|
|
- if(ch == ' ' || ch == ',') {
|
|
|
- // skip spaces and commas.
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // is is a duration?
|
|
|
- while(ch >= '0' && ch <= '9') {
|
|
|
- if(duration == -1) {
|
|
|
- duration = 0;
|
|
|
- }
|
|
|
- duration *= 10;
|
|
|
- duration += ch - '0';
|
|
|
- ch = furi_string_get_char(notes, ++i);
|
|
|
- }
|
|
|
- if(duration == -1) {
|
|
|
- duration = model->data.duration;
|
|
|
- }
|
|
|
-
|
|
|
- // it should be note.
|
|
|
- if(ch < 'A' && ch > 'G' && ch < 'a' && ch > 'g' && ch != 'P' && ch != 'p') {
|
|
|
- FURI_LOG_D(TAG, "Invalid note: %c", ch);
|
|
|
- // invalid note
|
|
|
- continue;
|
|
|
- }
|
|
|
- bool sharp = furi_string_get_char(notes, i + 1) == '#';
|
|
|
-
|
|
|
- // convert to frequency (octave 2)
|
|
|
- float frequency = 0;
|
|
|
- ch = toupper(ch);
|
|
|
- if(ch == 'P') {
|
|
|
- frequency = 6.0;
|
|
|
- } else if(ch == 'C') {
|
|
|
- frequency = !sharp ? 130.0 : 138.6;
|
|
|
- } else if(ch == 'D') {
|
|
|
- frequency = !sharp ? 146.8 : 155.6;
|
|
|
- } else if(ch == 'E') {
|
|
|
- frequency = 164.8;
|
|
|
- } else if(ch == 'F') {
|
|
|
- frequency = !sharp ? 174.6 : 185.0;
|
|
|
- } else if(ch == 'G') {
|
|
|
- frequency = !sharp ? 196.0 : 207.7;
|
|
|
- } else if(ch == 'A') {
|
|
|
- frequency = !sharp ? 220.0 : 233.1;
|
|
|
- } else if(ch == 'B') {
|
|
|
- frequency = 246.9;
|
|
|
- } else {
|
|
|
- FURI_LOG_D(TAG, "Invalid note: %c, %d", ch, (int)ch);
|
|
|
- // invalid note
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if(sharp) {
|
|
|
- i++;
|
|
|
- }
|
|
|
-
|
|
|
- ch = furi_string_get_char(notes, ++i);
|
|
|
- if(ch == '.') {
|
|
|
- dot = true; // 50% longer
|
|
|
- ch = furi_string_get_char(notes, ++i);
|
|
|
- }
|
|
|
-
|
|
|
- while(ch >= '0' && ch <= '9') {
|
|
|
- if(octave == -1) {
|
|
|
- octave = 0;
|
|
|
- }
|
|
|
- octave *= 10;
|
|
|
- octave += ch - '0';
|
|
|
- ch = furi_string_get_char(notes, ++i);
|
|
|
- }
|
|
|
- if(octave == -1) {
|
|
|
- octave = model->data.octave;
|
|
|
- }
|
|
|
-
|
|
|
- if(ch == '.') {
|
|
|
- dot = true; // 50% longer
|
|
|
- }
|
|
|
-
|
|
|
- if(octave < 2) {
|
|
|
- frequency /= 2.0;
|
|
|
- } else {
|
|
|
- for(int i = 2; i < octave; i++) {
|
|
|
- frequency *= 2.0;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- uint32_t pulse = (1000000 / frequency) / 2;
|
|
|
- // 4/4 timing, duration of quarter note (4) is 500ms.
|
|
|
- float count = (1000000.0 / (pulse * 2.0)) * 2.0 / duration;
|
|
|
- count *= 120.0f;
|
|
|
- count /= model->data.bpm;
|
|
|
-
|
|
|
- if(dot) {
|
|
|
- count += (count / 2.0f);
|
|
|
- }
|
|
|
-
|
|
|
- if(count < 1.0f) {
|
|
|
- count = 1.0;
|
|
|
- }
|
|
|
-
|
|
|
- if(duration <= 1) {
|
|
|
- count *= 0.98; // whole note.
|
|
|
- } else if(duration <= 2) {
|
|
|
- count *= 0.95; // half note.
|
|
|
- } else if(duration <= 4) {
|
|
|
- count *= 0.90; // quarter note.
|
|
|
- } else {
|
|
|
- count *= 0.90;
|
|
|
- }
|
|
|
-
|
|
|
- float duration_tone = ((uint32_t)count) * pulse * 2;
|
|
|
- float duration_beat =
|
|
|
- (1000000.0 * 2.0 / duration * 120.0 / model->data.bpm) * (dot ? 1.5 : 1.0);
|
|
|
- float duration_rem = (duration_beat - duration_tone) / 2.0f;
|
|
|
- uint32_t rem_counter = 1;
|
|
|
- while(duration_rem > 20000.0f) {
|
|
|
- rem_counter *= 2;
|
|
|
- duration_rem /= 2.0;
|
|
|
- }
|
|
|
-
|
|
|
- FURI_LOG_D(
|
|
|
- TAG,
|
|
|
- "octave: %d, duration: %d, freq: %f, dot: %c, pulse: %ld, count: %f, bpm: %ld",
|
|
|
- octave,
|
|
|
- duration,
|
|
|
- (double)frequency,
|
|
|
- dot ? 'Y' : 'N',
|
|
|
- pulse,
|
|
|
- (double)count,
|
|
|
- model->data.bpm);
|
|
|
-
|
|
|
- FURI_LOG_D(
|
|
|
- TAG,
|
|
|
- "beat: %f tone: %f rem-us: %f rem-cnt: %ld",
|
|
|
- (double)duration_beat,
|
|
|
- (double)duration_tone,
|
|
|
- (double)duration_rem,
|
|
|
- rem_counter);
|
|
|
-
|
|
|
- fmf2sub_file_write(file, "\nRAW_Data:");
|
|
|
- furi_string_printf(tmp_string, " %ld %ld", pulse, -pulse);
|
|
|
- for(uint32_t i = 0; i < (uint32_t)count; i++) {
|
|
|
- if(i % 256 == 255) {
|
|
|
- fmf2sub_file_write(file, "\nRAW_Data:");
|
|
|
- }
|
|
|
- fmf2sub_file_write(file, furi_string_get_cstr(tmp_string));
|
|
|
- }
|
|
|
-
|
|
|
- fmf2sub_file_write(file, "\nRAW_Data:");
|
|
|
- furi_string_printf(
|
|
|
- tmp_string, " %ld -%ld", (uint32_t)duration_rem, (uint32_t)duration_rem);
|
|
|
- for(uint32_t i = 0; i < rem_counter; i++) {
|
|
|
- fmf2sub_file_write(file, furi_string_get_cstr(tmp_string));
|
|
|
- }
|
|
|
-
|
|
|
- octave = -1;
|
|
|
- duration = -1;
|
|
|
- dot = false;
|
|
|
- }
|
|
|
- furi_string_free(tmp_string);
|
|
|
-
|
|
|
- // write file
|
|
|
- model->state = Fmf2SubStateConverted;
|
|
|
- } else {
|
|
|
- FURI_LOG_D(TAG, "Failed to create file: %s", furi_string_get_cstr(file_path));
|
|
|
- model->state = Fmf2SubStateError;
|
|
|
- }
|
|
|
-
|
|
|
- storage_file_close(file);
|
|
|
- storage_file_free(file);
|
|
|
- furi_record_close(RECORD_STORAGE);
|
|
|
- furi_string_free(file_path);
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback for custom events.
|
|
|
- * @details This function is called when a custom event is sent to the view dispatcher.
|
|
|
- * @param event The event id - Fmf2SubEventId value.
|
|
|
- * @param context The context - Fmf2SubApp object.
|
|
|
-*/
|
|
|
-static bool fmf2sub_view_convert_custom_event_callback(uint32_t event, void* context) {
|
|
|
- Fmf2SubApp* app = (Fmf2SubApp*)context;
|
|
|
- switch(event) {
|
|
|
- case Fmf2SubEventIdRedrawScreen:
|
|
|
- // Redraw screen by passing true to last parameter of with_view_model.
|
|
|
- {
|
|
|
- bool redraw = true;
|
|
|
- with_view_model(
|
|
|
- app->view_convert, Fmf2SubConvertModel * _model, { UNUSED(_model); }, redraw);
|
|
|
- return true;
|
|
|
- }
|
|
|
- case Fmf2SubEventIdOkPressed: {
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- { model->state = Fmf2SubStateIdle; },
|
|
|
- false);
|
|
|
- DialogsFileBrowserOptions browser_options;
|
|
|
- dialog_file_browser_set_basic_options(&browser_options, "", &I_fmf_10x10);
|
|
|
- browser_options.hide_dot_files = true;
|
|
|
- browser_options.hide_ext = false;
|
|
|
- browser_options.base_path = FMF_LOAD_PATH;
|
|
|
- furi_string_set(app->file_path, browser_options.base_path);
|
|
|
- if(dialog_file_browser_show(
|
|
|
- app->dialogs, app->file_path, app->file_path, &browser_options)) {
|
|
|
- view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdLoadFile);
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
- case Fmf2SubEventIdLoadFile: {
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- {
|
|
|
- model->state = Fmf2SubStateLoading;
|
|
|
- fmf2sub_load_fmf_file(app, model);
|
|
|
- },
|
|
|
- true);
|
|
|
- return true;
|
|
|
- }
|
|
|
- case Fmf2SubEventIdCreateSub: {
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- {
|
|
|
- model->state = Fmf2SubStateConverting;
|
|
|
- FURI_LOG_D(TAG, "Loaded file: %s", furi_string_get_cstr(app->file_path));
|
|
|
- FURI_LOG_D(TAG, "BPM: %ld", model->data.bpm);
|
|
|
- FURI_LOG_D(TAG, "Duration: %ld", model->data.duration);
|
|
|
- FURI_LOG_D(TAG, "Octave: %ld", model->data.octave);
|
|
|
- FURI_LOG_D(TAG, "Notes: %s", furi_string_get_cstr(model->data.notes));
|
|
|
-
|
|
|
- fmf2sub_save_sub_file(app, model);
|
|
|
- },
|
|
|
- true);
|
|
|
- return true;
|
|
|
- }
|
|
|
- default:
|
|
|
- return false;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Callback for convert screen input.
|
|
|
- * @details This function is called when the user presses a button while on the convert screen.
|
|
|
- * @param event The event - InputEvent object.
|
|
|
- * @param context The context - Fmf2SubApp object.
|
|
|
- * @return true if the event was handled, false otherwise.
|
|
|
-*/
|
|
|
-static bool fmf2sub_view_convert_input_callback(InputEvent* event, void* context) {
|
|
|
- Fmf2SubApp* app = (Fmf2SubApp*)context;
|
|
|
- UNUSED(app);
|
|
|
-
|
|
|
- if(event->type == InputTypeShort) {
|
|
|
- if(event->key == InputKeyLeft) {
|
|
|
- bool redraw = true;
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- {
|
|
|
- if(model->setting_button_index > 0) {
|
|
|
- model->setting_button_index--;
|
|
|
- variable_item_set_current_value_text(
|
|
|
- app->variable_item_button,
|
|
|
- setting_button_names[model->setting_button_index]);
|
|
|
- variable_item_set_current_value_index(
|
|
|
- app->variable_item_button, model->setting_button_index);
|
|
|
- }
|
|
|
- },
|
|
|
- redraw);
|
|
|
- } else if(event->key == InputKeyRight) {
|
|
|
- bool redraw = true;
|
|
|
- with_view_model(
|
|
|
- app->view_convert,
|
|
|
- Fmf2SubConvertModel * model,
|
|
|
- {
|
|
|
- if(model->setting_button_index + 1 <
|
|
|
- (uint8_t)COUNT_OF(setting_button_values)) {
|
|
|
- model->setting_button_index++;
|
|
|
- variable_item_set_current_value_text(
|
|
|
- app->variable_item_button,
|
|
|
- setting_button_names[model->setting_button_index]);
|
|
|
- variable_item_set_current_value_index(
|
|
|
- app->variable_item_button, model->setting_button_index);
|
|
|
- }
|
|
|
- },
|
|
|
- redraw);
|
|
|
- }
|
|
|
- } else if(event->type == InputTypePress) {
|
|
|
- if(event->key == InputKeyOk) {
|
|
|
- view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdOkPressed);
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Allocate the fmf2sub application.
|
|
|
- * @details This function allocates the fmf2sub application resources.
|
|
|
- * @return Fmf2SubApp object.
|
|
|
-*/
|
|
|
-static Fmf2SubApp* fmf2sub_app_alloc() {
|
|
|
- Fmf2SubApp* app = (Fmf2SubApp*)malloc(sizeof(Fmf2SubApp));
|
|
|
-
|
|
|
- Gui* gui = furi_record_open(RECORD_GUI);
|
|
|
-
|
|
|
- app->view_dispatcher = view_dispatcher_alloc();
|
|
|
- view_dispatcher_enable_queue(app->view_dispatcher);
|
|
|
- view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
|
|
|
- view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
|
|
-
|
|
|
- app->submenu = submenu_alloc();
|
|
|
- submenu_add_item(
|
|
|
- app->submenu, "Configure", Fmf2SubSubmenuIndexConfigure, fmf2sub_submenu_callback, app);
|
|
|
- submenu_add_item(
|
|
|
- app->submenu, "Convert", Fmf2SubSubmenuIndexConvert, fmf2sub_submenu_callback, app);
|
|
|
- submenu_add_item(
|
|
|
- app->submenu, "About", Fmf2SubSubmenuIndexAbout, fmf2sub_submenu_callback, app);
|
|
|
- view_set_previous_callback(submenu_get_view(app->submenu), fmf2sub_navigation_exit_callback);
|
|
|
- view_dispatcher_add_view(
|
|
|
- app->view_dispatcher, Fmf2SubViewSubmenu, submenu_get_view(app->submenu));
|
|
|
- view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewSubmenu);
|
|
|
-
|
|
|
- app->text_input = text_input_alloc();
|
|
|
- view_dispatcher_add_view(
|
|
|
- app->view_dispatcher, Fmf2SubViewTextInput, text_input_get_view(app->text_input));
|
|
|
- app->temp_buffer_size = 32;
|
|
|
- app->temp_buffer = (char*)malloc(app->temp_buffer_size);
|
|
|
-
|
|
|
- app->variable_item_list_config = variable_item_list_alloc();
|
|
|
- variable_item_list_reset(app->variable_item_list_config);
|
|
|
- //variable_item_list_set_header(app->variable_item_list_config, "Flipboard Signal Config");
|
|
|
- VariableItem* item = variable_item_list_add(
|
|
|
- app->variable_item_list_config,
|
|
|
- setting_frequency_label,
|
|
|
- COUNT_OF(setting_frequency_values),
|
|
|
- fmf2sub_setting_frequency_change,
|
|
|
- app);
|
|
|
- uint8_t setting_frequency_index = 33;
|
|
|
- variable_item_set_current_value_index(item, setting_frequency_index);
|
|
|
- variable_item_set_current_value_text(item, setting_frequency_names[setting_frequency_index]);
|
|
|
-
|
|
|
- item = variable_item_list_add(
|
|
|
- app->variable_item_list_config,
|
|
|
- setting_modulation_label,
|
|
|
- COUNT_OF(setting_modulation_values),
|
|
|
- fmf2sub_setting_modulation_change,
|
|
|
- app);
|
|
|
- uint8_t setting_modulation_index = 1;
|
|
|
- variable_item_set_current_value_index(item, setting_modulation_index);
|
|
|
- variable_item_set_current_value_text(item, setting_modulation_names[setting_modulation_index]);
|
|
|
-
|
|
|
- app->variable_item_button = variable_item_list_add(
|
|
|
- app->variable_item_list_config,
|
|
|
- setting_button_label,
|
|
|
- COUNT_OF(setting_button_values),
|
|
|
- fmf2sub_setting_button_change,
|
|
|
- app);
|
|
|
- uint8_t setting_button_index = 0;
|
|
|
- variable_item_set_current_value_index(app->variable_item_button, setting_button_index);
|
|
|
- variable_item_set_current_value_text(
|
|
|
- app->variable_item_button, setting_button_names[setting_button_index]);
|
|
|
-
|
|
|
- view_set_previous_callback(
|
|
|
- variable_item_list_get_view(app->variable_item_list_config),
|
|
|
- fmf2sub_navigation_submenu_callback);
|
|
|
-
|
|
|
- view_dispatcher_add_view(
|
|
|
- app->view_dispatcher,
|
|
|
- Fmf2SubViewConfigure,
|
|
|
- variable_item_list_get_view(app->variable_item_list_config));
|
|
|
-
|
|
|
- app->view_convert = view_alloc();
|
|
|
- view_set_draw_callback(app->view_convert, fmf2sub_view_convert_draw_callback);
|
|
|
- view_set_input_callback(app->view_convert, fmf2sub_view_convert_input_callback);
|
|
|
- view_set_previous_callback(app->view_convert, fmf2sub_navigation_submenu_callback);
|
|
|
- view_set_enter_callback(app->view_convert, fmf2sub_view_convert_enter_callback);
|
|
|
- view_set_exit_callback(app->view_convert, fmf2sub_view_convert_exit_callback);
|
|
|
- view_set_context(app->view_convert, app);
|
|
|
- view_set_custom_callback(app->view_convert, fmf2sub_view_convert_custom_event_callback);
|
|
|
- view_allocate_model(app->view_convert, ViewModelTypeLockFree, sizeof(Fmf2SubConvertModel));
|
|
|
- Fmf2SubConvertModel* model = view_get_model(app->view_convert);
|
|
|
- model->setting_frequency_index = setting_frequency_index;
|
|
|
- model->setting_modulation_index = setting_modulation_index;
|
|
|
- model->setting_button_index = setting_button_index;
|
|
|
- view_dispatcher_add_view(app->view_dispatcher, Fmf2SubViewConvert, app->view_convert);
|
|
|
-
|
|
|
- app->widget_about = widget_alloc();
|
|
|
- widget_add_text_scroll_element(
|
|
|
- app->widget_about,
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- 128,
|
|
|
- 64,
|
|
|
- "Music to Sub-GHz v1.2!\n\n"
|
|
|
- "Converts music files (.FMF)\n"
|
|
|
- "or (.TXT) to Sub-GHz format\n"
|
|
|
- "(.SUB) Files. Flip#.sub is\n"
|
|
|
- "written to the SD Card's\n"
|
|
|
- "subghz folder. Another\n"
|
|
|
- "Flipper Zero with sound\n"
|
|
|
- "turned on doing a Read RAW\n"
|
|
|
- "in the Sub-GHz app can\n"
|
|
|
- "listen to the music!\n"
|
|
|
- "Use Flipboard Signal app to\n"
|
|
|
- "send signals or use the\n"
|
|
|
- "Sub-GHz app. Enjoy!\n\n"
|
|
|
- "author: @codeallnight\nhttps://discord.com/invite/NsjCvqwPAd\nhttps://youtube.com/@MrDerekJamison");
|
|
|
- view_set_previous_callback(
|
|
|
- widget_get_view(app->widget_about), fmf2sub_navigation_submenu_callback);
|
|
|
- view_dispatcher_add_view(
|
|
|
- app->view_dispatcher, Fmf2SubViewAbout, widget_get_view(app->widget_about));
|
|
|
-
|
|
|
- app->file_path = furi_string_alloc();
|
|
|
- app->dialogs = furi_record_open(RECORD_DIALOGS);
|
|
|
-
|
|
|
- return app;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Free the fmf2sub application.
|
|
|
- * @details This function frees the fmf2sub application resources.
|
|
|
- * @param app The fmf2sub application object.
|
|
|
-*/
|
|
|
-static void fmf2sub_app_free(Fmf2SubApp* app) {
|
|
|
- view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewTextInput);
|
|
|
- text_input_free(app->text_input);
|
|
|
- free(app->temp_buffer);
|
|
|
- view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewAbout);
|
|
|
- widget_free(app->widget_about);
|
|
|
- view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewConvert);
|
|
|
- view_free(app->view_convert);
|
|
|
- view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewConfigure);
|
|
|
- variable_item_list_free(app->variable_item_list_config);
|
|
|
- view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewSubmenu);
|
|
|
- submenu_free(app->submenu);
|
|
|
- view_dispatcher_free(app->view_dispatcher);
|
|
|
- furi_record_close(RECORD_GUI);
|
|
|
-
|
|
|
- furi_string_free(app->file_path);
|
|
|
- furi_record_close(RECORD_DIALOGS);
|
|
|
-
|
|
|
- free(app);
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @brief Main function for fmf2sub application.
|
|
|
- * @details This function is the entry point for the fmf2sub application. It should be defined in
|
|
|
- * application.fam as the entry_point setting.
|
|
|
- * @param _p Input parameter - unused
|
|
|
- * @return 0 - Success
|
|
|
-*/
|
|
|
-int32_t fmf_to_sub_app(void* _p) {
|
|
|
- UNUSED(_p);
|
|
|
-
|
|
|
- Fmf2SubApp* app = fmf2sub_app_alloc();
|
|
|
- view_dispatcher_run(app->view_dispatcher);
|
|
|
-
|
|
|
- fmf2sub_app_free(app);
|
|
|
- return 0;
|
|
|
-}
|