Explorar el Código

Add flizzer_tracker from https://github.com/LTVA1/flizzer_tracker

git-subtree-dir: flizzer_tracker
git-subtree-mainline: 3850ec681d51676b5bc20c199adf0a903be46e95
git-subtree-split: 33f9905f32513733a7a866fbc7c348e33b23e0d2
Willy-JL hace 2 años
padre
commit
7ce99ddec3
Se han modificado 76 ficheros con 7696 adiciones y 0 borrados
  1. 191 0
      flizzer_tracker/.clang-format
  2. BIN
      flizzer_tracker/.flipcorg/gallery/Screenshot-20230215-154807.png
  3. BIN
      flizzer_tracker/.flipcorg/gallery/Screenshot-20230215-155127.png
  4. 2 0
      flizzer_tracker/.gitattributes
  5. 1 0
      flizzer_tracker/.gitsubtree
  6. 21 0
      flizzer_tracker/LICENSE
  7. 4 0
      flizzer_tracker/README.md
  8. 2 0
      flizzer_tracker/README_CATALOG.md
  9. 16 0
      flizzer_tracker/application.fam
  10. 8 0
      flizzer_tracker/audio_modes.c
  11. 259 0
      flizzer_tracker/diskop.c
  12. 13 0
      flizzer_tracker/diskop.h
  13. 10 0
      flizzer_tracker/docs/changelog.md
  14. 218 0
      flizzer_tracker/flizzer_tracker.c
  15. 226 0
      flizzer_tracker/flizzer_tracker.h
  16. BIN
      flizzer_tracker/flizzer_tracker.png
  17. 313 0
      flizzer_tracker/flizzer_tracker_hal.c
  18. 43 0
      flizzer_tracker/flizzer_tracker_hal.h
  19. 31 0
      flizzer_tracker/font.h
  20. BIN
      flizzer_tracker/images/channel_off.png
  21. BIN
      flizzer_tracker/images/channel_on.png
  22. BIN
      flizzer_tracker/images/checkbox_checked.png
  23. BIN
      flizzer_tracker/images/checkbox_empty.png
  24. BIN
      flizzer_tracker/images/flizzer_tracker_instrument.png
  25. BIN
      flizzer_tracker/images/flizzer_tracker_module.png
  26. BIN
      flizzer_tracker/images/help.png
  27. BIN
      flizzer_tracker/images/note_release.png
  28. 299 0
      flizzer_tracker/init_deinit.c
  29. 14 0
      flizzer_tracker/init_deinit.h
  30. 536 0
      flizzer_tracker/input/instrument.c
  31. 12 0
      flizzer_tracker/input/instrument.h
  32. 239 0
      flizzer_tracker/input/instrument_program.c
  33. 12 0
      flizzer_tracker/input/instrument_program.h
  34. 410 0
      flizzer_tracker/input/pattern.c
  35. 14 0
      flizzer_tracker/input/pattern.h
  36. 209 0
      flizzer_tracker/input/sequence.c
  37. 12 0
      flizzer_tracker/input/sequence.h
  38. 224 0
      flizzer_tracker/input/songinfo.c
  39. 14 0
      flizzer_tracker/input/songinfo.h
  40. 501 0
      flizzer_tracker/input_event.c
  41. 40 0
      flizzer_tracker/input_event.h
  42. 2 0
      flizzer_tracker/macros.h
  43. BIN
      flizzer_tracker/screenshots/inst.png
  44. BIN
      flizzer_tracker/screenshots/pat.png
  45. 36 0
      flizzer_tracker/sound_engine/freqs.c
  46. 11 0
      flizzer_tracker/sound_engine/freqs.h
  47. 203 0
      flizzer_tracker/sound_engine/sound_engine.c
  48. 23 0
      flizzer_tracker/sound_engine/sound_engine.h
  49. 59 0
      flizzer_tracker/sound_engine/sound_engine_adsr.c
  50. 9 0
      flizzer_tracker/sound_engine/sound_engine_adsr.h
  51. 102 0
      flizzer_tracker/sound_engine/sound_engine_defs.h
  52. 28 0
      flizzer_tracker/sound_engine/sound_engine_filter.c
  53. 9 0
      flizzer_tracker/sound_engine/sound_engine_filter.h
  54. 278 0
      flizzer_tracker/sound_engine/sound_engine_osc.c
  55. 8 0
      flizzer_tracker/sound_engine/sound_engine_osc.h
  56. 127 0
      flizzer_tracker/tracker_engine/diskop.c
  57. 12 0
      flizzer_tracker/tracker_engine/diskop.h
  58. 466 0
      flizzer_tracker/tracker_engine/do_effects.c
  59. 10 0
      flizzer_tracker/tracker_engine/do_effects.h
  60. 696 0
      flizzer_tracker/tracker_engine/tracker_engine.c
  61. 28 0
      flizzer_tracker/tracker_engine/tracker_engine.h
  62. 232 0
      flizzer_tracker/tracker_engine/tracker_engine_defs.h
  63. 202 0
      flizzer_tracker/util.c
  64. 26 0
      flizzer_tracker/util.h
  65. 4 0
      flizzer_tracker/view/char_array.c
  66. 669 0
      flizzer_tracker/view/instrument_editor.c
  67. 11 0
      flizzer_tracker/view/instrument_editor.h
  68. 72 0
      flizzer_tracker/view/opcode_description.c
  69. 12 0
      flizzer_tracker/view/opcode_description.h
  70. 442 0
      flizzer_tracker/view/pattern_editor.c
  71. 25 0
      flizzer_tracker/view/pattern_editor.h
  72. BIN
      flizzer_tracker/wiki_images/instrument_editor.png
  73. BIN
      flizzer_tracker/wiki_images/instrument_program.png
  74. BIN
      flizzer_tracker/wiki_images/main_screen.png
  75. BIN
      flizzer_tracker/wiki_images/pattern_row.png
  76. BIN
      flizzer_tracker/wiki_images/sequence_loop.png

+ 191 - 0
flizzer_tracker/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

BIN
flizzer_tracker/.flipcorg/gallery/Screenshot-20230215-154807.png


BIN
flizzer_tracker/.flipcorg/gallery/Screenshot-20230215-155127.png


+ 2 - 0
flizzer_tracker/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 1 - 0
flizzer_tracker/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/LTVA1/flizzer_tracker main

+ 21 - 0
flizzer_tracker/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 LTVA1
+
+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.

+ 4 - 0
flizzer_tracker/README.md

@@ -0,0 +1,4 @@
+# Flizzer Tracker
+ A Flipper Zero chiptune tracker. Supports 4 channels, external (through PA6 pin) and internal (built-in buzzer) audio output. Each channel has a functionality akin to MOS Technology SID sound chip channel.
+
+[Telegram channel](https://t.me/flizzer_tracker)

+ 2 - 0
flizzer_tracker/README_CATALOG.md

@@ -0,0 +1,2 @@
+# Flizzer Tracker
+ A Flipper Zero chiptune tracker. Supports 4 channels, external (through PA6 pin) and internal (built-in buzzer) audio output. Each channel has a functionality akin to MOS Technology SID sound chip channel.

+ 16 - 0
flizzer_tracker/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="flizzer_tracker",
+    name="Flizzer Tracker",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="flizzer_tracker_app",
+    cdefines=["APP_FLIZZER_TRACKER"],
+    stack_size=2 * 1024,
+    order=90,
+	fap_version=(0, 3),
+	fap_description="An advanced Flipper Zero chiptune tracker with 4 channels",
+	fap_author="LTVA",
+    fap_weburl="https://github.com/LTVA1/flizzer_tracker",
+	fap_icon="flizzer_tracker.png",
+	fap_icon_assets="images",
+    fap_category="Media",
+)

+ 8 - 0
flizzer_tracker/audio_modes.c

@@ -0,0 +1,8 @@
+#include <stdbool.h>
+#include <stdint.h>
+
+char* audio_modes_text[2] = {
+    "Internal",
+    "External",
+};
+bool audio_modes_values[2] = {false, true};

+ 259 - 0
flizzer_tracker/diskop.c

@@ -0,0 +1,259 @@
+#include "diskop.h"
+
+#define CFG_FILENAME "settings.cfg"
+
+void save_instrument_inner(Stream* stream, Instrument* inst) {
+    size_t rwops = stream_write(stream, (uint8_t*)inst->name, sizeof(inst->name));
+    rwops = stream_write(stream, (uint8_t*)&inst->waveform, sizeof(inst->waveform));
+    rwops = stream_write(stream, (uint8_t*)&inst->flags, sizeof(inst->flags));
+    rwops = stream_write(
+        stream, (uint8_t*)&inst->sound_engine_flags, sizeof(inst->sound_engine_flags));
+
+    rwops = stream_write(stream, (uint8_t*)&inst->base_note, sizeof(inst->base_note));
+    rwops = stream_write(stream, (uint8_t*)&inst->finetune, sizeof(inst->finetune));
+
+    rwops = stream_write(stream, (uint8_t*)&inst->slide_speed, sizeof(inst->slide_speed));
+
+    rwops = stream_write(stream, (uint8_t*)&inst->adsr, sizeof(inst->adsr));
+    rwops = stream_write(stream, (uint8_t*)&inst->pw, sizeof(inst->pw));
+
+    if(inst->sound_engine_flags & SE_ENABLE_RING_MOD) {
+        rwops = stream_write(stream, (uint8_t*)&inst->ring_mod, sizeof(inst->ring_mod));
+    }
+
+    if(inst->sound_engine_flags & SE_ENABLE_HARD_SYNC) {
+        rwops = stream_write(stream, (uint8_t*)&inst->hard_sync, sizeof(inst->hard_sync));
+    }
+
+    uint8_t progsteps = 0;
+
+    for(uint8_t i = 0; i < INST_PROG_LEN; i++) {
+        if((inst->program[i] & 0x7fff) != TE_PROGRAM_NOP) {
+            progsteps = i + 1;
+        }
+    }
+
+    rwops = stream_write(stream, (uint8_t*)&progsteps, sizeof(progsteps));
+
+    if(progsteps > 0) {
+        rwops =
+            stream_write(stream, (uint8_t*)inst->program, progsteps * sizeof(inst->program[0]));
+    }
+
+    rwops = stream_write(stream, (uint8_t*)&inst->program_period, sizeof(inst->program_period));
+
+    if(inst->flags & TE_ENABLE_VIBRATO) {
+        rwops = stream_write(stream, (uint8_t*)&inst->vibrato_speed, sizeof(inst->vibrato_speed));
+        rwops = stream_write(stream, (uint8_t*)&inst->vibrato_depth, sizeof(inst->vibrato_depth));
+        rwops = stream_write(stream, (uint8_t*)&inst->vibrato_delay, sizeof(inst->vibrato_delay));
+    }
+
+    if(inst->flags & TE_ENABLE_PWM) {
+        rwops = stream_write(stream, (uint8_t*)&inst->pwm_speed, sizeof(inst->pwm_speed));
+        rwops = stream_write(stream, (uint8_t*)&inst->pwm_depth, sizeof(inst->pwm_depth));
+        rwops = stream_write(stream, (uint8_t*)&inst->pwm_delay, sizeof(inst->pwm_delay));
+    }
+
+    if(inst->sound_engine_flags & SE_ENABLE_FILTER) {
+        rwops = stream_write(stream, (uint8_t*)&inst->filter_cutoff, sizeof(inst->filter_cutoff));
+        rwops = stream_write(
+            stream, (uint8_t*)&inst->filter_resonance, sizeof(inst->filter_resonance));
+        rwops = stream_write(stream, (uint8_t*)&inst->filter_type, sizeof(inst->filter_type));
+    }
+
+    UNUSED(rwops);
+}
+
+bool save_instrument(FlizzerTrackerApp* tracker, FuriString* filepath) {
+    bool file_removed =
+        storage_simply_remove(tracker->storage, furi_string_get_cstr(filepath)); // just in case
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+
+    uint8_t version = TRACKER_ENGINE_VERSION;
+    size_t rwops =
+        stream_write(tracker->stream, (uint8_t*)INST_FILE_SIG, sizeof(INST_FILE_SIG) - 1);
+    rwops = stream_write(tracker->stream, (uint8_t*)&version, sizeof(uint8_t));
+
+    Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+
+    save_instrument_inner(tracker->stream, inst);
+
+    file_stream_close(tracker->stream);
+    tracker->is_saving_instrument = false;
+    furi_string_free(filepath);
+
+    UNUSED(file_removed);
+    UNUSED(open_file);
+    UNUSED(rwops);
+    return false;
+}
+
+bool save_song(FlizzerTrackerApp* tracker, FuriString* filepath) {
+    bool file_removed =
+        storage_simply_remove(tracker->storage, furi_string_get_cstr(filepath)); // just in case
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+
+    uint8_t version = TRACKER_ENGINE_VERSION;
+    size_t rwops =
+        stream_write(tracker->stream, (uint8_t*)SONG_FILE_SIG, sizeof(SONG_FILE_SIG) - 1);
+    rwops = stream_write(tracker->stream, (uint8_t*)&version, sizeof(uint8_t));
+
+    TrackerSong* song = &tracker->song;
+
+    /*for(uint32_t i = 0; i < 23444; i++)
+    {
+        rwops = stream_write(tracker->stream, (uint8_t*)&song->loop_end, sizeof(uint8_t));
+    }*/
+
+    rwops = stream_write(tracker->stream, (uint8_t*)song->song_name, sizeof(song->song_name));
+    rwops = stream_write(tracker->stream, (uint8_t*)&song->loop_start, sizeof(song->loop_start));
+    rwops = stream_write(tracker->stream, (uint8_t*)&song->loop_end, sizeof(song->loop_end));
+    rwops = stream_write(
+        tracker->stream, (uint8_t*)&song->pattern_length, sizeof(song->pattern_length));
+
+    rwops = stream_write(tracker->stream, (uint8_t*)&song->speed, sizeof(song->speed));
+    rwops = stream_write(tracker->stream, (uint8_t*)&song->rate, sizeof(song->rate));
+
+    rwops = stream_write(
+        tracker->stream, (uint8_t*)&song->num_sequence_steps, sizeof(song->num_sequence_steps));
+
+    for(uint16_t i = 0; i < song->num_sequence_steps; i++) {
+        rwops = stream_write(
+            tracker->stream,
+            (uint8_t*)&song->sequence.sequence_step[i],
+            sizeof(song->sequence.sequence_step[0]));
+    }
+
+    rwops =
+        stream_write(tracker->stream, (uint8_t*)&song->num_patterns, sizeof(song->num_patterns));
+
+    for(uint16_t i = 0; i < song->num_patterns; i++) {
+        rwops = stream_write(
+            tracker->stream,
+            (uint8_t*)song->pattern[i].step,
+            sizeof(TrackerSongPatternStep) * (song->pattern_length));
+    }
+
+    rwops = stream_write(
+        tracker->stream, (uint8_t*)&song->num_instruments, sizeof(song->num_instruments));
+
+    for(uint16_t i = 0; i < song->num_instruments; i++) {
+        save_instrument_inner(tracker->stream, song->instrument[i]);
+    }
+
+    file_stream_close(tracker->stream);
+    tracker->is_saving = false;
+    furi_string_free(filepath);
+
+    UNUSED(file_removed);
+    UNUSED(open_file);
+    UNUSED(rwops);
+    return false;
+}
+
+bool load_song_util(FlizzerTrackerApp* tracker, FuriString* filepath) {
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_READ, FSOM_OPEN_ALWAYS);
+
+    bool result = load_song(&tracker->song, tracker->stream);
+
+    tracker->is_loading = false;
+    file_stream_close(tracker->stream);
+    furi_string_free(filepath);
+    UNUSED(open_file);
+    return result;
+}
+
+bool load_instrument_disk(TrackerSong* song, uint8_t inst, Stream* stream) {
+    set_default_instrument(song->instrument[inst]);
+
+    char header[sizeof(INST_FILE_SIG) + 2] = {0};
+    size_t rwops = stream_read(stream, (uint8_t*)&header, sizeof(INST_FILE_SIG) - 1);
+    header[sizeof(INST_FILE_SIG)] = '\0';
+
+    uint8_t version = 0;
+
+    if(strcmp(header, INST_FILE_SIG) == 0) {
+        rwops = stream_read(stream, (uint8_t*)&version, sizeof(version));
+
+        if(version <= TRACKER_ENGINE_VERSION) {
+            load_instrument_inner(stream, song->instrument[inst], version);
+        }
+    }
+
+    UNUSED(rwops);
+    return false;
+}
+
+bool load_instrument_util(FlizzerTrackerApp* tracker, FuriString* filepath) {
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_READ, FSOM_OPEN_ALWAYS);
+
+    bool result =
+        load_instrument_disk(&tracker->song, tracker->current_instrument, tracker->stream);
+
+    tracker->is_loading_instrument = false;
+    file_stream_close(tracker->stream);
+    furi_string_free(filepath);
+    UNUSED(open_file);
+    return result;
+}
+
+void save_config(FlizzerTrackerApp* tracker) {
+    // stream_read_line
+    FuriString* filepath = furi_string_alloc();
+    FuriString* config_line = furi_string_alloc();
+    furi_string_cat_printf(filepath, "%s/%s", FLIZZER_TRACKER_FOLDER, CFG_FILENAME);
+
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+    UNUSED(open_file);
+
+    furi_string_cat_printf(
+        config_line, "%s = %s\n", "external_audio", tracker->external_audio ? "true" : "false");
+    stream_write_string(tracker->stream, config_line);
+
+    file_stream_close(tracker->stream);
+    furi_string_free(filepath);
+    furi_string_free(config_line);
+}
+
+void load_config(FlizzerTrackerApp* tracker) {
+    FuriString* filepath = furi_string_alloc();
+    FuriString* config_line = furi_string_alloc();
+    furi_string_cat_printf(filepath, "%s/%s", FLIZZER_TRACKER_FOLDER, CFG_FILENAME);
+
+    bool open_file = file_stream_open(
+        tracker->stream, furi_string_get_cstr(filepath), FSAM_READ, FSOM_OPEN_ALWAYS);
+    UNUSED(open_file);
+
+    stream_read_line(tracker->stream, config_line);
+
+    sscanf(
+        furi_string_get_cstr(config_line), "%s%s%s", tracker->param, tracker->eq, tracker->value);
+
+    if(strcmp(tracker->param, "external_audio") == 0) {
+        if(strcmp(tracker->value, "false") == 0) {
+            tracker->external_audio = false;
+            // strcpy(tracker->value, "false_");
+        }
+
+        if(strcmp(tracker->value, "true") == 0) {
+            tracker->external_audio = true;
+            // strcpy(tracker->value, "true_");
+        }
+
+        sound_engine_init(
+            &tracker->sound_engine,
+            tracker->sound_engine.sample_rate,
+            tracker->external_audio,
+            tracker->sound_engine.audio_buffer_size);
+        // sound_engine_set_audio_output(tracker->external_audio);
+    }
+
+    file_stream_close(tracker->stream);
+    furi_string_free(filepath);
+    furi_string_free(config_line);
+}

+ 13 - 0
flizzer_tracker/diskop.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include "flizzer_tracker.h"
+#include "tracker_engine/diskop.h"
+
+bool save_song(FlizzerTrackerApp* tracker, FuriString* filepath);
+bool save_instrument(FlizzerTrackerApp* tracker, FuriString* filepath);
+
+bool load_song_util(FlizzerTrackerApp* tracker, FuriString* filepath);
+bool load_instrument_util(FlizzerTrackerApp* tracker, FuriString* filepath);
+
+void save_config(FlizzerTrackerApp* tracker);
+void load_config(FlizzerTrackerApp* tracker);

+ 10 - 0
flizzer_tracker/docs/changelog.md

@@ -0,0 +1,10 @@
+# Flizzer Tracker v0.2 #
+
+## Added ##
+- Save/load instruments in separate .fzi files
+- Pattern editor now occupies full screen when you focus on it
+- Copypaste menu (hold Back to open it when focused on pattern editor), operates on whole patterns
+
+# Flizzer Tracker v0.1 #
+
+- Initial release

+ 218 - 0
flizzer_tracker/flizzer_tracker.c

@@ -0,0 +1,218 @@
+#include "flizzer_tracker.h"
+#include "diskop.h"
+#include "init_deinit.h"
+#include "input_event.h"
+#include "util.h"
+#include "view/instrument_editor.h"
+#include "view/pattern_editor.h"
+
+#include "font.h"
+#include <flizzer_tracker_icons.h>
+
+void draw_callback(Canvas* canvas, void* ctx) {
+    TrackerViewModel* model = (TrackerViewModel*)ctx;
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)(model->tracker);
+
+    canvas_set_color(canvas, ColorXOR);
+
+    if(tracker->is_loading || tracker->is_loading_instrument) {
+        canvas_draw_str(canvas, 10, 10, "Loading...");
+        return;
+    }
+
+    if(tracker->is_saving || tracker->is_saving_instrument) {
+        canvas_draw_str(canvas, 10, 10, "Saving...");
+        return;
+    }
+
+    if(tracker->showing_help) {
+        canvas_draw_icon(canvas, 0, 0, &I_help);
+        return;
+    }
+
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_tom_thumb_4x6_tr);
+
+    switch(tracker->mode) {
+    case PATTERN_VIEW: {
+        if(tracker->tracker_engine.song == NULL) {
+            stop();
+            tracker_engine_set_song(&tracker->tracker_engine, &tracker->song);
+        }
+
+        if(tracker->focus != EDIT_PATTERN) {
+            draw_songinfo_view(canvas, tracker);
+        }
+
+        if(tracker->focus != EDIT_PATTERN) {
+            draw_sequence_view(canvas, tracker);
+        }
+
+        draw_pattern_view(canvas, tracker);
+        break;
+    }
+
+    case INST_EDITOR_VIEW: {
+        draw_instrument_view(canvas, tracker);
+        draw_instrument_program_view(canvas, tracker);
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+bool input_callback(InputEvent* input_event, void* ctx) {
+    // Проверяем, что контекст не нулевой
+    furi_assert(ctx);
+    TrackerView* tracker_view = (TrackerView*)ctx;
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)(tracker_view->context);
+
+    bool consumed = false;
+
+    if(input_event->key == InputKeyBack && input_event->type == InputTypeShort) {
+        tracker->period = furi_get_tick() - tracker->current_time;
+        tracker->current_time = furi_get_tick();
+
+        tracker->was_it_back_keypress = true;
+    }
+
+    else if(input_event->type == InputTypeShort || input_event->type == InputTypeLong) {
+        tracker->was_it_back_keypress = false;
+        tracker->period = 0;
+    }
+
+    uint32_t final_period = (tracker->was_it_back_keypress ? tracker->period : 0);
+
+    FlizzerTrackerEvent event = {
+        .type = EventTypeInput, .input = *input_event, .period = final_period};
+
+    if(!(tracker->is_loading) && !(tracker->is_saving)) {
+        furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+    }
+
+    consumed = true;
+    return consumed;
+}
+
+int32_t flizzer_tracker_app(void* p) {
+    UNUSED(p);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    bool st = storage_simply_mkdir(storage, APPSDATA_FOLDER);
+    st = storage_simply_mkdir(storage, FLIZZER_TRACKER_FOLDER);
+    st = storage_simply_mkdir(storage, FLIZZER_TRACKER_INSTRUMENTS_FOLDER);
+    UNUSED(st);
+    furi_record_close(RECORD_STORAGE);
+
+    FlizzerTrackerApp* tracker = init_tracker(44100, 50, true, 1024);
+
+    // Текущее событие типа кастомного типа FlizzerTrackerEvent
+    FlizzerTrackerEvent event;
+
+    view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+
+    // Бесконечный цикл обработки очереди событий
+    while(!(tracker->quit)) {
+        // Выбираем событие из очереди в переменную event (ждём бесконечно долго, если очередь пуста)
+        // и проверяем, что у нас получилось это сделать
+        furi_check(
+            furi_message_queue_get(tracker->event_queue, &event, FuriWaitForever) == FuriStatusOk);
+
+        // Наше событие — это нажатие кнопки
+        if(event.type == EventTypeInput) {
+            process_input_event(tracker, &event);
+        }
+
+        if(event.type == EventTypeSaveSong) {
+            save_song(tracker, tracker->filepath);
+        }
+
+        if(event.type == EventTypeSaveInstrument) {
+            save_instrument(tracker, tracker->filepath);
+        }
+
+        if(event.type == EventTypeLoadSong) {
+            stop_song(tracker);
+
+            tracker->tracker_engine.sequence_position = tracker->tracker_engine.pattern_position =
+                tracker->current_instrument = 0;
+
+            tracker->dialogs = furi_record_open(RECORD_DIALOGS);
+            tracker->is_loading = true;
+
+            FuriString* path;
+            path = furi_string_alloc();
+            furi_string_set(path, FLIZZER_TRACKER_FOLDER);
+
+            DialogsFileBrowserOptions browser_options;
+            dialog_file_browser_set_basic_options(
+                &browser_options, SONG_FILE_EXT, &I_flizzer_tracker_module);
+            browser_options.base_path = FLIZZER_TRACKER_FOLDER;
+            browser_options.hide_ext = false;
+
+            bool ret = dialog_file_browser_show(tracker->dialogs, path, path, &browser_options);
+
+            furi_record_close(RECORD_DIALOGS);
+
+            const char* cpath = furi_string_get_cstr(path);
+
+            if(ret && strcmp(&cpath[strlen(cpath) - 4], SONG_FILE_EXT) == 0) {
+                bool result = load_song_util(tracker, path);
+                UNUSED(result);
+            }
+
+            else {
+                furi_string_free(path);
+                tracker->is_loading = false;
+            }
+        }
+
+        if(event.type == EventTypeLoadInstrument) {
+            stop_song(tracker);
+
+            tracker->dialogs = furi_record_open(RECORD_DIALOGS);
+            tracker->is_loading_instrument = true;
+
+            FuriString* path;
+            path = furi_string_alloc();
+            furi_string_set(path, FLIZZER_TRACKER_INSTRUMENTS_FOLDER);
+
+            DialogsFileBrowserOptions browser_options;
+            dialog_file_browser_set_basic_options(
+                &browser_options, INST_FILE_EXT, &I_flizzer_tracker_instrument);
+            browser_options.base_path = FLIZZER_TRACKER_FOLDER;
+            browser_options.hide_ext = false;
+
+            bool ret = dialog_file_browser_show(tracker->dialogs, path, path, &browser_options);
+
+            furi_record_close(RECORD_DIALOGS);
+
+            const char* cpath = furi_string_get_cstr(path);
+
+            if(ret && strcmp(&cpath[strlen(cpath) - 4], INST_FILE_EXT) == 0) {
+                bool result = load_instrument_util(tracker, path);
+                UNUSED(result);
+            }
+
+            else {
+                furi_string_free(path);
+                tracker->is_loading = false;
+            }
+        }
+
+        if(event.type == EventTypeSetAudioMode) {
+            sound_engine_PWM_timer_init(tracker->external_audio);
+
+            tracker->sound_engine.external_audio_output = tracker->external_audio;
+        }
+    }
+
+    stop();
+
+    save_config(tracker);
+
+    deinit_tracker(tracker);
+
+    return 0;
+}

+ 226 - 0
flizzer_tracker/flizzer_tracker.h

@@ -0,0 +1,226 @@
+#pragma once
+
+#include <cli/cli.h>
+#include <dialogs/dialogs.h>
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <stdio.h>
+#include <storage/storage.h>
+#include <toolbox/stream/file_stream.h>
+
+#include <gui/modules/text_input.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/view_dispatcher.h>
+
+#include "flizzer_tracker_hal.h"
+#include "sound_engine/freqs.h"
+#include "sound_engine/sound_engine_defs.h"
+#include "sound_engine/sound_engine_filter.h"
+#include "tracker_engine/tracker_engine_defs.h"
+
+#define APPSDATA_FOLDER "/ext/apps_data"
+#define FLIZZER_TRACKER_FOLDER "/ext/apps_data/flizzer_tracker"
+#define FLIZZER_TRACKER_INSTRUMENTS_FOLDER "/ext/apps_data/flizzer_tracker/instruments"
+#define FILE_NAME_LEN 64
+
+typedef enum {
+    EventTypeInput,
+    EventTypeSaveSong,
+    EventTypeLoadSong,
+    EventTypeLoadInstrument,
+    EventTypeSaveInstrument,
+    EventTypeSetAudioMode,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+    uint32_t period;
+} FlizzerTrackerEvent;
+
+typedef enum {
+    PATTERN_VIEW,
+    INST_EDITOR_VIEW,
+    EXPORT_WAV_VIEW,
+} TrackerMode;
+
+typedef enum {
+    EDIT_PATTERN,
+    EDIT_SEQUENCE,
+    EDIT_SONGINFO,
+    EDIT_INSTRUMENT,
+    EDIT_PROGRAM,
+} TrackerFocus;
+
+typedef enum {
+    SI_PATTERNPOS,
+    SI_SEQUENCEPOS,
+    SI_SONGSPEED,
+    SI_SONGRATE,
+    SI_MASTERVOL,
+
+    SI_SONGNAME,
+    SI_CURRENTINSTRUMENT,
+    SI_INSTRUMENTNAME,
+    /* ========  */
+    SI_PARAMS,
+} SongInfoParam;
+
+typedef enum {
+    INST_CURRENTINSTRUMENT,
+    INST_INSTRUMENTNAME,
+
+    INST_CURRENT_NOTE,
+    INST_FINETUNE,
+
+    INST_SLIDESPEED,
+    INST_SETPW,
+    INST_PW,
+    INST_SETCUTOFF,
+
+    INST_WAVE_NOISE,
+    INST_WAVE_PULSE,
+    INST_WAVE_TRIANGLE,
+    INST_WAVE_SAWTOOTH,
+    INST_WAVE_NOISE_METAL,
+    INST_WAVE_SINE,
+
+    INST_ATTACK,
+    INST_DECAY,
+    INST_SUSTAIN,
+    INST_RELEASE,
+    INST_VOLUME,
+
+    INST_ENABLEFILTER,
+    INST_FILTERCUTOFF,
+    INST_FILTERRESONANCE,
+
+    INST_FILTERTYPE,
+    INST_ENABLERINGMOD,
+    INST_RINGMODSRC,
+    INST_ENABLEHARDSYNC,
+    INST_HARDSYNCSRC,
+
+    INST_RETRIGGERONSLIDE,
+    INST_ENABLEKEYSYNC,
+
+    INST_ENABLEVIBRATO,
+    INST_VIBRATOSPEED,
+    INST_VIBRATODEPTH,
+    INST_VIBRATODELAY,
+
+    INST_ENABLEPWM,
+    INST_PWMSPEED,
+    INST_PWMDEPTH,
+    INST_PWMDELAY,
+
+    INST_PROGRESTART,
+    INST_PROGRAMEPERIOD,
+    /* ========= */
+    INST_PARAMS,
+} InstrumentParam;
+
+typedef struct {
+    View* view;
+    void* context;
+} TrackerView;
+
+typedef enum {
+    VIEW_TRACKER,
+    VIEW_KEYBOARD,
+    VIEW_SUBMENU_PATTERN,
+    VIEW_SUBMENU_PATTERN_COPYPASTE,
+    VIEW_SUBMENU_INSTRUMENT,
+    VIEW_FILE_OVERWRITE,
+    VIEW_INSTRUMENT_FILE_OVERWRITE,
+    VIEW_SETTINGS,
+} FlizzerTrackerViews;
+
+typedef enum {
+    SUBMENU_PATTERN_LOAD_SONG,
+    SUBMENU_PATTERN_SAVE_SONG,
+    SUBMENU_PATTERN_SETTINGS,
+    SUBMENU_PATTERN_HELP,
+    SUBMENU_PATTERN_EXIT,
+} PatternSubmenuParams;
+
+typedef enum {
+    SUBMENU_PATTERN_COPYPASTE_COPY,
+    SUBMENU_PATTERN_COPYPASTE_PASTE,
+    SUBMENU_PATTERN_COPYPASTE_CUT,
+    SUBMENU_PATTERN_COPYPASTE_CLEAR,
+} PatternCopypasteSubmenuParams;
+
+typedef enum {
+    SUBMENU_INSTRUMENT_LOAD,
+    SUBMENU_INSTRUMENT_SAVE,
+    SUBMENU_INSTRUMENT_EXIT,
+} InstrumentSubmenuParams;
+
+typedef struct {
+    NotificationApp* notification;
+    FuriMessageQueue* event_queue;
+    Gui* gui;
+    TrackerView* tracker_view;
+    ViewDispatcher* view_dispatcher;
+    TextInput* text_input;
+    Storage* storage;
+    Stream* stream;
+    FuriString* filepath;
+    DialogsApp* dialogs;
+    Submenu* pattern_submenu;
+    Submenu* pattern_copypaste_submenu;
+    Submenu* instrument_submenu;
+    VariableItemList* settings_list;
+    Widget* overwrite_file_widget;
+    Widget* overwrite_instrument_file_widget;
+    char filename[FILE_NAME_LEN + 1];
+    bool was_it_back_keypress;
+    uint32_t current_time;
+    uint32_t period;
+
+    bool external_audio;
+
+    SoundEngine sound_engine;
+    TrackerEngine tracker_engine;
+
+    TrackerSong song;
+
+    uint8_t selected_param;
+
+    uint8_t mode, focus;
+    uint8_t patternx, current_channel, current_digit, program_position, current_program_step,
+        current_instrument, current_note, current_volume;
+
+    uint8_t inst_editor_shift;
+
+    int16_t source_pattern_index;
+
+    bool editing;
+    bool was_editing;
+
+    bool is_loading;
+    bool is_saving;
+    bool is_loading_instrument;
+    bool is_saving_instrument;
+    bool showing_help;
+
+    bool cut_pattern; //if we need to clear the pattern we pasted from
+
+    bool quit;
+
+    char eq[2];
+    char param[80];
+    char value[10];
+} FlizzerTrackerApp;
+
+typedef struct {
+    FlizzerTrackerApp* tracker;
+} TrackerViewModel;
+
+void draw_callback(Canvas* canvas, void* ctx);
+bool input_callback(InputEvent* input_event, void* ctx);

BIN
flizzer_tracker/flizzer_tracker.png


+ 313 - 0
flizzer_tracker/flizzer_tracker_hal.c

@@ -0,0 +1,313 @@
+#include "flizzer_tracker_hal.h"
+#include "flizzer_tracker.h"
+
+void sound_engine_dma_isr(void* ctx) {
+    SoundEngine* sound_engine = (SoundEngine*)ctx;
+
+    // sound_engine->counter++;
+
+    // half of transfer
+    if(LL_DMA_IsActiveFlag_HT1(DMA1)) {
+        LL_DMA_ClearFlag_HT1(DMA1);
+        // fill first half of buffer
+        uint16_t* audio_buffer = sound_engine->audio_buffer;
+        uint32_t audio_buffer_length = sound_engine->audio_buffer_size / 2;
+        sound_engine_fill_buffer(sound_engine, audio_buffer, audio_buffer_length);
+    }
+
+    // transfer complete
+    if(LL_DMA_IsActiveFlag_TC1(DMA1)) {
+        LL_DMA_ClearFlag_TC1(DMA1);
+        // fill second half of buffer
+        uint32_t audio_buffer_length = sound_engine->audio_buffer_size / 2;
+        uint16_t* audio_buffer = &sound_engine->audio_buffer[audio_buffer_length];
+        sound_engine_fill_buffer(sound_engine, audio_buffer, audio_buffer_length);
+    }
+}
+
+void tracker_engine_timer_isr(
+    void* ctx) // the tracker engine interrupt is of higher priority than sound engine one so it can run at the middle of filling the buffer, thus allowing sample-accurate tight effect timing
+{
+    TrackerEngine* tracker_engine = (TrackerEngine*)ctx;
+    // tracker_engine->counter++;
+
+    if(LL_TIM_IsActiveFlag_UPDATE(TRACKER_ENGINE_TIMER)) {
+        LL_TIM_ClearFlag_UPDATE(TRACKER_ENGINE_TIMER);
+        tracker_engine_advance_tick(tracker_engine);
+    }
+}
+
+void sound_engine_PWM_timer_init(bool external_audio_output) // external audio on pin PA6
+{
+    if(external_audio_output) {
+        /*if(furi_hal_speaker_is_mine()) {
+            furi_hal_speaker_release();
+        }*/
+
+        //LL_TIM_DisableAllOutputs(SPEAKER_PWM_TIMER);
+        //LL_TIM_DisableCounter(SPEAKER_PWM_TIMER);
+
+        if(!(furi_hal_speaker_is_mine())) {
+            if(furi_hal_speaker_acquire(1000)) {
+                LL_TIM_DisableAllOutputs(SPEAKER_PWM_TIMER);
+                LL_TIM_DisableCounter(SPEAKER_PWM_TIMER);
+
+                LL_TIM_InitTypeDef TIM_InitStruct = {0};
+                LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+
+                TIM_InitStruct.Prescaler = 0;
+                TIM_InitStruct.Autoreload =
+                    1023; // 10-bit PWM resolution at around 60 kHz PWM rate
+                TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+                LL_TIM_Init(SPEAKER_PWM_TIMER, &TIM_InitStruct);
+
+                TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+                TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+                TIM_OC_InitStruct.CompareValue = 0;
+                LL_TIM_OC_Init(SPEAKER_PWM_TIMER, SPEAKER_PWM_TIMER_CHANNEL, &TIM_OC_InitStruct);
+
+                SPEAKER_PWM_TIMER->CNT = 0;
+
+                LL_TIM_EnableAllOutputs(SPEAKER_PWM_TIMER);
+                LL_TIM_EnableCounter(SPEAKER_PWM_TIMER);
+            }
+        }
+
+        furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+
+        LL_TIM_InitTypeDef TIM_InitStruct = {0};
+        LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+
+        TIM_InitStruct.Prescaler = 0;
+        TIM_InitStruct.Autoreload = 1023; // 10-bit PWM resolution at around 60 kHz PWM rate
+        TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+        LL_TIM_Init(SPEAKER_PWM_TIMER, &TIM_InitStruct);
+
+        TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+        TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+        TIM_OC_InitStruct.CompareValue = 0;
+        LL_TIM_OC_Init(SPEAKER_PWM_TIMER, SPEAKER_PWM_TIMER_CHANNEL, &TIM_OC_InitStruct);
+
+        SPEAKER_PWM_TIMER->CNT = 0;
+
+        LL_TIM_EnableAllOutputs(SPEAKER_PWM_TIMER);
+        LL_TIM_EnableCounter(SPEAKER_PWM_TIMER);
+
+        furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        //furi_hal_gpio_init_ex(&gpio_ext_pa6, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16);
+    }
+
+    else {
+        if(!(furi_hal_speaker_is_mine())) {
+            if(furi_hal_speaker_acquire(1000)) {
+                LL_TIM_DisableAllOutputs(SPEAKER_PWM_TIMER);
+                LL_TIM_DisableCounter(SPEAKER_PWM_TIMER);
+
+                LL_TIM_InitTypeDef TIM_InitStruct = {0};
+                LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+
+                TIM_InitStruct.Prescaler = 0;
+                TIM_InitStruct.Autoreload =
+                    1023; // 10-bit PWM resolution at around 60 kHz PWM rate
+                TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+                LL_TIM_Init(SPEAKER_PWM_TIMER, &TIM_InitStruct);
+
+                TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+                TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+                TIM_OC_InitStruct.CompareValue = 0;
+                LL_TIM_OC_Init(SPEAKER_PWM_TIMER, SPEAKER_PWM_TIMER_CHANNEL, &TIM_OC_InitStruct);
+
+                SPEAKER_PWM_TIMER->CNT = 0;
+
+                LL_TIM_EnableAllOutputs(SPEAKER_PWM_TIMER);
+                LL_TIM_EnableCounter(SPEAKER_PWM_TIMER);
+            }
+        }
+
+        furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        //furi_hal_gpio_init_ex(&gpio_ext_pa6, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16);
+    }
+
+    furi_hal_gpio_init_ex(
+        &gpio_ext_pa6, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16);
+}
+
+void sound_engine_set_audio_output(bool external_audio_output) {
+    if(external_audio_output) {
+        furi_hal_gpio_init_ex(
+            &gpio_ext_pa6, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16);
+
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_speaker_release();
+        }
+    }
+
+    else {
+        if(!(furi_hal_speaker_is_mine())) {
+            bool unu = furi_hal_speaker_acquire(1000);
+            UNUSED(unu);
+        }
+
+        furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+    }
+}
+
+void sound_engine_timer_init(uint32_t sample_rate) // external audio on pin PA6
+{
+    if(!furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
+        furi_hal_bus_enable(FuriHalBusTIM1);
+    }
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+
+    TIM_InitStruct.Prescaler = 0;
+    TIM_InitStruct.Autoreload =
+        TIMER_BASE_CLOCK / sample_rate - 1; // to support various sample rates
+    TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+    LL_TIM_Init(SAMPLE_RATE_TIMER, &TIM_InitStruct);
+
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    LL_TIM_OC_Init(SAMPLE_RATE_TIMER, SPEAKER_PWM_TIMER_CHANNEL, &TIM_OC_InitStruct);
+
+    LL_TIM_EnableAllOutputs(SAMPLE_RATE_TIMER);
+
+    SAMPLE_RATE_TIMER->CNT = 0;
+}
+
+void tracker_engine_timer_init(uint8_t rate) // 0-255 hz
+{
+    if(!furi_hal_bus_is_enabled(FuriHalBusTIM2)) {
+        furi_hal_bus_enable(FuriHalBusTIM2);
+    }
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+
+    TIM_InitStruct.Prescaler = 0; // using 32-bit timer
+    TIM_InitStruct.Autoreload =
+        (uint32_t)TIMER_BASE_CLOCK / (uint32_t)rate - 1; // to support various tracker engine rates
+    TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+    LL_TIM_Init(TRACKER_ENGINE_TIMER, &TIM_InitStruct);
+
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    LL_TIM_OC_Init(TRACKER_ENGINE_TIMER, SPEAKER_PWM_TIMER_CHANNEL, &TIM_OC_InitStruct);
+
+    LL_TIM_EnableIT_UPDATE(TRACKER_ENGINE_TIMER);
+
+    TRACKER_ENGINE_TIMER->CNT = 0;
+}
+
+void tracker_engine_set_rate(uint8_t rate) {
+    if(!furi_hal_bus_is_enabled(FuriHalBusTIM2)) {
+        furi_hal_bus_enable(FuriHalBusTIM2);
+    }
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+
+    TIM_InitStruct.Prescaler = 0; // using 32-bit timer
+    TIM_InitStruct.Autoreload =
+        (uint32_t)TIMER_BASE_CLOCK / (uint32_t)rate - 1; // to support various tracker engine rates
+    TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+    LL_TIM_Init(TRACKER_ENGINE_TIMER, &TIM_InitStruct);
+
+    TRACKER_ENGINE_TIMER->CNT = 0;
+}
+
+void tracker_engine_init_hardware(uint8_t rate) {
+    tracker_engine_timer_init(rate);
+}
+
+void sound_engine_dma_init(uint32_t address, uint32_t size) {
+    uint32_t dma_dst = (uint32_t) & (SPEAKER_PWM_TIMER->CCR1);
+
+    LL_DMA_ConfigAddresses(DMA_INSTANCE, address, dma_dst, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetDataLength(DMA_INSTANCE, size);
+
+    LL_DMA_SetPeriphRequest(DMA_INSTANCE, LL_DMAMUX_REQ_TIM1_UP);
+    LL_DMA_SetDataTransferDirection(DMA_INSTANCE, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetChannelPriorityLevel(DMA_INSTANCE, LL_DMA_PRIORITY_VERYHIGH);
+    LL_DMA_SetMode(DMA_INSTANCE, LL_DMA_MODE_CIRCULAR);
+    LL_DMA_SetPeriphIncMode(DMA_INSTANCE, LL_DMA_PERIPH_NOINCREMENT);
+    LL_DMA_SetMemoryIncMode(DMA_INSTANCE, LL_DMA_MEMORY_INCREMENT);
+    LL_DMA_SetPeriphSize(DMA_INSTANCE, LL_DMA_PDATAALIGN_HALFWORD);
+    LL_DMA_SetMemorySize(DMA_INSTANCE, LL_DMA_MDATAALIGN_HALFWORD);
+
+    LL_DMA_EnableIT_TC(DMA_INSTANCE);
+    LL_DMA_EnableIT_HT(DMA_INSTANCE);
+}
+
+void sound_engine_init_hardware(
+    uint32_t sample_rate,
+    bool external_audio_output,
+    uint16_t* audio_buffer,
+    uint32_t audio_buffer_size) {
+    sound_engine_dma_init((uint32_t)audio_buffer, audio_buffer_size);
+    sound_engine_timer_init(sample_rate);
+    sound_engine_PWM_timer_init(external_audio_output);
+}
+
+void sound_engine_dma_start() {
+    LL_DMA_EnableChannel(DMA_INSTANCE);
+    LL_TIM_EnableDMAReq_UPDATE(SAMPLE_RATE_TIMER);
+}
+
+void sound_engine_dma_stop() {
+    LL_DMA_DisableChannel(DMA_INSTANCE);
+}
+
+void sound_engine_start() {
+    SAMPLE_RATE_TIMER->CNT = 0;
+    LL_TIM_EnableCounter(SAMPLE_RATE_TIMER);
+
+    sound_engine_dma_start();
+}
+
+void sound_engine_stop() {
+    LL_TIM_DisableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_DisableCounter(SAMPLE_RATE_TIMER);
+
+    sound_engine_dma_stop();
+}
+
+void sound_engine_deinit_timer() {
+    LL_TIM_DisableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_DisableAllOutputs(SPEAKER_PWM_TIMER);
+
+    LL_TIM_DisableCounter(SPEAKER_PWM_TIMER);
+
+    if(furi_hal_speaker_is_mine()) {
+        furi_hal_speaker_release();
+    }
+
+    if(furi_hal_bus_is_enabled(FuriHalBusTIM2)) {
+        furi_hal_bus_disable(FuriHalBusTIM2);
+    }
+    if(furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
+        furi_hal_bus_disable(FuriHalBusTIM1);
+    }
+}
+
+void tracker_engine_start() {
+    TRACKER_ENGINE_TIMER->CNT = 0;
+
+    LL_TIM_EnableAllOutputs(TRACKER_ENGINE_TIMER);
+    LL_TIM_EnableCounter(TRACKER_ENGINE_TIMER);
+}
+
+void tracker_engine_stop() {
+    LL_TIM_DisableAllOutputs(TRACKER_ENGINE_TIMER);
+    LL_TIM_DisableCounter(TRACKER_ENGINE_TIMER);
+}
+
+void play() {
+    tracker_engine_start();
+    sound_engine_start();
+}
+
+void stop() {
+    sound_engine_stop();
+    tracker_engine_stop();
+}

+ 43 - 0
flizzer_tracker/flizzer_tracker_hal.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include "sound_engine/sound_engine.h"
+#include "tracker_engine/tracker_engine.h"
+
+#include <stm32wbxx_ll_dma.h>
+#include <stm32wbxx_ll_gpio.h>
+#include <stm32wbxx_ll_tim.h>
+
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_resources.h>
+
+#define SPEAKER_PWM_TIMER TIM16
+#define SAMPLE_RATE_TIMER TIM1
+#define TRACKER_ENGINE_TIMER TIM2
+
+#define SPEAKER_PWM_TIMER_CHANNEL LL_TIM_CHANNEL_CH1
+
+#define TIMER_BASE_CLOCK 64000000 /* CPU frequency, 64 MHz */
+
+#define DMA_INSTANCE DMA1, LL_DMA_CHANNEL_1
+
+void sound_engine_dma_isr(void* ctx);
+void tracker_engine_timer_isr(void* ctx);
+void sound_engine_init_hardware(
+    uint32_t sample_rate,
+    bool external_audio_output,
+    uint16_t* audio_buffer,
+    uint32_t audio_buffer_size);
+void sound_engine_dma_init(uint32_t address, uint32_t size);
+void sound_engine_PWM_timer_init(bool external_audio_output);
+void sound_engine_set_audio_output(bool external_audio_output);
+void tracker_engine_init_hardware(uint8_t rate);
+void tracker_engine_timer_init(uint8_t rate);
+void tracker_engine_set_rate(uint8_t rate);
+void sound_engine_start();
+void sound_engine_stop();
+void stop();
+void play();
+void tracker_engine_stop();
+void sound_engine_deinit_timer();
+void tracker_engine_start();

+ 31 - 0
flizzer_tracker/font.h

@@ -0,0 +1,31 @@
+#include <stdint.h>
+
+/*
+Fontname: -Raccoon-Fixed4x6-Medium-R-Normal--6-60-75-75-P-40-ISO10646-1
+Copyright:
+Glyphs: 95/203
+BBX Build Mode: 0
+*/
+// this is a modified version with dot and semicolon moved 1 pixel to the left; lowercase symbols removed to save space
+// changed "G", "N" and "V" glyphs
+const uint8_t u8g2_font_tom_thumb_4x6_tr[610] =
+    "a\0\2\2\2\3\2\3\4\3\5\0\0\5\0\5\0\1`\0\0\2E\0\4@\62\1\4@\62\2"
+    "\4@\62\3\4@\62\4\4@\62\5\4@\62\6\4@\62\7\4@\62\10\4@\62\11\4@\62\12"
+    "\4@\62\13\4@\62\14\4@\62\15\4@\62\16\4@\62\17\4@\62\20\4@\62\21\4@\62\22"
+    "\4@\62\23\4@\62\24\4@\62\25\4@\62\26\4@\62\27\4@\62\30\4@\62\31\4@\62\32"
+    "\4@\62\33\4@\62\34\4@\62\35\4@\62\36\4@\62\37\4@\62 \4@\62!\5u\62+"
+    "\42\6\313\63I\5#\10W\62i\250\241\2$\10Wr#\216\230\0%\10W\62\31\265Q\0&\10"
+    "W\62J\215\224\4'\5\351\63\2(\6vr\252\14)\7V\62\61%\5*\6O\63\251\3+\7"
+    "\317ri%\0,\5Jr\12-\5G\63\3.\5E\62\1/\7W\262U\31\1\60\7Wr\313"
+    "Z\0\61\6Vr\253\1\62\7W\62\32\244r\63\11W\62\32\244\14\26\0\64\7W\62I\215X\65"
+    "\10W\62#j\260\0\66\7Wrs\244\21\67\7W\62\63\225\21\70\10W\62#\15\65\2\71\10W"
+    "\62#\215\270\0:\5\315\62);\7Rr\31(\0<\10W\262\251\6\31\4=\6\317\62\33\14>"
+    "\11W\62\31d\220J\0\77\10W\62\63e\230\0@\7Wr\325\320@A\7Wr\325P*B\10"
+    "W\62*\255\264\0C\7Wr\263\6\2D\7W\62*Y\13E\7W\62#\216\70F\10W\62#"
+    "\216\30\1G\7Wr\63\251$H\10W\62I\15\245\2I\7W\62+V\3J\7W\262\245\252\0"
+    "K\10W\62I\255\244\2L\6W\62\261\71M\10W\62i\14\245\2N\7W\62*\271\2O\7W"
+    "r\225U\1P\10W\62*\255\30\1Q\7Wr\225\32IR\7W\62*\215US\10Wr\33d"
+    "\260\0T\7W\62+\266\0U\7W\62\311\225\4V\7W\62\311U\1W\10W\62I\215\241\2X"
+    "\10W\62I\265T\0Y\10W\62I\225\25\0Z\7W\62\63\225\3[\7W\62#\226\3\134\7\317"
+    "\62\31d\20]\7W\62\263\34\1^\5\313s\15_\5G\62\3`\5\312\63\61\0\0\0\4\377\377"
+    "\0";

BIN
flizzer_tracker/images/channel_off.png


BIN
flizzer_tracker/images/channel_on.png


BIN
flizzer_tracker/images/checkbox_checked.png


BIN
flizzer_tracker/images/checkbox_empty.png


BIN
flizzer_tracker/images/flizzer_tracker_instrument.png


BIN
flizzer_tracker/images/flizzer_tracker_module.png


BIN
flizzer_tracker/images/help.png


BIN
flizzer_tracker/images/note_release.png


+ 299 - 0
flizzer_tracker/init_deinit.c

@@ -0,0 +1,299 @@
+#include "init_deinit.h"
+#include "input_event.h"
+
+#include "diskop.h"
+
+#define AUDIO_MODES_COUNT 2
+
+TrackerView* tracker_view_alloc(FlizzerTrackerApp* tracker) {
+    TrackerView* tracker_view = malloc(sizeof(TrackerView));
+    tracker_view->view = view_alloc();
+    tracker_view->context = tracker;
+    view_set_context(tracker_view->view, tracker_view);
+    view_allocate_model(tracker_view->view, ViewModelTypeLocking, sizeof(TrackerViewModel));
+    view_set_draw_callback(tracker_view->view, draw_callback);
+    view_set_input_callback(tracker_view->view, input_callback);
+
+    return tracker_view;
+}
+
+void tracker_view_free(TrackerView* tracker_view) {
+    furi_assert(tracker_view);
+    view_free(tracker_view->view);
+    free(tracker_view);
+}
+
+uint8_t my_value_index_bool(
+    const bool value,
+    const bool values[],
+    uint8_t
+        values_count) // why the fuck it gives unresolved symbol if I include it from toolbox???!!!
+{
+    uint8_t index = 0;
+
+    for(uint8_t i = 0; i < values_count; i++) {
+        if(value == values[i]) {
+            index = i;
+            break;
+        }
+    }
+
+    return index;
+}
+
+FlizzerTrackerApp* init_tracker(
+    uint32_t sample_rate,
+    uint8_t rate,
+    bool external_audio_output,
+    uint32_t audio_buffer_size) {
+    FlizzerTrackerApp* tracker = malloc(sizeof(FlizzerTrackerApp));
+    memset(tracker, 0, sizeof(FlizzerTrackerApp));
+
+    tracker->external_audio = external_audio_output;
+
+    sound_engine_init(
+        &tracker->sound_engine, sample_rate, external_audio_output, audio_buffer_size);
+    tracker_engine_init(&tracker->tracker_engine, rate, &tracker->sound_engine);
+
+    tracker->tracker_engine.song = &tracker->song;
+
+    tracker->current_note = MIDDLE_C;
+
+    // Очередь событий на 8 элементов размера FlizzerTrackerEvent
+    tracker->event_queue = furi_message_queue_alloc(8, sizeof(FlizzerTrackerEvent));
+
+    tracker->gui = furi_record_open(RECORD_GUI);
+    tracker->view_dispatcher = view_dispatcher_alloc();
+
+    tracker->tracker_view = tracker_view_alloc(tracker);
+
+    view_dispatcher_add_view(tracker->view_dispatcher, VIEW_TRACKER, tracker->tracker_view->view);
+    view_dispatcher_attach_to_gui(
+        tracker->view_dispatcher, tracker->gui, ViewDispatcherTypeFullscreen);
+
+    with_view_model(
+        tracker->tracker_view->view, TrackerViewModel * model, { model->tracker = tracker; }, true);
+
+    tracker->storage = furi_record_open(RECORD_STORAGE);
+    tracker->stream = file_stream_alloc(tracker->storage);
+
+    tracker->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        tracker->view_dispatcher, VIEW_KEYBOARD, text_input_get_view(tracker->text_input));
+
+    tracker->pattern_submenu = submenu_alloc();
+    tracker->pattern_copypaste_submenu = submenu_alloc();
+    tracker->instrument_submenu = submenu_alloc();
+
+    view_set_previous_callback(submenu_get_view(tracker->pattern_submenu), submenu_exit_callback);
+    view_set_previous_callback(
+        submenu_get_view(tracker->pattern_copypaste_submenu), submenu_exit_callback);
+    view_set_previous_callback(
+        submenu_get_view(tracker->instrument_submenu), submenu_exit_callback);
+
+    submenu_add_item(
+        tracker->pattern_submenu,
+        "Load song",
+        SUBMENU_PATTERN_LOAD_SONG,
+        submenu_callback,
+        tracker);
+    submenu_add_item(
+        tracker->pattern_submenu,
+        "Save song",
+        SUBMENU_PATTERN_SAVE_SONG,
+        submenu_callback,
+        tracker);
+    submenu_add_item(
+        tracker->pattern_submenu, "Settings", SUBMENU_PATTERN_SETTINGS, submenu_callback, tracker);
+    submenu_add_item(
+        tracker->pattern_submenu, "Help", SUBMENU_PATTERN_HELP, submenu_callback, tracker);
+    submenu_add_item(
+        tracker->pattern_submenu, "Exit", SUBMENU_PATTERN_EXIT, submenu_callback, tracker);
+
+    submenu_add_item(
+        tracker->instrument_submenu,
+        "Load instrument",
+        SUBMENU_INSTRUMENT_LOAD,
+        submenu_callback,
+        tracker);
+    submenu_add_item(
+        tracker->instrument_submenu,
+        "Save instrument",
+        SUBMENU_INSTRUMENT_SAVE,
+        submenu_callback,
+        tracker);
+    submenu_add_item(
+        tracker->instrument_submenu, "Exit", SUBMENU_INSTRUMENT_EXIT, submenu_callback, tracker);
+
+    submenu_add_item(
+        tracker->pattern_copypaste_submenu,
+        "Copy",
+        SUBMENU_PATTERN_COPYPASTE_COPY,
+        submenu_copypaste_callback,
+        tracker);
+    submenu_add_item(
+        tracker->pattern_copypaste_submenu,
+        "Paste",
+        SUBMENU_PATTERN_COPYPASTE_PASTE,
+        submenu_copypaste_callback,
+        tracker);
+    submenu_add_item(
+        tracker->pattern_copypaste_submenu,
+        "Cut",
+        SUBMENU_PATTERN_COPYPASTE_CUT,
+        submenu_copypaste_callback,
+        tracker);
+    submenu_add_item(
+        tracker->pattern_copypaste_submenu,
+        "Clear",
+        SUBMENU_PATTERN_COPYPASTE_CLEAR,
+        submenu_copypaste_callback,
+        tracker);
+
+    view_dispatcher_add_view(
+        tracker->view_dispatcher,
+        VIEW_SUBMENU_PATTERN,
+        submenu_get_view(tracker->pattern_submenu));
+    view_dispatcher_add_view(
+        tracker->view_dispatcher,
+        VIEW_SUBMENU_PATTERN_COPYPASTE,
+        submenu_get_view(tracker->pattern_copypaste_submenu));
+    view_dispatcher_add_view(
+        tracker->view_dispatcher,
+        VIEW_SUBMENU_INSTRUMENT,
+        submenu_get_view(tracker->instrument_submenu));
+
+    load_config(tracker);
+
+    tracker->settings_list = variable_item_list_alloc();
+    View* view = variable_item_list_get_view(tracker->settings_list);
+    view_set_previous_callback(view, submenu_settings_exit_callback);
+
+    VariableItem* item;
+    uint8_t value_index;
+
+    item = variable_item_list_add(
+        tracker->settings_list,
+        "Audio output",
+        AUDIO_MODES_COUNT,
+        audio_output_changed_callback,
+        tracker);
+    value_index =
+        my_value_index_bool(tracker->external_audio, audio_modes_values, AUDIO_MODES_COUNT);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, audio_modes_text[value_index]);
+
+    view_dispatcher_add_view(tracker->view_dispatcher, VIEW_SETTINGS, view);
+
+    tracker->overwrite_file_widget = widget_alloc();
+
+    widget_add_button_element(
+        tracker->overwrite_file_widget,
+        GuiButtonTypeLeft,
+        "No",
+        (ButtonCallback)overwrite_file_widget_no_input_callback,
+        tracker);
+    widget_add_button_element(
+        tracker->overwrite_file_widget,
+        GuiButtonTypeRight,
+        "Yes",
+        (ButtonCallback)overwrite_file_widget_yes_input_callback,
+        tracker);
+
+    widget_add_text_scroll_element(
+        tracker->overwrite_file_widget,
+        0,
+        0,
+        128,
+        64,
+        "This song file already exists,\n do you want to overwrite it?");
+
+    view_dispatcher_add_view(
+        tracker->view_dispatcher,
+        VIEW_FILE_OVERWRITE,
+        widget_get_view(tracker->overwrite_file_widget));
+
+    tracker->overwrite_instrument_file_widget = widget_alloc();
+
+    widget_add_button_element(
+        tracker->overwrite_instrument_file_widget,
+        GuiButtonTypeLeft,
+        "No",
+        (ButtonCallback)overwrite_instrument_file_widget_no_input_callback,
+        tracker);
+    widget_add_button_element(
+        tracker->overwrite_instrument_file_widget,
+        GuiButtonTypeRight,
+        "Yes",
+        (ButtonCallback)overwrite_instrument_file_widget_yes_input_callback,
+        tracker);
+
+    widget_add_text_scroll_element(
+        tracker->overwrite_instrument_file_widget,
+        0,
+        0,
+        128,
+        64,
+        "This instrument file already\nexists, do you want to\noverwrite it?");
+
+    view_dispatcher_add_view(
+        tracker->view_dispatcher,
+        VIEW_INSTRUMENT_FILE_OVERWRITE,
+        widget_get_view(tracker->overwrite_instrument_file_widget));
+
+    tracker->notification = furi_record_open(RECORD_NOTIFICATION);
+    notification_message(tracker->notification, &sequence_display_backlight_enforce_on);
+
+    set_default_song(tracker);
+
+    tracker->focus = EDIT_SONGINFO;
+    tracker->source_pattern_index = -1;
+
+    return tracker;
+}
+
+void deinit_tracker(FlizzerTrackerApp* tracker) {
+    notification_message(tracker->notification, &sequence_display_backlight_enforce_auto);
+    furi_record_close(RECORD_NOTIFICATION);
+
+    // Специальная очистка памяти, занимаемой очередью
+    furi_message_queue_free(tracker->event_queue);
+
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_SETTINGS);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_FILE_OVERWRITE);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_SUBMENU_INSTRUMENT);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_INSTRUMENT_FILE_OVERWRITE);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_SUBMENU_PATTERN_COPYPASTE);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_SUBMENU_PATTERN);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+    view_dispatcher_remove_view(tracker->view_dispatcher, VIEW_TRACKER);
+
+    text_input_free(tracker->text_input);
+
+    variable_item_list_free(tracker->settings_list);
+
+    submenu_free(tracker->pattern_submenu);
+    submenu_free(tracker->pattern_copypaste_submenu);
+    submenu_free(tracker->instrument_submenu);
+
+    widget_free(tracker->overwrite_file_widget);
+    widget_free(tracker->overwrite_instrument_file_widget);
+
+    view_dispatcher_free(tracker->view_dispatcher);
+
+    tracker_view_free(tracker->tracker_view);
+    furi_record_close(RECORD_GUI);
+
+    stream_free(tracker->stream);
+    furi_record_close(RECORD_STORAGE);
+
+    sound_engine_deinit(&tracker->sound_engine);
+
+    if(tracker->tracker_engine.song == NULL) {
+        tracker_engine_set_song(&tracker->tracker_engine, &tracker->song);
+    }
+
+    tracker_engine_deinit(&tracker->tracker_engine, false);
+
+    free(tracker);
+}

+ 14 - 0
flizzer_tracker/init_deinit.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "flizzer_tracker.h"
+#include "flizzer_tracker_hal.h"
+
+extern bool audio_modes_values[];
+extern char* audio_modes_text[];
+
+FlizzerTrackerApp* init_tracker(
+    uint32_t sample_rate,
+    uint8_t rate,
+    bool external_audio_output,
+    uint32_t audio_buffer_size);
+void deinit_tracker(FlizzerTrackerApp* tracker);

+ 536 - 0
flizzer_tracker/input/instrument.c

@@ -0,0 +1,536 @@
+#include "instrument.h"
+#include "songinfo.h"
+
+void edit_instrument_param(FlizzerTrackerApp* tracker, uint8_t selected_param, int8_t delta) {
+    if(!(tracker->current_digit)) {
+        delta *= 16;
+    }
+
+    Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+
+    switch(selected_param) {
+    case INST_CURRENTINSTRUMENT: {
+        int16_t inst = tracker->current_instrument;
+
+        int8_t inst_delta = delta > 0 ? 1 : -1;
+
+        inst += inst_delta;
+
+        clamp(inst, 0, 0, tracker->song.num_instruments);
+
+        if(check_and_allocate_instrument(&tracker->song, (uint8_t)inst)) {
+            tracker->current_instrument = inst;
+        }
+
+        break;
+    }
+
+    case INST_INSTRUMENTNAME: {
+        text_input_set_header_text(tracker->text_input, "Instrument name:");
+        text_input_set_result_callback(
+            tracker->text_input,
+            return_from_keyboard_callback,
+            tracker,
+            (char*)&inst->name,
+            MUS_INST_NAME_LEN + 1,
+            false);
+
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+        break;
+    }
+
+    case INST_CURRENT_NOTE: {
+        int8_t note_delta = 0;
+
+        if(delta < 0) {
+            if(tracker->current_digit) {
+                note_delta = -12;
+            }
+
+            else {
+                note_delta = -1;
+            }
+        }
+
+        if(delta > 0) {
+            if(tracker->current_digit) {
+                note_delta = 12;
+            }
+
+            else {
+                note_delta = 1;
+            }
+        }
+
+        clamp(inst->base_note, note_delta, 0, MAX_NOTE);
+
+        break;
+    }
+
+    case INST_FINETUNE: {
+        int8_t fine_delta = 0;
+
+        if(delta < 0) {
+            if(tracker->current_digit) {
+                fine_delta = -1;
+            }
+
+            else {
+                fine_delta = -10;
+            }
+        }
+
+        if(delta > 0) {
+            if(tracker->current_digit) {
+                fine_delta = 1;
+            }
+
+            else {
+                fine_delta = 10;
+            }
+        }
+
+        inst->finetune += fine_delta;
+
+        break;
+    }
+
+    case INST_SLIDESPEED: {
+        if((int16_t)inst->slide_speed + (int16_t)delta >= 0 &&
+           (int16_t)inst->slide_speed + (int16_t)delta <= 0xff) {
+            inst->slide_speed += delta;
+        }
+
+        break;
+    }
+
+    case INST_SETPW: {
+        flipbit(inst->flags, TE_SET_PW);
+        break;
+    }
+
+    case INST_PW: {
+        if((int16_t)inst->pw + (int16_t)delta >= 0 && (int16_t)inst->pw + (int16_t)delta <= 0xff) {
+            inst->pw += delta;
+        }
+
+        break;
+    }
+
+    case INST_SETCUTOFF: {
+        flipbit(inst->flags, TE_SET_CUTOFF);
+        break;
+    }
+
+    case INST_WAVE_NOISE: {
+        flipbit(inst->waveform, SE_WAVEFORM_NOISE);
+        break;
+    }
+
+    case INST_WAVE_PULSE: {
+        flipbit(inst->waveform, SE_WAVEFORM_PULSE);
+        break;
+    }
+
+    case INST_WAVE_TRIANGLE: {
+        flipbit(inst->waveform, SE_WAVEFORM_TRIANGLE);
+        break;
+    }
+
+    case INST_WAVE_SAWTOOTH: {
+        flipbit(inst->waveform, SE_WAVEFORM_SAW);
+        break;
+    }
+
+    case INST_WAVE_NOISE_METAL: {
+        flipbit(inst->waveform, SE_WAVEFORM_NOISE_METAL);
+        break;
+    }
+
+    case INST_WAVE_SINE: {
+        flipbit(inst->waveform, SE_WAVEFORM_SINE);
+        break;
+    }
+
+    case INST_ATTACK: {
+        if((int16_t)inst->adsr.a + (int16_t)delta >= 0 &&
+           (int16_t)inst->adsr.a + (int16_t)delta <= 0xff) {
+            inst->adsr.a += delta;
+        }
+
+        break;
+    }
+
+    case INST_DECAY: {
+        if((int16_t)inst->adsr.d + (int16_t)delta >= 0 &&
+           (int16_t)inst->adsr.d + (int16_t)delta <= 0xff) {
+            inst->adsr.d += delta;
+        }
+
+        break;
+    }
+
+    case INST_SUSTAIN: {
+        if((int16_t)inst->adsr.s + (int16_t)delta >= 0 &&
+           (int16_t)inst->adsr.s + (int16_t)delta <= 0xff) {
+            inst->adsr.s += delta;
+        }
+
+        break;
+    }
+
+    case INST_RELEASE: {
+        if((int16_t)inst->adsr.r + (int16_t)delta >= 0 &&
+           (int16_t)inst->adsr.r + (int16_t)delta <= 0xff) {
+            inst->adsr.r += delta;
+        }
+
+        break;
+    }
+
+    case INST_VOLUME: {
+        if((int16_t)inst->adsr.volume + (int16_t)delta >= 0 &&
+           (int16_t)inst->adsr.volume + (int16_t)delta <= 0xff) {
+            inst->adsr.volume += delta;
+        }
+
+        break;
+    }
+
+    case INST_ENABLEFILTER: {
+        flipbit(inst->sound_engine_flags, SE_ENABLE_FILTER);
+        break;
+    }
+
+    case INST_FILTERCUTOFF: {
+        if((int16_t)inst->filter_cutoff + (int16_t)delta >= 0 &&
+           (int16_t)inst->filter_cutoff + (int16_t)delta <= 0xff) {
+            inst->filter_cutoff += delta;
+        }
+
+        break;
+    }
+
+    case INST_FILTERRESONANCE: {
+        if((int16_t)inst->filter_resonance + (int16_t)delta >= 0 &&
+           (int16_t)inst->filter_resonance + (int16_t)delta <= 0xff) {
+            inst->filter_resonance += delta;
+        }
+
+        break;
+    }
+
+    case INST_FILTERTYPE: {
+        int8_t flt_delta = (delta > 0 ? 1 : -1);
+
+        if((int16_t)inst->filter_type + (int16_t)flt_delta >= 0 &&
+           (int16_t)inst->filter_type + (int16_t)flt_delta < FIL_MODES) {
+            inst->filter_type += flt_delta;
+        }
+
+        break;
+    }
+
+    case INST_ENABLERINGMOD: {
+        flipbit(inst->sound_engine_flags, SE_ENABLE_RING_MOD);
+        break;
+    }
+
+    case INST_RINGMODSRC: {
+        if((int16_t)inst->ring_mod + (int16_t)delta >= 0 &&
+           (int16_t)inst->ring_mod + (int16_t)delta < SONG_MAX_CHANNELS) {
+            inst->ring_mod += delta;
+        }
+
+        if((int16_t)inst->ring_mod + (int16_t)delta < 0) {
+            inst->ring_mod = 0xff; // 0xff = self
+        }
+
+        if((int16_t)inst->ring_mod == 0xff && (int16_t)delta > 0) {
+            inst->ring_mod = 0;
+        }
+
+        break;
+    }
+
+    case INST_ENABLEHARDSYNC: {
+        flipbit(inst->sound_engine_flags, SE_ENABLE_HARD_SYNC);
+        break;
+    }
+
+    case INST_HARDSYNCSRC: {
+        if((int16_t)inst->hard_sync + (int16_t)delta >= 0 &&
+           (int16_t)inst->hard_sync + (int16_t)delta < SONG_MAX_CHANNELS) {
+            inst->hard_sync += delta;
+        }
+
+        if((int16_t)inst->hard_sync + (int16_t)delta < 0) {
+            inst->hard_sync = 0xff; // 0xff = self
+        }
+
+        if((int16_t)inst->hard_sync == 0xff && (int16_t)delta > 0) {
+            inst->hard_sync = 0;
+        }
+
+        break;
+    }
+
+    case INST_RETRIGGERONSLIDE: {
+        flipbit(inst->flags, TE_RETRIGGER_ON_SLIDE);
+        break;
+    }
+
+    case INST_ENABLEKEYSYNC: {
+        flipbit(inst->sound_engine_flags, SE_ENABLE_KEYDOWN_SYNC);
+        break;
+    }
+
+    case INST_ENABLEVIBRATO: {
+        flipbit(inst->flags, TE_ENABLE_VIBRATO);
+        break;
+    }
+
+    case INST_VIBRATOSPEED: {
+        if((int16_t)inst->vibrato_speed + (int16_t)delta >= 0 &&
+           (int16_t)inst->vibrato_speed + (int16_t)delta <= 0xff) {
+            inst->vibrato_speed += delta;
+        }
+
+        break;
+    }
+
+    case INST_VIBRATODEPTH: {
+        if((int16_t)inst->vibrato_depth + (int16_t)delta >= 0 &&
+           (int16_t)inst->vibrato_depth + (int16_t)delta <= 0xff) {
+            inst->vibrato_depth += delta;
+        }
+
+        break;
+    }
+
+    case INST_VIBRATODELAY: {
+        if((int16_t)inst->vibrato_delay + (int16_t)delta >= 0 &&
+           (int16_t)inst->vibrato_delay + (int16_t)delta <= 0xff) {
+            inst->vibrato_delay += delta;
+        }
+
+        break;
+    }
+
+    case INST_ENABLEPWM: {
+        flipbit(inst->flags, TE_ENABLE_PWM);
+        break;
+    }
+
+    case INST_PWMSPEED: {
+        if((int16_t)inst->pwm_speed + (int16_t)delta >= 0 &&
+           (int16_t)inst->pwm_speed + (int16_t)delta <= 0xff) {
+            inst->pwm_speed += delta;
+        }
+
+        break;
+    }
+
+    case INST_PWMDEPTH: {
+        if((int16_t)inst->pwm_depth + (int16_t)delta >= 0 &&
+           (int16_t)inst->pwm_depth + (int16_t)delta <= 0xff) {
+            inst->pwm_depth += delta;
+        }
+
+        break;
+    }
+
+    case INST_PWMDELAY: {
+        if((int16_t)inst->pwm_delay + (int16_t)delta >= 0 &&
+           (int16_t)inst->pwm_delay + (int16_t)delta <= 0xff) {
+            inst->pwm_delay += delta;
+        }
+
+        break;
+    }
+
+    case INST_PROGRESTART: {
+        flipbit(inst->flags, TE_PROG_NO_RESTART);
+        break;
+    }
+
+    case INST_PROGRAMEPERIOD: {
+        if((int16_t)inst->program_period + (int16_t)delta >= 0 &&
+           (int16_t)inst->program_period + (int16_t)delta <= 0xff) {
+            inst->program_period += delta;
+        }
+
+        break;
+    }
+    }
+}
+
+void instrument_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeShort &&
+       !tracker->tracker_engine.playing) {
+        tracker->editing = !(tracker->editing);
+        return;
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeLong && !tracker->editing) {
+        reset_buffer(&tracker->sound_engine);
+        tracker_engine_set_song(&tracker->tracker_engine, NULL);
+
+        for(int i = 1; i < SONG_MAX_CHANNELS; i++) {
+            tracker->tracker_engine.channel[i].channel_flags &= TEC_PLAYING;
+            tracker->tracker_engine.sound_engine->channel[i].frequency = 0;
+            tracker->tracker_engine.sound_engine->channel[i].waveform = 0;
+        }
+
+        Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+        tracker_engine_trigger_instrument_internal(
+            &tracker->tracker_engine, 0, inst, (MIDDLE_C << 8));
+        tracker->tracker_engine.playing = true;
+        play();
+        return;
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeRelease &&
+       !tracker->editing) {
+        SoundEngineChannel* se_channel = &tracker->sound_engine.channel[0];
+        sound_engine_enable_gate(&tracker->sound_engine, se_channel, false);
+        return;
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeShort) {
+        switch(tracker->selected_param) {
+        default: {
+            tracker->current_digit++;
+
+            if(tracker->current_digit > 1) {
+                tracker->selected_param++;
+
+                tracker->current_digit = 0;
+
+                if(tracker->selected_param > INST_PARAMS - 1) {
+                    tracker->selected_param = 0;
+                }
+            }
+
+            break;
+        }
+
+        case INST_CURRENTINSTRUMENT:
+        case INST_INSTRUMENTNAME:
+        case INST_SETPW:
+        case INST_SETCUTOFF:
+        case INST_WAVE_NOISE:
+        case INST_WAVE_PULSE:
+        case INST_WAVE_TRIANGLE:
+        case INST_WAVE_SAWTOOTH:
+        case INST_WAVE_NOISE_METAL:
+        case INST_WAVE_SINE:
+        case INST_ENABLEFILTER:
+        case INST_FILTERTYPE:
+        case INST_ENABLERINGMOD:
+        case INST_RINGMODSRC:
+        case INST_ENABLEHARDSYNC:
+        case INST_HARDSYNCSRC:
+        case INST_RETRIGGERONSLIDE:
+        case INST_ENABLEKEYSYNC:
+        case INST_ENABLEVIBRATO:
+        case INST_ENABLEPWM:
+        case INST_PROGRESTART: {
+            tracker->selected_param++;
+
+            tracker->current_digit = 1;
+
+            if(tracker->selected_param > INST_PARAMS - 1) {
+                tracker->selected_param = 0;
+            }
+
+            break;
+        }
+        }
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeShort) {
+        switch(tracker->selected_param) {
+        default: {
+            tracker->current_digit--;
+
+            if(tracker->current_digit > 1) // unsigned int overflow
+            {
+                tracker->selected_param--;
+
+                tracker->current_digit = 1;
+
+                if(tracker->selected_param > INST_PARAMS - 1) // unsigned int overflow
+                {
+                    tracker->selected_param = INST_PARAMS - 1;
+                }
+            }
+
+            break;
+        }
+
+        case INST_CURRENTINSTRUMENT:
+        case INST_INSTRUMENTNAME:
+        case INST_SETPW:
+        case INST_SETCUTOFF:
+        case INST_WAVE_NOISE:
+        case INST_WAVE_PULSE:
+        case INST_WAVE_TRIANGLE:
+        case INST_WAVE_SAWTOOTH:
+        case INST_WAVE_NOISE_METAL:
+        case INST_WAVE_SINE:
+        case INST_ENABLEFILTER:
+        case INST_FILTERTYPE:
+        case INST_ENABLERINGMOD:
+        case INST_RINGMODSRC:
+        case INST_ENABLEHARDSYNC:
+        case INST_HARDSYNCSRC:
+        case INST_RETRIGGERONSLIDE:
+        case INST_ENABLEKEYSYNC:
+        case INST_ENABLEVIBRATO:
+        case INST_ENABLEPWM:
+        case INST_PROGRESTART: {
+            tracker->selected_param--;
+
+            tracker->current_digit = 1;
+
+            if(tracker->selected_param > INST_PARAMS - 1) // unsigned int overflow
+            {
+                tracker->selected_param = INST_PARAMS - 1;
+            }
+
+            break;
+        }
+        }
+
+        return;
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeShort) {
+        if(tracker->editing) {
+            edit_instrument_param(tracker, tracker->selected_param, -1);
+        }
+
+        return;
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeShort) {
+        if(tracker->editing) {
+            edit_instrument_param(tracker, tracker->selected_param, 1);
+        }
+
+        return;
+    }
+
+    if(tracker->selected_param > INST_VIBRATODELAY) {
+        tracker->inst_editor_shift = 6;
+    }
+
+    if(tracker->selected_param > INST_PWMDELAY) {
+        tracker->inst_editor_shift = 12;
+    }
+
+    if(tracker->selected_param < INST_CURRENT_NOTE) {
+        tracker->inst_editor_shift = 0;
+    }
+}

+ 12 - 0
flizzer_tracker/input/instrument.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "../flizzer_tracker.h"
+#include "../sound_engine/sound_engine_defs.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "../util.h"
+
+void instrument_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);

+ 239 - 0
flizzer_tracker/input/instrument_program.c

@@ -0,0 +1,239 @@
+#include "instrument_program.h"
+#include "../macros.h"
+
+void instrument_program_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeShort) {
+        tracker->editing = !(tracker->editing);
+        return;
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeShort &&
+       tracker->editing) {
+        tracker->current_digit = my_min(2, tracker->current_digit + 1);
+        return;
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeLong && tracker->editing) {
+        Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+
+        if(tracker->current_program_step < INST_PROG_LEN - 1) {
+            if((inst->program[tracker->current_program_step] & 0x7fff) < TE_PROGRAM_LOOP_BEGIN &&
+               ((inst->program[tracker->current_program_step + 1] & 0x7fff) <
+                    TE_PROGRAM_LOOP_BEGIN ||
+                (inst->program[tracker->current_program_step + 1] & 0x7f00) ==
+                    TE_PROGRAM_LOOP_END)) // so we can unite with loop end as in klystrack
+            {
+                inst->program[tracker->current_program_step] ^= 0x8000; // flipping unite bit
+            }
+        }
+
+        return;
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeShort &&
+       tracker->editing) {
+        tracker->current_digit = fmax(0, (int16_t)tracker->current_digit - 1);
+        return;
+    }
+
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+       tracker->editing) {
+        Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+        inst->program[tracker->current_program_step] = TE_PROGRAM_NOP;
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            if((int16_t)tracker->current_program_step - 1 >= 0) {
+                tracker->current_program_step--;
+
+                if(tracker->program_position > tracker->current_program_step) {
+                    tracker->program_position = tracker->current_program_step;
+                }
+            }
+
+            else {
+                tracker->current_program_step = INST_PROG_LEN - 1;
+
+                tracker->program_position = INST_PROG_LEN - 1 - 7;
+            }
+        }
+
+        if(tracker->editing) {
+            Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+            uint16_t opcode = inst->program[tracker->current_program_step];
+
+            switch(tracker->current_digit) {
+            case 0: // MSB
+            {
+                uint8_t param = ((opcode & 0x7f00) >> 8);
+
+                if(param < 0xff) {
+                    param++;
+                }
+
+                if((inst->program[tracker->current_program_step] & 0x7fff) == TE_PROGRAM_NOP) {
+                    param = 0;
+                    inst->program[tracker->current_program_step] = 0;
+                }
+
+                param &= 0x7f;
+
+                inst->program[tracker->current_program_step] &= 0x80ff;
+                inst->program[tracker->current_program_step] |= ((uint16_t)param << 8);
+
+                break;
+            }
+
+            case 1: // upper digit of param, e.g. eXx
+            {
+                int8_t nibble = ((opcode & 0x00f0) >> 4);
+
+                if(nibble + 1 <= 0xf) {
+                    nibble++;
+                }
+
+                else {
+                    nibble = 0;
+                }
+
+                inst->program[tracker->current_program_step] &= 0xff0f;
+                inst->program[tracker->current_program_step] |= (nibble << 4);
+
+                break;
+            }
+
+            case 2: // lower digit of param, e.g. exX
+            {
+                int8_t nibble = (opcode & 0x000f);
+
+                if(nibble + 1 <= 0xf) {
+                    nibble++;
+                }
+
+                else {
+                    nibble = 0;
+                }
+
+                inst->program[tracker->current_program_step] &= 0xfff0;
+                inst->program[tracker->current_program_step] |= nibble;
+
+                break;
+            }
+
+            default:
+                break;
+            }
+        }
+
+        return;
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            if(tracker->current_program_step + 1 < INST_PROG_LEN) {
+                tracker->current_program_step++;
+
+                if(tracker->program_position < tracker->current_program_step - 7) {
+                    tracker->program_position = tracker->current_program_step - 7;
+                }
+            }
+
+            else {
+                tracker->current_program_step = 0;
+
+                tracker->program_position = 0;
+            }
+        }
+
+        if(tracker->editing) {
+            Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+            uint16_t opcode = inst->program[tracker->current_program_step];
+
+            switch(tracker->current_digit) {
+            case 0: // MSB
+            {
+                uint8_t param = ((opcode & 0x7f00) >> 8);
+
+                if(param < (TE_PROGRAM_JUMP >> 8) && param > 0) {
+                    param--;
+
+                    inst->program[tracker->current_program_step] &= 0x80ff;
+                    inst->program[tracker->current_program_step] |= ((uint16_t)param << 8);
+                }
+
+                if((inst->program[tracker->current_program_step] & 0x7f00) == TE_PROGRAM_JUMP &&
+                   (inst->program[tracker->current_program_step] & 0x7fff) != TE_PROGRAM_END &&
+                   (inst->program[tracker->current_program_step] & 0x7fff) != TE_PROGRAM_NOP) {
+                    inst->program[tracker->current_program_step] =
+                        TE_PROGRAM_LOOP_END |
+                        (inst->program[tracker->current_program_step] & 0x8000);
+                }
+
+                if((inst->program[tracker->current_program_step] & 0x7fff) == TE_PROGRAM_END) {
+                    // param = (TE_PROGRAM_JUMP >> 8);
+                    inst->program[tracker->current_program_step] =
+                        TE_PROGRAM_JUMP | (inst->program[tracker->current_program_step] & 0x8000);
+                }
+
+                if((inst->program[tracker->current_program_step] & 0x7fff) == TE_PROGRAM_NOP) {
+                    // param = (TE_PROGRAM_END >> 8);
+                    inst->program[tracker->current_program_step] =
+                        TE_PROGRAM_END | (inst->program[tracker->current_program_step] & 0x8000);
+                }
+
+                if((inst->program[tracker->current_program_step] & 0x7f00) ==
+                   (TE_PROGRAM_LOOP_BEGIN - 0x100)) {
+                    // param = (TE_PROGRAM_END >> 8);
+                    inst->program[tracker->current_program_step] =
+                        TE_EFFECT_TRIGGER_RELEASE |
+                        (inst->program[tracker->current_program_step] & 0x8000);
+                }
+
+                break;
+            }
+
+            case 1: // upper digit of param, e.g. eXx
+            {
+                int8_t nibble = ((opcode & 0x00f0) >> 4);
+
+                if(nibble - 1 >= 0) {
+                    nibble--;
+                }
+
+                else {
+                    nibble = 0xf;
+                }
+
+                inst->program[tracker->current_program_step] &= 0xff0f;
+                inst->program[tracker->current_program_step] |= (nibble << 4);
+
+                break;
+            }
+
+            case 2: // lower digit of param, e.g. exX
+            {
+                int8_t nibble = (opcode & 0x000f);
+
+                if(nibble - 1 >= 0) {
+                    nibble--;
+                }
+
+                else {
+                    nibble = 0xf;
+                }
+
+                inst->program[tracker->current_program_step] &= 0xfff0;
+                inst->program[tracker->current_program_step] |= nibble;
+
+                break;
+            }
+
+            default:
+                break;
+            }
+        }
+
+        return;
+    }
+}

+ 12 - 0
flizzer_tracker/input/instrument_program.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "../flizzer_tracker.h"
+#include "../sound_engine/sound_engine_defs.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "../util.h"
+
+void instrument_program_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);

+ 410 - 0
flizzer_tracker/input/pattern.c

@@ -0,0 +1,410 @@
+#include "pattern.h"
+
+uint8_t get_field(uint8_t patternx) {
+    uint8_t field = 0;
+
+    if(patternx <= 1) field = 0;
+    if(patternx == 2) field = 1;
+    if(patternx == 3) field = 2;
+    if(patternx > 3) field = 3;
+
+    return field;
+}
+
+void edit_note(
+    FlizzerTrackerApp* tracker,
+    TrackerSongPatternStep* step,
+    int8_t delta) // here we need data about last note if we place a new note
+{
+    int16_t note = tracker_engine_get_note(step);
+
+    if(note == MUS_NOTE_RELEASE) {
+        if(delta < 0) {
+            set_note(step, MUS_NOTE_CUT);
+        }
+
+        return;
+    }
+
+    if(note == MUS_NOTE_CUT) {
+        if(delta > 0) {
+            set_note(step, MUS_NOTE_RELEASE);
+        }
+
+        return;
+    }
+
+    if(note == MUS_NOTE_NONE) {
+        note =
+            tracker->current_note; // remember which note we entered earlier and use it as reference
+    }
+
+    clamp(note, delta, 0, MAX_NOTE);
+
+    set_note(step, (uint8_t)note);
+    set_instrument(step, tracker->current_instrument);
+
+    tracker->current_note = (uint8_t)note;
+}
+
+void edit_instrument(FlizzerTrackerApp* tracker, TrackerSongPatternStep* step, int8_t delta) {
+    int16_t inst = tracker_engine_get_instrument(step);
+
+    if(inst == MUS_NOTE_INSTRUMENT_NONE) {
+        if(delta > 0) {
+            inst = tracker->current_instrument;
+        }
+
+        else {
+            inst = MUS_NOTE_INSTRUMENT_NONE - 1;
+        }
+    }
+
+    clamp(inst, delta, 0, tracker->song.num_instruments - 1);
+    tracker->current_instrument = inst; // remember last instrument
+    set_instrument(step, (uint8_t)inst);
+}
+
+void edit_volume(FlizzerTrackerApp* tracker, TrackerSongPatternStep* step, int8_t delta) {
+    int16_t vol = tracker_engine_get_volume(step);
+
+    vol = tracker->current_volume;
+
+    if(vol + delta < 0) {
+        vol = MUS_NOTE_VOLUME_NONE - 1 - delta;
+    }
+
+    if(vol + delta >= MUS_NOTE_VOLUME_NONE) {
+        vol = 0 - delta;
+    }
+
+    clamp(vol, delta, 0, MUS_NOTE_VOLUME_NONE - 1);
+
+    set_volume(step, (uint8_t)vol);
+
+    tracker->current_volume = vol;
+}
+
+void edit_command(TrackerSongPatternStep* step, uint8_t digit, int8_t delta) {
+    int32_t command = tracker_engine_get_command(step);
+
+    switch(digit) {
+    case 0: // upper 7 bits
+    {
+        int16_t fx_name = ((command & 0x7f00) >> 8);
+
+        if(fx_name + delta > 35) // loop
+        { // 0-9 and then A-Z
+            fx_name = 0;
+        }
+
+        else if(fx_name + delta < 0) {
+            fx_name = 35;
+        }
+
+        else {
+            fx_name += delta;
+        }
+
+        command &= 0x00ff;
+
+        command |= (fx_name << 8);
+
+        set_command(step, (uint16_t)command);
+
+        break;
+    }
+
+    case 1: // upper digit of command param
+    {
+        int8_t upper_digit = ((command & 0x00f0) >> 4);
+
+        if(upper_digit + delta > 0xf) // loop
+        {
+            upper_digit = 0;
+        }
+
+        else if(upper_digit + delta < 0) {
+            upper_digit = 0xf;
+        }
+
+        else {
+            upper_digit += delta;
+        }
+
+        command &= 0xff0f;
+
+        command |= (upper_digit << 4);
+
+        set_command(step, (uint16_t)command);
+
+        break;
+    }
+
+    case 2: // lower digit of command param
+    {
+        int8_t lower_digit = (command & 0x000f);
+
+        if(lower_digit + delta > 0xf) // loop
+        {
+            lower_digit = 0;
+        }
+
+        else if(lower_digit + delta < 0) {
+            lower_digit = 0xf;
+        }
+
+        else {
+            lower_digit += delta;
+        }
+
+        command &= 0xfff0;
+
+        command |= lower_digit;
+
+        set_command(step, (uint16_t)command);
+
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void delete_field(TrackerSongPatternStep* step, uint8_t field) {
+    switch(field) {
+    case 0: // note
+    {
+        set_note(step, MUS_NOTE_NONE);
+        set_instrument(step, MUS_NOTE_INSTRUMENT_NONE); // also delete instrument
+        break;
+    }
+
+    case 1: // instrument
+    {
+        set_instrument(step, MUS_NOTE_INSTRUMENT_NONE);
+        break;
+    }
+
+    case 2: // volume
+    {
+        set_volume(step, MUS_NOTE_VOLUME_NONE);
+        break;
+    }
+
+    case 3: // command
+    {
+        set_command(step, 0);
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void edit_pattern_step(FlizzerTrackerApp* tracker, TrackerSongPatternStep* step, int8_t delta) {
+    switch(get_field(tracker->patternx)) {
+    case 0: // note
+    {
+        if(tracker->patternx) // editing octave
+        {
+            edit_note(tracker, step, 12 * delta);
+        }
+
+        else // editing note
+        {
+            edit_note(tracker, step, delta);
+        }
+
+        break;
+    }
+
+    case 1: // instrument
+    {
+        edit_instrument(tracker, step, delta);
+        break;
+    }
+
+    case 2: // volume
+    {
+        edit_volume(tracker, step, delta);
+        break;
+    }
+
+    case 3: // command
+    {
+        uint8_t digit = 0;
+        if(tracker->patternx == 4) digit = 0;
+        if(tracker->patternx == 5) digit = 1;
+        if(tracker->patternx == 6) digit = 2;
+        edit_command(step, digit, delta);
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void pattern_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeLong &&
+       !(tracker->editing)) {
+        flipbit(
+            tracker->tracker_engine.channel[tracker->current_channel].channel_flags, TEC_DISABLED);
+        return;
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeLong &&
+       !(tracker->editing)) {
+        tracker->tracker_engine.pattern_position =
+            tracker->tracker_engine.song->pattern_length - 1; // go to pattern last row
+        return;
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeLong &&
+       !(tracker->editing)) {
+        tracker->tracker_engine.pattern_position = 0; // return to pattern 1st row
+        return;
+    }
+
+    uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+    uint8_t current_pattern =
+        tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+            .pattern_indices[tracker->current_channel];
+    uint16_t pattern_step = tracker->tracker_engine.pattern_position;
+
+    uint16_t pattern_length = tracker->tracker_engine.song->pattern_length;
+
+    TrackerSongPattern* pattern = &tracker->tracker_engine.song->pattern[current_pattern];
+
+    TrackerSongPatternStep* step = NULL;
+
+    if(pattern_step < pattern_length) {
+        step = &pattern->step[pattern_step];
+    }
+
+    if(!(step)) return;
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeShort &&
+       !tracker->tracker_engine.playing) {
+        tracker->editing = !tracker->editing;
+
+        if(tracker->editing) {
+            // stop_song(tracker);
+        }
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeLong) {
+        if(!(tracker->editing)) {
+            if(tracker->tracker_engine.playing) {
+                stop_song(tracker);
+            }
+
+            else {
+                if(tracker->tracker_engine.pattern_position == tracker->song.pattern_length - 1 &&
+                   tracker->tracker_engine.sequence_position ==
+                       tracker->song.num_sequence_steps -
+                           1) // if we are at the very end of the song
+                {
+                    stop_song(tracker);
+                }
+
+                else {
+                    play_song(tracker, true);
+                }
+            }
+        }
+
+        else {
+            if(get_field(tracker->patternx) == 0) {
+                set_note(step, MUS_NOTE_RELEASE);
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeShort) {
+        tracker->patternx++;
+
+        if(tracker->patternx > MAX_PATTERNX - 1) {
+            tracker->current_channel++;
+
+            tracker->patternx = 0;
+
+            if(tracker->current_channel > SONG_MAX_CHANNELS - 1) {
+                tracker->current_channel = 0;
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeShort) {
+        tracker->patternx--;
+
+        if(tracker->patternx > MAX_PATTERNX - 1) // unsigned int overflow
+        {
+            tracker->current_channel--;
+
+            tracker->patternx = MAX_PATTERNX - 1;
+
+            if(tracker->current_channel > SONG_MAX_CHANNELS - 1) // unsigned int overflow
+            {
+                tracker->current_channel = SONG_MAX_CHANNELS - 1;
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            tracker->tracker_engine.pattern_position++;
+
+            if(tracker->tracker_engine.pattern_position >
+                   tracker->tracker_engine.song->pattern_length - 1 &&
+               tracker->tracker_engine.sequence_position <
+                   tracker->tracker_engine.song->num_sequence_steps - 1) {
+                tracker->tracker_engine.pattern_position = 0;
+                tracker->tracker_engine.sequence_position++;
+            }
+
+            else if(
+                tracker->tracker_engine.pattern_position >
+                tracker->tracker_engine.song->pattern_length - 1) {
+                tracker->tracker_engine.pattern_position =
+                    tracker->tracker_engine.song->pattern_length - 1;
+            }
+        }
+
+        if(tracker->editing) {
+            edit_pattern_step(tracker, step, -1);
+        }
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            int16_t temp_pattern_position = tracker->tracker_engine.pattern_position - 1;
+
+            if(temp_pattern_position < 0) {
+                if(tracker->tracker_engine.sequence_position > 0) {
+                    tracker->tracker_engine.sequence_position--;
+                    tracker->tracker_engine.pattern_position =
+                        tracker->tracker_engine.song->pattern_length - 1;
+                }
+            }
+
+            else {
+                tracker->tracker_engine.pattern_position--;
+            }
+        }
+
+        if(tracker->editing) {
+            edit_pattern_step(tracker, step, 1);
+        }
+    }
+
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+       tracker->editing) {
+        uint8_t field = get_field(tracker->patternx);
+
+        delete_field(step, field);
+    }
+}

+ 14 - 0
flizzer_tracker/input/pattern.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "../flizzer_tracker.h"
+#include "../sound_engine/sound_engine_defs.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "../util.h"
+
+#define MAX_PATTERNX (2 + 1 + 1 + 3)
+
+void pattern_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);

+ 209 - 0
flizzer_tracker/input/sequence.c

@@ -0,0 +1,209 @@
+#include "sequence.h"
+
+void delete_sequence_step(FlizzerTrackerApp* tracker) {
+    uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+    uint8_t* pattern = &tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+                            .pattern_indices[tracker->current_channel];
+    *pattern = 0;
+}
+
+void edit_sequence_step(FlizzerTrackerApp* tracker, int8_t delta) {
+    uint8_t digit = tracker->current_digit;
+
+    uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+    uint8_t pattern_index = tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+                                .pattern_indices[tracker->current_channel];
+
+    uint8_t* pattern = &tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+                            .pattern_indices[tracker->current_channel];
+    uint8_t temp_pattern = *pattern;
+
+    switch(digit) {
+    case 0: // upper nibble
+    {
+        int8_t nibble = ((pattern_index & 0xf0) >> 4);
+
+        if(nibble + delta < 0) {
+            nibble = 0xf;
+        }
+
+        else if(nibble + delta > 0xf) {
+            nibble = 0;
+        }
+
+        else {
+            nibble += delta;
+        }
+
+        temp_pattern &= 0x0f;
+        temp_pattern |= (nibble << 4);
+
+        break;
+    }
+
+    case 1: // lower nibble
+    {
+        int8_t nibble = (pattern_index & 0x0f);
+
+        if(nibble + delta < 0) {
+            nibble = 0xf;
+        }
+
+        else if(nibble + delta > 0xf) {
+            nibble = 0;
+        }
+
+        else {
+            nibble += delta;
+        }
+
+        temp_pattern &= 0xf0;
+        temp_pattern |= nibble;
+
+        break;
+    }
+    }
+
+    if(check_and_allocate_pattern(&tracker->song, temp_pattern)) {
+        *pattern = temp_pattern;
+    }
+}
+
+void sequence_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeShort &&
+       !tracker->tracker_engine.playing) {
+        tracker->editing = !tracker->editing;
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeLong) {
+        if(!(tracker->editing)) {
+            if(tracker->tracker_engine.playing) {
+                stop_song(tracker);
+            }
+
+            else {
+                if(tracker->tracker_engine.pattern_position == tracker->song.pattern_length - 1 &&
+                   tracker->tracker_engine.sequence_position ==
+                       tracker->song.num_sequence_steps -
+                           1) // if we are at the very end of the song
+                {
+                    stop_song(tracker);
+                }
+
+                else {
+                    play_song(tracker, true);
+                }
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeShort) {
+        tracker->current_digit++;
+
+        if(tracker->current_digit > 1) {
+            tracker->current_channel++;
+
+            tracker->current_digit = 0;
+
+            if(tracker->current_channel > SONG_MAX_CHANNELS - 1) {
+                tracker->current_channel = 0;
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeShort) {
+        tracker->current_digit--;
+
+        if(tracker->current_digit > 1) // unsigned int overflow
+        {
+            tracker->current_channel--;
+
+            tracker->current_digit = 1;
+
+            if(tracker->current_channel > SONG_MAX_CHANNELS - 1) // unsigned int overflow
+            {
+                tracker->current_channel = SONG_MAX_CHANNELS - 1;
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            tracker->tracker_engine.sequence_position++;
+
+            if(tracker->tracker_engine.sequence_position >=
+               tracker->tracker_engine.song->num_sequence_steps) {
+                tracker->tracker_engine.sequence_position = 0;
+            }
+        }
+
+        if(tracker->editing) {
+            edit_sequence_step(tracker, -1);
+        }
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeShort) {
+        if(!(tracker->editing)) {
+            int16_t temp_sequence_position = tracker->tracker_engine.sequence_position - 1;
+
+            if(temp_sequence_position < 0) {
+                tracker->tracker_engine.sequence_position =
+                    tracker->tracker_engine.song->num_sequence_steps - 1;
+            }
+
+            else {
+                tracker->tracker_engine.sequence_position--;
+            }
+        }
+
+        if(tracker->editing) {
+            edit_sequence_step(tracker, 1);
+        }
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeLong &&
+       !(tracker->editing)) // set loop begin or loop end for the song
+    {
+        TrackerSong* song = &tracker->song;
+
+        if(song->loop_start == song->loop_end && song->loop_end == 0) // if both are 0
+        {
+            song->loop_end = tracker->tracker_engine.sequence_position;
+        }
+
+        else {
+            if(tracker->tracker_engine.sequence_position < song->loop_end) {
+                song->loop_start = tracker->tracker_engine.sequence_position;
+            }
+
+            if(tracker->tracker_engine.sequence_position > song->loop_start) {
+                song->loop_end = tracker->tracker_engine.sequence_position;
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeLong &&
+       !(tracker->editing)) // erase loop begin and loop end points
+    {
+        TrackerSong* song = &tracker->song;
+
+        song->loop_start = song->loop_end = 0;
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeLong &&
+       !(tracker->editing)) // jump to the beginning
+    {
+        tracker->tracker_engine.sequence_position = 0;
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeLong &&
+       !(tracker->editing)) // jump to the end
+    {
+        tracker->tracker_engine.sequence_position = tracker->song.num_sequence_steps - 1;
+    }
+
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+       tracker->editing) {
+        delete_sequence_step(tracker);
+    }
+}

+ 12 - 0
flizzer_tracker/input/sequence.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "../flizzer_tracker.h"
+#include "../sound_engine/sound_engine_defs.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "../util.h"
+
+void sequence_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);

+ 224 - 0
flizzer_tracker/input/songinfo.c

@@ -0,0 +1,224 @@
+#include "songinfo.h"
+
+#include "../diskop.h"
+
+void edit_songinfo_param(FlizzerTrackerApp* tracker, uint8_t selected_param, int8_t delta) {
+    if(!(tracker->current_digit)) {
+        delta *= 16;
+    }
+
+    switch(selected_param) {
+    case SI_PATTERNPOS: {
+        uint16_t new_length = tracker->song.pattern_length;
+
+        if((int16_t)new_length + (int16_t)delta > 0 &&
+           (int16_t)new_length + (int16_t)delta <= 0x100) {
+            new_length += delta;
+            change_pattern_length(&tracker->song, new_length);
+
+            if(tracker->tracker_engine.pattern_position >= new_length) {
+                tracker->tracker_engine.pattern_position = new_length - 1;
+            }
+        }
+
+        break;
+    }
+
+    case SI_SEQUENCEPOS: {
+        if((int16_t)tracker->song.num_sequence_steps + (int16_t)delta > 0 &&
+           (int16_t)tracker->song.num_sequence_steps + (int16_t)delta <= 0x100) {
+            tracker->song.num_sequence_steps += delta;
+
+            if(tracker->tracker_engine.sequence_position >= tracker->song.num_sequence_steps) {
+                tracker->tracker_engine.sequence_position = tracker->song.num_sequence_steps - 1;
+            }
+        }
+
+        break;
+    }
+
+    case SI_SONGSPEED: {
+        if((int16_t)tracker->song.speed + (int16_t)delta > 1 &&
+           (int16_t)tracker->song.speed + (int16_t)delta <= 0xff) {
+            tracker->song.speed += delta;
+        }
+
+        break;
+    }
+
+    case SI_SONGRATE: {
+        if((int16_t)tracker->song.rate + (int16_t)delta > 1 &&
+           (int16_t)tracker->song.rate + (int16_t)delta <= 0xff) {
+            tracker->song.rate += delta;
+        }
+
+        break;
+    }
+
+    case SI_MASTERVOL: {
+        if((int16_t)tracker->tracker_engine.master_volume + (int16_t)delta > 0 &&
+           (int16_t)tracker->tracker_engine.master_volume + (int16_t)delta <= 0xff) {
+            tracker->tracker_engine.master_volume += delta;
+        }
+
+        break;
+    }
+
+    case SI_SONGNAME: {
+        text_input_set_header_text(tracker->text_input, "Song name:");
+        text_input_set_result_callback(
+            tracker->text_input,
+            return_from_keyboard_callback,
+            tracker,
+            (char*)&tracker->song.song_name,
+            MUS_SONG_NAME_LEN + 1,
+            false);
+
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+        break;
+    }
+
+    case SI_CURRENTINSTRUMENT: {
+        int16_t inst = tracker->current_instrument;
+
+        int8_t inst_delta = delta > 0 ? 1 : -1;
+
+        inst += inst_delta;
+
+        clamp(inst, 0, 0, tracker->song.num_instruments - 1);
+
+        tracker->current_instrument = inst;
+
+        break;
+    }
+
+    case SI_INSTRUMENTNAME: {
+        text_input_set_header_text(tracker->text_input, "Instrument name:");
+        text_input_set_result_callback(
+            tracker->text_input,
+            return_from_keyboard_callback,
+            tracker,
+            (char*)&tracker->song.instrument[tracker->current_instrument]->name,
+            MUS_INST_NAME_LEN + 1,
+            false);
+
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void songinfo_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeShort &&
+       !tracker->tracker_engine.playing) {
+        tracker->editing = !tracker->editing;
+    }
+
+    if(event->input.key == InputKeyOk && event->input.type == InputTypeLong) {
+        if(!(tracker->editing)) {
+            if(tracker->tracker_engine.playing) {
+                stop_song(tracker);
+            }
+
+            else {
+                if(tracker->tracker_engine.pattern_position == tracker->song.pattern_length - 1 &&
+                   tracker->tracker_engine.sequence_position ==
+                       tracker->song.num_sequence_steps -
+                           1) // if we are at the very end of the song
+                {
+                    stop_song(tracker);
+                }
+
+                else {
+                    play_song(tracker, true);
+                }
+            }
+        }
+    }
+
+    if(event->input.key == InputKeyRight && event->input.type == InputTypeShort) {
+        switch(tracker->selected_param) {
+        default: {
+            tracker->current_digit++;
+
+            if(tracker->current_digit > 1) {
+                tracker->selected_param++;
+
+                tracker->current_digit = 0;
+
+                if(tracker->selected_param > SI_PARAMS - 1) {
+                    tracker->selected_param = 0;
+                }
+            }
+
+            break;
+        }
+
+        case SI_CURRENTINSTRUMENT:
+        case SI_SONGNAME:
+        case SI_INSTRUMENTNAME: {
+            tracker->selected_param++;
+
+            tracker->current_digit = 0;
+
+            if(tracker->selected_param > SI_PARAMS - 1) {
+                tracker->selected_param = 0;
+            }
+
+            break;
+        }
+        }
+    }
+
+    if(event->input.key == InputKeyLeft && event->input.type == InputTypeShort) {
+        switch(tracker->selected_param) {
+        default: {
+            tracker->current_digit--;
+
+            if(tracker->current_digit > 1) // unsigned int overflow
+            {
+                tracker->selected_param--;
+
+                tracker->current_digit = 1;
+
+                if(tracker->selected_param > SI_PARAMS - 1) // unsigned int overflow
+                {
+                    tracker->selected_param = SI_PARAMS - 1;
+                }
+            }
+
+            break;
+        }
+
+        case SI_CURRENTINSTRUMENT:
+        case SI_SONGNAME:
+        case SI_INSTRUMENTNAME: {
+            tracker->selected_param--;
+
+            tracker->current_digit = 0;
+
+            if(tracker->selected_param > SI_PARAMS - 1) // unsigned int overflow
+            {
+                tracker->selected_param = SI_PARAMS - 1;
+            }
+
+            break;
+        }
+        }
+    }
+
+    if(event->input.key == InputKeyDown && event->input.type == InputTypeShort) {
+        if(tracker->editing) {
+            edit_songinfo_param(tracker, tracker->selected_param, -1);
+        }
+    }
+
+    if(event->input.key == InputKeyUp && event->input.type == InputTypeShort) {
+        if(tracker->editing) {
+            edit_songinfo_param(tracker, tracker->selected_param, 1);
+        }
+    }
+}

+ 14 - 0
flizzer_tracker/input/songinfo.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <ctype.h>
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "../flizzer_tracker.h"
+#include "../sound_engine/sound_engine_defs.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "../util.h"
+
+void songinfo_edit_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);
+void return_from_keyboard_callback(void* ctx);

+ 501 - 0
flizzer_tracker/input_event.c

@@ -0,0 +1,501 @@
+#include "input_event.h"
+
+#include "diskop.h"
+
+#define AUDIO_MODES_COUNT 2
+
+void return_from_keyboard_callback(void* ctx) {
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)ctx;
+
+    if(!tracker->is_loading && !tracker->is_saving && !tracker->is_loading_instrument &&
+       !tracker->is_saving_instrument) {
+        uint8_t string_length = 0;
+        char* string = NULL;
+
+        if(tracker->focus == EDIT_SONGINFO && tracker->mode == PATTERN_VIEW) {
+            switch(tracker->selected_param) {
+            case SI_SONGNAME: {
+                string_length = MUS_SONG_NAME_LEN;
+                string = (char*)&tracker->song.song_name;
+                break;
+            }
+
+            case SI_INSTRUMENTNAME: {
+                string_length = MUS_INST_NAME_LEN;
+                string = (char*)&tracker->song.instrument[tracker->current_instrument]->name;
+                break;
+            }
+            }
+        }
+
+        if(tracker->focus == EDIT_INSTRUMENT && tracker->mode == INST_EDITOR_VIEW) {
+            switch(tracker->selected_param) {
+            case INST_INSTRUMENTNAME: {
+                string_length = MUS_INST_NAME_LEN;
+                string = (char*)&tracker->song.instrument[tracker->current_instrument]->name;
+                break;
+            }
+            }
+        }
+
+        if(string == NULL || string_length == 0) return;
+
+        for(uint8_t i = 0; i < string_length;
+            i++) // I tinyfied the font by deleting lowercase chars, and I don't like the lowercase chars of any 3x5 pixels font
+        {
+            string[i] = toupper(string[i]);
+        }
+    }
+
+    view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+
+    if(tracker->is_saving) {
+        stop_song(tracker);
+
+        tracker->filepath = furi_string_alloc();
+        furi_string_cat_printf(
+            tracker->filepath, "%s/%s%s", FLIZZER_TRACKER_FOLDER, tracker->filename, SONG_FILE_EXT);
+
+        if(storage_file_exists(tracker->storage, furi_string_get_cstr(tracker->filepath))) {
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_FILE_OVERWRITE);
+            return;
+        }
+
+        else {
+            FlizzerTrackerEvent event = {.type = EventTypeSaveSong, .input = {{0}}, .period = 0};
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+        }
+    }
+
+    if(tracker->is_saving_instrument) {
+        stop_song(tracker);
+
+        tracker->filepath = furi_string_alloc();
+        furi_string_cat_printf(
+            tracker->filepath,
+            "%s/%s%s",
+            FLIZZER_TRACKER_INSTRUMENTS_FOLDER,
+            tracker->filename,
+            INST_FILE_EXT);
+
+        if(storage_file_exists(tracker->storage, furi_string_get_cstr(tracker->filepath))) {
+            view_dispatcher_switch_to_view(
+                tracker->view_dispatcher, VIEW_INSTRUMENT_FILE_OVERWRITE);
+            return;
+        }
+
+        else {
+            FlizzerTrackerEvent event = {
+                .type = EventTypeSaveInstrument, .input = {{0}}, .period = 0};
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+        }
+    }
+}
+
+void overwrite_file_widget_yes_input_callback(GuiButtonType result, InputType type, void* ctx) {
+    UNUSED(result);
+
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)ctx;
+
+    if(type == InputTypeShort) {
+        tracker->is_saving = true;
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+        // save_song(tracker, tracker->filepath);
+        static FlizzerTrackerEvent event = {
+            .type = EventTypeSaveSong, .input = {{0}}, .period = 0};
+        furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+    }
+}
+
+void overwrite_file_widget_no_input_callback(GuiButtonType result, InputType type, void* ctx) {
+    UNUSED(result);
+
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)ctx;
+
+    if(type == InputTypeShort) {
+        tracker->is_saving = false;
+        furi_string_free(tracker->filepath);
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+    }
+}
+
+void overwrite_instrument_file_widget_yes_input_callback(
+    GuiButtonType result,
+    InputType type,
+    void* ctx) {
+    UNUSED(result);
+
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)ctx;
+
+    if(type == InputTypeShort) {
+        tracker->is_saving_instrument = true;
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+        // save_song(tracker, tracker->filepath);
+        static FlizzerTrackerEvent event = {
+            .type = EventTypeSaveInstrument, .input = {{0}}, .period = 0};
+        furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+    }
+}
+
+void overwrite_instrument_file_widget_no_input_callback(
+    GuiButtonType result,
+    InputType type,
+    void* ctx) {
+    UNUSED(result);
+
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)ctx;
+
+    if(type == InputTypeShort) {
+        tracker->is_saving_instrument = false;
+        furi_string_free(tracker->filepath);
+        view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+    }
+}
+
+uint32_t submenu_settings_exit_callback(void* context) {
+    UNUSED(context);
+    return VIEW_SUBMENU_PATTERN;
+}
+
+uint32_t submenu_exit_callback(void* context) {
+    UNUSED(context);
+    return VIEW_TRACKER;
+}
+
+void submenu_callback(void* context, uint32_t index) {
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)context;
+
+    switch(tracker->mode) {
+    case PATTERN_VIEW: {
+        switch(index) {
+        case SUBMENU_PATTERN_EXIT: {
+            tracker->quit = true;
+
+            static InputEvent inevent = {.sequence = 0, .key = InputKeyLeft, .type = InputTypeMAX};
+            FlizzerTrackerEvent event = {
+                .type = EventTypeInput,
+                .input = inevent,
+                .period =
+                    0}; // making an event so tracker does not wait for next keypress and exits immediately
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+            break;
+        }
+
+        case SUBMENU_PATTERN_HELP: {
+            tracker->showing_help = true;
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+            break;
+        }
+
+        case SUBMENU_PATTERN_SAVE_SONG: {
+            text_input_set_header_text(tracker->text_input, "Song filename:");
+            memset(&tracker->filename, 0, FILE_NAME_LEN);
+            text_input_set_result_callback(
+                tracker->text_input,
+                return_from_keyboard_callback,
+                tracker,
+                (char*)&tracker->filename,
+                FILE_NAME_LEN,
+                true);
+
+            tracker->is_saving = true;
+
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+            break;
+        }
+
+        case SUBMENU_PATTERN_LOAD_SONG: {
+            FlizzerTrackerEvent event = {.type = EventTypeLoadSong, .input = {{0}}, .period = 0};
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+            break;
+        }
+
+        case SUBMENU_PATTERN_SETTINGS: {
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_SETTINGS);
+            break;
+        }
+
+        default:
+            break;
+        }
+
+        break;
+    }
+
+    case INST_EDITOR_VIEW: {
+        switch(index) {
+        case SUBMENU_INSTRUMENT_EXIT: {
+            tracker->quit = true;
+
+            static InputEvent inevent = {.sequence = 0, .key = InputKeyLeft, .type = InputTypeMAX};
+            FlizzerTrackerEvent event = {
+                .type = EventTypeInput,
+                .input = inevent,
+                .period =
+                    0}; // making an event so tracker does not wait for next keypress and exits immediately
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+            break;
+        }
+
+        case SUBMENU_INSTRUMENT_SAVE: {
+            text_input_set_header_text(tracker->text_input, "Instrument filename:");
+            memset(&tracker->filename, 0, FILE_NAME_LEN);
+            text_input_set_result_callback(
+                tracker->text_input,
+                return_from_keyboard_callback,
+                tracker,
+                (char*)&tracker->filename,
+                FILE_NAME_LEN,
+                true);
+
+            tracker->is_saving_instrument = true;
+
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_KEYBOARD);
+            break;
+        }
+
+        case SUBMENU_INSTRUMENT_LOAD: {
+            FlizzerTrackerEvent event = {
+                .type = EventTypeLoadInstrument, .input = {{0}}, .period = 0};
+            furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+            break;
+        }
+
+        default:
+            break;
+        }
+
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void submenu_copypaste_callback(void* context, uint32_t index) {
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)context;
+
+    uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+    uint8_t current_pattern_index =
+        tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+            .pattern_indices[tracker->current_channel];
+
+    TrackerSongPattern* source_pattern;
+
+    if(tracker->source_pattern_index >= 0) {
+        source_pattern = &tracker->song.pattern[tracker->source_pattern_index];
+    }
+
+    TrackerSongPattern* current_pattern = &tracker->song.pattern[current_pattern_index];
+
+    uint16_t pattern_length = tracker->tracker_engine.song->pattern_length;
+
+    switch(index) {
+    case SUBMENU_PATTERN_COPYPASTE_COPY: {
+        tracker->source_pattern_index = current_pattern_index;
+        tracker->cut_pattern = false;
+        break;
+    }
+
+    case SUBMENU_PATTERN_COPYPASTE_PASTE: {
+        if(tracker->source_pattern_index >= 0) {
+            memcpy(
+                current_pattern->step,
+                source_pattern->step,
+                sizeof(TrackerSongPatternStep) * pattern_length);
+
+            if(tracker->cut_pattern) {
+                set_empty_pattern(source_pattern, pattern_length);
+                tracker->cut_pattern = false;
+            }
+        }
+        break;
+    }
+
+    case SUBMENU_PATTERN_COPYPASTE_CUT: {
+        tracker->source_pattern_index = current_pattern_index;
+        tracker->cut_pattern = true;
+        break;
+    }
+
+    case SUBMENU_PATTERN_COPYPASTE_CLEAR: {
+        set_empty_pattern(current_pattern, pattern_length);
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_TRACKER);
+}
+
+void audio_output_changed_callback(VariableItem* item) {
+    FlizzerTrackerApp* tracker = (FlizzerTrackerApp*)variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, audio_modes_text[(index > 1 ? 1 : index)]);
+
+    if(tracker) {
+        tracker->external_audio = (bool)index;
+
+        tracker->external_audio = audio_modes_values[(index > 1 ? 1 : index)];
+
+        // sound_engine_init(&tracker->sound_engine, tracker->sound_engine.sample_rate, tracker->external_audio, tracker->sound_engine.audio_buffer_size);
+        // sound_engine_init_hardware(tracker->sound_engine.sample_rate, tracker->external_audio, tracker->sound_engine.audio_buffer, tracker->sound_engine.audio_buffer_size);
+
+        FlizzerTrackerEvent event = {.type = EventTypeSetAudioMode, .input = {{0}}, .period = 0};
+        furi_message_queue_put(tracker->event_queue, &event, FuriWaitForever);
+
+        UNUSED(event);
+    }
+}
+
+void cycle_focus(FlizzerTrackerApp* tracker) {
+    switch(tracker->mode) {
+    case PATTERN_VIEW: {
+        tracker->focus++;
+
+        if(tracker->focus > EDIT_SONGINFO) {
+            tracker->focus = EDIT_PATTERN;
+        }
+
+        break;
+    }
+
+    case INST_EDITOR_VIEW: {
+        tracker->focus++;
+
+        if(tracker->focus > EDIT_PROGRAM) {
+            tracker->focus = EDIT_INSTRUMENT;
+
+            if(tracker->current_digit > 1) {
+                tracker->current_digit = 1;
+            }
+        }
+
+        break;
+    }
+
+    default:
+        break;
+    }
+}
+
+void cycle_view(FlizzerTrackerApp* tracker) {
+    if(tracker->mode == PATTERN_VIEW) {
+        tracker->mode = INST_EDITOR_VIEW;
+        tracker->focus = EDIT_INSTRUMENT;
+
+        tracker->selected_param = 0;
+        tracker->current_digit = 0;
+
+        return;
+    }
+
+    if(tracker->mode == INST_EDITOR_VIEW) {
+        tracker->mode = PATTERN_VIEW;
+        tracker->focus = EDIT_PATTERN;
+
+        if(tracker->tracker_engine.song == NULL) {
+            stop_song(tracker);
+            tracker_engine_set_song(&tracker->tracker_engine, &tracker->song);
+        }
+
+        tracker->selected_param = 0;
+        tracker->current_digit = 0;
+
+        return;
+    }
+}
+
+void process_input_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event) {
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+       tracker->showing_help) {
+        tracker->showing_help = false;
+        return;
+    }
+
+    if(tracker->showing_help || tracker->is_loading || tracker->is_saving ||
+       tracker->is_loading_instrument || tracker->is_saving_instrument)
+        return; //do not react until these are finished
+
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+       event->period > 0 && event->period < 300 && !(tracker->editing)) {
+        cycle_view(tracker);
+        stop_song(tracker);
+        return;
+    }
+
+    else if(
+        event->input.key == InputKeyBack && event->input.type == InputTypeShort &&
+        !(tracker->editing)) {
+        cycle_focus(tracker);
+        //stop_song(tracker);
+        return;
+    }
+
+    if(event->input.key == InputKeyBack && event->input.type == InputTypeLong) {
+        switch(tracker->mode) {
+        case PATTERN_VIEW: {
+            if(tracker->focus == EDIT_PATTERN) {
+                submenu_set_selected_item(
+                    tracker->pattern_copypaste_submenu, SUBMENU_PATTERN_COPYPASTE_COPY);
+                view_dispatcher_switch_to_view(
+                    tracker->view_dispatcher, VIEW_SUBMENU_PATTERN_COPYPASTE);
+            }
+
+            else {
+                submenu_set_selected_item(tracker->pattern_submenu, SUBMENU_PATTERN_LOAD_SONG);
+                view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_SUBMENU_PATTERN);
+            }
+            break;
+        }
+
+        case INST_EDITOR_VIEW: {
+            submenu_set_selected_item(tracker->instrument_submenu, SUBMENU_INSTRUMENT_LOAD);
+            view_dispatcher_switch_to_view(tracker->view_dispatcher, VIEW_SUBMENU_INSTRUMENT);
+            break;
+        }
+
+        default:
+            break;
+        }
+
+        return;
+    }
+
+    switch(tracker->focus) {
+    case EDIT_PATTERN: {
+        pattern_edit_event(tracker, event);
+        break;
+    }
+
+    case EDIT_SEQUENCE: {
+        sequence_edit_event(tracker, event);
+        break;
+    }
+
+    case EDIT_SONGINFO: {
+        songinfo_edit_event(tracker, event);
+        break;
+    }
+
+    case EDIT_INSTRUMENT: {
+        instrument_edit_event(tracker, event);
+        break;
+    }
+
+    case EDIT_PROGRAM: {
+        instrument_program_edit_event(tracker, event);
+        break;
+    }
+
+    default:
+        break;
+    }
+}

+ 40 - 0
flizzer_tracker/input_event.h

@@ -0,0 +1,40 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+#include <stdio.h>
+
+#include "flizzer_tracker.h"
+#include "sound_engine/sound_engine_defs.h"
+#include "tracker_engine/tracker_engine_defs.h"
+#include "util.h"
+
+#include "input/instrument.h"
+#include "input/instrument_program.h"
+#include "input/pattern.h"
+#include "input/sequence.h"
+#include "input/songinfo.h"
+
+extern bool audio_modes_values[];
+extern char* audio_modes_text[];
+
+void return_from_keyboard_callback(void* ctx);
+
+void overwrite_file_widget_yes_input_callback(GuiButtonType result, InputType type, void* ctx);
+void overwrite_file_widget_no_input_callback(GuiButtonType result, InputType type, void* ctx);
+
+void overwrite_instrument_file_widget_yes_input_callback(
+    GuiButtonType result,
+    InputType type,
+    void* ctx);
+void overwrite_instrument_file_widget_no_input_callback(
+    GuiButtonType result,
+    InputType type,
+    void* ctx);
+
+uint32_t submenu_exit_callback(void* context);
+uint32_t submenu_settings_exit_callback(void* context);
+void submenu_callback(void* context, uint32_t index);
+void submenu_copypaste_callback(void* context, uint32_t index);
+void audio_output_changed_callback(VariableItem* item);
+void process_input_event(FlizzerTrackerApp* tracker, FlizzerTrackerEvent* event);

+ 2 - 0
flizzer_tracker/macros.h

@@ -0,0 +1,2 @@
+#define my_min(a, b) (((a) < (b)) ? (a) : (b))
+#define my_max(a, b) (((a) > (b)) ? (a) : (b))

BIN
flizzer_tracker/screenshots/inst.png


BIN
flizzer_tracker/screenshots/pat.png


+ 36 - 0
flizzer_tracker/sound_engine/freqs.c

@@ -0,0 +1,36 @@
+#include "freqs.h"
+
+const uint32_t frequency_table[FREQ_TAB_SIZE] = {
+    (uint32_t)(2093.00 * 1024), // 7th octave, the highest in this tracker
+    (uint32_t)(2217.46 * 1024), // frequency precision is 1 / 1024th of Hz
+    (uint32_t)(2349.32 * 1024),
+    (uint32_t)(2489.02 * 1024),
+    (uint32_t)(2637.02 * 1024),
+    (uint32_t)(2793.83 * 1024),
+    (uint32_t)(2959.96 * 1024),
+    (uint32_t)(3135.96 * 1024),
+    (uint32_t)(3322.44 * 1024),
+    (uint32_t)(3520.00 * 1024),
+    (uint32_t)(3729.31 * 1024),
+    (uint32_t)(3951.07 * 1024),
+};
+
+uint32_t get_freq(uint16_t note) {
+    if(note >= ((FREQ_TAB_SIZE * 8) << 8)) {
+        return frequency_table[FREQ_TAB_SIZE - 1];
+    }
+
+    if((note & 0xff) == 0) {
+        return frequency_table[((note >> 8) % 12)] /
+               (2 << (((NUM_OCTAVES) - ((note >> 8) / 12)) - 2)); // wrap to one octave
+    }
+
+    else {
+        uint64_t f1 = frequency_table[((note >> 8) % 12)] /
+                      (uint64_t)(2 << (((NUM_OCTAVES) - ((note >> 8) / 12)) - 2));
+        uint64_t f2 = frequency_table[(((note >> 8) + 1) % 12)] /
+                      (uint64_t)(2 << (((NUM_OCTAVES) - (((note >> 8) + 1) / 12)) - 2));
+
+        return f1 + (uint64_t)((f2 - f1) * (uint64_t)(note & 0xff)) / (uint64_t)256;
+    }
+}

+ 11 - 0
flizzer_tracker/sound_engine/freqs.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <furi.h>
+#include <stdio.h>
+
+#define FREQ_TAB_SIZE 12 /* one octave */
+#define NUM_OCTAVES 8 /* 0-7th octaves */
+
+extern const uint32_t frequency_table[FREQ_TAB_SIZE];
+
+uint32_t get_freq(uint16_t note);

+ 203 - 0
flizzer_tracker/sound_engine/sound_engine.c

@@ -0,0 +1,203 @@
+#include "sound_engine.h"
+#include "../flizzer_tracker_hal.h"
+
+#include <furi_hal.h>
+
+#define PI 3.1415
+
+void sound_engine_init(
+    SoundEngine* sound_engine,
+    uint32_t sample_rate,
+    bool external_audio_output,
+    uint32_t audio_buffer_size) {
+    if(sound_engine->audio_buffer) {
+        free(sound_engine->audio_buffer);
+    }
+
+    if(sound_engine->sine_lut) {
+        free(sound_engine->sine_lut);
+    }
+
+    memset(sound_engine, 0, sizeof(SoundEngine));
+
+    sound_engine->audio_buffer = malloc(audio_buffer_size * sizeof(sound_engine->audio_buffer[0]));
+    memset(sound_engine->audio_buffer, 0, sizeof(SoundEngine));
+    sound_engine->audio_buffer_size = audio_buffer_size;
+    sound_engine->sample_rate = sample_rate;
+    sound_engine->external_audio_output = external_audio_output;
+
+    for(int i = 0; i < NUM_CHANNELS; ++i) {
+        sound_engine->channel[i].lfsr = RANDOM_SEED;
+    }
+
+    for(int i = 0; i < SINE_LUT_SIZE; ++i) {
+        sound_engine->sine_lut[i] = (uint8_t)((sinf(i / 64.0 * PI) + 1.0) * 127.0);
+    }
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
+
+    furi_hal_interrupt_set_isr_ex(
+        FuriHalInterruptIdDma1Ch1, 15, sound_engine_dma_isr, sound_engine);
+
+    sound_engine_init_hardware(
+        sample_rate, external_audio_output, sound_engine->audio_buffer, audio_buffer_size);
+}
+
+void sound_engine_deinit(SoundEngine* sound_engine) {
+    free(sound_engine->audio_buffer);
+
+    if(!(sound_engine->external_audio_output)) {
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_speaker_release();
+        }
+    }
+
+    else {
+        furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+    }
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
+    sound_engine_stop();
+    sound_engine_deinit_timer();
+}
+
+void sound_engine_set_channel_frequency(
+    SoundEngine* sound_engine,
+    SoundEngineChannel* channel,
+    uint16_t note) {
+    uint32_t frequency = get_freq(note);
+
+    if(frequency != 0) {
+        channel->frequency = (uint64_t)(ACC_LENGTH) / (uint64_t)1024 * (uint64_t)(frequency) /
+                             (uint64_t)sound_engine->sample_rate;
+    }
+
+    else {
+        channel->frequency = 0;
+    }
+}
+
+void sound_engine_enable_gate(SoundEngine* sound_engine, SoundEngineChannel* channel, bool enable) {
+    if(enable) {
+        channel->adsr.envelope = 0;
+        channel->adsr.envelope_speed = envspd(sound_engine, channel->adsr.a);
+        channel->adsr.envelope_state = ATTACK;
+
+        channel->flags |= SE_ENABLE_GATE;
+
+        if(channel->flags & SE_ENABLE_KEYDOWN_SYNC) {
+            channel->accumulator = 0;
+        }
+    }
+
+    else {
+        channel->adsr.envelope_state = RELEASE;
+        channel->adsr.envelope_speed = envspd(sound_engine, channel->adsr.r);
+    }
+}
+
+void sound_engine_fill_buffer(
+    SoundEngine* sound_engine,
+    uint16_t* audio_buffer,
+    uint32_t audio_buffer_size) {
+    int32_t channel_output[NUM_CHANNELS];
+    int32_t channel_output_final[NUM_CHANNELS];
+
+    for(uint32_t i = 0; i < audio_buffer_size; ++i) {
+        int32_t output = WAVE_AMP * 2;
+
+        for(uint32_t chan = 0; chan < NUM_CHANNELS; ++chan) {
+            SoundEngineChannel* channel = &sound_engine->channel[chan];
+
+            if(channel->frequency > 0) {
+                uint32_t prev_acc = channel->accumulator;
+
+                channel->accumulator += channel->frequency;
+
+                channel->sync_bit |= (channel->accumulator & ACC_LENGTH);
+
+                channel->accumulator &= ACC_LENGTH - 1;
+
+                if(channel->flags & SE_ENABLE_HARD_SYNC) {
+                    uint8_t hard_sync_src = channel->hard_sync == 0xff ? i : channel->hard_sync;
+
+                    if(sound_engine->channel[hard_sync_src].sync_bit) {
+                        channel->accumulator = 0;
+                    }
+                }
+
+                channel_output[chan] =
+                    sound_engine_osc(sound_engine, channel, prev_acc) - WAVE_AMP / 2;
+
+                if(channel->flags & SE_ENABLE_RING_MOD) {
+                    uint8_t ring_mod_src = channel->ring_mod == 0xff ? i : channel->ring_mod;
+                    channel_output[chan] =
+                        channel_output[chan] * channel_output[ring_mod_src] / WAVE_AMP;
+                }
+
+                channel_output_final[chan] = sound_engine_cycle_and_output_adsr(
+                    channel_output[chan], sound_engine, &channel->adsr, &channel->flags);
+
+                if(channel->flags & SE_ENABLE_FILTER) {
+                    if(channel->filter_mode != 0) {
+                        sound_engine_filter_cycle(&channel->filter, channel_output_final[chan]);
+
+                        switch(channel->filter_mode) {
+                        case FIL_OUTPUT_LOWPASS: {
+                            channel_output_final[chan] =
+                                sound_engine_output_lowpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_HIGHPASS: {
+                            channel_output_final[chan] =
+                                sound_engine_output_highpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_BANDPASS: {
+                            channel_output_final[chan] =
+                                sound_engine_output_bandpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_LOW_HIGH: {
+                            channel_output_final[chan] =
+                                sound_engine_output_lowpass(&channel->filter) +
+                                sound_engine_output_highpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_HIGH_BAND: {
+                            channel_output_final[chan] =
+                                sound_engine_output_highpass(&channel->filter) +
+                                sound_engine_output_bandpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_LOW_BAND: {
+                            channel_output_final[chan] =
+                                sound_engine_output_lowpass(&channel->filter) +
+                                sound_engine_output_bandpass(&channel->filter);
+                            break;
+                        }
+
+                        case FIL_OUTPUT_LOW_HIGH_BAND: {
+                            channel_output_final[chan] =
+                                sound_engine_output_lowpass(&channel->filter) +
+                                sound_engine_output_highpass(&channel->filter) +
+                                sound_engine_output_bandpass(&channel->filter);
+                            break;
+                        }
+                        }
+                    }
+                }
+
+                output += channel_output_final[chan];
+            }
+        }
+
+        //audio_buffer[i] = output / (64 * 4);
+        audio_buffer[i] = output >> 8;
+    }
+}

+ 23 - 0
flizzer_tracker/sound_engine/sound_engine.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include "freqs.h"
+#include "sound_engine_adsr.h"
+#include "sound_engine_defs.h"
+#include "sound_engine_filter.h"
+#include "sound_engine_osc.h"
+
+void sound_engine_init(
+    SoundEngine* sound_engine,
+    uint32_t sample_rate,
+    bool external_audio_output,
+    uint32_t audio_buffer_size);
+void sound_engine_deinit(SoundEngine* sound_engine);
+void sound_engine_set_channel_frequency(
+    SoundEngine* sound_engine,
+    SoundEngineChannel* channel,
+    uint16_t note);
+void sound_engine_fill_buffer(
+    SoundEngine* sound_engine,
+    uint16_t* audio_buffer,
+    uint32_t audio_buffer_size);
+void sound_engine_enable_gate(SoundEngine* sound_engine, SoundEngineChannel* channel, bool enable);

+ 59 - 0
flizzer_tracker/sound_engine/sound_engine_adsr.c

@@ -0,0 +1,59 @@
+#include "sound_engine_adsr.h"
+
+int32_t sound_engine_cycle_and_output_adsr(
+    int32_t input,
+    SoundEngine* eng,
+    SoundEngineADSR* adsr,
+    uint16_t* flags) {
+    switch(adsr->envelope_state) {
+    case ATTACK: {
+        adsr->envelope += adsr->envelope_speed;
+
+        if(adsr->envelope >= MAX_ADSR) {
+            adsr->envelope_state = DECAY;
+            adsr->envelope = MAX_ADSR;
+
+            adsr->envelope_speed = envspd(eng, adsr->d);
+        }
+
+        break;
+    }
+
+    case DECAY: {
+        if(adsr->envelope > ((uint32_t)adsr->s << 17) + adsr->envelope_speed) {
+            adsr->envelope -= adsr->envelope_speed;
+        }
+
+        else {
+            adsr->envelope = (uint32_t)adsr->s << 17;
+            adsr->envelope_state = (adsr->s == 0) ? RELEASE : SUSTAIN;
+
+            adsr->envelope_speed = envspd(eng, adsr->r);
+        }
+
+        break;
+    }
+
+    case SUSTAIN:
+    case DONE: {
+        break;
+    }
+
+    case RELEASE: {
+        if(adsr->envelope > adsr->envelope_speed) {
+            adsr->envelope -= adsr->envelope_speed;
+        }
+
+        else {
+            adsr->envelope_state = DONE;
+            *flags &= ~SE_ENABLE_GATE;
+            adsr->envelope = 0;
+        }
+
+        break;
+    }
+    }
+
+    return (int32_t)((int32_t)input * (int32_t)(adsr->envelope >> 10) / (int32_t)(MAX_ADSR >> 10) *
+                     (int32_t)adsr->volume / (int32_t)MAX_ADSR_VOLUME);
+}

+ 9 - 0
flizzer_tracker/sound_engine/sound_engine_adsr.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "sound_engine_defs.h"
+
+int32_t sound_engine_cycle_and_output_adsr(
+    int32_t input,
+    SoundEngine* eng,
+    SoundEngineADSR* adsr,
+    uint16_t* flags);

+ 102 - 0
flizzer_tracker/sound_engine/sound_engine_defs.h

@@ -0,0 +1,102 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#define NUM_CHANNELS 4
+
+#define RANDOM_SEED 0xf31782ce
+
+#define ACC_BITS 23
+#define ACC_LENGTH (1 << (ACC_BITS - 1))
+
+#define OUTPUT_BITS 16
+#define WAVE_AMP (1 << OUTPUT_BITS)
+
+#define SINE_LUT_SIZE 256
+#define SINE_LUT_BITDEPTH 8
+
+#define MAX_ADSR (0xff << 17)
+#define MAX_ADSR_VOLUME 0x80
+#define BASE_FREQ 22050
+#define envspd(eng, slope)                                                                     \
+    ((slope) != 0 ?                                                                            \
+         (((uint64_t)MAX_ADSR / ((slope) * (slope)*256 / 8)) * BASE_FREQ / eng->sample_rate) : \
+         ((uint64_t)MAX_ADSR * BASE_FREQ / eng->sample_rate))
+
+typedef enum {
+    SE_WAVEFORM_NONE = 0,
+    SE_WAVEFORM_NOISE = 1,
+    SE_WAVEFORM_PULSE = 2,
+    SE_WAVEFORM_TRIANGLE = 4,
+    SE_WAVEFORM_SAW = 8,
+    SE_WAVEFORM_NOISE_METAL = 16,
+    SE_WAVEFORM_SINE = 32,
+} SoundEngineWaveformType;
+
+typedef enum {
+    SE_ENABLE_FILTER = 1,
+    SE_ENABLE_GATE = 2,
+    SE_ENABLE_RING_MOD = 4,
+    SE_ENABLE_HARD_SYNC = 8,
+    SE_ENABLE_KEYDOWN_SYNC = 16, // sync oscillators on keydown
+} SoundEngineFlags;
+
+typedef enum {
+    FIL_OUTPUT_LOWPASS = 1,
+    FIL_OUTPUT_HIGHPASS = 2,
+    FIL_OUTPUT_BANDPASS = 3,
+    FIL_OUTPUT_LOW_HIGH = 4,
+    FIL_OUTPUT_HIGH_BAND = 5,
+    FIL_OUTPUT_LOW_BAND = 6,
+    FIL_OUTPUT_LOW_HIGH_BAND = 7,
+    /* ============ */
+    FIL_MODES = 8,
+} SoundEngineFilterModes;
+
+typedef enum {
+    ATTACK = 1,
+    DECAY = 2,
+    SUSTAIN = 3,
+    RELEASE = 4,
+    DONE = 5,
+} SoundEngineEnvelopeStates;
+
+typedef struct {
+    uint8_t a, d, s, r, volume, envelope_state;
+    uint32_t envelope, envelope_speed;
+} SoundEngineADSR;
+
+typedef struct {
+    int32_t cutoff, resonance, low, high, band;
+} SoundEngineFilter;
+
+typedef struct {
+    uint32_t accumulator;
+    uint32_t frequency;
+    uint8_t waveform;
+    uint16_t pw;
+    uint32_t lfsr;
+    SoundEngineADSR adsr;
+
+    uint16_t flags;
+
+    uint8_t ring_mod, hard_sync; // 0xff = self
+    uint8_t sync_bit;
+
+    uint8_t filter_mode;
+
+    SoundEngineFilter filter;
+} SoundEngineChannel;
+
+typedef struct {
+    SoundEngineChannel channel[NUM_CHANNELS];
+    uint32_t sample_rate;
+    uint16_t* audio_buffer;
+    uint32_t audio_buffer_size;
+    bool external_audio_output;
+    uint8_t sine_lut[SINE_LUT_SIZE];
+
+    // uint32_t counter; //for debug
+} SoundEngine;

+ 28 - 0
flizzer_tracker/sound_engine/sound_engine_filter.c

@@ -0,0 +1,28 @@
+#include "sound_engine_filter.h"
+
+void sound_engine_filter_set_coeff(SoundEngineFilter* flt, uint32_t frequency, uint16_t resonance) {
+    flt->cutoff = (frequency << 5);
+    flt->resonance = ((int32_t)resonance * 11 / 6) - 200;
+}
+
+void sound_engine_filter_cycle(
+    SoundEngineFilter* flt,
+    int32_t input) // don't ask me how it works, stolen from Furnace tracker TSU synth
+{
+    input /= 8;
+    flt->low = flt->low + ((flt->cutoff * flt->band) >> 16);
+    flt->high = input - flt->low - (((256 - flt->resonance) * flt->band) >> 8);
+    flt->band = ((flt->cutoff * flt->high) >> 16) + flt->band;
+}
+
+int32_t sound_engine_output_lowpass(SoundEngineFilter* flt) {
+    return flt->low * 8;
+}
+
+int32_t sound_engine_output_highpass(SoundEngineFilter* flt) {
+    return flt->high * 8;
+}
+
+int32_t sound_engine_output_bandpass(SoundEngineFilter* flt) {
+    return flt->band * 8;
+}

+ 9 - 0
flizzer_tracker/sound_engine/sound_engine_filter.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "sound_engine_defs.h"
+
+void sound_engine_filter_set_coeff(SoundEngineFilter* flt, uint32_t frequency, uint16_t resonance);
+void sound_engine_filter_cycle(SoundEngineFilter* flt, int32_t input);
+int32_t sound_engine_output_lowpass(SoundEngineFilter* flt);
+int32_t sound_engine_output_highpass(SoundEngineFilter* flt);
+int32_t sound_engine_output_bandpass(SoundEngineFilter* flt);

+ 278 - 0
flizzer_tracker/sound_engine/sound_engine_osc.c

@@ -0,0 +1,278 @@
+#include "sound_engine_osc.h"
+
+static inline uint16_t sound_engine_pulse(uint32_t acc, uint32_t pw) // 0-FFF pulse width range
+{
+    return (
+        ((acc >> (((uint32_t)ACC_BITS - 17))) >= ((pw == 0xfff ? pw + 1 : pw) << 4) ?
+             (WAVE_AMP - 1) :
+             0));
+}
+
+static inline uint16_t sound_engine_saw(uint32_t acc) {
+    return (acc >> (ACC_BITS - OUTPUT_BITS - 1)) & (WAVE_AMP - 1);
+}
+
+uint16_t sound_engine_triangle(uint32_t acc) {
+    return (
+        (((acc & (ACC_LENGTH / 2)) ? ~acc : acc) >> (ACC_BITS - OUTPUT_BITS - 2)) &
+        (WAVE_AMP * 2 - 1));
+}
+
+static inline uint16_t sound_engine_sine(uint32_t acc, SoundEngine* sound_engine) {
+    return (
+        (uint16_t)sound_engine->sine_lut[(acc >> (ACC_BITS - SINE_LUT_BITDEPTH))]
+        << (OUTPUT_BITS - SINE_LUT_BITDEPTH));
+}
+
+inline static void shift_lfsr(uint32_t* v, uint32_t tap_0, uint32_t tap_1) {
+    typedef uint32_t T;
+    const T zero = (T)(0);
+    const T lsb = zero + (T)(1);
+    const T feedback = ((lsb << (tap_0)) ^ (lsb << (tap_1)));
+
+    *v = (*v >> 1) ^ ((zero - (*v & lsb)) & feedback);
+}
+
+static inline uint16_t sound_engine_noise(SoundEngineChannel* channel, uint32_t prev_acc) {
+    if((prev_acc & (ACC_LENGTH / 32)) != (channel->accumulator & (ACC_LENGTH / 32))) {
+        if(channel->waveform & SE_WAVEFORM_NOISE_METAL) {
+            shift_lfsr(&channel->lfsr, 14, 8);
+            channel->lfsr &= (1 << (14 + 1)) - 1;
+        }
+
+        else {
+            shift_lfsr(&channel->lfsr, 22, 17);
+            channel->lfsr &= (1 << (22 + 1)) - 1;
+        }
+    }
+
+    return (channel->lfsr) & (WAVE_AMP - 1);
+}
+
+uint16_t
+    sound_engine_osc(SoundEngine* sound_engine, SoundEngineChannel* channel, uint32_t prev_acc) {
+    switch(channel->waveform) {
+    case SE_WAVEFORM_NOISE:
+    case SE_WAVEFORM_NOISE_METAL:
+    case(SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_noise(channel, prev_acc);
+        break;
+    }
+
+    case SE_WAVEFORM_PULSE: {
+        return sound_engine_pulse(channel->accumulator, channel->pw);
+        break;
+    }
+
+    case SE_WAVEFORM_TRIANGLE: {
+        return sound_engine_triangle(channel->accumulator);
+        break;
+    }
+
+    case SE_WAVEFORM_SAW: {
+        return sound_engine_saw(channel->accumulator);
+        break;
+    }
+
+    case SE_WAVEFORM_SINE: {
+        return sound_engine_sine(channel->accumulator, sound_engine);
+        break;
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_triangle(channel->accumulator) & sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator);
+    }
+
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_noise(channel, prev_acc) & sound_engine_triangle(channel->accumulator);
+    }
+
+    case(SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_saw(channel->accumulator) & sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_saw(channel->accumulator);
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_saw(channel->accumulator) & sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW): {
+        return sound_engine_triangle(channel->accumulator) &
+               sound_engine_saw(channel->accumulator);
+    }
+
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_triangle(channel->accumulator) &
+               sound_engine_saw(channel->accumulator) & sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_saw(channel->accumulator);
+    }
+
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(
+        SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE |
+        SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_saw(channel->accumulator) & sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE): {
+        return sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE_METAL):
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_PULSE | SE_WAVEFORM_NOISE |
+        SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_SAW): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_saw(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE |
+        SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_saw(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE):
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE_METAL):
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW | SE_WAVEFORM_NOISE |
+        SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    case(SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine);
+    }
+
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW |
+        SE_WAVEFORM_NOISE):
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW |
+        SE_WAVEFORM_NOISE_METAL):
+    case(
+        SE_WAVEFORM_SINE | SE_WAVEFORM_PULSE | SE_WAVEFORM_TRIANGLE | SE_WAVEFORM_SAW |
+        SE_WAVEFORM_NOISE | SE_WAVEFORM_NOISE_METAL): {
+        return sound_engine_saw(channel->accumulator) &
+               sound_engine_pulse(channel->accumulator, channel->pw) &
+               sound_engine_triangle(channel->accumulator) &
+               sound_engine_sine(channel->accumulator, sound_engine) &
+               sound_engine_noise(channel, prev_acc);
+    }
+
+    default:
+        break;
+    }
+
+    return WAVE_AMP / 2;
+}

+ 8 - 0
flizzer_tracker/sound_engine/sound_engine_osc.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include "sound_engine_defs.h"
+
+uint16_t sound_engine_triangle(uint32_t acc);
+
+uint16_t
+    sound_engine_osc(SoundEngine* sound_engine, SoundEngineChannel* channel, uint32_t prev_acc);

+ 127 - 0
flizzer_tracker/tracker_engine/diskop.c

@@ -0,0 +1,127 @@
+#include "diskop.h"
+
+void load_instrument_inner(Stream* stream, Instrument* inst, uint8_t version) {
+    UNUSED(version);
+
+    size_t rwops = stream_read(stream, (uint8_t*)inst->name, sizeof(inst->name));
+    rwops = stream_read(stream, (uint8_t*)&inst->waveform, sizeof(inst->waveform));
+    rwops = stream_read(stream, (uint8_t*)&inst->flags, sizeof(inst->flags));
+    rwops =
+        stream_read(stream, (uint8_t*)&inst->sound_engine_flags, sizeof(inst->sound_engine_flags));
+
+    rwops = stream_read(stream, (uint8_t*)&inst->base_note, sizeof(inst->base_note));
+    rwops = stream_read(stream, (uint8_t*)&inst->finetune, sizeof(inst->finetune));
+
+    rwops = stream_read(stream, (uint8_t*)&inst->slide_speed, sizeof(inst->slide_speed));
+
+    rwops = stream_read(stream, (uint8_t*)&inst->adsr, sizeof(inst->adsr));
+    rwops = stream_read(stream, (uint8_t*)&inst->pw, sizeof(inst->pw));
+
+    if(inst->sound_engine_flags & SE_ENABLE_RING_MOD) {
+        rwops = stream_read(stream, (uint8_t*)&inst->ring_mod, sizeof(inst->ring_mod));
+    }
+
+    if(inst->sound_engine_flags & SE_ENABLE_HARD_SYNC) {
+        rwops = stream_read(stream, (uint8_t*)&inst->hard_sync, sizeof(inst->hard_sync));
+    }
+
+    uint8_t progsteps = 0;
+
+    rwops = stream_read(stream, (uint8_t*)&progsteps, sizeof(progsteps));
+
+    if(progsteps > 0) {
+        rwops = stream_read(stream, (uint8_t*)inst->program, progsteps * sizeof(inst->program[0]));
+    }
+
+    rwops = stream_read(stream, (uint8_t*)&inst->program_period, sizeof(inst->program_period));
+
+    if(inst->flags & TE_ENABLE_VIBRATO) {
+        rwops = stream_read(stream, (uint8_t*)&inst->vibrato_speed, sizeof(inst->vibrato_speed));
+        rwops = stream_read(stream, (uint8_t*)&inst->vibrato_depth, sizeof(inst->vibrato_depth));
+        rwops = stream_read(stream, (uint8_t*)&inst->vibrato_delay, sizeof(inst->vibrato_delay));
+    }
+
+    if(inst->flags & TE_ENABLE_PWM) {
+        rwops = stream_read(stream, (uint8_t*)&inst->pwm_speed, sizeof(inst->pwm_speed));
+        rwops = stream_read(stream, (uint8_t*)&inst->pwm_depth, sizeof(inst->pwm_depth));
+        rwops = stream_read(stream, (uint8_t*)&inst->pwm_delay, sizeof(inst->pwm_delay));
+    }
+
+    if(inst->sound_engine_flags & SE_ENABLE_FILTER) {
+        rwops = stream_read(stream, (uint8_t*)&inst->filter_cutoff, sizeof(inst->filter_cutoff));
+        rwops =
+            stream_read(stream, (uint8_t*)&inst->filter_resonance, sizeof(inst->filter_resonance));
+        rwops = stream_read(stream, (uint8_t*)&inst->filter_type, sizeof(inst->filter_type));
+    }
+
+    UNUSED(rwops);
+}
+
+bool load_song_inner(TrackerSong* song, Stream* stream) {
+    uint8_t version = 0;
+    size_t rwops = stream_read(stream, (uint8_t*)&version, sizeof(version));
+
+    if(version >
+       TRACKER_ENGINE_VERSION) // if song is of newer version this version of tracker engine can't support
+    {
+        return false;
+    }
+
+    tracker_engine_deinit_song(song, false);
+    memset(song, 0, sizeof(TrackerSong));
+
+    rwops = stream_read(stream, (uint8_t*)song->song_name, sizeof(song->song_name));
+    rwops = stream_read(stream, (uint8_t*)&song->loop_start, sizeof(song->loop_start));
+    rwops = stream_read(stream, (uint8_t*)&song->loop_end, sizeof(song->loop_end));
+    rwops = stream_read(stream, (uint8_t*)&song->pattern_length, sizeof(song->pattern_length));
+
+    rwops = stream_read(stream, (uint8_t*)&song->speed, sizeof(song->speed));
+    rwops = stream_read(stream, (uint8_t*)&song->rate, sizeof(song->rate));
+
+    rwops =
+        stream_read(stream, (uint8_t*)&song->num_sequence_steps, sizeof(song->num_sequence_steps));
+
+    for(uint16_t i = 0; i < song->num_sequence_steps; i++) {
+        rwops = stream_read(
+            stream,
+            (uint8_t*)&song->sequence.sequence_step[i],
+            sizeof(song->sequence.sequence_step[0]));
+    }
+
+    rwops = stream_read(stream, (uint8_t*)&song->num_patterns, sizeof(song->num_patterns));
+
+    for(uint16_t i = 0; i < song->num_patterns; i++) {
+        song->pattern[i].step = (TrackerSongPatternStep*)malloc(
+            sizeof(TrackerSongPatternStep) * (song->pattern_length));
+        set_empty_pattern(&song->pattern[i], song->pattern_length);
+        rwops = stream_read(
+            stream,
+            (uint8_t*)song->pattern[i].step,
+            sizeof(TrackerSongPatternStep) * (song->pattern_length));
+    }
+
+    rwops = stream_read(stream, (uint8_t*)&song->num_instruments, sizeof(song->num_instruments));
+
+    for(uint16_t i = 0; i < song->num_instruments; i++) {
+        song->instrument[i] = (Instrument*)malloc(sizeof(Instrument));
+        set_default_instrument(song->instrument[i]);
+        load_instrument_inner(stream, song->instrument[i], version);
+    }
+
+    UNUSED(rwops);
+    return false;
+}
+
+bool load_song(TrackerSong* song, Stream* stream) {
+    char header[sizeof(SONG_FILE_SIG) + 2] = {0};
+    size_t rwops = stream_read(stream, (uint8_t*)&header, sizeof(SONG_FILE_SIG) - 1);
+    header[sizeof(SONG_FILE_SIG)] = '\0';
+
+    if(strcmp(header, SONG_FILE_SIG) == 0) {
+        bool result = load_song_inner(song, stream);
+        UNUSED(result);
+    }
+
+    UNUSED(rwops);
+    return false;
+}

+ 12 - 0
flizzer_tracker/tracker_engine/diskop.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include "tracker_engine.h"
+#include "tracker_engine_defs.h"
+#include <stdbool.h>
+#include <stdio.h>
+#include <storage/storage.h>
+#include <toolbox/stream/file_stream.h>
+
+bool load_song(TrackerSong* song, Stream* stream);
+bool load_instrument(Instrument* inst, Stream* stream);
+void load_instrument_inner(Stream* stream, Instrument* inst, uint8_t version);

+ 466 - 0
flizzer_tracker/tracker_engine/do_effects.c

@@ -0,0 +1,466 @@
+#include "do_effects.h"
+#include <furi.h>
+
+#include "../sound_engine/sound_engine.h"
+#include "../sound_engine/sound_engine_filter.h"
+#include "tracker_engine.h"
+
+void do_command(
+    uint16_t opcode,
+    TrackerEngine* tracker_engine,
+    uint8_t channel,
+    uint8_t tick,
+    bool from_program) {
+    UNUSED(from_program);
+
+    TrackerEngineChannel* te_channel = &tracker_engine->channel[channel];
+    SoundEngineChannel* se_channel = &tracker_engine->sound_engine->channel[channel];
+
+    switch(opcode & 0x7f00) {
+    case TE_EFFECT_ARPEGGIO: {
+        if(tick == 0) {
+            if(te_channel->fixed_note != 0xffff) {
+                te_channel->note = te_channel->last_note;
+                te_channel->fixed_note = 0xffff;
+            }
+
+            if((opcode & 0xff) == 0xf0)
+                te_channel->arpeggio_note = te_channel->extarp1;
+            else if((opcode & 0xff) == 0xf1)
+                te_channel->arpeggio_note = te_channel->extarp2;
+            else
+                te_channel->arpeggio_note = (opcode & 0xff);
+        }
+        break;
+    }
+
+    case TE_EFFECT_PORTAMENTO_UP: {
+        uint32_t prev = te_channel->note;
+
+        te_channel->note += ((opcode & 0xff) << 2);
+        if(prev > te_channel->note) te_channel->note = 0xffff;
+
+        te_channel->target_note = te_channel->note;
+        break;
+    }
+
+    case TE_EFFECT_PORTAMENTO_DOWN: {
+        int32_t prev = te_channel->note;
+
+        te_channel->note -= ((opcode & 0xff) << 2);
+        if(prev < te_channel->note) te_channel->note = 0;
+
+        te_channel->target_note = te_channel->note;
+        break;
+    }
+
+    case TE_EFFECT_VIBRATO: {
+        if(tick == 0) {
+            if(opcode & 0xff) {
+                te_channel->flags |= TE_ENABLE_VIBRATO;
+
+                te_channel->vibrato_speed = (opcode & 0xf0);
+                te_channel->vibrato_depth = ((opcode & 0x0f) << 4);
+            }
+
+            else {
+                te_channel->flags &= ~(TE_ENABLE_VIBRATO);
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_PWM: {
+        if(tick == 0) {
+            if(opcode & 0xff) {
+                te_channel->flags |= TE_ENABLE_PWM;
+
+                te_channel->pwm_speed = (opcode & 0xf0);
+                te_channel->pwm_depth = ((opcode & 0x0f) << 4);
+            }
+
+            else {
+                te_channel->flags &= ~(TE_ENABLE_PWM);
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_PW: {
+        if(tick == 0) {
+            te_channel->pw = ((opcode & 0xff) << 4);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_PW_UP: {
+        int16_t temp_pw = te_channel->pw + (int16_t)(opcode & 0xff);
+
+        if(temp_pw < 0) temp_pw = 0;
+        if(temp_pw > 0xfff) temp_pw = 0xfff;
+
+        te_channel->pw = temp_pw;
+
+        break;
+    }
+
+    case TE_EFFECT_PW_DOWN: {
+        int16_t temp_pw = te_channel->pw - (int16_t)(opcode & 0xff);
+
+        if(temp_pw < 0) temp_pw = 0;
+        if(temp_pw > 0xfff) temp_pw = 0xfff;
+
+        te_channel->pw = temp_pw;
+
+        break;
+    }
+
+    case TE_EFFECT_SET_CUTOFF: {
+        if(tick == 0) {
+            te_channel->filter_cutoff = ((opcode & 0xff) << 3);
+            sound_engine_filter_set_coeff(
+                &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_VOLUME_FADE: {
+        if(!(te_channel->channel_flags & TEC_DISABLED)) {
+            te_channel->volume -= (opcode & 0xf);
+            if(te_channel->volume > MAX_ADSR_VOLUME) te_channel->volume = 0;
+            te_channel->volume += ((opcode >> 4) & 0xf);
+            if(te_channel->volume > MAX_ADSR_VOLUME) te_channel->volume = MAX_ADSR_VOLUME;
+
+            se_channel->adsr.volume = (int32_t)te_channel->volume;
+            se_channel->adsr.volume = (int32_t)se_channel->adsr.volume *
+                                      (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_WAVEFORM: {
+        if(tick == 0) {
+            se_channel->waveform = (opcode & 0x3f);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_VOLUME: {
+        if(tick == 0) {
+            if(!(te_channel->channel_flags & TEC_DISABLED)) {
+                te_channel->volume = opcode & 0xff;
+
+                se_channel->adsr.volume = (int32_t)te_channel->volume;
+                se_channel->adsr.volume = (int32_t)se_channel->adsr.volume *
+                                          (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_EXT: {
+        switch(opcode & 0x7ff0) {
+        case TE_EFFECT_EXT_TOGGLE_FILTER: {
+            if(tick == 0) {
+                if(opcode & 0xf) {
+                    se_channel->flags |= SE_ENABLE_FILTER;
+                }
+
+                else {
+                    se_channel->flags &= ~SE_ENABLE_FILTER;
+                }
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_PORTA_DN: {
+            if(tick == 0) {
+                int32_t prev = te_channel->note;
+
+                te_channel->note -= (opcode & 0xf);
+                if(prev < te_channel->note) te_channel->note = 0;
+
+                te_channel->target_note = te_channel->note;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_PORTA_UP: {
+            if(tick == 0) {
+                uint32_t prev = te_channel->note;
+
+                te_channel->note += (opcode & 0xf);
+                if(prev > te_channel->note) te_channel->note = 0xffff;
+
+                te_channel->target_note = te_channel->note;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_FILTER_MODE: {
+            if(tick == 0) {
+                se_channel->filter_mode = (opcode & 0xf);
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_RETRIGGER: {
+            if((opcode & 0xf) > 0 && (tick % (opcode & 0xf)) == 0) {
+                uint8_t prev_vol_tr = te_channel->volume;
+                uint8_t prev_vol_cyd = se_channel->adsr.volume;
+                tracker_engine_trigger_instrument_internal(
+                    tracker_engine, channel, te_channel->instrument, te_channel->last_note);
+                te_channel->volume = prev_vol_tr;
+                se_channel->adsr.volume = prev_vol_cyd;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_FINE_VOLUME_DOWN: {
+            if(tick == 0) {
+                te_channel->volume -= (opcode & 0xf);
+
+                if(te_channel->volume > MAX_ADSR_VOLUME) te_channel->volume = 0;
+
+                se_channel->adsr.volume = (int32_t)te_channel->volume;
+                se_channel->adsr.volume = (int32_t)se_channel->adsr.volume *
+                                          (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_FINE_VOLUME_UP: {
+            if(tick == 0) {
+                te_channel->volume += (opcode & 0xf);
+
+                if(te_channel->volume > MAX_ADSR_VOLUME) te_channel->volume = MAX_ADSR_VOLUME;
+
+                se_channel->adsr.volume = (int32_t)te_channel->volume;
+                se_channel->adsr.volume = (int32_t)se_channel->adsr.volume *
+                                          (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_NOTE_CUT: {
+            if((opcode & 0xf) <= tick) {
+                se_channel->adsr.volume = 0;
+                te_channel->volume = 0;
+            }
+
+            break;
+        }
+
+        case TE_EFFECT_EXT_PHASE_RESET: {
+            if(tick == (opcode & 0xf)) {
+                se_channel->accumulator = 0;
+                se_channel->lfsr = RANDOM_SEED;
+            }
+
+            break;
+        }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_SPEED_PROG_PERIOD: {
+        if(tick == 0) {
+            if(from_program) {
+                te_channel->program_period = opcode & 0xff;
+            }
+
+            else {
+                tracker_engine->song->speed = opcode & 0xff;
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_CUTOFF_UP: {
+        te_channel->filter_cutoff += (opcode & 0xff);
+
+        if(te_channel->filter_cutoff > 0x7ff) {
+            te_channel->filter_cutoff = 0x7ff;
+        }
+
+        sound_engine_filter_set_coeff(
+            &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+
+        break;
+    }
+
+    case TE_EFFECT_CUTOFF_DOWN: {
+        te_channel->filter_cutoff -= (opcode & 0xff);
+
+        if(te_channel->filter_cutoff > 0x7ff) // unsigned int overflow
+        {
+            te_channel->filter_cutoff = 0;
+        }
+
+        sound_engine_filter_set_coeff(
+            &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+
+        break;
+    }
+
+    case TE_EFFECT_SET_RESONANCE: {
+        if(tick == 0) {
+            te_channel->filter_resonance = (opcode & 0xff);
+            sound_engine_filter_set_coeff(
+                &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_RESONANCE_UP: {
+        te_channel->filter_resonance += (opcode & 0xff);
+
+        if(te_channel->filter_resonance > 0xff) {
+            te_channel->filter_resonance = 0xff;
+        }
+
+        sound_engine_filter_set_coeff(
+            &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+        break;
+    }
+
+    case TE_EFFECT_RESONANCE_DOWN: {
+        te_channel->filter_resonance -= (opcode & 0xff);
+
+        if(te_channel->filter_resonance > 0xff) {
+            te_channel->filter_resonance = 0;
+        }
+
+        sound_engine_filter_set_coeff(
+            &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+        break;
+    }
+
+    case TE_EFFECT_SET_RING_MOD_SRC: {
+        if(tick == 0) {
+            se_channel->ring_mod = (opcode & 0xff);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_HARD_SYNC_SRC: {
+        if(tick == 0) {
+            se_channel->hard_sync = (opcode & 0xff);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_ATTACK: {
+        if(tick == 0) {
+            se_channel->adsr.a = (opcode & 0xff);
+
+            if(se_channel->adsr.envelope_state == ATTACK) {
+                se_channel->adsr.envelope_speed =
+                    envspd(tracker_engine->sound_engine, se_channel->adsr.a);
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_DECAY: {
+        if(tick == 0) {
+            se_channel->adsr.d = (opcode & 0xff);
+
+            if(se_channel->adsr.envelope_state == DECAY) {
+                se_channel->adsr.envelope_speed =
+                    envspd(tracker_engine->sound_engine, se_channel->adsr.d);
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_SUSTAIN: {
+        if(tick == 0) {
+            se_channel->adsr.s = (opcode & 0xff);
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_SET_RELEASE: {
+        if(tick == 0) {
+            se_channel->adsr.r = (opcode & 0xff);
+
+            if(se_channel->adsr.envelope_state == RELEASE) {
+                se_channel->adsr.envelope_speed =
+                    envspd(tracker_engine->sound_engine, se_channel->adsr.r);
+            }
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_PROGRAM_RESTART: {
+        if(tick == 0) {
+            te_channel->program_counter = 0;
+            te_channel->program_loop = 0;
+            te_channel->program_period = 0;
+            te_channel->program_tick = 0;
+        }
+
+        break;
+    }
+
+    case TE_EFFECT_PORTA_UP_SEMITONE: {
+        uint32_t prev = te_channel->note;
+
+        te_channel->note += ((opcode & 0xff) << 8);
+        if(prev > te_channel->note) te_channel->note = 0xffff;
+
+        te_channel->target_note = te_channel->note;
+        break;
+    }
+
+    case TE_EFFECT_PORTA_DOWN_SEMITONE: {
+        int32_t prev = te_channel->note;
+
+        te_channel->note -= ((opcode & 0xff) << 8);
+        if(prev < te_channel->note) te_channel->note = 0;
+
+        te_channel->target_note = te_channel->note;
+        break;
+    }
+
+    case TE_EFFECT_ARPEGGIO_ABS: {
+        te_channel->arpeggio_note = 0;
+        te_channel->fixed_note = ((opcode & 0xff) << 8);
+
+        break;
+    }
+
+    case TE_EFFECT_TRIGGER_RELEASE: {
+        sound_engine_enable_gate(tracker_engine->sound_engine, se_channel, 0);
+
+        break;
+    }
+
+    default:
+        break;
+    }
+}

+ 10 - 0
flizzer_tracker/tracker_engine/do_effects.h

@@ -0,0 +1,10 @@
+#include "tracker_engine_defs.h"
+#include <stdbool.h>
+#include <stdio.h>
+
+void do_command(
+    uint16_t opcode,
+    TrackerEngine* tracker_engine,
+    uint8_t channel,
+    uint8_t tick,
+    bool from_program);

+ 696 - 0
flizzer_tracker/tracker_engine/tracker_engine.c

@@ -0,0 +1,696 @@
+#include "tracker_engine.h"
+
+#include "../flizzer_tracker_hal.h"
+#include "../macros.h"
+
+#include "../sound_engine/sound_engine_osc.h"
+#include <furi_hal.h>
+
+void tracker_engine_init(TrackerEngine* tracker_engine, uint8_t rate, SoundEngine* sound_engine) {
+    memset(tracker_engine, 0, sizeof(TrackerEngine));
+
+    furi_hal_interrupt_set_isr_ex(
+        FuriHalInterruptIdTIM2, 14, tracker_engine_timer_isr, (void*)tracker_engine);
+    tracker_engine_init_hardware(rate);
+
+    tracker_engine->sound_engine = sound_engine;
+    tracker_engine->rate = rate;
+}
+
+void tracker_engine_deinit_song(TrackerSong* song, bool free_song) {
+    for(int i = 0; i < MAX_PATTERNS; i++) {
+        if(song->pattern[i].step != NULL) {
+            free(song->pattern[i].step);
+        }
+    }
+
+    for(int i = 0; i < MAX_INSTRUMENTS; i++) {
+        if(song->instrument[i] != NULL) {
+            free(song->instrument[i]);
+        }
+    }
+
+    if(free_song) {
+        free(song);
+    }
+}
+
+void tracker_engine_deinit(TrackerEngine* tracker_engine, bool free_song) {
+    tracker_engine_deinit_song(tracker_engine->song, free_song);
+
+    furi_hal_interrupt_set_isr_ex(FuriHalInterruptIdTIM2, 13, NULL, NULL);
+    tracker_engine_stop();
+}
+
+void set_note(TrackerSongPatternStep* step, uint8_t note) {
+    step->note &= 0x80;
+    step->note |= (note & 0x7f);
+}
+
+void set_instrument(TrackerSongPatternStep* step, uint8_t inst) {
+    step->note &= 0x7f;
+    step->inst_vol &= 0x0f;
+
+    step->note |= ((inst & 0x10) << 3);
+    step->inst_vol |= ((inst & 0xf) << 4);
+}
+
+void set_volume(TrackerSongPatternStep* step, uint8_t vol) {
+    step->command &= 0x7fff;
+    step->inst_vol &= 0xf0;
+
+    step->command |= ((vol & 0x10) << 11);
+    step->inst_vol |= (vol & 0xf);
+}
+
+void set_command(TrackerSongPatternStep* step, uint16_t command) {
+    step->command &= 0x8000;
+    step->command |= command & (0x7fff);
+}
+
+void set_default_instrument(Instrument* inst) {
+    memset(inst, 0, sizeof(Instrument));
+
+    inst->flags = TE_SET_CUTOFF | TE_SET_PW | TE_ENABLE_VIBRATO;
+    inst->sound_engine_flags = SE_ENABLE_KEYDOWN_SYNC;
+
+    inst->base_note = MIDDLE_C;
+
+    inst->waveform = SE_WAVEFORM_PULSE;
+    inst->pw = 0x80;
+
+    inst->adsr.a = 0x4;
+    inst->adsr.d = 0x28;
+    inst->adsr.volume = 0x80;
+
+    inst->filter_type = FIL_OUTPUT_LOWPASS;
+    inst->filter_cutoff = 0xff;
+
+    inst->program_period = 1;
+
+    for(int i = 0; i < INST_PROG_LEN; i++) {
+        inst->program[i] = TE_PROGRAM_NOP;
+    }
+
+    inst->vibrato_speed = 0x60;
+    inst->vibrato_depth = 0x20;
+    inst->vibrato_delay = 0x20;
+}
+
+void set_empty_pattern(TrackerSongPattern* pattern, uint16_t pattern_length) {
+    for(uint16_t i = 0; i < pattern_length; i++) {
+        TrackerSongPatternStep* step = &pattern->step[i];
+
+        set_note(step, MUS_NOTE_NONE);
+        set_instrument(step, MUS_NOTE_INSTRUMENT_NONE);
+        set_volume(step, MUS_NOTE_VOLUME_NONE);
+        set_command(step, 0);
+    }
+}
+
+uint8_t tracker_engine_get_note(TrackerSongPatternStep* step) {
+    return (step->note & 0x7f);
+}
+
+uint8_t tracker_engine_get_instrument(TrackerSongPatternStep* step) {
+    return ((step->note & 0x80) >> 3) | ((step->inst_vol & 0xf0) >> 4);
+}
+
+uint8_t tracker_engine_get_volume(TrackerSongPatternStep* step) {
+    return (step->inst_vol & 0xf) | ((step->command & 0x8000) >> 11);
+}
+
+uint16_t tracker_engine_get_command(TrackerSongPatternStep* step) {
+    return (step->command & 0x7fff);
+}
+
+void tracker_engine_set_note(
+    TrackerEngine* tracker_engine,
+    uint8_t chan,
+    uint16_t note,
+    bool update_note) {
+    if(update_note) tracker_engine->channel[chan].note = note;
+
+    sound_engine_set_channel_frequency(
+        tracker_engine->sound_engine, &tracker_engine->sound_engine->channel[chan], note);
+}
+
+void tracker_engine_set_song(TrackerEngine* tracker_engine, TrackerSong* song) {
+    tracker_engine->song = song;
+}
+
+void tracker_engine_trigger_instrument_internal(
+    TrackerEngine* tracker_engine,
+    uint8_t chan,
+    Instrument* pinst,
+    uint16_t note) {
+    SoundEngineChannel* se_channel = &tracker_engine->sound_engine->channel[chan];
+    TrackerEngineChannel* te_channel = &tracker_engine->channel[chan];
+
+    te_channel->channel_flags = TEC_PLAYING | (te_channel->channel_flags & TEC_DISABLED);
+
+    te_channel->program_period = pinst->program_period;
+
+    if(!(pinst->flags & TE_PROG_NO_RESTART) && pinst->program_period > 0) {
+        te_channel->channel_flags |= TEC_PROGRAM_RUNNING;
+
+        te_channel->program_counter = 0;
+        te_channel->program_loop = 1;
+        te_channel->program_tick = 0;
+    }
+
+    te_channel->instrument = pinst;
+
+    se_channel->waveform = pinst->waveform;
+    se_channel->flags = pinst->sound_engine_flags;
+
+    te_channel->flags = pinst->flags;
+
+    te_channel->arpeggio_note = 0;
+    te_channel->fixed_note = 0xffff;
+
+    note += (uint16_t)(((int16_t)pinst->base_note - MIDDLE_C) << 8);
+    tracker_engine_set_note(tracker_engine, chan, note + (int16_t)pinst->finetune, true);
+
+    te_channel->last_note = te_channel->target_note = note + (int16_t)pinst->finetune;
+
+    te_channel->extarp1 = te_channel->extarp2 = 0;
+
+    if(pinst->flags & TE_ENABLE_VIBRATO) {
+        te_channel->vibrato_speed = pinst->vibrato_speed;
+        te_channel->vibrato_depth = pinst->vibrato_depth;
+        te_channel->vibrato_delay = pinst->vibrato_delay;
+    }
+
+    if(pinst->flags & TE_ENABLE_PWM) {
+        te_channel->pwm_speed = pinst->pwm_speed;
+        te_channel->pwm_depth = pinst->pwm_depth;
+        te_channel->pwm_delay = pinst->pwm_delay;
+    }
+
+    if(pinst->sound_engine_flags & SE_ENABLE_KEYDOWN_SYNC) {
+        te_channel->vibrato_position = ((ACC_LENGTH / 2 / 2) << 9);
+        te_channel->pwm_position = ((ACC_LENGTH / 2 / 2) << 9);
+
+        se_channel->accumulator = 0;
+        se_channel->lfsr = RANDOM_SEED;
+    }
+
+    if(pinst->flags & TE_SET_CUTOFF) {
+        te_channel->filter_cutoff = ((uint16_t)pinst->filter_cutoff << 3);
+        te_channel->filter_resonance = (uint16_t)pinst->filter_resonance;
+
+        se_channel->filter.low = 0;
+        se_channel->filter.high = 0;
+        se_channel->filter.band = 0;
+
+        sound_engine_filter_set_coeff(
+            &se_channel->filter, te_channel->filter_cutoff, te_channel->filter_resonance);
+    }
+
+    if(pinst->sound_engine_flags & SE_ENABLE_FILTER) {
+        te_channel->filter_type = pinst->filter_type;
+        se_channel->filter_mode = te_channel->filter_type;
+    }
+
+    if(pinst->flags & TE_SET_PW) {
+        te_channel->pw = (pinst->pw << 4);
+        se_channel->pw = (pinst->pw << 4);
+    }
+
+    se_channel->ring_mod = pinst->ring_mod;
+    se_channel->hard_sync = pinst->hard_sync;
+
+    te_channel->slide_speed = pinst->slide_speed;
+
+    se_channel->adsr.a = pinst->adsr.a;
+    se_channel->adsr.d = pinst->adsr.d;
+    se_channel->adsr.s = pinst->adsr.s;
+    se_channel->adsr.r = pinst->adsr.r;
+    se_channel->adsr.volume = pinst->adsr.volume;
+    se_channel->adsr.volume = (int32_t)se_channel->adsr.volume *
+                              (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+
+    te_channel->volume = pinst->adsr.volume;
+    te_channel->volume =
+        (int32_t)te_channel->volume * (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+
+    sound_engine_enable_gate(
+        tracker_engine->sound_engine, &tracker_engine->sound_engine->channel[chan], true);
+}
+
+void tracker_engine_execute_track_command(
+    TrackerEngine* tracker_engine,
+    uint8_t chan,
+    TrackerSongPatternStep* step,
+    bool first_tick) {
+    UNUSED(first_tick);
+    UNUSED(tracker_engine);
+    UNUSED(chan);
+
+    uint8_t vol = tracker_engine_get_volume(step);
+    uint16_t opcode = tracker_engine_get_command(step);
+
+    if(vol != MUS_NOTE_VOLUME_NONE &&
+       !(tracker_engine->channel[chan].channel_flags & TEC_DISABLED)) {
+        tracker_engine->sound_engine->channel[chan].adsr.volume =
+            (int32_t)tracker_engine->channel[chan].volume * (int32_t)vol / (MUS_NOTE_VOLUME_NONE);
+        // tracker_engine->sound_engine->channel[chan].adsr.volume = (int32_t)tracker_engine->sound_engine->channel[chan].adsr.volume * (int32_t)tracker_engine->channel[chan].instrument->adsr.volume / MAX_ADSR_VOLUME * (int32_t)tracker_engine->master_volume / MAX_ADSR_VOLUME;
+    }
+
+    if(tracker_engine->channel[chan].instrument != NULL && opcode != 0) {
+        if((opcode & 0x7f00) == TE_EFFECT_ARPEGGIO) {
+            tracker_engine->channel[chan].extarp1 = ((opcode & 0xf0) >> 4);
+            tracker_engine->channel[chan].extarp2 = (opcode & 0xf);
+        }
+
+        else {
+            do_command(opcode, tracker_engine, chan, tracker_engine->current_tick, false);
+        }
+    }
+
+    if(tracker_engine->channel[chan].channel_flags & TEC_DISABLED) {
+        tracker_engine->sound_engine->channel[chan].adsr.volume = 0;
+    }
+}
+
+void tracker_engine_execute_program_tick(
+    TrackerEngine* tracker_engine,
+    uint8_t chan,
+    uint8_t advance) {
+    TrackerEngineChannel* te_channel = &tracker_engine->channel[chan];
+    uint8_t tick = te_channel->program_tick;
+    uint8_t visited[INST_PROG_LEN] = {0};
+
+do_it_again:;
+
+    const uint16_t inst = te_channel->instrument->program[tick];
+
+    if((inst & 0x7fff) == TE_PROGRAM_END) {
+        te_channel->channel_flags &= ~(TEC_PROGRAM_RUNNING);
+        return;
+    }
+
+    uint8_t dont_reloop = 0;
+
+    if((inst & 0x7fff) != TE_PROGRAM_NOP) {
+        switch(inst & 0x7f00) {
+        case TE_PROGRAM_JUMP: {
+            if(!visited[tick]) {
+                visited[tick] = 1;
+                tick = inst & (INST_PROG_LEN - 1);
+            }
+
+            else
+                return;
+
+            break;
+        }
+
+        case TE_PROGRAM_LOOP_BEGIN:
+            break;
+
+        case TE_PROGRAM_LOOP_END: {
+            if(te_channel->program_loop == (inst & 0xff)) {
+                if(advance) te_channel->program_loop = 1;
+            }
+
+            else {
+                if(advance) ++te_channel->program_loop;
+
+                uint8_t l = 0;
+
+                while((te_channel->instrument->program[tick] & 0x7f00) != TE_PROGRAM_LOOP_BEGIN &&
+                      tick > 0) {
+                    --tick;
+                    if(!(te_channel->instrument->program[tick] & 0x8000)) ++l;
+                }
+
+                --tick;
+
+                dont_reloop = l <= 1;
+            }
+
+            break;
+        }
+
+        default: {
+            do_command(inst, tracker_engine, chan, te_channel->program_counter, true);
+            break;
+        }
+        }
+    }
+
+    if((inst & 0x7fff) == TE_PROGRAM_NOP || (inst & 0x7f00) != TE_PROGRAM_JUMP) {
+        ++tick;
+        if(tick >= INST_PROG_LEN) {
+            tick = 0;
+        }
+    }
+
+    // skip to next on msb
+
+    if(((inst & 0x8000) || ((inst & 0x7f00) == TE_PROGRAM_LOOP_BEGIN) ||
+        ((inst & 0x7f00) == TE_PROGRAM_JUMP)) &&
+       (inst & 0x7fff) != TE_PROGRAM_NOP && !dont_reloop) {
+        goto do_it_again;
+    }
+
+    if(advance) {
+        te_channel->program_tick = tick;
+    }
+}
+
+void tracker_engine_advance_channel(TrackerEngine* tracker_engine, uint8_t chan) {
+    SoundEngineChannel* se_channel = &tracker_engine->sound_engine->channel[chan];
+    TrackerEngineChannel* te_channel = &tracker_engine->channel[chan];
+
+    if(te_channel->channel_flags & TEC_PLAYING) {
+        if(!(se_channel->flags & SE_ENABLE_GATE)) {
+            te_channel->flags &= ~(TEC_PLAYING);
+        }
+
+        if(te_channel->slide_speed != 0) {
+            if(te_channel->target_note > te_channel->note) {
+                te_channel->note += my_min(
+                    te_channel->slide_speed * 4, te_channel->target_note - te_channel->note);
+            }
+
+            else if(te_channel->target_note < te_channel->note) {
+                te_channel->note -= my_min(
+                    te_channel->slide_speed * 4, te_channel->note - te_channel->target_note);
+            }
+        }
+
+        if(te_channel->channel_flags & TEC_PROGRAM_RUNNING) {
+            uint8_t u = (te_channel->program_counter + 1) >= te_channel->program_period;
+            tracker_engine_execute_program_tick(tracker_engine, chan, u);
+            ++te_channel->program_counter;
+
+            if(u) te_channel->program_counter = 0;
+        }
+
+        int16_t vib = 0;
+        int32_t pwm = 0;
+
+        if(te_channel->flags & TE_ENABLE_VIBRATO) {
+            if(te_channel->vibrato_delay > 0) {
+                te_channel->vibrato_delay--;
+            }
+
+            else {
+                te_channel->vibrato_position += ((uint32_t)te_channel->vibrato_speed << 21);
+                vib =
+                    (int32_t)(sound_engine_triangle(te_channel->vibrato_position >> 9) - WAVE_AMP / 2) *
+                    (int32_t)te_channel->vibrato_depth / (256 * 128);
+            }
+        }
+
+        if(te_channel->flags & TE_ENABLE_PWM) {
+            if(te_channel->pwm_delay > 0) {
+                te_channel->pwm_delay--;
+            }
+
+            else {
+                te_channel->pwm_position +=
+                    ((uint32_t)te_channel->pwm_speed
+                     << 20); // so minimum PWM speed is even lower than minimum vibrato speed
+                pwm = ((int32_t)sound_engine_triangle((te_channel->pwm_position) >> 9) -
+                       WAVE_AMP / 2) *
+                      (int32_t)te_channel->pwm_depth / (256 * 16);
+            }
+
+            int16_t final_pwm = (int16_t)tracker_engine->channel[chan].pw + pwm;
+
+            if(final_pwm < 0) {
+                final_pwm = 0;
+            }
+
+            if(final_pwm > 0xfff) {
+                final_pwm = 0xfff;
+            }
+
+            tracker_engine->sound_engine->channel[chan].pw = final_pwm;
+        }
+
+        else {
+            tracker_engine->sound_engine->channel[chan].pw = tracker_engine->channel[chan].pw;
+        }
+
+        int32_t chn_note =
+            (int16_t)(te_channel->fixed_note != 0xffff ? te_channel->fixed_note : te_channel->note) +
+            vib + ((int16_t)te_channel->arpeggio_note << 8);
+
+        if(chn_note < 0) {
+            chn_note = 0;
+        }
+
+        if(chn_note > ((12 * 7 + 11) << 8)) {
+            chn_note = ((12 * 7 + 11) << 8); // highest note is B-7
+        }
+
+        tracker_engine_set_note(tracker_engine, chan, (uint16_t)chn_note, false);
+    }
+
+    if(tracker_engine->channel[chan].channel_flags &
+       TEC_DISABLED) // so we can't set some non-zero volme from inst program too
+    {
+        tracker_engine->sound_engine->channel[chan].adsr.volume = 0;
+    }
+}
+
+void tracker_engine_advance_tick(TrackerEngine* tracker_engine) {
+    if(!(tracker_engine->playing)) return;
+
+    if(!(tracker_engine->sound_engine)) return;
+
+    TrackerSong* song = tracker_engine->song;
+
+    uint16_t opcode = 0;
+
+    for(uint8_t chan = 0; chan < SONG_MAX_CHANNELS; chan++) {
+        SoundEngineChannel* se_channel = &tracker_engine->sound_engine->channel[chan];
+        TrackerEngineChannel* te_channel = &tracker_engine->channel[chan];
+
+        if(tracker_engine->song) {
+            uint16_t sequence_position = tracker_engine->sequence_position;
+            uint8_t current_pattern =
+                song->sequence.sequence_step[sequence_position].pattern_indices[chan];
+            uint8_t pattern_step = tracker_engine->pattern_position;
+
+            TrackerSongPattern* pattern = &song->pattern[current_pattern];
+
+            uint8_t note_delay = 0;
+
+            opcode = tracker_engine_get_command(&pattern->step[pattern_step]);
+
+            if((opcode & 0x7ff0) == TE_EFFECT_EXT_NOTE_DELAY) {
+                note_delay = (opcode & 0xf);
+            }
+
+            if(tracker_engine->current_tick == note_delay) {
+                uint8_t note = tracker_engine_get_note(&pattern->step[pattern_step]);
+                uint8_t inst = tracker_engine_get_instrument(&pattern->step[pattern_step]);
+
+                Instrument* pinst = NULL;
+
+                if(inst == MUS_NOTE_INSTRUMENT_NONE) {
+                    pinst = te_channel->instrument;
+                }
+
+                else {
+                    if(inst < song->num_instruments) {
+                        pinst = song->instrument[inst];
+                        te_channel->instrument = pinst;
+                    }
+                }
+
+                if(note == MUS_NOTE_CUT) {
+                    sound_engine_enable_gate(tracker_engine->sound_engine, se_channel, 0);
+                    se_channel->adsr.volume = 0;
+                    te_channel->volume = 0;
+                }
+
+                if(note == MUS_NOTE_RELEASE) {
+                    sound_engine_enable_gate(tracker_engine->sound_engine, se_channel, 0);
+                }
+
+                else if(
+                    pinst && note != MUS_NOTE_RELEASE && note != MUS_NOTE_CUT &&
+                    note != MUS_NOTE_NONE) {
+                    uint8_t prev_adsr_volume = se_channel->adsr.volume;
+
+                    if((opcode & 0x7f00) == TE_EFFECT_SLIDE) {
+                        if(pinst->flags & TE_RETRIGGER_ON_SLIDE) {
+                            uint16_t temp_note = te_channel->note;
+                            tracker_engine_trigger_instrument_internal(
+                                tracker_engine, chan, pinst, note << 8);
+                            te_channel->note = temp_note;
+                        }
+
+                        te_channel->target_note =
+                            ((note + pinst->base_note - MIDDLE_C) << 8) + pinst->finetune;
+                        te_channel->slide_speed = (opcode & 0xff);
+                    }
+
+                    else if((opcode & 0x7f00) == TE_EFFECT_LEGATO) {
+                        te_channel->note = te_channel->target_note = te_channel->last_note =
+                            ((note + pinst->base_note - MIDDLE_C) << 8) + pinst->finetune;
+                    }
+
+                    else {
+                        tracker_engine_trigger_instrument_internal(
+                            tracker_engine, chan, pinst, note << 8);
+                        te_channel->note =
+                            ((note + pinst->base_note - MIDDLE_C) << 8) + pinst->finetune;
+
+                        te_channel->target_note =
+                            ((note + pinst->base_note - MIDDLE_C) << 8) + pinst->finetune;
+                    }
+
+                    if(inst == MUS_NOTE_INSTRUMENT_NONE) {
+                        se_channel->adsr.volume = prev_adsr_volume;
+                    }
+                }
+            }
+
+            tracker_engine_execute_track_command(
+                tracker_engine,
+                chan,
+                &pattern->step[pattern_step],
+                tracker_engine->current_tick == note_delay);
+        }
+
+        tracker_engine_advance_channel(
+            tracker_engine,
+            chan); // this will be executed even if the song pointer is NULL; handy for live instrument playback from inst editor ("jams")
+    }
+
+    if(tracker_engine->song) {
+        tracker_engine->current_tick++;
+
+        if(tracker_engine->current_tick >= song->speed) {
+            bool flag = true;
+
+            for(int chan = 0; chan < SONG_MAX_CHANNELS; ++chan) {
+                uint16_t sequence_position = tracker_engine->sequence_position;
+                uint8_t current_pattern =
+                    song->sequence.sequence_step[sequence_position].pattern_indices[chan];
+                uint8_t pattern_step = tracker_engine->pattern_position;
+
+                TrackerSongPattern* pattern = &song->pattern[current_pattern];
+
+                opcode = tracker_engine_get_command(&pattern->step[pattern_step]);
+
+                if((opcode & 0x7ff0) == TE_EFFECT_EXT_PATTERN_LOOP) {
+                    if(opcode & 0xf) // loop end
+                    {
+                        if(!(tracker_engine->in_loop)) {
+                            tracker_engine->loops_left = (opcode & 0xf);
+                            tracker_engine->in_loop = true;
+
+                            for(int j = tracker_engine->pattern_position; j >= 0; j--) {
+                                if(tracker_engine_get_command(&pattern->step[j]) ==
+                                   TE_EFFECT_EXT_PATTERN_LOOP) // search for loop start
+                                {
+                                    tracker_engine->pattern_position =
+                                        fmax((int16_t)j - 1, 0); // jump to loop start
+
+                                    goto out;
+                                }
+                            }
+                        }
+
+                        else {
+                            tracker_engine->loops_left--;
+
+                            if(tracker_engine->loops_left == 0) {
+                                tracker_engine->in_loop = false;
+                                goto out;
+                            }
+
+                            for(int j = tracker_engine->pattern_position; j >= 0; j--) {
+                                if(tracker_engine_get_command(&pattern->step[j]) ==
+                                   TE_EFFECT_EXT_PATTERN_LOOP) // search for loop start
+                                {
+                                    tracker_engine->pattern_position =
+                                        fmax((int16_t)j - 1, 0); // jump to loop start
+
+                                    goto out;
+                                }
+                            }
+                        }
+                    }
+
+                    else // loop start
+                    {
+                    }
+
+                out:;
+                }
+
+                if((opcode & 0x7f00) == TE_EFFECT_SKIP_PATTERN) {
+                    tracker_engine->sequence_position++;
+                    tracker_engine->pattern_position = 0;
+
+                    flag = false;
+
+                    if(tracker_engine->sequence_position >= song->num_sequence_steps) {
+                        tracker_engine->playing = false;
+                        tracker_engine->sequence_position--;
+                        tracker_engine->pattern_position = song->pattern_length - 1;
+
+                        for(int i = 0; i < SONG_MAX_CHANNELS; i++) {
+                            sound_engine_enable_gate(
+                                tracker_engine->sound_engine,
+                                &tracker_engine->sound_engine->channel[i],
+                                false);
+                        }
+
+                        goto end_process;
+                    }
+                }
+            }
+
+            if(flag) {
+                tracker_engine->pattern_position++;
+            }
+
+            tracker_engine->current_tick = 0;
+
+            if(tracker_engine->pattern_position >= song->pattern_length) {
+                tracker_engine->pattern_position = 0;
+
+                if(song->loop_start != 0 || song->loop_end != 0) {
+                    if(tracker_engine->sequence_position == song->loop_end) {
+                        tracker_engine->sequence_position =
+                            song->loop_start; // infinite loop between loop start and loop end
+                    }
+
+                    else {
+                        tracker_engine->sequence_position++;
+                    }
+                }
+
+                else {
+                    tracker_engine->sequence_position++;
+                }
+
+                if(tracker_engine->sequence_position >= song->num_sequence_steps) {
+                    tracker_engine->playing = false;
+                    tracker_engine->sequence_position--;
+                    tracker_engine->pattern_position = song->pattern_length - 1;
+
+                    for(int i = 0; i < SONG_MAX_CHANNELS; i++) {
+                        sound_engine_enable_gate(
+                            tracker_engine->sound_engine,
+                            &tracker_engine->sound_engine->channel[i],
+                            false);
+                    }
+                }
+            }
+        }
+    }
+
+end_process:;
+}

+ 28 - 0
flizzer_tracker/tracker_engine/tracker_engine.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include "do_effects.h"
+#include "tracker_engine_defs.h"
+
+void tracker_engine_init(TrackerEngine* tracker_engine, uint8_t rate, SoundEngine* sound_engine);
+void tracker_engine_deinit(TrackerEngine* tracker_engine, bool free_song);
+void tracker_engine_advance_tick(TrackerEngine* tracker_engine);
+void tracker_engine_set_song(TrackerEngine* tracker_engine, TrackerSong* song);
+void tracker_engine_deinit_song(TrackerSong* song, bool free_song);
+void tracker_engine_trigger_instrument_internal(
+    TrackerEngine* tracker_engine,
+    uint8_t chan,
+    Instrument* pinst,
+    uint16_t note);
+
+uint8_t tracker_engine_get_note(TrackerSongPatternStep* step);
+uint8_t tracker_engine_get_instrument(TrackerSongPatternStep* step);
+uint8_t tracker_engine_get_volume(TrackerSongPatternStep* step);
+uint16_t tracker_engine_get_command(TrackerSongPatternStep* step);
+
+void set_note(TrackerSongPatternStep* step, uint8_t note);
+void set_instrument(TrackerSongPatternStep* step, uint8_t inst);
+void set_volume(TrackerSongPatternStep* step, uint8_t vol);
+void set_command(TrackerSongPatternStep* step, uint16_t command);
+
+void set_default_instrument(Instrument* inst);
+void set_empty_pattern(TrackerSongPattern* pattern, uint16_t pattern_length);

+ 232 - 0
flizzer_tracker/tracker_engine/tracker_engine_defs.h

@@ -0,0 +1,232 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "../sound_engine/sound_engine_defs.h"
+
+#define INST_PROG_LEN 16
+#define MUS_SONG_NAME_LEN 16
+#define MUS_INST_NAME_LEN (MUS_SONG_NAME_LEN - 3)
+
+#define SONG_MAX_CHANNELS NUM_CHANNELS
+#define MAX_INSTRUMENTS 31
+#define MAX_PATTERN_LENGTH 256
+#define MAX_PATTERNS 256
+#define MAX_SEQUENCE_LENGTH 256
+
+#define MUS_NOTE_NONE 127
+#define MUS_NOTE_RELEASE 126
+#define MUS_NOTE_CUT 125
+
+#define MUS_NOTE_INSTRUMENT_NONE 31
+#define MUS_NOTE_VOLUME_NONE 31
+
+#define SONG_FILE_SIG "FZT!SONG"
+#define SONG_FILE_EXT ".fzt"
+#define INST_FILE_SIG "FZT!INST"
+#define INST_FILE_EXT ".fzi"
+
+#define TRACKER_ENGINE_VERSION 1
+
+#define MIDDLE_C (12 * 4)
+#define MAX_NOTE (12 * 7 + 11)
+
+typedef enum {
+    TE_ENABLE_VIBRATO = 1,
+    TE_ENABLE_PWM = 2,
+    TE_PROG_NO_RESTART = 4,
+    TE_SET_CUTOFF = 8,
+    TE_SET_PW = 16,
+    TE_RETRIGGER_ON_SLIDE = 32, // call trigger instrument function even if slide command is there
+} TrackerEngineFlags;
+
+typedef enum {
+    TEC_PLAYING = 1,
+    TEC_PROGRAM_RUNNING = 2,
+    TEC_DISABLED = 4,
+} TrackerEngineChannelFlags;
+
+typedef enum {
+    TE_EFFECT_ARPEGGIO = 0x0000,
+    TE_EFFECT_PORTAMENTO_UP = 0x0100,
+    TE_EFFECT_PORTAMENTO_DOWN = 0x0200,
+    TE_EFFECT_SLIDE = 0x0300,
+    TE_EFFECT_VIBRATO = 0x0400,
+    TE_EFFECT_PWM = 0x0500,
+    TE_EFFECT_SET_PW = 0x0600,
+    TE_EFFECT_PW_DOWN = 0x0700,
+    TE_EFFECT_PW_UP = 0x0800,
+    TE_EFFECT_SET_CUTOFF = 0x0900,
+    TE_EFFECT_VOLUME_FADE = 0x0a00,
+    TE_EFFECT_SET_WAVEFORM = 0x0b00,
+    TE_EFFECT_SET_VOLUME = 0x0c00,
+    TE_EFFECT_SKIP_PATTERN = 0x0d00,
+
+    TE_EFFECT_EXT = 0x0e00,
+    TE_EFFECT_EXT_TOGGLE_FILTER = 0x0e00,
+    TE_EFFECT_EXT_PORTA_UP = 0x0e10,
+    TE_EFFECT_EXT_PORTA_DN = 0x0e20,
+    TE_EFFECT_EXT_FILTER_MODE = 0x0e30,
+    TE_EFFECT_EXT_PATTERN_LOOP =
+        0x0e60, // e60 = start, e61-e6f = end and indication how many loops you want
+    TE_EFFECT_EXT_RETRIGGER = 0x0e90,
+    TE_EFFECT_EXT_FINE_VOLUME_DOWN = 0x0ea0,
+    TE_EFFECT_EXT_FINE_VOLUME_UP = 0x0eb0,
+    TE_EFFECT_EXT_NOTE_CUT = 0x0ec0,
+    TE_EFFECT_EXT_NOTE_DELAY = 0x0ed0,
+    TE_EFFECT_EXT_PHASE_RESET = 0x0ef0,
+
+    TE_EFFECT_SET_SPEED_PROG_PERIOD = 0x0f00,
+    TE_EFFECT_CUTOFF_UP = 0x1000, // Gxx
+    TE_EFFECT_CUTOFF_DOWN = 0x1100, // Hxx
+    TE_EFFECT_SET_RESONANCE = 0x1200, // Ixx
+    TE_EFFECT_RESONANCE_UP = 0x1300, // Jxx
+    TE_EFFECT_RESONANCE_DOWN = 0x1400, // Kxx
+
+    TE_EFFECT_SET_ATTACK = 0x1500, // Lxx
+    TE_EFFECT_SET_DECAY = 0x1600, // Mxx
+    TE_EFFECT_SET_SUSTAIN = 0x1700, // Nxx
+    TE_EFFECT_SET_RELEASE = 0x1800, // Oxx
+    TE_EFFECT_PROGRAM_RESTART = 0x1900, // Pxx
+    /*
+    TE_EFFECT_ = 0x1a00, //Qxx
+    */
+
+    TE_EFFECT_SET_RING_MOD_SRC = 0x1b00, // Rxx
+    TE_EFFECT_SET_HARD_SYNC_SRC = 0x1c00, // Sxx
+
+    TE_EFFECT_PORTA_UP_SEMITONE = 0x1d00, // Txx
+    TE_EFFECT_PORTA_DOWN_SEMITONE = 0x1e00, // Uxx
+    /*
+    TE_EFFECT_ = 0x1f00, //Vxx
+    TE_EFFECT_ = 0x2000, //Wxx
+    */
+
+    TE_EFFECT_LEGATO = 0x2100, // Xxx
+    TE_EFFECT_ARPEGGIO_ABS = 0x2200, // Yxx
+    TE_EFFECT_TRIGGER_RELEASE = 0x2300, // Zxx
+
+    /* These effects work only in instrument program */
+    TE_PROGRAM_LOOP_BEGIN = 0x7d00,
+    TE_PROGRAM_LOOP_END = 0x7e00,
+    TE_PROGRAM_JUMP = 0x7f00,
+    TE_PROGRAM_NOP = 0x7ffe,
+    TE_PROGRAM_END = 0x7fff,
+} EffectCommandsOpcodes;
+
+typedef struct {
+    uint8_t a, d, s, r, volume;
+} InstrumentAdsr;
+
+typedef struct {
+    char name[MUS_INST_NAME_LEN + 1];
+
+    uint8_t waveform;
+    uint16_t flags;
+    uint16_t sound_engine_flags;
+
+    uint8_t slide_speed;
+
+    InstrumentAdsr adsr;
+
+    uint8_t ring_mod, hard_sync; // 0xff = self
+
+    uint8_t pw; // store only one byte since we don't have the luxury of virtually unlimited memory!
+
+    uint16_t program
+        [INST_PROG_LEN]; // MSB is unite bit (indicates this and next command must be executed at once)
+    uint8_t program_period;
+
+    uint8_t vibrato_speed, vibrato_depth, vibrato_delay;
+    uint8_t pwm_speed, pwm_depth, pwm_delay;
+
+    uint8_t filter_cutoff, filter_resonance, filter_type;
+
+    uint8_t base_note;
+    int8_t finetune;
+} Instrument;
+
+typedef struct {
+    Instrument* instrument;
+
+    uint16_t flags;
+
+    uint8_t channel_flags;
+
+    uint16_t note, target_note, last_note, fixed_note;
+    int16_t arpeggio_note;
+
+    uint8_t volume;
+
+    uint8_t program_counter, program_tick, program_loop, program_period;
+
+    uint16_t filter_cutoff, filter_resonance;
+    uint8_t filter_type;
+
+    uint8_t vibrato_speed, vibrato_depth, vibrato_delay;
+    uint8_t pwm_speed, pwm_depth, pwm_delay;
+
+    uint32_t vibrato_position, pwm_position; // basically accumulators
+
+    uint8_t extarp1, extarp2;
+
+    uint16_t pw;
+
+    uint8_t slide_speed;
+} TrackerEngineChannel;
+
+typedef struct {
+    uint8_t note; // MSB is used for instrument number MSB
+    uint8_t inst_vol; // high nibble + MSB from note = instrument, low nibble = 4 volume LSBs
+    uint16_t command; // MSB used as volume MSB
+} TrackerSongPatternStep;
+
+typedef struct {
+    TrackerSongPatternStep* step;
+} TrackerSongPattern;
+
+typedef struct {
+    uint8_t pattern_indices[SONG_MAX_CHANNELS];
+} TrackerSongSequenceStep;
+
+typedef struct {
+    TrackerSongSequenceStep sequence_step[MAX_SEQUENCE_LENGTH];
+} TrackerSongSequence;
+
+typedef struct {
+    Instrument* instrument[MAX_INSTRUMENTS];
+    TrackerSongPattern pattern[MAX_PATTERNS];
+    TrackerSongSequence sequence;
+
+    uint8_t num_patterns, num_instruments;
+    uint16_t num_sequence_steps;
+    uint16_t pattern_length;
+
+    char song_name[MUS_SONG_NAME_LEN + 1];
+    uint8_t speed, rate;
+
+    uint8_t loop_start, loop_end;
+} TrackerSong;
+
+typedef struct {
+    TrackerEngineChannel channel[SONG_MAX_CHANNELS];
+
+    TrackerSong* song;
+    SoundEngine* sound_engine;
+
+    uint16_t pattern_position, sequence_position;
+    int16_t current_tick;
+    uint16_t absolute_position; // sequence_position * pattern_length + pattern_position
+
+    uint8_t speed, rate;
+    uint8_t master_volume;
+
+    bool playing; // if we reach the end of the song and song does not loop we just stop there
+
+    bool in_loop; // for E6X (pattern loop) command
+    uint8_t loops_left;
+
+    // uint32_t counter; //for debug
+} TrackerEngine;

+ 202 - 0
flizzer_tracker/util.c

@@ -0,0 +1,202 @@
+#include "util.h"
+#include "macros.h"
+
+void reset_buffer(SoundEngine* sound_engine) {
+    for(uint16_t i = 0; i < sound_engine->audio_buffer_size; i++) {
+        sound_engine->audio_buffer[i] = 512;
+    }
+}
+
+void stop_song(FlizzerTrackerApp* tracker) {
+    tracker->tracker_engine.playing = false;
+    tracker->editing = tracker->was_editing;
+
+    for(int i = 0; i < SONG_MAX_CHANNELS; i++) {
+        tracker->sound_engine.channel[i].adsr.volume = 0;
+        tracker->tracker_engine.channel[i].channel_flags &= ~(TEC_PROGRAM_RUNNING);
+    }
+
+    stop();
+
+    reset_buffer(&tracker->sound_engine);
+}
+
+void play_song(FlizzerTrackerApp* tracker, bool from_cursor) {
+    uint16_t temppos = tracker->tracker_engine.pattern_position;
+
+    stop_song(tracker);
+
+    sound_engine_dma_init(
+        (uint32_t)tracker->sound_engine.audio_buffer, tracker->sound_engine.audio_buffer_size);
+
+    tracker->tracker_engine.playing = true;
+
+    tracker->was_editing = tracker->editing;
+    tracker->editing = false;
+
+    if(!(from_cursor)) {
+        tracker->tracker_engine.pattern_position = 0;
+        temppos = 0;
+    }
+
+    tracker_engine_timer_init(tracker->song.rate);
+
+    /*sound_engine_init_hardware(tracker->sound_engine.sample_rate,
+    tracker->sound_engine.external_audio_output,
+    tracker->sound_engine.audio_buffer,
+    tracker->sound_engine.audio_buffer_size);
+    tracker_engine_init_hardware(tracker->song.rate);*/
+
+    tracker->tracker_engine.current_tick = 0;
+    tracker_engine_set_song(&tracker->tracker_engine, &tracker->song);
+
+    for(uint8_t i = 0; i < SONG_MAX_CHANNELS; i++) {
+        bool was_disabled = tracker->tracker_engine.channel[i].channel_flags & TEC_DISABLED;
+
+        memset(&tracker->sound_engine.channel[i], 0, sizeof(SoundEngineChannel));
+        memset(&tracker->tracker_engine.channel[i], 0, sizeof(TrackerEngineChannel));
+
+        if(was_disabled) {
+            tracker->tracker_engine.channel[i].channel_flags |= TEC_DISABLED;
+        }
+    }
+
+    tracker->tracker_engine.pattern_position = temppos;
+
+    play();
+}
+
+bool is_pattern_empty(TrackerSong* song, uint8_t pattern) {
+    TrackerSongPattern song_pattern = song->pattern[pattern];
+
+    for(int i = 0; i < song->pattern_length; i++) {
+        TrackerSongPatternStep* step = &song_pattern.step[i];
+
+        if(tracker_engine_get_note(step) != MUS_NOTE_NONE ||
+           tracker_engine_get_instrument(step) != MUS_NOTE_INSTRUMENT_NONE ||
+           tracker_engine_get_volume(step) != MUS_NOTE_VOLUME_NONE ||
+           tracker_engine_get_command(step) != 0) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool check_and_allocate_pattern(TrackerSong* song, uint8_t pattern) {
+    if(pattern < song->num_patterns) // we can set this pattern since it already exists
+    {
+        return true;
+    }
+
+    else {
+        if(song->pattern[pattern - 1].step == NULL)
+            return false; // if we hop through several patterns (e.g. editing upper digit)
+
+        if(!(is_pattern_empty(
+               song, pattern - 1))) // don't let the user flood the song with empty patterns
+        {
+            song->pattern[pattern].step =
+                malloc(sizeof(TrackerSongPatternStep) * song->pattern_length);
+            set_empty_pattern(&song->pattern[pattern], song->pattern_length);
+            song->num_patterns++;
+            return true;
+        }
+
+        else {
+            return false;
+        }
+    }
+}
+
+void resize_pattern(TrackerSongPattern* pattern, uint16_t old_length, uint16_t new_length) {
+    TrackerSongPattern temp;
+    temp.step = malloc((new_length) * sizeof(TrackerSongPatternStep));
+
+    set_empty_pattern(&temp, new_length);
+    memcpy(
+        temp.step, pattern->step, my_min(old_length, new_length) * sizeof(TrackerSongPatternStep));
+
+    free(pattern->step);
+    pattern->step = temp.step;
+}
+
+void change_pattern_length(TrackerSong* song, uint16_t new_length) {
+    for(int i = 0; i < MAX_PATTERNS; i++) {
+        if(song->pattern[i].step) {
+            resize_pattern(&song->pattern[i], song->pattern_length, new_length);
+        }
+    }
+
+    song->pattern_length = new_length;
+}
+
+bool is_default_instrument(Instrument* inst) {
+    Instrument* ref = malloc(sizeof(Instrument));
+    set_default_instrument(ref);
+    bool is_default = memcmp(ref, inst, sizeof(Instrument)) != 0 ? false : true;
+    free(ref);
+    return is_default;
+}
+
+bool check_and_allocate_instrument(TrackerSong* song, uint8_t inst) {
+    if(inst < song->num_instruments) // we can go to this instrument since it already exists
+    {
+        return true;
+    }
+
+    else {
+        if(inst >= MAX_INSTRUMENTS) return false;
+
+        if(!(is_default_instrument(
+               song->instrument
+                   [inst - 1]))) // don't let the user flood the song with default instrument
+        {
+            song->instrument[inst] = malloc(sizeof(Instrument));
+            set_default_instrument(song->instrument[inst]);
+            song->num_instruments++;
+            return true;
+        }
+
+        else {
+            return false;
+        }
+    }
+}
+
+void set_default_song(FlizzerTrackerApp* tracker) {
+    tracker->tracker_engine.master_volume = 0x80;
+
+    tracker->song.speed = 6;
+    tracker->song.rate = tracker->tracker_engine.rate;
+    tracker->song.num_instruments = 1;
+    tracker->song.num_patterns = 5;
+    tracker->song.num_sequence_steps = 1;
+    tracker->song.pattern_length = 64;
+
+    tracker->song.sequence.sequence_step[0].pattern_indices[0] = 1;
+    tracker->song.sequence.sequence_step[0].pattern_indices[1] = 2;
+    tracker->song.sequence.sequence_step[0].pattern_indices[2] = 3;
+    tracker->song.sequence.sequence_step[0].pattern_indices[3] = 4;
+
+    for(int i = 0; i < 5; i++) {
+        tracker->song.pattern[i].step = malloc(64 * sizeof(TrackerSongPatternStep));
+        memset(tracker->song.pattern[i].step, 0, 64 * sizeof(TrackerSongPatternStep));
+    }
+
+    for(int i = 0; i < 64; ++i) {
+        for(int j = 0; j < 5; j++) {
+            set_note(&tracker->song.pattern[j].step[i], MUS_NOTE_NONE);
+
+            set_instrument(&tracker->song.pattern[j].step[i], MUS_NOTE_INSTRUMENT_NONE);
+
+            set_volume(&tracker->song.pattern[j].step[i], MUS_NOTE_VOLUME_NONE);
+        }
+    }
+
+    tracker->song.instrument[0] = malloc(sizeof(Instrument));
+
+    set_default_instrument(tracker->song.instrument[0]);
+
+    tracker->tracker_engine.playing = false;
+}

+ 26 - 0
flizzer_tracker/util.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "flizzer_tracker.h"
+#include "sound_engine/sound_engine_defs.h"
+#include "tracker_engine/tracker_engine.h"
+#include "tracker_engine/tracker_engine_defs.h"
+
+#include "macros.h"
+
+#define clamp(val, add, _min, _max) val = my_min(_max, my_max(_min, (int32_t)val + add))
+#define flipbit(val, bit) \
+    { val ^= bit; };
+
+void reset_buffer(SoundEngine* sound_engine);
+void play_song(FlizzerTrackerApp* tracker, bool from_cursor);
+void stop_song(FlizzerTrackerApp* tracker);
+
+bool is_pattern_empty(TrackerSong* song, uint8_t pattern);
+bool check_and_allocate_pattern(TrackerSong* song, uint8_t pattern);
+void change_pattern_length(TrackerSong* song, uint16_t new_length);
+
+bool check_and_allocate_instrument(TrackerSong* song, uint8_t inst);
+void set_default_song(FlizzerTrackerApp* tracker);

+ 4 - 0
flizzer_tracker/view/char_array.c

@@ -0,0 +1,4 @@
+const char to_char_array[] = {
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
+    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+};

+ 669 - 0
flizzer_tracker/view/instrument_editor.c

@@ -0,0 +1,669 @@
+#include "instrument_editor.h"
+#include "pattern_editor.h"
+
+#include "../macros.h"
+#include "opcode_description.h"
+
+#include <flizzer_tracker_icons.h>
+
+void draw_inst_flag(
+    FlizzerTrackerApp* tracker,
+    Canvas* canvas,
+    uint8_t focus,
+    uint8_t param,
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    uint16_t flags,
+    uint16_t mask) {
+    canvas_draw_icon(canvas, x, y - 5, ((flags & mask) ? &I_checkbox_checked : &I_checkbox_empty));
+    canvas_draw_str(canvas, x + 6, y, text);
+
+    if(tracker->focus == focus && tracker->selected_param == param && tracker->editing) {
+        if(text[strlen(text) - 1] == ':') {
+            canvas_draw_box(canvas, x + 5, y - 6, strlen(text) * 4 - 1, 7);
+        }
+
+        else {
+            canvas_draw_box(canvas, x + 5, y - 6, strlen(text) * 4 + 1, 7);
+        }
+    }
+
+    if(tracker->focus == focus && tracker->selected_param == param && !(tracker->editing)) {
+        if(text[strlen(text) - 1] == ':') {
+            canvas_draw_frame(canvas, x + 5, y - 6, strlen(text) * 4 - 1, 7);
+        }
+
+        else {
+            canvas_draw_frame(canvas, x + 5, y - 6, strlen(text) * 4 + 1, 7);
+        }
+    }
+}
+
+void draw_inst_text_one_digit(
+    FlizzerTrackerApp* tracker,
+    Canvas* canvas,
+    uint8_t focus,
+    uint8_t param,
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    uint8_t value) // text MUST end with semicolon
+{
+    canvas_draw_str(canvas, x, y, text);
+    char buffer[4];
+    snprintf(buffer, sizeof(buffer), "%01X", (value & 0xF));
+    canvas_draw_str(canvas, x + strlen(text) * 4 - 2, y, buffer);
+
+    if(tracker->focus == focus && tracker->selected_param == param && tracker->editing) {
+        canvas_draw_box(canvas, x + strlen(text) * 4 - 3, y - 6, 5, 7);
+    }
+
+    if(tracker->focus == focus && tracker->selected_param == param && !(tracker->editing)) {
+        canvas_draw_frame(canvas, x + strlen(text) * 4 - 3, y - 6, 5, 7);
+    }
+}
+
+void draw_inst_text_two_digits(
+    FlizzerTrackerApp* tracker,
+    Canvas* canvas,
+    uint8_t focus,
+    uint8_t param,
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    uint8_t value) // text MUST end with semicolon
+{
+    canvas_draw_str(canvas, x, y, text);
+    char buffer[4];
+    snprintf(buffer, sizeof(buffer), "%02X", value);
+    canvas_draw_str(canvas, x + strlen(text) * 4 - 2, y, buffer);
+
+    if(tracker->focus == focus && tracker->selected_param == param && tracker->editing) {
+        canvas_draw_box(
+            canvas, x + strlen(text) * 4 + 4 * tracker->current_digit - 3, y - 6, 5, 7);
+    }
+
+    if(tracker->focus == focus && tracker->selected_param == param && !(tracker->editing)) {
+        canvas_draw_frame(
+            canvas, x + strlen(text) * 4 + 4 * tracker->current_digit - 3, y - 6, 5, 7);
+    }
+}
+
+static const char* filter_types[] = {
+    "NONE",
+    "LOW",
+    "HIGH",
+    "BAND",
+    "LOHI",
+    "HIBD",
+    "LOBD",
+    "ALL",
+};
+
+static const char* instrument_editor_params_description[] = {
+    "CURRENT INSTRUMENT",
+    "CURRENT INSTRUMENT NAME",
+    "INSTRUMENT BASE NOTE",
+    "INSTRUMENT FINETUNE",
+    "SLIDE SPEED",
+    "SET PULSE WIDTH ON KEYDOWN",
+    "PULSE WIDTH",
+    "SET FILTER PARAMETERS ON KEYDOWN",
+    "NOISE WAVEFORM",
+    "PULSE WAVEFORM",
+    "TRIANGLE WAVEFORM",
+    "SAWTOOTH WAVEFORM",
+    "METALLIC NOISE WAVEFORM",
+    "SINE WAVEFORM",
+    "ENVELOPE ATTACK",
+    "ENVELOPE DECAY",
+    "ENVELOPE SUSTAIN",
+    "ENVELOPE RELEASE",
+    "ENVELOPE VOLUME",
+    "ENABLE FILTER",
+    "FILTER CUTOFF FREQUENCY",
+    "FILTER RESONANCE",
+    "FILTER TYPE (NONE=OFF)",
+    "ENABLE RING MODULATION",
+    "RINGMOD SOURCE CHANNEL (F=SELF)",
+    "ENABLE HARD SYNC",
+    "HARDSYNC SOURCE CHANNEL (F=SELF)",
+    "RETRIGGER INSTRUMENT ON SLIDE",
+    "SYNC OSCILLATORS ON KEYDOWN",
+    "ENABLE VIBRATO",
+    "VIBRATO SPEED",
+    "VIBRATO DEPTH",
+    "VIBRATO DELAY (IN TICKS)",
+    "ENABLE PWM",
+    "PWM SPEED",
+    "PWM DEPTH",
+    "PWM DELAY (IN TICKS)",
+    "DON'T RESTART PROGRAM ON KEYDOWN",
+    "PROG.PERIOD (00 = PROGRAM OFF)",
+};
+
+void draw_instrument_view(Canvas* canvas, FlizzerTrackerApp* tracker) {
+    SoundEngineChannel* se_channel = &tracker->sound_engine.channel[0];
+    if(!(se_channel->flags & SE_ENABLE_GATE) && tracker->tracker_engine.song == NULL) {
+        stop();
+        tracker->tracker_engine.playing = false;
+        tracker_engine_set_song(&tracker->tracker_engine, &tracker->song);
+    }
+
+    char buffer[30];
+    Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+    uint8_t shift = tracker->inst_editor_shift;
+
+    if(shift < 6) {
+        snprintf(buffer, sizeof(buffer), "INST:%c", to_char(tracker->current_instrument));
+        draw_generic_n_digit_field(
+            tracker, canvas, EDIT_INSTRUMENT, INST_CURRENTINSTRUMENT, buffer, 0, 5 - shift, 1);
+        snprintf(
+            buffer,
+            sizeof(buffer),
+            "%s",
+            tracker->song.instrument[tracker->current_instrument]->name);
+        draw_generic_n_digit_field(
+            tracker, canvas, EDIT_INSTRUMENT, INST_INSTRUMENTNAME, buffer, 4 * 7 - 1, 5 - shift, 1);
+    }
+
+    if(shift < 12) {
+        snprintf(buffer, sizeof(buffer), "NOTE:%s", notename(inst->base_note));
+        canvas_draw_str(canvas, 0, 11 - shift, buffer);
+
+        if(tracker->editing && tracker->focus == EDIT_INSTRUMENT &&
+           tracker->selected_param == INST_CURRENT_NOTE) {
+            if(tracker->current_digit) {
+                canvas_draw_box(canvas, 19 + 2 * 4, 5 - shift, 5, 7);
+            }
+
+            else {
+                canvas_draw_box(canvas, 19, 5 - shift, 5 + 4, 7);
+            }
+        }
+
+        if(!(tracker->editing) && tracker->focus == EDIT_INSTRUMENT &&
+           tracker->selected_param == INST_CURRENT_NOTE) {
+            if(tracker->current_digit) {
+                canvas_draw_frame(canvas, 19 + 2 * 4, 5 - shift, 5, 7);
+            }
+
+            else {
+                canvas_draw_frame(canvas, 19, 5 - shift, 5 + 4, 7);
+            }
+        }
+
+        snprintf(buffer, sizeof(buffer), "FINE:%+02d", inst->finetune);
+        canvas_draw_str(canvas, 37, 11 - shift, buffer);
+
+        if(tracker->editing && tracker->focus == EDIT_INSTRUMENT &&
+           tracker->selected_param == INST_FINETUNE) {
+            if(tracker->current_digit) {
+                canvas_draw_box(canvas, 60 + 4, 5 - shift, 5, 7);
+            }
+
+            else {
+                canvas_draw_box(canvas, 60, 5 - shift, 5, 7);
+            }
+        }
+
+        if(!(tracker->editing) && tracker->focus == EDIT_INSTRUMENT &&
+           tracker->selected_param == INST_FINETUNE) {
+            if(tracker->current_digit) {
+                canvas_draw_frame(canvas, 60 + 4, 5 - shift, 5, 7);
+            }
+
+            else {
+                canvas_draw_frame(canvas, 60, 5 - shift, 5, 7);
+            }
+        }
+    }
+
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_SLIDESPEED,
+        "SL.SPD:",
+        0,
+        17 - shift,
+        inst->slide_speed);
+
+    draw_inst_flag(
+        tracker, canvas, EDIT_INSTRUMENT, INST_SETPW, "PW:", 36, 17 - shift, inst->flags, TE_SET_PW);
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_PW, "", 54, 17 - shift, inst->pw);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_SETCUTOFF,
+        "CUT",
+        61,
+        17 - shift,
+        inst->flags,
+        TE_SET_CUTOFF);
+
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_NOISE,
+        "N",
+        0,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_NOISE);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_PULSE,
+        "P",
+        10,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_PULSE);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_TRIANGLE,
+        "T",
+        20,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_TRIANGLE);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_SAWTOOTH,
+        "S",
+        30,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_SAW);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_NOISE_METAL,
+        "M",
+        40,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_NOISE_METAL);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_WAVE_SINE,
+        "SINE",
+        50,
+        23 - shift,
+        inst->waveform,
+        SE_WAVEFORM_SINE);
+
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_ATTACK, "A:", 0, 29 - shift, inst->adsr.a);
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_DECAY, "D:", 16, 29 - shift, inst->adsr.d);
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_SUSTAIN, "S:", 32, 29 - shift, inst->adsr.s);
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_RELEASE, "R:", 48, 29 - shift, inst->adsr.r);
+    draw_inst_text_two_digits(
+        tracker, canvas, EDIT_INSTRUMENT, INST_VOLUME, "V:", 64, 29 - shift, inst->adsr.volume);
+
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_ENABLEFILTER,
+        "FIL",
+        0,
+        35 - shift,
+        inst->sound_engine_flags,
+        SE_ENABLE_FILTER);
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_FILTERCUTOFF,
+        "CUT:",
+        20,
+        35 - shift,
+        inst->filter_cutoff);
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_FILTERRESONANCE,
+        "RES:",
+        44,
+        35 - shift,
+        inst->filter_resonance);
+
+    snprintf(buffer, sizeof(buffer), "TYPE:%s", filter_types[inst->filter_type]);
+    canvas_draw_str(canvas, 0, 41 - shift, buffer);
+
+    if(tracker->editing && tracker->focus == EDIT_INSTRUMENT &&
+       tracker->selected_param == INST_FILTERTYPE) {
+        canvas_draw_box(
+            canvas, 19, 35 - shift, strlen(filter_types[inst->filter_type]) * 4 + 1, 7);
+    }
+
+    if(!(tracker->editing) && tracker->focus == EDIT_INSTRUMENT &&
+       tracker->selected_param == INST_FILTERTYPE) {
+        canvas_draw_frame(
+            canvas, 19, 35 - shift, strlen(filter_types[inst->filter_type]) * 4 + 1, 7);
+    }
+
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_ENABLERINGMOD,
+        "R:",
+        38,
+        41 - shift,
+        inst->sound_engine_flags,
+        SE_ENABLE_RING_MOD);
+    draw_inst_text_one_digit(
+        tracker, canvas, EDIT_INSTRUMENT, INST_RINGMODSRC, "", 52, 41 - shift, inst->ring_mod);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_ENABLEHARDSYNC,
+        "H:",
+        56,
+        41 - shift,
+        inst->sound_engine_flags,
+        SE_ENABLE_HARD_SYNC);
+    draw_inst_text_one_digit(
+        tracker, canvas, EDIT_INSTRUMENT, INST_HARDSYNCSRC, "", 70, 41 - shift, inst->hard_sync);
+
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_RETRIGGERONSLIDE,
+        "SL.RETRIG",
+        0,
+        47 - shift,
+        inst->flags,
+        TE_RETRIGGER_ON_SLIDE);
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_ENABLEKEYSYNC,
+        "KSYNC",
+        44,
+        47 - shift,
+        inst->sound_engine_flags,
+        SE_ENABLE_KEYDOWN_SYNC);
+
+    draw_inst_flag(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_ENABLEVIBRATO,
+        "VIB",
+        0,
+        53 - shift,
+        inst->flags,
+        TE_ENABLE_VIBRATO);
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_VIBRATOSPEED,
+        "S:",
+        20,
+        53 - shift,
+        inst->vibrato_speed);
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_VIBRATODEPTH,
+        "D:",
+        36,
+        53 - shift,
+        inst->vibrato_depth);
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_VIBRATODELAY,
+        "DEL:",
+        52,
+        53 - shift,
+        inst->vibrato_delay);
+
+    if(shift >= 6) {
+        draw_inst_flag(
+            tracker,
+            canvas,
+            EDIT_INSTRUMENT,
+            INST_ENABLEPWM,
+            "PWM",
+            0,
+            59 - shift,
+            inst->flags,
+            TE_ENABLE_PWM);
+        draw_inst_text_two_digits(
+            tracker, canvas, EDIT_INSTRUMENT, INST_PWMSPEED, "S:", 20, 59 - shift, inst->pwm_speed);
+        draw_inst_text_two_digits(
+            tracker, canvas, EDIT_INSTRUMENT, INST_PWMDEPTH, "D:", 36, 59 - shift, inst->pwm_depth);
+        draw_inst_text_two_digits(
+            tracker,
+            canvas,
+            EDIT_INSTRUMENT,
+            INST_PWMDELAY,
+            "DEL:",
+            52,
+            59 - shift,
+            inst->pwm_delay);
+    }
+
+    if(shift >= 12) {
+        draw_inst_flag(
+            tracker,
+            canvas,
+            EDIT_INSTRUMENT,
+            INST_PROGRESTART,
+            "NO PROG.RESTART",
+            0,
+            65 - shift,
+            inst->flags,
+            TE_PROG_NO_RESTART);
+    }
+
+    draw_inst_text_two_digits(
+        tracker,
+        canvas,
+        EDIT_INSTRUMENT,
+        INST_PROGRAMEPERIOD,
+        "P.PERIOD:",
+        81,
+        56,
+        inst->program_period);
+
+    canvas_draw_line(canvas, 0, 57, 127, 57);
+    canvas_draw_line(canvas, 79, 0, 79, 56);
+    canvas_draw_line(canvas, 80, 49, 127, 49);
+
+    if(tracker->focus == EDIT_INSTRUMENT) {
+        canvas_draw_str(
+            canvas, 0, 64, instrument_editor_params_description[tracker->selected_param]);
+    }
+}
+
+char command_get_char(uint16_t command) {
+    if((command >> 8) < 36) {
+        return to_char_array[(command >> 8)];
+    }
+
+    if(command == TE_PROGRAM_END) {
+        return ':';
+    }
+
+    if((command & 0xff00) == TE_PROGRAM_JUMP) {
+        return '^';
+    }
+
+    if((command & 0xff00) == TE_PROGRAM_LOOP_END) {
+        return '>';
+    }
+
+    if((command & 0xff00) == TE_PROGRAM_LOOP_BEGIN) {
+        return '<';
+    }
+
+    return '?';
+}
+
+void draw_program_step(Canvas* canvas, uint8_t y, FlizzerTrackerApp* tracker, uint8_t index) {
+    char buffer[15];
+
+    Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+    uint16_t opcode = inst->program[index];
+
+    if(opcode != TE_PROGRAM_NOP) {
+        if((opcode & 0x7f00) == TE_EFFECT_ARPEGGIO) {
+            if((opcode & 0xff) != 0xf0 && (opcode & 0xff) != 0xf1) {
+                snprintf(
+                    buffer,
+                    sizeof(buffer),
+                    "%01X %c%02X %s",
+                    index,
+                    command_get_char(opcode & 0x7fff),
+                    (opcode & 0xff),
+                    notename(my_min(
+                        12 * 7 + 11,
+                        (opcode & 0xff) +
+                            tracker->song.instrument[tracker->current_instrument]->base_note)));
+            }
+
+            else {
+                snprintf(
+                    buffer,
+                    sizeof(buffer),
+                    "%01X %c%02X %s",
+                    index,
+                    command_get_char(opcode & 0x7fff),
+                    (opcode & 0xff),
+                    notename((opcode & 0xff)));
+            }
+        }
+
+        else if((opcode & 0x7f00) == TE_EFFECT_ARPEGGIO_ABS) {
+            snprintf(
+                buffer,
+                sizeof(buffer),
+                "%01X %c%02X F.%s",
+                index,
+                command_get_char(opcode & 0x7fff),
+                (opcode & 0xff),
+                notename(opcode & 0xff));
+        }
+
+        else {
+            snprintf(
+                buffer,
+                sizeof(buffer),
+                "%01X %c%02X %s",
+                index,
+                command_get_char(opcode & 0x7fff),
+                (opcode & 0xff),
+                get_opcode_description(opcode, true) ? get_opcode_description(opcode, true) : "");
+        }
+
+        if(opcode & 0x8000) {
+            if(index == 0) {
+                canvas_draw_line(canvas, 84 + 4 * 4 + 2, y, 84 + 4 * 4 + 2, y - 3);
+                canvas_draw_dot(canvas, 84 + 4 * 4 + 1, y - 4);
+            }
+
+            if(index > 0 && !(inst->program[index - 1] & 0x8000)) {
+                canvas_draw_line(canvas, 84 + 4 * 4 + 2, y, 84 + 4 * 4 + 2, y - 3);
+                canvas_draw_dot(canvas, 84 + 4 * 4 + 1, y - 4);
+            }
+
+            if(index > 0 && (inst->program[index - 1] & 0x8000)) {
+                canvas_draw_line(canvas, 84 + 4 * 4 + 2, y, 84 + 4 * 4 + 2, y - 5);
+            }
+        }
+
+        else {
+            if(index > 0 && (inst->program[index - 1] & 0x8000)) {
+                canvas_draw_line(canvas, 84 + 4 * 4 + 2, y - 3, 84 + 4 * 4 + 2, y - 5);
+                canvas_draw_dot(canvas, 84 + 4 * 4 + 1, y - 2);
+            }
+        }
+    }
+
+    else {
+        snprintf(buffer, sizeof(buffer), "%01X ---", index);
+    }
+
+    canvas_draw_str(canvas, 81, y, buffer);
+}
+
+void draw_instrument_program_view(Canvas* canvas, FlizzerTrackerApp* tracker) {
+    Instrument* inst = tracker->song.instrument[tracker->current_instrument];
+
+    for(uint8_t i = tracker->program_position;
+        i < my_min(INST_PROG_LEN, tracker->program_position + 8);
+        i++) {
+        draw_program_step(canvas, 6 + 6 * i - tracker->program_position * 6, tracker, i);
+
+        if(i == tracker->current_program_step && tracker->focus == EDIT_PROGRAM) {
+            if(tracker->editing) {
+                canvas_draw_box(
+                    canvas,
+                    80 + 8 + tracker->current_digit * 4,
+                    6 * i - tracker->program_position * 6,
+                    5,
+                    7);
+            }
+
+            else {
+                canvas_draw_box(canvas, 80, 6 * i - tracker->program_position * 6, 5, 7);
+            }
+        }
+    }
+
+    // draw arrow pointing at current program step
+
+    for(uint8_t i = 0; i < SONG_MAX_CHANNELS; i++) {
+        if(tracker->tracker_engine.channel[i].instrument == inst &&
+           (tracker->tracker_engine.channel[i].channel_flags & TEC_PROGRAM_RUNNING) &&
+           (tracker->tracker_engine.sound_engine->channel[i].flags & SE_ENABLE_GATE)) {
+            if(tracker->tracker_engine.channel[i].program_tick >= tracker->program_position &&
+               tracker->tracker_engine.channel[i].program_tick < tracker->program_position + 8) {
+                canvas_draw_str(
+                    canvas,
+                    85,
+                    6 * tracker->tracker_engine.channel[i].program_tick -
+                        tracker->program_position * 6 + 6,
+                    ">");
+                break;
+            }
+        }
+    }
+
+    if(tracker->focus == EDIT_PROGRAM) {
+        uint16_t opcode = (inst->program[tracker->current_program_step] & 0x7fff);
+        canvas_draw_str(
+            canvas,
+            0,
+            64,
+            get_opcode_description(opcode, false) ? get_opcode_description(opcode, false) : "");
+    }
+}

+ 11 - 0
flizzer_tracker/view/instrument_editor.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include "../flizzer_tracker.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+#include "pattern_editor.h"
+
+#include <furi.h>
+#include <gui/gui.h>
+
+void draw_instrument_view(Canvas* canvas, FlizzerTrackerApp* tracker);
+void draw_instrument_program_view(Canvas* canvas, FlizzerTrackerApp* tracker);

+ 72 - 0
flizzer_tracker/view/opcode_description.c

@@ -0,0 +1,72 @@
+#include "opcode_description.h"
+#include <stdint.h>
+
+static const OpcodeDescription opcode_desc[] = {
+    {TE_PROGRAM_LOOP_BEGIN, 0x7f00, "PROGRAM LOOP BEGIN", "L.BEG."},
+    {TE_PROGRAM_LOOP_END, 0x7f00, "PROGRAM LOOP END", "L.END"},
+
+    {TE_PROGRAM_NOP, 0x7fff, "NO OPERATION", ""},
+    {TE_PROGRAM_END, 0x7fff, "PROGRAM END", "PR.END"},
+    {TE_PROGRAM_JUMP, 0x7f00, "JUMP TO POSITION", "GOTO"},
+
+    //====================================================
+
+    {TE_EFFECT_ARPEGGIO, 0x7f00, "RELATIVE ARPEGGIO NOTE", ""},
+    {TE_EFFECT_PORTAMENTO_UP, 0x7f00, "PORTAMENTO UP", "PORTUP"},
+    {TE_EFFECT_PORTAMENTO_DOWN, 0x7f00, "PORTAMENTO DOWN", "PORTDN"},
+    {TE_EFFECT_SLIDE, 0x7f00, "SLIDE", "SLIDE"},
+    {TE_EFFECT_VIBRATO, 0x7f00, "VIBRATO", "VIB"},
+    {TE_EFFECT_PWM, 0x7f00, "PULSE WIDTH MODIFICATION", "PWM"},
+
+    {TE_EFFECT_SET_PW, 0x7f00, "SET PULSE WIDTH", "SET PW"},
+    {TE_EFFECT_PW_DOWN, 0x7f00, "PULSE WIDTH DOWN", "PWDOWN"},
+    {TE_EFFECT_PW_UP, 0x7f00, "PULSE WIDTH UP", "PW UP"},
+    {TE_EFFECT_SET_CUTOFF, 0x7f00, "SET FILTER CUTOFF", "F.CUT"},
+
+    {TE_EFFECT_VOLUME_FADE, 0x7f00, "VOLUME FADE", "V.FADE"},
+    {TE_EFFECT_SET_WAVEFORM, 0x7f00, "SET WAVEFORM", "S.WAVE"},
+    {TE_EFFECT_SET_VOLUME, 0x7f00, "SET VOLUME", "VOLUME"},
+    {TE_EFFECT_SKIP_PATTERN, 0x7f00, "SKIP PATTERN", "P.SKIP"},
+
+    {TE_EFFECT_EXT_TOGGLE_FILTER, 0x7ff0, "TOGGLE FILTER (0=OFF,1-F=ON)", "T.FILT"},
+    {TE_EFFECT_EXT_PORTA_UP, 0x7ff0, "FINE PORTAMENTO UP", "PUP F."},
+    {TE_EFFECT_EXT_PORTA_DN, 0x7ff0, "FINE PORTAMENTO DOWN", "PDN F."},
+    {TE_EFFECT_EXT_FILTER_MODE, 0x7ff0, "SET FILTER MODE", "F.MODE"},
+    {TE_EFFECT_EXT_PATTERN_LOOP, 0x7ff0, "PATTERN LOOP:E60=BEGIN,E6X=END", "PAT.L."},
+    {TE_EFFECT_EXT_RETRIGGER, 0x7ff0, "RETRIGGER AT TICK X (X>0)", "RETRIG"},
+    {TE_EFFECT_EXT_FINE_VOLUME_DOWN, 0x7ff0, "FINE VOLUME DOWN", "VDN F."},
+    {TE_EFFECT_EXT_FINE_VOLUME_UP, 0x7ff0, "FINE VOLUME UP", "VUP F."},
+    {TE_EFFECT_EXT_NOTE_CUT, 0x7ff0, "NOTE CUT", "N.CUT"},
+    {TE_EFFECT_EXT_NOTE_DELAY, 0x7ff0, "NOTE DELAY", "N.DEL."},
+    {TE_EFFECT_EXT_PHASE_RESET, 0x7ff0, "PHASE RESET ON TICK X", "PH.RES."},
+
+    {TE_EFFECT_SET_SPEED_PROG_PERIOD, 0x7f00, "SET SPEED (PROG.PER.IN PROGRAM)", "P.PER."},
+    {TE_EFFECT_CUTOFF_UP, 0x7f00, "FILTER CUTOFF UP", "CUT.UP"},
+    {TE_EFFECT_CUTOFF_DOWN, 0x7f00, "FILTER CUTOFF DOWN", "CUT.DN"},
+    {TE_EFFECT_SET_RESONANCE, 0x7f00, "SET FILTER RESONANCE", "F.RES."},
+    {TE_EFFECT_RESONANCE_UP, 0x7f00, "FILTER RESONANCE UP", "F.R.UP"},
+    {TE_EFFECT_RESONANCE_DOWN, 0x7f00, "FILTER RESONANCE DOWN", "F.R.DN"},
+    {TE_EFFECT_SET_ATTACK, 0x7f00, "SET ENVELOPE ATTACK", "ADSR A"},
+    {TE_EFFECT_SET_DECAY, 0x7f00, "SET ENVELOPE DECAY", "ADSR D"},
+    {TE_EFFECT_SET_SUSTAIN, 0x7f00, "SET ENVELOPE SUSTAIN", "ADSR S"},
+    {TE_EFFECT_SET_RELEASE, 0x7f00, "SET ENVELOPE RELEASE", "ADSR R"},
+    {TE_EFFECT_PROGRAM_RESTART, 0x7f00, "RESTART INSTRUMENT PROGRAM", "P.RES."},
+    {TE_EFFECT_SET_RING_MOD_SRC, 0x7f00, "SET RING MODULATION SOURCE CH.", "R.SRC"},
+    {TE_EFFECT_SET_HARD_SYNC_SRC, 0x7f00, "SET HARD SYNC SOURCE CHANNEL", "S.SRC"},
+    {TE_EFFECT_PORTA_UP_SEMITONE, 0x7f00, "PORTAMENTO UP (SEMITONES)", "PU.SEM"},
+    {TE_EFFECT_PORTA_DOWN_SEMITONE, 0x7f00, "PORTAMENTO DOWN (SEMITONES)", "PD.SEM"},
+    {TE_EFFECT_LEGATO, 0x7f00, "LEGATO", "LEGATO"},
+    {TE_EFFECT_ARPEGGIO_ABS, 0x7f00, "ABSOLUTE ARPEGGIO NOTE", ""},
+    {TE_EFFECT_TRIGGER_RELEASE, 0x7f00, "TRIGGER RELEASE", "TR.REL"},
+    {0, 0, NULL, NULL},
+};
+
+char* get_opcode_description(uint16_t opcode, bool short_description) {
+    for(int i = 0; opcode_desc[i].name != NULL; i++) {
+        if(opcode_desc[i].opcode == (opcode & opcode_desc[i].mask)) {
+            return short_description ? opcode_desc[i].shortname : opcode_desc[i].name;
+        }
+    }
+
+    return NULL;
+}

+ 12 - 0
flizzer_tracker/view/opcode_description.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include "../tracker_engine/tracker_engine_defs.h"
+#include <stdio.h>
+
+typedef struct {
+    uint16_t opcode;
+    uint16_t mask;
+    char *name, *shortname;
+} OpcodeDescription;
+
+char* get_opcode_description(uint16_t opcode, bool short_description);

+ 442 - 0
flizzer_tracker/view/pattern_editor.c

@@ -0,0 +1,442 @@
+#include "pattern_editor.h"
+#include "../macros.h"
+
+#include <flizzer_tracker_icons.h>
+
+#define PATTERN_EDITOR_Y ((tracker->focus == EDIT_PATTERN) ? 4 : (64 - (6 * 5) - 1))
+
+static const char* notenames[] = {
+    "C-",
+    "C#",
+    "D-",
+    "D#",
+    "E-",
+    "F-",
+    "F#",
+    "G-",
+    "G#",
+    "A-",
+    "A#",
+    "B-",
+};
+
+char* notename(uint8_t note) {
+    static char buffer[6];
+
+    if(note == MUS_NOTE_CUT) {
+        snprintf(buffer, sizeof(buffer), "%s", "OFF");
+        return buffer;
+    }
+
+    if(note == MUS_NOTE_RELEASE) {
+        snprintf(buffer, sizeof(buffer), "%s", "   ");
+        return buffer;
+    }
+
+    if(note == 0xf0) // external arpeggio notes
+    {
+        snprintf(buffer, sizeof(buffer), "%s", "EXT.0");
+        return buffer;
+    }
+
+    if(note == 0xf1) {
+        snprintf(buffer, sizeof(buffer), "%s", "EXT.1");
+        return buffer;
+    }
+
+    else {
+        uint8_t final_note = my_min(12 * 7 + 11, note);
+        snprintf(buffer, sizeof(buffer), "%s%d", notenames[final_note % 12], final_note / 12);
+    }
+
+    return buffer;
+}
+
+char to_char(uint8_t number) {
+    return to_char_array[number];
+}
+
+void draw_pattern_view(Canvas* canvas, FlizzerTrackerApp* tracker) {
+    char command_buffer[6] = {0};
+    char buffer[11] = {0};
+
+    canvas_draw_line(canvas, 0, PATTERN_EDITOR_Y, 127, PATTERN_EDITOR_Y);
+
+    for(int i = 0; i < SONG_MAX_CHANNELS; ++i) {
+        uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+        uint8_t current_pattern =
+            tracker->tracker_engine.song->sequence.sequence_step[sequence_position]
+                .pattern_indices[i];
+        uint16_t pattern_step = tracker->tracker_engine.pattern_position;
+
+        uint16_t pattern_length = tracker->tracker_engine.song->pattern_length;
+
+        TrackerSongPattern* pattern = &tracker->tracker_engine.song->pattern[current_pattern];
+
+        for(uint8_t pos = 0; pos < ((tracker->focus == EDIT_PATTERN) ? 9 : 5); ++pos) {
+            TrackerSongPatternStep* step = NULL;
+
+            if(pattern_step - ((tracker->focus == EDIT_PATTERN) ? 4 : 2) + pos >= 0 &&
+               pattern_step - ((tracker->focus == EDIT_PATTERN) ? 4 : 2) + pos < pattern_length) {
+                step =
+                    &pattern->step[pattern_step + pos - ((tracker->focus == EDIT_PATTERN) ? 4 : 2)];
+            }
+
+            uint8_t string_x = i * 32;
+            uint8_t string_y =
+                PATTERN_EDITOR_Y + 6 * pos + 6 + ((tracker->focus == EDIT_PATTERN) ? 3 : 1);
+
+            if(step) {
+                uint8_t note = tracker_engine_get_note(step);
+                uint8_t inst = tracker_engine_get_instrument(step);
+                uint8_t vol = tracker_engine_get_volume(step);
+                uint16_t command = tracker_engine_get_command(step);
+
+                char inst_ch = to_char(inst);
+                char vol_ch = to_char(vol);
+                char command_ch = to_char(command >> 8);
+
+                if(inst == MUS_NOTE_INSTRUMENT_NONE) {
+                    inst_ch = '-';
+                }
+
+                if(vol == MUS_NOTE_VOLUME_NONE) {
+                    vol_ch = '-';
+                }
+
+                if(command == 0) {
+                    snprintf(command_buffer, sizeof(command_buffer), "---");
+                }
+
+                else {
+                    snprintf(
+                        command_buffer,
+                        sizeof(command_buffer),
+                        "%c%02X",
+                        command_ch,
+                        (command & 0xff));
+                }
+
+                snprintf(
+                    buffer,
+                    sizeof(buffer),
+                    "%s%c%c%s",
+                    (note == MUS_NOTE_NONE ? "---" : notename(note)),
+                    inst_ch,
+                    vol_ch,
+                    command_buffer);
+
+                canvas_draw_str(canvas, string_x, string_y, buffer);
+
+                if(note == MUS_NOTE_RELEASE) {
+                    canvas_draw_icon(canvas, string_x, string_y - 5, &I_note_release);
+                }
+            }
+        }
+    }
+
+    if(tracker->editing && tracker->focus == EDIT_PATTERN) {
+        uint16_t x = tracker->current_channel * 32 + tracker->patternx * 4 +
+                     (tracker->patternx > 0 ? 4 : 0) - 1;
+        uint16_t y = PATTERN_EDITOR_Y + 6 * ((tracker->focus == EDIT_PATTERN) ? 4 : 2) +
+                     ((tracker->focus == EDIT_PATTERN) ? 3 : 1);
+
+        canvas_draw_box(canvas, x, y, (tracker->patternx > 0 ? 5 : 9), 7);
+    }
+
+    if(!(tracker->editing) && tracker->focus == EDIT_PATTERN) {
+        uint16_t x = tracker->current_channel * 32 + tracker->patternx * 4 +
+                     (tracker->patternx > 0 ? 4 : 0) - 1;
+        uint16_t y = PATTERN_EDITOR_Y + 6 * ((tracker->focus == EDIT_PATTERN) ? 4 : 2) +
+                     ((tracker->focus == EDIT_PATTERN) ? 3 : 1);
+
+        canvas_draw_frame(canvas, x, y, (tracker->patternx > 0 ? 5 : 9), 7);
+    }
+
+    canvas_set_color(canvas, ColorBlack);
+
+    for(int i = 1; i < SONG_MAX_CHANNELS; ++i) {
+        for(int y = PATTERN_EDITOR_Y + 1; y < 64; y += 2) {
+            canvas_draw_dot(canvas, i * 32 - 1, y);
+        }
+    }
+
+    for(int i = 0; i < SONG_MAX_CHANNELS; ++i) {
+        if(tracker->tracker_engine.channel[i].channel_flags & TEC_DISABLED) {
+            canvas_draw_icon(canvas, 13 + 32 * i, PATTERN_EDITOR_Y - 3, &I_channel_off);
+        }
+
+        else {
+            canvas_draw_icon(canvas, 13 + 32 * i, PATTERN_EDITOR_Y - 3, &I_channel_on);
+        }
+    }
+
+    canvas_set_color(canvas, ColorXOR);
+}
+
+#define SEQ_SLIDER_X (4 * (4 * 2 + 1) + 2)
+#define SEQ_SLIDER_Y (32)
+
+void draw_sequence_view(Canvas* canvas, FlizzerTrackerApp* tracker) {
+    char buffer[4];
+
+    uint8_t sequence_position = tracker->tracker_engine.sequence_position;
+    TrackerSong* song = &tracker->song;
+
+    for(int pos = sequence_position - 2; pos < sequence_position + 3; pos++) {
+        if(pos >= 0 && pos < tracker->song.num_sequence_steps) {
+            for(int i = 0; i < SONG_MAX_CHANNELS; ++i) {
+                uint8_t current_pattern =
+                    tracker->tracker_engine.song->sequence.sequence_step[pos].pattern_indices[i];
+
+                uint8_t x = i * (4 * 2 + 1) + 3;
+                uint8_t y = (pos - (sequence_position - 2)) * 6 + 5;
+
+                snprintf(buffer, sizeof(buffer), "%02X", current_pattern);
+                canvas_draw_str(canvas, x, y, buffer);
+            }
+        }
+    }
+
+    if(song->loop_start != 0 || song->loop_end != 0) {
+        canvas_set_color(canvas, ColorBlack);
+
+        for(int pos = sequence_position - 2; pos < sequence_position + 3; pos++) {
+            if(pos >= 0 && pos < tracker->song.num_sequence_steps) {
+                if(pos == song->loop_start) {
+                    int16_t y = (pos - (sequence_position - 2)) * 6;
+
+                    canvas_draw_line(canvas, 0, fmax(y, 0), 1, fmax(y, 0));
+                    canvas_draw_line(canvas, 0, fmax(y, 0), 0, fmax(y + 4, 0));
+                }
+
+                if(pos > song->loop_start && pos < song->loop_end) {
+                    int16_t y = (pos - (sequence_position - 2)) * 6;
+
+                    canvas_draw_line(canvas, 0, fmax(y - 1, 0), 0, fmax(y + 4, 0));
+                }
+
+                if(pos == song->loop_end) {
+                    int16_t y = (pos - (sequence_position - 2)) * 6;
+
+                    canvas_draw_line(canvas, 0, fmax(y + 4, 0), 1, fmax(y + 4, 0));
+                    canvas_draw_line(canvas, 0, fmax(y - 1, 0), 0, fmax(y + 4, 0));
+
+                    break;
+                }
+            }
+        }
+
+        canvas_set_color(canvas, ColorXOR);
+    }
+
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_line(canvas, SEQ_SLIDER_X, 0, SEQ_SLIDER_X + 2, 0);
+    canvas_draw_line(canvas, SEQ_SLIDER_X, SEQ_SLIDER_Y, SEQ_SLIDER_X + 2, SEQ_SLIDER_Y);
+
+    canvas_draw_line(canvas, SEQ_SLIDER_X, 0, SEQ_SLIDER_X, SEQ_SLIDER_Y);
+    canvas_draw_line(canvas, SEQ_SLIDER_X + 2, 0, SEQ_SLIDER_X + 2, SEQ_SLIDER_Y);
+
+    uint8_t start_pos =
+        sequence_position * (SEQ_SLIDER_Y - 2) / tracker->song.num_sequence_steps + 1;
+    uint8_t slider_length = (SEQ_SLIDER_Y - 2) / tracker->song.num_sequence_steps + 1;
+
+    canvas_draw_line(
+        canvas, SEQ_SLIDER_X + 1, start_pos, SEQ_SLIDER_X + 1, (start_pos + slider_length));
+
+    canvas_set_color(canvas, ColorXOR);
+
+    if(tracker->editing && tracker->focus == EDIT_SEQUENCE) {
+        uint8_t x = tracker->current_channel * (4 + 4 + 1) + (tracker->current_digit ? 4 : 0) + 2;
+        uint8_t y = 11;
+
+        canvas_draw_box(canvas, x, y, 5, 7);
+    }
+
+    if(!(tracker->editing) && tracker->focus == EDIT_SEQUENCE) {
+        uint8_t x = tracker->current_channel * (4 + 4 + 1) + (tracker->current_digit ? 4 : 0) + 2;
+        uint8_t y = 11;
+
+        canvas_draw_frame(canvas, x, y, 5, 7);
+    }
+}
+
+#define member_size(type, member) sizeof(((type*)0)->member)
+
+#define SONG_HEADER_SIZE                                                                        \
+    (member_size(TrackerSong, song_name) + member_size(TrackerSong, speed) +                    \
+     member_size(TrackerSong, rate) + member_size(TrackerSong, loop_start) +                    \
+     member_size(TrackerSong, loop_end) + member_size(TrackerSong, num_patterns) +              \
+     member_size(TrackerSong, num_sequence_steps) + member_size(TrackerSong, num_instruments) + \
+     member_size(TrackerSong, pattern_length))
+
+uint32_t calculate_song_size(TrackerSong* song) {
+    uint32_t song_size =
+        SONG_HEADER_SIZE + sizeof(Instrument) * song->num_instruments +
+        sizeof(TrackerSongPatternStep) * song->num_patterns * song->pattern_length +
+        sizeof(TrackerSongSequenceStep) * song->num_sequence_steps;
+    return song_size;
+}
+
+void draw_generic_n_digit_field(
+    FlizzerTrackerApp* tracker,
+    Canvas* canvas,
+    uint8_t focus,
+    uint8_t param,
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    uint8_t digits) // last 1-2 symbols are digits we are editing
+{
+    canvas_draw_str(canvas, x, y, text);
+
+    if(tracker->focus == focus && tracker->selected_param == param && tracker->editing) {
+        bool select_string = true;
+
+        if(tracker->focus == EDIT_SONGINFO) {
+            if(param != SI_SONGNAME && param != SI_INSTRUMENTNAME) {
+                select_string = false;
+            }
+        }
+
+        if(tracker->focus == EDIT_INSTRUMENT) {
+            if(param != INST_INSTRUMENTNAME) {
+                select_string = false;
+            }
+        }
+
+        if(!(select_string)) {
+            if(tracker->focus == EDIT_INSTRUMENT && param == INST_CURRENTINSTRUMENT) {
+                canvas_draw_box(canvas, x + strlen(text) * 4 - digits * 4 - 1, y - 6, 5, 7);
+            }
+
+            else {
+                canvas_draw_box(
+                    canvas,
+                    x + strlen(text) * 4 - digits * 4 + tracker->current_digit * 4 - 1,
+                    y - 6,
+                    5,
+                    7);
+            }
+        }
+
+        else {
+            canvas_draw_box(canvas, x - 1, y - 6, fmax(5, strlen(text) * 4 + 1), 7);
+        }
+    }
+
+    if(tracker->focus == focus && tracker->selected_param == param && !(tracker->editing)) {
+        bool select_string = true;
+
+        if(tracker->focus == EDIT_SONGINFO) {
+            if(param != SI_SONGNAME && param != SI_INSTRUMENTNAME) {
+                select_string = false;
+            }
+        }
+
+        if(tracker->focus == EDIT_INSTRUMENT) {
+            if(param != INST_INSTRUMENTNAME) {
+                select_string = false;
+            }
+        }
+
+        if(!(select_string)) {
+            if(tracker->focus == EDIT_INSTRUMENT && param == INST_CURRENTINSTRUMENT) {
+                canvas_draw_frame(canvas, x + strlen(text) * 4 - digits * 4 - 1, y - 6, 5, 7);
+            }
+
+            else {
+                canvas_draw_frame(
+                    canvas,
+                    x + strlen(text) * 4 - digits * 4 + tracker->current_digit * 4 - 1,
+                    y - 6,
+                    5,
+                    7);
+            }
+        }
+
+        else {
+            canvas_draw_frame(canvas, x - 1, y - 6, fmax(5, strlen(text) * 4 + 1), 7);
+        }
+    }
+}
+
+void draw_songinfo_view(Canvas* canvas, FlizzerTrackerApp* tracker) {
+    char buffer[30];
+
+    snprintf(
+        buffer,
+        sizeof(buffer),
+        "PAT.P.%02X/%02X",
+        tracker->tracker_engine.pattern_position,
+        tracker->song.pattern_length - 1);
+    draw_generic_n_digit_field(tracker, canvas, EDIT_SONGINFO, SI_PATTERNPOS, buffer, 42, 5, 2);
+    snprintf(
+        buffer,
+        sizeof(buffer),
+        "SEQ.P.%02X/%02X",
+        tracker->tracker_engine.sequence_position,
+        tracker->song.num_sequence_steps - 1);
+    draw_generic_n_digit_field(tracker, canvas, EDIT_SONGINFO, SI_SEQUENCEPOS, buffer, 42, 11, 2);
+    snprintf(buffer, sizeof(buffer), "SPD.%02X", tracker->song.speed);
+    draw_generic_n_digit_field(tracker, canvas, EDIT_SONGINFO, SI_SONGSPEED, buffer, 42, 17, 2);
+    snprintf(buffer, sizeof(buffer), "RATE %02X", tracker->song.rate);
+    draw_generic_n_digit_field(
+        tracker, canvas, EDIT_SONGINFO, SI_SONGRATE, buffer, 42 + 4 * 7, 17, 2);
+    snprintf(buffer, sizeof(buffer), "VOL %02X", tracker->tracker_engine.master_volume);
+    draw_generic_n_digit_field(
+        tracker, canvas, EDIT_SONGINFO, SI_MASTERVOL, buffer, 42 + 4 * 7 + 4 * 8, 17, 2);
+
+    snprintf(buffer, sizeof(buffer), "SONG:");
+    canvas_draw_str(canvas, 42, 23, buffer);
+    snprintf(buffer, sizeof(buffer), "%s", tracker->song.song_name);
+    draw_generic_n_digit_field(
+        tracker, canvas, EDIT_SONGINFO, SI_SONGNAME, buffer, 42 + 4 * 5, 23, 1);
+
+    snprintf(buffer, sizeof(buffer), "INST:%c", to_char(tracker->current_instrument));
+    draw_generic_n_digit_field(
+        tracker, canvas, EDIT_SONGINFO, SI_CURRENTINSTRUMENT, buffer, 42, 29, 1);
+    snprintf(
+        buffer, sizeof(buffer), "%s", tracker->song.instrument[tracker->current_instrument]->name);
+    draw_generic_n_digit_field(
+        tracker, canvas, EDIT_SONGINFO, SI_INSTRUMENTNAME, buffer, 42 + 4 * 7, 29, 1);
+
+    uint32_t song_size = calculate_song_size(&tracker->song);
+    uint32_t free_bytes = memmgr_get_free_heap();
+    canvas_draw_line(canvas, 128 - 4 * 10 - 2, 0, 128 - 4 * 10 - 2, 10);
+
+    char song_size_buffer[19];
+    char free_bytes_buffer[19];
+
+    if(song_size > 9999) {
+        snprintf(
+            song_size_buffer,
+            sizeof(song_size_buffer),
+            "TUNE:%ld%c%01ldK",
+            song_size / 1024,
+            '.',
+            (song_size % 1024) / 103);
+    }
+
+    else {
+        snprintf(song_size_buffer, sizeof(song_size_buffer), "TUNE:%ld", song_size);
+    }
+
+    if(free_bytes > 9999) {
+        snprintf(
+            free_bytes_buffer,
+            sizeof(song_size_buffer),
+            "FREE:%ld%c%01ldK",
+            free_bytes / 1024,
+            '.',
+            (free_bytes % 1024) / 103);
+    }
+
+    else {
+        snprintf(free_bytes_buffer, sizeof(song_size_buffer), "FREE:%ld", free_bytes);
+    }
+
+    canvas_draw_str(canvas, 128 - 4 * 10, 5, song_size_buffer);
+    canvas_draw_str(canvas, 128 - 4 * 10, 11, free_bytes_buffer);
+}

+ 25 - 0
flizzer_tracker/view/pattern_editor.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include "../flizzer_tracker.h"
+#include "../tracker_engine/tracker_engine_defs.h"
+
+#include <furi.h>
+#include <gui/gui.h>
+
+extern const char to_char_array[];
+
+void draw_pattern_view(Canvas* canvas, FlizzerTrackerApp* tracker);
+void draw_sequence_view(Canvas* canvas, FlizzerTrackerApp* tracker);
+void draw_songinfo_view(Canvas* canvas, FlizzerTrackerApp* tracker);
+
+void draw_generic_n_digit_field(
+    FlizzerTrackerApp* tracker,
+    Canvas* canvas,
+    uint8_t focus,
+    uint8_t param,
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    uint8_t digits);
+char to_char(uint8_t number);
+char* notename(uint8_t note);

BIN
flizzer_tracker/wiki_images/instrument_editor.png


BIN
flizzer_tracker/wiki_images/instrument_program.png


BIN
flizzer_tracker/wiki_images/main_screen.png


BIN
flizzer_tracker/wiki_images/pattern_row.png


BIN
flizzer_tracker/wiki_images/sequence_loop.png