app.c 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. #include <furi.h>
  2. #include <furi_hal.h>
  3. #include <gui/gui.h>
  4. #include <gui/view.h>
  5. #include <gui/view_dispatcher.h>
  6. #include <gui/modules/submenu.h>
  7. #include <gui/modules/text_input.h>
  8. #include <gui/modules/widget.h>
  9. #include <gui/modules/variable_item_list.h>
  10. #include <dialogs/dialogs.h>
  11. #include <storage/storage.h>
  12. #include <flipper_format.h>
  13. #include "fmf_to_sub_icons.h"
  14. #define TAG "FMF to SUB"
  15. #define FMF_FILE_EXTENSION ".fmf"
  16. #define FMF_LOAD_PATH \
  17. EXT_PATH("apps_data") \
  18. "/" \
  19. "music_player"
  20. // Our application menu.
  21. typedef enum {
  22. Fmf2SubSubmenuIndexConfigure,
  23. Fmf2SubSubmenuIndexConvert,
  24. Fmf2SubSubmenuIndexAbout,
  25. } Fmf2SubSubmenuIndex;
  26. // Each view is a screen we show the user.
  27. typedef enum {
  28. Fmf2SubViewSubmenu, // The menu when the app starts
  29. Fmf2SubViewTextInput, // Input for configuring text settings
  30. Fmf2SubViewConfigure, // The configuration screen
  31. Fmf2SubViewConvert, // The main screen
  32. Fmf2SubViewAbout, // The about screen with directions, link to social channel, etc.
  33. } Fmf2SubView;
  34. typedef enum {
  35. Fmf2SubEventIdRedrawScreen = 0, // Custom event to redraw the screen
  36. Fmf2SubEventIdOkPressed = 1, // Custom event to process OK button getting pressed down
  37. Fmf2SubEventIdLoadFile = 2, // Custom event to load the file
  38. Fmf2SubEventIdCreateSub = 3, // Custom event to create the sub file
  39. } Fmf2SubEventId;
  40. typedef struct {
  41. ViewDispatcher* view_dispatcher; // Switches between our views
  42. DialogsApp* dialogs; // Shows dialogs like file browser
  43. Submenu* submenu; // The application menu
  44. TextInput* text_input; // The text input screen
  45. VariableItemList* variable_item_list_config; // The configuration screen
  46. VariableItem* variable_item_button; // The button on FlipBoard
  47. View* view_convert; // The main screen
  48. Widget* widget_about; // The about screen
  49. FuriString* file_path; // The path to the file
  50. char* temp_buffer; // Temporary buffer for text input
  51. uint32_t temp_buffer_size; // Size of temporary buffer
  52. FuriTimer* timer; // Timer for redrawing the screen
  53. } Fmf2SubApp;
  54. typedef enum {
  55. Fmf2SubStateIdle,
  56. Fmf2SubStateLoading,
  57. Fmf2SubStateConverting,
  58. Fmf2SubStateConverted,
  59. Fmf2SubStateError,
  60. } Fmf2SubState;
  61. typedef struct {
  62. uint32_t bpm;
  63. uint32_t duration;
  64. uint32_t octave;
  65. FuriString* notes;
  66. } Fmf2SubData;
  67. typedef struct {
  68. uint8_t setting_frequency_index; // The frequency
  69. uint8_t setting_modulation_index; // The modulation
  70. uint8_t setting_button_index; // The button on FlipBoard
  71. Fmf2SubState state; // The state of the application
  72. Fmf2SubData data; // The data from the file
  73. } Fmf2SubConvertModel;
  74. /**
  75. * @brief Callback for exiting the application.
  76. * @details This function is called when user press back button. We return VIEW_NONE to
  77. * indicate that we want to exit the application.
  78. * @param _context The context - unused
  79. * @return next view id (VIEW_NONE)
  80. */
  81. static uint32_t fmf2sub_navigation_exit_callback(void* _context) {
  82. UNUSED(_context);
  83. return VIEW_NONE;
  84. }
  85. /**
  86. * @brief Callback for returning to submenu.
  87. * @details This function is called when user press back button. We return ViewSubmenu to
  88. * indicate that we want to navigate to the submenu.
  89. * @param _context The context - unused
  90. * @return next view id (ViewSubmenu)
  91. */
  92. static uint32_t fmf2sub_navigation_submenu_callback(void* _context) {
  93. UNUSED(_context);
  94. return Fmf2SubViewSubmenu;
  95. }
  96. /**
  97. * @brief Handle submenu item selection.
  98. * @details This function is called when user selects an item from the submenu.
  99. * @param context The context - Fmf2SubApp object.
  100. * @param index The Fmf2SubSubmenuIndex item that was clicked.
  101. */
  102. static void fmf2sub_submenu_callback(void* context, uint32_t index) {
  103. Fmf2SubApp* app = (Fmf2SubApp*)context;
  104. switch(index) {
  105. case Fmf2SubSubmenuIndexConfigure:
  106. view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewConfigure);
  107. break;
  108. case Fmf2SubSubmenuIndexConvert:
  109. view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewConvert);
  110. break;
  111. case Fmf2SubSubmenuIndexAbout:
  112. view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewAbout);
  113. break;
  114. default:
  115. break;
  116. }
  117. }
  118. /**
  119. * Frequency settings and values.
  120. */
  121. static const char* setting_frequency_label = "Frequency";
  122. static char* setting_frequency_values[] = {
  123. "300000000", "302757000", "303875000", "303900000", "304250000", "307000000", "307500000",
  124. "307800000", "309000000", "310000000", "312000000", "312100000", "313000000", "313850000",
  125. "314000000", "314350000", "314980000", "315000000", "318000000", "330000000", "345000000",
  126. "348000000", "387000000", "390000000", "418000000", "430000000", "431000000", "431500000",
  127. "433075000", "433220000", "433420000", "433657070", "433889000", "433920000", "434075000",
  128. "434176948", "434390000", "434420000", "434775000", "438900000", "440175000", "464000000",
  129. "779000000", "868350000", "868400000", "868800000", "868950000", "906400000", "915000000",
  130. "925000000", "928000000"};
  131. static char* setting_frequency_names[] = {
  132. "300.00", "302.75", "303.88", "303.90", "304.25", "307.00", "307.50", "307.80", "309.00",
  133. "310.00", "312.00", "312.10", "313.00", "313.85", "314.00", "314.35", "314.98", "315.00",
  134. "318.00", "330.00", "345.00", "348.00", //
  135. "387.00", "390.00", "418.00", "430.00", "431.00", "431.50", "433.07", "433.22", "433.42",
  136. "433.66", "433.89", "433.92", "434.07", "434.18", "434.39", "434.42", "434.78", "438.90",
  137. "440.18", "464.00", //
  138. "779.00", "868.35", "868.40", "868.80", "868.95", "906.40", "915.00", "925.00", "928.00"};
  139. static void fmf2sub_setting_frequency_change(VariableItem* item) {
  140. Fmf2SubApp* app = variable_item_get_context(item);
  141. uint8_t index = variable_item_get_current_value_index(item);
  142. variable_item_set_current_value_text(item, setting_frequency_names[index]);
  143. Fmf2SubConvertModel* model = view_get_model(app->view_convert);
  144. model->setting_frequency_index = index;
  145. }
  146. /**
  147. * Modulation
  148. */
  149. static const char* setting_modulation_label = "Modulation";
  150. static char* setting_modulation_values[] = {
  151. "FuriHalSubGhzPresetOok270Async",
  152. "FuriHalSubGhzPresetOok650Async",
  153. "FuriHalSubGhzPreset2FSKDev238Async",
  154. "FuriHalSubGhzPreset2FSKDev476Async"};
  155. static char* setting_modulation_names[] = {"AM270", "AM650", "FM238", "FM476"};
  156. static void fmf2sub_setting_modulation_change(VariableItem* item) {
  157. Fmf2SubApp* app = variable_item_get_context(item);
  158. uint8_t index = variable_item_get_current_value_index(item);
  159. variable_item_set_current_value_text(item, setting_modulation_names[index]);
  160. Fmf2SubConvertModel* model = view_get_model(app->view_convert);
  161. model->setting_modulation_index = index;
  162. }
  163. /**
  164. * Flipboard button
  165. */
  166. static const char* setting_button_label = "FlipButtons";
  167. static char* setting_button_values[] = {
  168. "Flip1.sub",
  169. "Flip2.sub",
  170. "Flip3.sub",
  171. "Flip4.sub",
  172. "Flip5.sub",
  173. "Flip6.sub",
  174. "Flip7.sub",
  175. "Flip8.sub",
  176. "Flip9.sub",
  177. "Flip10.sub",
  178. "Flip11.sub",
  179. "Flip12.sub",
  180. "Flip13.sub",
  181. "Flip14.sub",
  182. "Flip15.sub"};
  183. static char* setting_button_names[] = {
  184. "1",
  185. "2",
  186. "1+2",
  187. "3",
  188. "1+3",
  189. "2+3",
  190. "1+2+3",
  191. "4",
  192. "1+4",
  193. "2+4",
  194. "1+2+4",
  195. "3+4",
  196. "1+3+4",
  197. "2+3+4",
  198. "All"};
  199. static void fmf2sub_setting_button_change(VariableItem* item) {
  200. Fmf2SubApp* app = variable_item_get_context(item);
  201. uint8_t index = variable_item_get_current_value_index(item);
  202. variable_item_set_current_value_text(item, setting_button_names[index]);
  203. Fmf2SubConvertModel* model = view_get_model(app->view_convert);
  204. model->setting_button_index = index;
  205. }
  206. /**
  207. * @brief Callback for drawing the convert screen.
  208. * @details This function is called when the screen needs to be redrawn, like when the model gets updated.
  209. * @param canvas The canvas to draw on.
  210. * @param model The model - MyModel object.
  211. */
  212. static void fmf2sub_view_convert_draw_callback(Canvas* canvas, void* model) {
  213. Fmf2SubConvertModel* my_model = (Fmf2SubConvertModel*)model;
  214. canvas_set_font(canvas, FontSecondary);
  215. canvas_draw_str(canvas, 1, 10, "Press OK to select Flipper");
  216. canvas_draw_str(canvas, 1, 20, "Music File (.FMF) to convert");
  217. canvas_draw_str(canvas, 1, 30, "to Sub-GHz format (.SUB).");
  218. canvas_draw_str(canvas, 10, 40, "FlipBoard buttons:");
  219. canvas_set_font(canvas, FontPrimary);
  220. canvas_draw_str(canvas, 90, 40, setting_button_names[my_model->setting_button_index]);
  221. if(my_model->data.notes && my_model->state == Fmf2SubStateConverting) {
  222. canvas_draw_str(canvas, 1, 50, furi_string_get_cstr(my_model->data.notes));
  223. } else {
  224. canvas_draw_str(canvas, 40, 50, setting_button_values[my_model->setting_button_index]);
  225. }
  226. canvas_set_font(canvas, FontPrimary);
  227. if(my_model->state == Fmf2SubStateLoading) {
  228. canvas_draw_str(canvas, 1, 60, "Loading...");
  229. } else if(my_model->state == Fmf2SubStateError) {
  230. canvas_draw_str(canvas, 1, 60, "Error!");
  231. } else if(my_model->state == Fmf2SubStateConverting) {
  232. canvas_draw_str(canvas, 1, 60, "Converting...");
  233. } else if(my_model->state == Fmf2SubStateConverted) {
  234. canvas_draw_str(canvas, 1, 60, "Saved in Sub-GHz folder");
  235. } else {
  236. canvas_draw_str(canvas, 1, 60, "Press OK to choose file");
  237. }
  238. }
  239. /**
  240. * @brief Callback when the user starts the convert screen.
  241. * @details This function is called when the user enters the convert screen.
  242. * @param context The context - Fmf2SubApp object.
  243. */
  244. static void fmf2sub_view_convert_enter_callback(void* context) {
  245. UNUSED(context);
  246. }
  247. /**
  248. * @brief Callback when the user exits the convert screen.
  249. * @details This function is called when the user exits the convert screen.
  250. * @param context The context - Fmf2SubApp object.
  251. */
  252. static void fmf2sub_view_convert_exit_callback(void* context) {
  253. Fmf2SubApp* app = (Fmf2SubApp*)context;
  254. with_view_model(
  255. app->view_convert,
  256. Fmf2SubConvertModel * model,
  257. {
  258. model->state = Fmf2SubStateIdle;
  259. if(model->data.notes) {
  260. furi_string_free(model->data.notes);
  261. model->data.notes = NULL;
  262. }
  263. },
  264. false);
  265. }
  266. static uint32_t
  267. fmf2sub_extract_param(FuriString* song_settings, char param, uint32_t default_value) {
  268. uint16_t value = 0;
  269. char param_equal[3] = {param, '=', 0};
  270. size_t index = furi_string_search_str(song_settings, param_equal);
  271. if(index != FURI_STRING_FAILURE) {
  272. index += 2;
  273. do {
  274. char ch = furi_string_get_char(song_settings, index++);
  275. if(ch < '0' || ch > '9') {
  276. break;
  277. }
  278. value *= 10;
  279. value += ch - '0';
  280. } while(true);
  281. } else {
  282. value = default_value;
  283. }
  284. return value;
  285. }
  286. static void fmf2sub_load_txt_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
  287. UNUSED(app);
  288. UNUSED(model);
  289. bool error = false;
  290. Storage* storage = furi_record_open(RECORD_STORAGE);
  291. File* file = storage_file_alloc(storage);
  292. do {
  293. if(storage_file_open(
  294. file, furi_string_get_cstr(app->file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
  295. char ch;
  296. while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
  297. if(ch == ':') {
  298. break;
  299. }
  300. }
  301. if(storage_file_eof(file)) {
  302. FURI_LOG_E(TAG, "Failed to find first delimiter.");
  303. error = true;
  304. break;
  305. }
  306. FuriString* song_settings = furi_string_alloc();
  307. while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
  308. if(ch == ':') {
  309. break;
  310. }
  311. furi_string_push_back(song_settings, ch);
  312. }
  313. model->data.duration = fmf2sub_extract_param(song_settings, 'd', 4);
  314. model->data.octave = fmf2sub_extract_param(song_settings, 'o', 5);
  315. model->data.bpm = fmf2sub_extract_param(song_settings, 'b', 120);
  316. furi_string_free(song_settings);
  317. if(storage_file_eof(file)) {
  318. FURI_LOG_E(TAG, "Failed to find second delimiter.");
  319. error = true;
  320. break;
  321. }
  322. model->data.notes = furi_string_alloc();
  323. while(storage_file_read(file, &ch, 1) && !storage_file_eof(file)) {
  324. furi_string_push_back(model->data.notes, ch);
  325. }
  326. }
  327. } while(false);
  328. storage_file_close(file);
  329. storage_file_free(file);
  330. furi_record_close(RECORD_STORAGE);
  331. if(error) {
  332. model->state = Fmf2SubStateError;
  333. } else {
  334. view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdCreateSub);
  335. }
  336. }
  337. static void fmf2sub_load_fmf_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
  338. UNUSED(model);
  339. FlipperFormat* ff;
  340. Storage* storage = furi_record_open(RECORD_STORAGE);
  341. FuriString* buf = furi_string_alloc();
  342. bool error = false;
  343. if(model->data.notes) {
  344. furi_string_free(model->data.notes);
  345. }
  346. model->data.notes = NULL;
  347. ff = flipper_format_buffered_file_alloc(storage);
  348. do {
  349. uint32_t format_version;
  350. if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(app->file_path))) {
  351. FURI_LOG_E(TAG, "Failed to open file: %s", furi_string_get_cstr(app->file_path));
  352. error = true;
  353. break;
  354. }
  355. if(!flipper_format_read_header(ff, buf, &format_version)) {
  356. FURI_LOG_E(TAG, "Failed to read settings header.");
  357. error = true;
  358. break;
  359. }
  360. flipper_format_read_uint32(ff, "BPM", &(model->data.bpm), 120);
  361. flipper_format_read_uint32(ff, "Duration", &(model->data.duration), 4);
  362. flipper_format_read_uint32(ff, "Octave", &(model->data.octave), 5);
  363. model->data.notes = furi_string_alloc();
  364. if(!flipper_format_read_string(ff, "Notes", model->data.notes)) {
  365. FURI_LOG_E(TAG, "Failed to read notes.");
  366. furi_string_free(model->data.notes);
  367. model->data.notes = NULL;
  368. error = true;
  369. }
  370. } while(false);
  371. flipper_format_buffered_file_close(ff);
  372. flipper_format_free(ff);
  373. furi_record_close(RECORD_STORAGE);
  374. furi_string_free(buf);
  375. if(error) {
  376. fmf2sub_load_txt_file(app, model);
  377. } else {
  378. view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdCreateSub);
  379. }
  380. }
  381. static void fmf2sub_file_write(File* file, const char* str) {
  382. storage_file_write(file, str, strlen(str));
  383. }
  384. void fmf2sub_save_sub_file(Fmf2SubApp* app, Fmf2SubConvertModel* model) {
  385. UNUSED(app);
  386. Storage* storage = furi_record_open(RECORD_STORAGE);
  387. File* file = storage_file_alloc(storage);
  388. FuriString* file_path = furi_string_alloc();
  389. furi_string_cat_printf(
  390. file_path,
  391. "%s/%s",
  392. EXT_PATH("/subghz/"),
  393. setting_button_values[model->setting_button_index]);
  394. if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_OPEN_ALWAYS)) {
  395. storage_file_truncate(file);
  396. FuriString* tmp_string = furi_string_alloc();
  397. fmf2sub_file_write(file, "Filetype: Flipper SubGhz RAW File\n");
  398. fmf2sub_file_write(file, "Version: 1\n");
  399. fmf2sub_file_write(file, "Frequency: ");
  400. fmf2sub_file_write(file, setting_frequency_values[model->setting_frequency_index]);
  401. fmf2sub_file_write(file, "\nPreset: ");
  402. fmf2sub_file_write(file, setting_modulation_values[model->setting_modulation_index]);
  403. fmf2sub_file_write(file, "\nProtocol: RAW");
  404. // process the notes
  405. FuriString* notes = model->data.notes;
  406. int16_t duration = -1;
  407. int16_t octave = -1;
  408. bool dot = false;
  409. for(size_t i = 0; i < furi_string_size(notes); i++) {
  410. char ch = furi_string_get_char(notes, i);
  411. if(ch == ' ' || ch == ',') {
  412. // skip spaces and commas.
  413. continue;
  414. }
  415. // is is a duration?
  416. while(ch >= '0' && ch <= '9') {
  417. if(duration == -1) {
  418. duration = 0;
  419. }
  420. duration *= 10;
  421. duration += ch - '0';
  422. ch = furi_string_get_char(notes, ++i);
  423. }
  424. if(duration == -1) {
  425. duration = model->data.duration;
  426. }
  427. // it should be note.
  428. if(ch < 'A' && ch > 'G' && ch < 'a' && ch > 'g' && ch != 'P' && ch != 'p') {
  429. FURI_LOG_D(TAG, "Invalid note: %c", ch);
  430. // invalid note
  431. continue;
  432. }
  433. bool sharp = furi_string_get_char(notes, i + 1) == '#';
  434. // convert to frequency (octave 2)
  435. float frequency = 0;
  436. ch = toupper(ch);
  437. if(ch == 'P') {
  438. frequency = 6.0;
  439. } else if(ch == 'C') {
  440. frequency = !sharp ? 130.0 : 138.6;
  441. } else if(ch == 'D') {
  442. frequency = !sharp ? 146.8 : 155.6;
  443. } else if(ch == 'E') {
  444. frequency = 164.8;
  445. } else if(ch == 'F') {
  446. frequency = !sharp ? 174.6 : 185.0;
  447. } else if(ch == 'G') {
  448. frequency = !sharp ? 196.0 : 207.7;
  449. } else if(ch == 'A') {
  450. frequency = !sharp ? 220.0 : 233.1;
  451. } else if(ch == 'B') {
  452. frequency = 246.9;
  453. } else {
  454. FURI_LOG_D(TAG, "Invalid note: %c, %d", ch, (int)ch);
  455. // invalid note
  456. continue;
  457. }
  458. if(sharp) {
  459. i++;
  460. }
  461. ch = furi_string_get_char(notes, ++i);
  462. if(ch == '.') {
  463. dot = true; // 50% longer
  464. ch = furi_string_get_char(notes, ++i);
  465. }
  466. while(ch >= '0' && ch <= '9') {
  467. if(octave == -1) {
  468. octave = 0;
  469. }
  470. octave *= 10;
  471. octave += ch - '0';
  472. ch = furi_string_get_char(notes, ++i);
  473. }
  474. if(octave == -1) {
  475. octave = model->data.octave;
  476. }
  477. if(ch == '.') {
  478. dot = true; // 50% longer
  479. }
  480. if(octave < 2) {
  481. frequency /= 2.0;
  482. } else {
  483. for(int i = 2; i < octave; i++) {
  484. frequency *= 2.0;
  485. }
  486. }
  487. uint32_t pulse = (1000000 / frequency) / 2;
  488. // 4/4 timing, duration of quarter note (4) is 500ms.
  489. float count = (1000000.0 / (pulse * 2.0)) * 2.0 / duration;
  490. count *= 120.0f;
  491. count /= model->data.bpm;
  492. if(dot) {
  493. count += (count / 2.0f);
  494. }
  495. if(count < 1.0f) {
  496. count = 1.0;
  497. }
  498. if(duration <= 1) {
  499. count *= 0.98; // whole note.
  500. } else if(duration <= 2) {
  501. count *= 0.95; // half note.
  502. } else if(duration <= 4) {
  503. count *= 0.90; // quarter note.
  504. } else {
  505. count *= 0.90;
  506. }
  507. float duration_tone = ((uint32_t)count) * pulse * 2;
  508. float duration_beat =
  509. (1000000.0 * 2.0 / duration * 120.0 / model->data.bpm) * (dot ? 1.5 : 1.0);
  510. float duration_rem = (duration_beat - duration_tone) / 2.0f;
  511. uint32_t rem_counter = 1;
  512. while(duration_rem > 20000.0f) {
  513. rem_counter *= 2;
  514. duration_rem /= 2.0;
  515. }
  516. FURI_LOG_D(
  517. TAG,
  518. "octave: %d, duration: %d, freq: %f, dot: %c, pulse: %ld, count: %f, bpm: %ld",
  519. octave,
  520. duration,
  521. (double)frequency,
  522. dot ? 'Y' : 'N',
  523. pulse,
  524. (double)count,
  525. model->data.bpm);
  526. FURI_LOG_D(
  527. TAG,
  528. "beat: %f tone: %f rem-us: %f rem-cnt: %ld",
  529. (double)duration_beat,
  530. (double)duration_tone,
  531. (double)duration_rem,
  532. rem_counter);
  533. fmf2sub_file_write(file, "\nRAW_Data:");
  534. furi_string_printf(tmp_string, " %ld %ld", pulse, -pulse);
  535. for(uint32_t i = 0; i < (uint32_t)count; i++) {
  536. if(i % 256 == 255) {
  537. fmf2sub_file_write(file, "\nRAW_Data:");
  538. }
  539. fmf2sub_file_write(file, furi_string_get_cstr(tmp_string));
  540. }
  541. fmf2sub_file_write(file, "\nRAW_Data:");
  542. furi_string_printf(
  543. tmp_string, " %ld -%ld", (uint32_t)duration_rem, (uint32_t)duration_rem);
  544. for(uint32_t i = 0; i < rem_counter; i++) {
  545. fmf2sub_file_write(file, furi_string_get_cstr(tmp_string));
  546. }
  547. octave = -1;
  548. duration = -1;
  549. dot = false;
  550. }
  551. furi_string_free(tmp_string);
  552. // write file
  553. model->state = Fmf2SubStateConverted;
  554. } else {
  555. FURI_LOG_D(TAG, "Failed to create file: %s", furi_string_get_cstr(file_path));
  556. model->state = Fmf2SubStateError;
  557. }
  558. storage_file_close(file);
  559. storage_file_free(file);
  560. furi_record_close(RECORD_STORAGE);
  561. furi_string_free(file_path);
  562. }
  563. /**
  564. * @brief Callback for custom events.
  565. * @details This function is called when a custom event is sent to the view dispatcher.
  566. * @param event The event id - Fmf2SubEventId value.
  567. * @param context The context - Fmf2SubApp object.
  568. */
  569. static bool fmf2sub_view_convert_custom_event_callback(uint32_t event, void* context) {
  570. Fmf2SubApp* app = (Fmf2SubApp*)context;
  571. switch(event) {
  572. case Fmf2SubEventIdRedrawScreen:
  573. // Redraw screen by passing true to last parameter of with_view_model.
  574. {
  575. bool redraw = true;
  576. with_view_model(
  577. app->view_convert, Fmf2SubConvertModel * _model, { UNUSED(_model); }, redraw);
  578. return true;
  579. }
  580. case Fmf2SubEventIdOkPressed: {
  581. with_view_model(
  582. app->view_convert,
  583. Fmf2SubConvertModel * model,
  584. { model->state = Fmf2SubStateIdle; },
  585. false);
  586. DialogsFileBrowserOptions browser_options;
  587. dialog_file_browser_set_basic_options(&browser_options, "", &I_fmf_10x10);
  588. browser_options.hide_dot_files = true;
  589. browser_options.hide_ext = false;
  590. browser_options.base_path = FMF_LOAD_PATH;
  591. furi_string_set(app->file_path, browser_options.base_path);
  592. if(dialog_file_browser_show(
  593. app->dialogs, app->file_path, app->file_path, &browser_options)) {
  594. view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdLoadFile);
  595. }
  596. return true;
  597. }
  598. case Fmf2SubEventIdLoadFile: {
  599. with_view_model(
  600. app->view_convert,
  601. Fmf2SubConvertModel * model,
  602. {
  603. model->state = Fmf2SubStateLoading;
  604. fmf2sub_load_fmf_file(app, model);
  605. },
  606. true);
  607. return true;
  608. }
  609. case Fmf2SubEventIdCreateSub: {
  610. with_view_model(
  611. app->view_convert,
  612. Fmf2SubConvertModel * model,
  613. {
  614. model->state = Fmf2SubStateConverting;
  615. FURI_LOG_D(TAG, "Loaded file: %s", furi_string_get_cstr(app->file_path));
  616. FURI_LOG_D(TAG, "BPM: %ld", model->data.bpm);
  617. FURI_LOG_D(TAG, "Duration: %ld", model->data.duration);
  618. FURI_LOG_D(TAG, "Octave: %ld", model->data.octave);
  619. FURI_LOG_D(TAG, "Notes: %s", furi_string_get_cstr(model->data.notes));
  620. fmf2sub_save_sub_file(app, model);
  621. },
  622. true);
  623. return true;
  624. }
  625. default:
  626. return false;
  627. }
  628. }
  629. /**
  630. * @brief Callback for convert screen input.
  631. * @details This function is called when the user presses a button while on the convert screen.
  632. * @param event The event - InputEvent object.
  633. * @param context The context - Fmf2SubApp object.
  634. * @return true if the event was handled, false otherwise.
  635. */
  636. static bool fmf2sub_view_convert_input_callback(InputEvent* event, void* context) {
  637. Fmf2SubApp* app = (Fmf2SubApp*)context;
  638. UNUSED(app);
  639. if(event->type == InputTypeShort) {
  640. if(event->key == InputKeyLeft) {
  641. bool redraw = true;
  642. with_view_model(
  643. app->view_convert,
  644. Fmf2SubConvertModel * model,
  645. {
  646. if(model->setting_button_index > 0) {
  647. model->setting_button_index--;
  648. variable_item_set_current_value_text(
  649. app->variable_item_button,
  650. setting_button_names[model->setting_button_index]);
  651. variable_item_set_current_value_index(
  652. app->variable_item_button, model->setting_button_index);
  653. }
  654. },
  655. redraw);
  656. } else if(event->key == InputKeyRight) {
  657. bool redraw = true;
  658. with_view_model(
  659. app->view_convert,
  660. Fmf2SubConvertModel * model,
  661. {
  662. if(model->setting_button_index + 1 <
  663. (uint8_t)COUNT_OF(setting_button_values)) {
  664. model->setting_button_index++;
  665. variable_item_set_current_value_text(
  666. app->variable_item_button,
  667. setting_button_names[model->setting_button_index]);
  668. variable_item_set_current_value_index(
  669. app->variable_item_button, model->setting_button_index);
  670. }
  671. },
  672. redraw);
  673. }
  674. } else if(event->type == InputTypePress) {
  675. if(event->key == InputKeyOk) {
  676. view_dispatcher_send_custom_event(app->view_dispatcher, Fmf2SubEventIdOkPressed);
  677. return true;
  678. }
  679. }
  680. return false;
  681. }
  682. /**
  683. * @brief Allocate the fmf2sub application.
  684. * @details This function allocates the fmf2sub application resources.
  685. * @return Fmf2SubApp object.
  686. */
  687. static Fmf2SubApp* fmf2sub_app_alloc() {
  688. Fmf2SubApp* app = (Fmf2SubApp*)malloc(sizeof(Fmf2SubApp));
  689. Gui* gui = furi_record_open(RECORD_GUI);
  690. app->view_dispatcher = view_dispatcher_alloc();
  691. view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
  692. view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
  693. app->submenu = submenu_alloc();
  694. submenu_add_item(
  695. app->submenu, "Configure", Fmf2SubSubmenuIndexConfigure, fmf2sub_submenu_callback, app);
  696. submenu_add_item(
  697. app->submenu, "Convert", Fmf2SubSubmenuIndexConvert, fmf2sub_submenu_callback, app);
  698. submenu_add_item(
  699. app->submenu, "About", Fmf2SubSubmenuIndexAbout, fmf2sub_submenu_callback, app);
  700. view_set_previous_callback(submenu_get_view(app->submenu), fmf2sub_navigation_exit_callback);
  701. view_dispatcher_add_view(
  702. app->view_dispatcher, Fmf2SubViewSubmenu, submenu_get_view(app->submenu));
  703. view_dispatcher_switch_to_view(app->view_dispatcher, Fmf2SubViewSubmenu);
  704. app->text_input = text_input_alloc();
  705. view_dispatcher_add_view(
  706. app->view_dispatcher, Fmf2SubViewTextInput, text_input_get_view(app->text_input));
  707. app->temp_buffer_size = 32;
  708. app->temp_buffer = (char*)malloc(app->temp_buffer_size);
  709. app->variable_item_list_config = variable_item_list_alloc();
  710. variable_item_list_reset(app->variable_item_list_config);
  711. //variable_item_list_set_header(app->variable_item_list_config, "Flipboard Signal Config");
  712. VariableItem* item = variable_item_list_add(
  713. app->variable_item_list_config,
  714. setting_frequency_label,
  715. COUNT_OF(setting_frequency_values),
  716. fmf2sub_setting_frequency_change,
  717. app);
  718. uint8_t setting_frequency_index = 33;
  719. variable_item_set_current_value_index(item, setting_frequency_index);
  720. variable_item_set_current_value_text(item, setting_frequency_names[setting_frequency_index]);
  721. item = variable_item_list_add(
  722. app->variable_item_list_config,
  723. setting_modulation_label,
  724. COUNT_OF(setting_modulation_values),
  725. fmf2sub_setting_modulation_change,
  726. app);
  727. uint8_t setting_modulation_index = 1;
  728. variable_item_set_current_value_index(item, setting_modulation_index);
  729. variable_item_set_current_value_text(item, setting_modulation_names[setting_modulation_index]);
  730. app->variable_item_button = variable_item_list_add(
  731. app->variable_item_list_config,
  732. setting_button_label,
  733. COUNT_OF(setting_button_values),
  734. fmf2sub_setting_button_change,
  735. app);
  736. uint8_t setting_button_index = 0;
  737. variable_item_set_current_value_index(app->variable_item_button, setting_button_index);
  738. variable_item_set_current_value_text(
  739. app->variable_item_button, setting_button_names[setting_button_index]);
  740. view_set_previous_callback(
  741. variable_item_list_get_view(app->variable_item_list_config),
  742. fmf2sub_navigation_submenu_callback);
  743. view_dispatcher_add_view(
  744. app->view_dispatcher,
  745. Fmf2SubViewConfigure,
  746. variable_item_list_get_view(app->variable_item_list_config));
  747. app->view_convert = view_alloc();
  748. view_set_draw_callback(app->view_convert, fmf2sub_view_convert_draw_callback);
  749. view_set_input_callback(app->view_convert, fmf2sub_view_convert_input_callback);
  750. view_set_previous_callback(app->view_convert, fmf2sub_navigation_submenu_callback);
  751. view_set_enter_callback(app->view_convert, fmf2sub_view_convert_enter_callback);
  752. view_set_exit_callback(app->view_convert, fmf2sub_view_convert_exit_callback);
  753. view_set_context(app->view_convert, app);
  754. view_set_custom_callback(app->view_convert, fmf2sub_view_convert_custom_event_callback);
  755. view_allocate_model(app->view_convert, ViewModelTypeLockFree, sizeof(Fmf2SubConvertModel));
  756. Fmf2SubConvertModel* model = view_get_model(app->view_convert);
  757. model->setting_frequency_index = setting_frequency_index;
  758. model->setting_modulation_index = setting_modulation_index;
  759. model->setting_button_index = setting_button_index;
  760. view_dispatcher_add_view(app->view_dispatcher, Fmf2SubViewConvert, app->view_convert);
  761. app->widget_about = widget_alloc();
  762. widget_add_text_scroll_element(
  763. app->widget_about,
  764. 0,
  765. 0,
  766. 128,
  767. 64,
  768. "Music to Sub-GHz v1.2!\n\n"
  769. "Converts music files (.FMF)\n"
  770. "or (.TXT) to Sub-GHz format\n"
  771. "(.SUB) Files. Flip#.sub is\n"
  772. "written to the SD Card's\n"
  773. "subghz folder. Another\n"
  774. "Flipper Zero with sound\n"
  775. "turned on doing a Read RAW\n"
  776. "in the Sub-GHz app can\n"
  777. "listen to the music!\n"
  778. "Use Flipboard Signal app to\n"
  779. "send signals or use the\n"
  780. "Sub-GHz app. Enjoy!\n\n"
  781. "author: @codeallnight\nhttps://discord.com/invite/NsjCvqwPAd\nhttps://youtube.com/@MrDerekJamison");
  782. view_set_previous_callback(
  783. widget_get_view(app->widget_about), fmf2sub_navigation_submenu_callback);
  784. view_dispatcher_add_view(
  785. app->view_dispatcher, Fmf2SubViewAbout, widget_get_view(app->widget_about));
  786. app->file_path = furi_string_alloc();
  787. app->dialogs = furi_record_open(RECORD_DIALOGS);
  788. return app;
  789. }
  790. /**
  791. * @brief Free the fmf2sub application.
  792. * @details This function frees the fmf2sub application resources.
  793. * @param app The fmf2sub application object.
  794. */
  795. static void fmf2sub_app_free(Fmf2SubApp* app) {
  796. view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewTextInput);
  797. text_input_free(app->text_input);
  798. free(app->temp_buffer);
  799. view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewAbout);
  800. widget_free(app->widget_about);
  801. view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewConvert);
  802. view_free(app->view_convert);
  803. view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewConfigure);
  804. variable_item_list_free(app->variable_item_list_config);
  805. view_dispatcher_remove_view(app->view_dispatcher, Fmf2SubViewSubmenu);
  806. submenu_free(app->submenu);
  807. view_dispatcher_free(app->view_dispatcher);
  808. furi_record_close(RECORD_GUI);
  809. furi_string_free(app->file_path);
  810. furi_record_close(RECORD_DIALOGS);
  811. free(app);
  812. }
  813. /**
  814. * @brief Main function for fmf2sub application.
  815. * @details This function is the entry point for the fmf2sub application. It should be defined in
  816. * application.fam as the entry_point setting.
  817. * @param _p Input parameter - unused
  818. * @return 0 - Success
  819. */
  820. int32_t fmf_to_sub_app(void* _p) {
  821. UNUSED(_p);
  822. Fmf2SubApp* app = fmf2sub_app_alloc();
  823. view_dispatcher_run(app->view_dispatcher);
  824. fmf2sub_app_free(app);
  825. return 0;
  826. }