├── icons └── .gitignore ├── zero_tracker.h ├── zero_tracker.png ├── README.md ├── application.fam ├── tracker_engine ├── speaker_hal.h ├── tracker.h ├── tracker_notes.h ├── speaker_hal.c ├── tracker_song.h └── tracker.c ├── view ├── tracker_view.h └── tracker_view.c ├── .github └── workflows │ └── build_dev.yml └── zero_tracker.c /icons/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zero_tracker.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zero_tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrZlo13/flipper-zero-music-tracker/HEAD/zero_tracker.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipper Zero music tracker 2 | -=-=- MVP Stage: minimum viable player -=-=- 3 | 4 | [>Get latest build<](https://nightly.link/DrZlo13/flipper-zero-music-tracker/workflows/build_dev/master/zero_tracker.fap.zip) 5 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="zero_tracker", 3 | name="Zero Tracker", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="zero_tracker_app", 6 | requires=[ 7 | "gui", 8 | ], 9 | stack_size=4 * 1024, 10 | order=20, 11 | fap_icon="zero_tracker.png", 12 | fap_category="Misc", 13 | fap_icon_assets="icons", 14 | ) 15 | -------------------------------------------------------------------------------- /tracker_engine/speaker_hal.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void tracker_speaker_init(); 4 | 5 | void tracker_speaker_deinit(); 6 | 7 | void tracker_speaker_play(float frequency, float pwm); 8 | 9 | void tracker_speaker_stop(); 10 | 11 | void tracker_interrupt_init(float freq, FuriHalInterruptISR isr, void* context); 12 | 13 | void tracker_interrupt_deinit(); 14 | 15 | void tracker_debug_init(); 16 | 17 | void tracker_debug_set(bool value); 18 | 19 | void tracker_debug_deinit(); -------------------------------------------------------------------------------- /view/tracker_view.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../tracker_engine/tracker.h" 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | typedef struct TrackerView TrackerView; 9 | 10 | TrackerView* tracker_view_alloc(); 11 | 12 | void tracker_view_free(TrackerView* tracker_view); 13 | 14 | View* tracker_view_get_view(TrackerView* tracker_view); 15 | 16 | typedef void (*TrackerViewCallback)(void* context); 17 | 18 | void tracker_view_set_back_callback( 19 | TrackerView* tracker_view, 20 | TrackerViewCallback callback, 21 | void* context); 22 | 23 | void tracker_view_set_song(TrackerView* tracker_view, const Song* song); 24 | 25 | void tracker_view_set_position(TrackerView* tracker_view, uint8_t order_list_index, uint8_t row); 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif -------------------------------------------------------------------------------- /.github/workflows/build_dev.yml: -------------------------------------------------------------------------------- 1 | name: "FAP: Build and lint" 2 | on: [push, pull_request] 3 | jobs: 4 | ufbt-build-action: 5 | runs-on: ubuntu-latest 6 | name: 'ufbt: Build for Dev branch' 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | - name: Build with ufbt 11 | uses: flipperdevices/flipperzero-ufbt-action@v0.1.0 12 | id: build-app 13 | with: 14 | # Set to 'release' to build for latest published release version 15 | sdk-channel: dev 16 | - name: Upload app artifacts 17 | uses: actions/upload-artifact@v3 18 | with: 19 | name: zero_tracker.fap.zip 20 | path: ${{ steps.build-app.outputs.fap-artifacts }} 21 | # You can remove this step if you don't want to check source code formatting 22 | - name: Lint sources 23 | uses: flipperdevices/flipperzero-ufbt-action@v0.1.0 24 | with: 25 | # skip SDK setup, we already did it in previous step 26 | skip-setup: true 27 | task: lint 28 | -------------------------------------------------------------------------------- /tracker_engine/tracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "tracker_notes.h" 3 | #include "tracker_song.h" 4 | 5 | typedef enum { 6 | TrackerPositionChanged, 7 | TrackerEndOfSong, 8 | } TrackerMessageType; 9 | 10 | typedef struct { 11 | TrackerMessageType type; 12 | union tracker_message_data { 13 | struct { 14 | uint8_t order_list_index; 15 | uint8_t row; 16 | } position; 17 | } data; 18 | } TrackerMessage; 19 | 20 | typedef void (*TrackerMessageCallback)(TrackerMessage message, void* context); 21 | 22 | typedef struct Tracker Tracker; 23 | 24 | Tracker* tracker_alloc(); 25 | 26 | void tracker_free(Tracker* tracker); 27 | 28 | void tracker_set_message_callback(Tracker* tracker, TrackerMessageCallback callback, void* context); 29 | 30 | void tracker_set_song(Tracker* tracker, const Song* song); 31 | 32 | void tracker_set_order_index(Tracker* tracker, uint8_t order_index); 33 | 34 | void tracker_set_row(Tracker* tracker, uint8_t row); 35 | 36 | void tracker_start(Tracker* tracker); 37 | 38 | void tracker_stop(Tracker* tracker); -------------------------------------------------------------------------------- /tracker_engine/tracker_notes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define NOTE_NONE 0 4 | #define NOTE_C2 1 5 | #define NOTE_Cs2 2 6 | #define NOTE_D2 3 7 | #define NOTE_Ds2 4 8 | #define NOTE_E2 5 9 | #define NOTE_F2 6 10 | #define NOTE_Fs2 7 11 | #define NOTE_G2 8 12 | #define NOTE_Gs2 9 13 | #define NOTE_A2 10 14 | #define NOTE_As2 11 15 | #define NOTE_B2 12 16 | #define NOTE_C3 13 17 | #define NOTE_Cs3 14 18 | #define NOTE_D3 15 19 | #define NOTE_Ds3 16 20 | #define NOTE_E3 17 21 | #define NOTE_F3 18 22 | #define NOTE_Fs3 19 23 | #define NOTE_G3 20 24 | #define NOTE_Gs3 21 25 | #define NOTE_A3 22 26 | #define NOTE_As3 23 27 | #define NOTE_B3 24 28 | #define NOTE_C4 25 29 | #define NOTE_Cs4 26 30 | #define NOTE_D4 27 31 | #define NOTE_Ds4 28 32 | #define NOTE_E4 29 33 | #define NOTE_F4 30 34 | #define NOTE_Fs4 31 35 | #define NOTE_G4 32 36 | #define NOTE_Gs4 33 37 | #define NOTE_A4 34 38 | #define NOTE_As4 35 39 | #define NOTE_B4 36 40 | #define NOTE_C5 37 41 | #define NOTE_Cs5 38 42 | #define NOTE_D5 39 43 | #define NOTE_Ds5 40 44 | #define NOTE_E5 41 45 | #define NOTE_F5 42 46 | #define NOTE_Fs5 43 47 | #define NOTE_G5 44 48 | #define NOTE_Gs5 45 49 | #define NOTE_A5 46 50 | #define NOTE_As5 47 51 | #define NOTE_B5 48 52 | #define NOTE_C6 49 53 | #define NOTE_Cs6 50 54 | #define NOTE_D6 51 55 | #define NOTE_Ds6 52 56 | #define NOTE_E6 53 57 | #define NOTE_F6 54 58 | #define NOTE_Fs6 55 59 | #define NOTE_G6 56 60 | #define NOTE_Gs6 57 61 | #define NOTE_A6 58 62 | #define NOTE_As6 59 63 | #define NOTE_B6 60 64 | #define NOTE_OFF 63 -------------------------------------------------------------------------------- /tracker_engine/speaker_hal.c: -------------------------------------------------------------------------------- 1 | #include "speaker_hal.h" 2 | 3 | #define FURI_HAL_SPEAKER_TIMER TIM16 4 | #define FURI_HAL_SPEAKER_CHANNEL LL_TIM_CHANNEL_CH1 5 | #define FURI_HAL_SPEAKER_PRESCALER 500 6 | 7 | void tracker_speaker_play(float frequency, float pwm) { 8 | uint32_t autoreload = (SystemCoreClock / FURI_HAL_SPEAKER_PRESCALER / frequency) - 1; 9 | if(autoreload < 2) { 10 | autoreload = 2; 11 | } else if(autoreload > UINT16_MAX) { 12 | autoreload = UINT16_MAX; 13 | } 14 | 15 | if(pwm < 0) pwm = 0; 16 | if(pwm > 1) pwm = 1; 17 | 18 | uint32_t compare_value = pwm * autoreload; 19 | 20 | if(compare_value == 0) { 21 | compare_value = 1; 22 | } 23 | 24 | if(LL_TIM_OC_GetCompareCH1(FURI_HAL_SPEAKER_TIMER) != compare_value) { 25 | LL_TIM_OC_SetCompareCH1(FURI_HAL_SPEAKER_TIMER, compare_value); 26 | } 27 | 28 | if(LL_TIM_GetAutoReload(FURI_HAL_SPEAKER_TIMER) != autoreload) { 29 | LL_TIM_SetAutoReload(FURI_HAL_SPEAKER_TIMER, autoreload); 30 | if(LL_TIM_GetCounter(FURI_HAL_SPEAKER_TIMER) > autoreload) { 31 | LL_TIM_SetCounter(FURI_HAL_SPEAKER_TIMER, 0); 32 | } 33 | } 34 | 35 | LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER); 36 | } 37 | 38 | void tracker_speaker_stop() { 39 | LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER); 40 | } 41 | 42 | void tracker_speaker_init() { 43 | furi_hal_speaker_start(200.0f, 0.01f); 44 | tracker_speaker_stop(); 45 | } 46 | 47 | void tracker_speaker_deinit() { 48 | furi_hal_speaker_stop(); 49 | } 50 | 51 | static FuriHalInterruptISR tracker_isr; 52 | static void* tracker_isr_context; 53 | static void tracker_interrupt_cb(void* context) { 54 | UNUSED(context); 55 | 56 | if(LL_TIM_IsActiveFlag_UPDATE(TIM2)) { 57 | LL_TIM_ClearFlag_UPDATE(TIM2); 58 | 59 | if(tracker_isr) { 60 | tracker_isr(tracker_isr_context); 61 | } 62 | } 63 | } 64 | 65 | void tracker_interrupt_init(float freq, FuriHalInterruptISR isr, void* context) { 66 | tracker_isr = isr; 67 | tracker_isr_context = context; 68 | 69 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, tracker_interrupt_cb, NULL); 70 | 71 | LL_TIM_InitTypeDef TIM_InitStruct = {0}; 72 | // Prescaler to get 1kHz clock 73 | TIM_InitStruct.Prescaler = SystemCoreClock / 1000000 - 1; 74 | TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP; 75 | // Auto reload to get freq Hz interrupt 76 | TIM_InitStruct.Autoreload = (1000000 / freq) - 1; 77 | TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1; 78 | LL_TIM_Init(TIM2, &TIM_InitStruct); 79 | LL_TIM_EnableIT_UPDATE(TIM2); 80 | LL_TIM_EnableAllOutputs(TIM2); 81 | LL_TIM_EnableCounter(TIM2); 82 | } 83 | 84 | void tracker_interrupt_deinit() { 85 | FURI_CRITICAL_ENTER(); 86 | LL_TIM_DeInit(TIM2); 87 | FURI_CRITICAL_EXIT(); 88 | 89 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL); 90 | } 91 | 92 | void tracker_debug_init() { 93 | furi_hal_gpio_init(&gpio_ext_pc3, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); 94 | } 95 | 96 | void tracker_debug_set(bool value) { 97 | furi_hal_gpio_write(&gpio_ext_pc3, value); 98 | } 99 | 100 | void tracker_debug_deinit() { 101 | furi_hal_gpio_init(&gpio_ext_pc3, GpioModeAnalog, GpioPullNo, GpioSpeedLow); 102 | } -------------------------------------------------------------------------------- /tracker_engine/tracker_song.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /** 5 | * @brief Row 6 | * 7 | * AH AL 8 | * FEDCBA98 76543210 9 | * nnnnnnee eedddddd 10 | * -------- -------- 11 | * nnnnnn = [0] do nothing, [1..60] note number, [61] note off, [62..63] not used 12 | * ee ee = [0..F] effect 13 | * 111222 = [0..63] or [0..7, 0..7] effect data 14 | */ 15 | typedef uint16_t Row; 16 | 17 | #define ROW_NOTE_MASK 0x3F 18 | #define ROW_EFFECT_MASK 0x0F 19 | #define ROW_EFFECT_DATA_MASK 0x3F 20 | 21 | typedef enum { 22 | // 0xy, x - first semitones offset, y - second semitones offset. 0 - no offset .. 7 - +7 semitones... 23 | // Play the arpeggio chord with three notes. The first note is the base note, the second and third are offset by x and y. 24 | // Each note plays one tick. 25 | EffectArpeggio = 0x00, 26 | 27 | // 1xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest. 28 | // Slide the note pitch up by xx Hz every tick. 29 | EffectSlideUp = 0x01, 30 | 31 | // 2xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest. 32 | // Slide the note pitch down by xx Hz every tick. 33 | EffectSlideDown = 0x02, 34 | 35 | // 3xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest. 36 | // Slide the already playing note pitch towards another one by xx Hz every tick. 37 | // The note value is saved until the note is playing, so you don't have to repeat the note value to continue sliding. 38 | EffectSlideToNote = 0x03, 39 | 40 | // 4xy, x - vibrato speed (0..7), y - vibrato depth (0..7). 41 | // Vibrato effect. The pitch of the note increases by x Hz each tick to a positive vibrato depth, then decreases to a negative depth. 42 | // Value 1 of depth means 1/7 of a semitone (about 14.28 ct), so value 7 means full semitone. 43 | // Note will play without vibrato on the first tick at the beginning of the effect. 44 | // Vibrato speed and depth are saved until the note is playing, and will be updated only if they are not zero, so you doesn't have to repeat them every tick. 45 | EffectVibrato = 0x04, 46 | 47 | // Effect05 = 0x05, 48 | // Effect06 = 0x06, 49 | // Effect07 = 0x07, 50 | // Effect08 = 0x08, 51 | // Effect09 = 0x09, 52 | // Effect0A = 0x0A, 53 | 54 | // Bxx, xx - pattern number 55 | // Jump to the order xx in the pattern order table at first tick of current row. 56 | // So if you want to jump to the pattern after note 4, you should put this effect on the 5th note. 57 | EffectJumpToOrder = 0x0B, 58 | 59 | // Cxx, xx - pwm value 60 | // Set the PWM value to xx for current row. 61 | EffectPWM = 0x0C, 62 | 63 | // Bxx, xx - row number 64 | // Jump to the row xx in next pattern at first tick of current row. 65 | // So if you want to jump to the pattern after note 4, you should put this effect on the 5th note. 66 | EffectBreakPattern = 0x0D, 67 | 68 | // Effect0E = 0x0E, 69 | 70 | // Fxx, xx - song speed, 0 - 1 tick per note, 1 - 2 ticks per note, 0x3F - 64 ticks per note. 71 | // Set the speed of the song in terms of ticks per note. 72 | // Will be applied at the first tick of current row. 73 | EffectSetSpeed = 0x0F, 74 | } Effect; 75 | 76 | #define EFFECT_DATA_2(x, y) ((x) | ((y) << 3)) 77 | #define EFFECT_DATA_GET_X(data) ((data)&0x07) 78 | #define EFFECT_DATA_GET_Y(data) (((data) >> 3) & 0x07) 79 | #define EFFECT_DATA_NONE 0 80 | #define EFFECT_DATA_1_MAX 0x3F 81 | #define EFFECT_DATA_2_MAX 0x07 82 | 83 | #define FREQUENCY_UNSET -1.0f 84 | 85 | #define PWM_MIN 0.01f 86 | #define PWM_MAX 0.5f 87 | #define PWM_DEFAULT PWM_MAX 88 | 89 | #define PATTERN_SIZE 64 90 | 91 | #define ROW_MAKE(note, effect, data) \ 92 | ((Row)(((note)&0x3F) | (((effect)&0xF) << 6) | (((data)&0x3F) << 10))) 93 | 94 | typedef struct { 95 | Row rows[PATTERN_SIZE]; 96 | } Channel; 97 | 98 | typedef struct { 99 | Channel* channels; 100 | } Pattern; 101 | 102 | typedef struct { 103 | uint8_t channels_count; 104 | uint8_t patterns_count; 105 | Pattern* patterns; 106 | uint8_t order_list_size; 107 | uint8_t* order_list; 108 | uint16_t ticks_per_second; 109 | } Song; -------------------------------------------------------------------------------- /view/tracker_view.c: -------------------------------------------------------------------------------- 1 | #include "tracker_view.h" 2 | #include 3 | #include 4 | 5 | typedef struct { 6 | const Song* song; 7 | uint8_t order_list_index; 8 | uint8_t row; 9 | } TrackerViewModel; 10 | 11 | struct TrackerView { 12 | View* view; 13 | void* back_context; 14 | TrackerViewCallback back_callback; 15 | }; 16 | 17 | static Channel* get_current_channel(TrackerViewModel* model) { 18 | uint8_t channel_id = 0; 19 | uint8_t pattern_id = model->song->order_list[model->order_list_index]; 20 | Pattern* pattern = &model->song->patterns[pattern_id]; 21 | return &pattern->channels[channel_id]; 22 | } 23 | 24 | static const char* get_note_from_id(uint8_t note) { 25 | #define NOTE_COUNT 12 26 | const char* notes[NOTE_COUNT] = { 27 | "C ", 28 | "C#", 29 | "D ", 30 | "D#", 31 | "E ", 32 | "F ", 33 | "F#", 34 | "G ", 35 | "G#", 36 | "A ", 37 | "A#", 38 | "B ", 39 | }; 40 | return notes[(note) % NOTE_COUNT]; 41 | #undef NOTE_COUNT 42 | } 43 | 44 | static uint8_t get_octave_from_id(uint8_t note) { 45 | return ((note) / 12) + 2; 46 | } 47 | 48 | static uint8_t get_first_row_id(uint8_t row) { 49 | return (row / 10) * 10; 50 | } 51 | 52 | static void 53 | draw_row(Canvas* canvas, uint8_t i, Channel* channel, uint8_t row, FuriString* buffer) { 54 | uint8_t x = 12 * (i + 1); 55 | uint8_t first_row_id = get_first_row_id(row); 56 | uint8_t current_row_id = first_row_id + i; 57 | 58 | if((current_row_id) >= 64) { 59 | return; 60 | } 61 | 62 | Row current_row = channel->rows[current_row_id]; 63 | uint8_t note = current_row & ROW_NOTE_MASK; 64 | uint8_t effect = (current_row >> 6) & ROW_EFFECT_MASK; 65 | uint8_t data = (current_row >> 10) & ROW_EFFECT_DATA_MASK; 66 | 67 | if(current_row_id == row) { 68 | canvas_set_color(canvas, ColorBlack); 69 | canvas_draw_line(canvas, x - 9, 1, x - 9, 62); 70 | canvas_draw_box(canvas, x - 8, 0, 9, 64); 71 | canvas_draw_line(canvas, x + 1, 1, x + 1, 62); 72 | canvas_set_color(canvas, ColorWhite); 73 | } 74 | 75 | furi_string_printf(buffer, "%02X", current_row_id); 76 | canvas_draw_str(canvas, x, 61, furi_string_get_cstr(buffer)); 77 | 78 | if(note > 0 && note < NOTE_OFF) { 79 | furi_string_printf( 80 | buffer, "%s%d", get_note_from_id(note - 1), get_octave_from_id(note - 1)); 81 | canvas_draw_str(canvas, x, 44, furi_string_get_cstr(buffer)); 82 | } else if(note == NOTE_OFF) { 83 | canvas_draw_str(canvas, x, 44, "OFF"); 84 | } else { 85 | canvas_draw_str(canvas, x, 44, "---"); 86 | } 87 | 88 | if(effect == 0 && data == 0) { 89 | canvas_draw_str(canvas, x, 21, "-"); 90 | canvas_draw_str(canvas, x, 12, "--"); 91 | } else { 92 | furi_string_printf(buffer, "%X", effect); 93 | canvas_draw_str(canvas, x, 21, furi_string_get_cstr(buffer)); 94 | 95 | if(effect == EffectArpeggio || effect == EffectVibrato) { 96 | uint8_t data_x = EFFECT_DATA_GET_X(data); 97 | uint8_t data_y = EFFECT_DATA_GET_Y(data); 98 | furi_string_printf(buffer, "%d%d", data_x, data_y); 99 | canvas_draw_str(canvas, x, 12, furi_string_get_cstr(buffer)); 100 | } else { 101 | furi_string_printf(buffer, "%02X", data); 102 | canvas_draw_str(canvas, x, 12, furi_string_get_cstr(buffer)); 103 | } 104 | } 105 | 106 | if(current_row_id == row) { 107 | canvas_set_color(canvas, ColorBlack); 108 | } 109 | } 110 | 111 | static void tracker_view_draw_callback(Canvas* canvas, void* _model) { 112 | TrackerViewModel* model = _model; 113 | if(model->song == NULL) { 114 | return; 115 | } 116 | 117 | canvas_set_font_direction(canvas, CanvasDirectionBottomToTop); 118 | canvas_set_font(canvas, FontKeyboard); 119 | 120 | Channel* channel = get_current_channel(model); 121 | FuriString* buffer = furi_string_alloc(); 122 | 123 | for(uint8_t i = 0; i < 10; i++) { 124 | draw_row(canvas, i, channel, model->row, buffer); 125 | } 126 | furi_string_free(buffer); 127 | } 128 | 129 | static bool tracker_view_input_callback(InputEvent* event, void* context) { 130 | TrackerView* tracker_view = context; 131 | 132 | if(tracker_view->back_callback) { 133 | if(event->type == InputTypeShort && event->key == InputKeyBack) { 134 | tracker_view->back_callback(tracker_view->back_context); 135 | return true; 136 | } 137 | } 138 | return false; 139 | } 140 | 141 | TrackerView* tracker_view_alloc() { 142 | TrackerView* tracker_view = malloc(sizeof(TrackerView)); 143 | tracker_view->view = view_alloc(); 144 | view_allocate_model(tracker_view->view, ViewModelTypeLocking, sizeof(TrackerViewModel)); 145 | view_set_context(tracker_view->view, tracker_view); 146 | view_set_draw_callback(tracker_view->view, (ViewDrawCallback)tracker_view_draw_callback); 147 | view_set_input_callback(tracker_view->view, (ViewInputCallback)tracker_view_input_callback); 148 | return tracker_view; 149 | } 150 | 151 | void tracker_view_free(TrackerView* tracker_view) { 152 | view_free(tracker_view->view); 153 | free(tracker_view); 154 | } 155 | 156 | View* tracker_view_get_view(TrackerView* tracker_view) { 157 | return tracker_view->view; 158 | } 159 | 160 | void tracker_view_set_back_callback( 161 | TrackerView* tracker_view, 162 | TrackerViewCallback callback, 163 | void* context) { 164 | tracker_view->back_callback = callback; 165 | tracker_view->back_context = context; 166 | } 167 | 168 | void tracker_view_set_song(TrackerView* tracker_view, const Song* song) { 169 | with_view_model( 170 | tracker_view->view, TrackerViewModel * model, { model->song = song; }, true); 171 | } 172 | 173 | void tracker_view_set_position(TrackerView* tracker_view, uint8_t order_list_index, uint8_t row) { 174 | with_view_model( 175 | tracker_view->view, 176 | TrackerViewModel * model, 177 | { 178 | model->order_list_index = order_list_index; 179 | model->row = row; 180 | }, 181 | true); 182 | } -------------------------------------------------------------------------------- /tracker_engine/tracker.c: -------------------------------------------------------------------------------- 1 | #include "tracker.h" 2 | #include 3 | #include "speaker_hal.h" 4 | 5 | // SongState song_state = { 6 | // .tick = 0, 7 | // .tick_limit = 2, 8 | // .row = 0, 9 | // }; 10 | 11 | typedef struct { 12 | uint8_t speed; 13 | uint8_t depth; 14 | int8_t direction; 15 | int8_t value; 16 | } IntegerOscillator; 17 | 18 | typedef struct { 19 | float frequency; 20 | float frequency_target; 21 | float pwm; 22 | bool play; 23 | IntegerOscillator vibrato; 24 | } ChannelState; 25 | 26 | typedef struct { 27 | ChannelState* channels; 28 | uint8_t tick; 29 | uint8_t tick_limit; 30 | 31 | uint8_t pattern_index; 32 | uint8_t row_index; 33 | uint8_t order_list_index; 34 | } SongState; 35 | 36 | typedef struct { 37 | uint8_t note; 38 | uint8_t effect; 39 | uint8_t data; 40 | } UnpackedRow; 41 | 42 | struct Tracker { 43 | const Song* song; 44 | bool playing; 45 | TrackerMessageCallback callback; 46 | void* context; 47 | SongState song_state; 48 | }; 49 | 50 | static void channels_state_init(ChannelState* channel) { 51 | channel->frequency = 0; 52 | channel->frequency_target = FREQUENCY_UNSET; 53 | channel->pwm = PWM_DEFAULT; 54 | channel->play = false; 55 | channel->vibrato.speed = 0; 56 | channel->vibrato.depth = 0; 57 | channel->vibrato.direction = 0; 58 | channel->vibrato.value = 0; 59 | } 60 | 61 | static void tracker_song_state_init(Tracker* tracker) { 62 | tracker->song_state.tick = 0; 63 | tracker->song_state.tick_limit = 2; 64 | tracker->song_state.row_index = 0; 65 | tracker->song_state.order_list_index = 0; 66 | tracker->song_state.pattern_index = tracker->song->order_list[0]; 67 | 68 | if(tracker->song_state.channels != NULL) { 69 | free(tracker->song_state.channels); 70 | } 71 | 72 | tracker->song_state.channels = malloc(sizeof(ChannelState) * tracker->song->channels_count); 73 | for(uint8_t i = 0; i < tracker->song->channels_count; i++) { 74 | channels_state_init(&tracker->song_state.channels[i]); 75 | } 76 | } 77 | 78 | static void tracker_song_state_clear(Tracker* tracker) { 79 | if(tracker->song_state.channels != NULL) { 80 | free(tracker->song_state.channels); 81 | tracker->song_state.channels = NULL; 82 | } 83 | } 84 | 85 | static uint8_t record_get_note(Row note) { 86 | return note & ROW_NOTE_MASK; 87 | } 88 | 89 | static uint8_t record_get_effect(Row note) { 90 | return (note >> 6) & ROW_EFFECT_MASK; 91 | } 92 | 93 | static uint8_t record_get_effect_data(Row note) { 94 | return (note >> 10) & ROW_EFFECT_DATA_MASK; 95 | } 96 | 97 | #define NOTES_PER_OCT 12 98 | const float notes_oct[NOTES_PER_OCT] = { 99 | 130.813f, 100 | 138.591f, 101 | 146.832f, 102 | 155.563f, 103 | 164.814f, 104 | 174.614f, 105 | 184.997f, 106 | 195.998f, 107 | 207.652f, 108 | 220.00f, 109 | 233.082f, 110 | 246.942f, 111 | }; 112 | 113 | static float note_to_freq(uint8_t note) { 114 | if(note == NOTE_NONE) return 0.0f; 115 | note = note - NOTE_C2; 116 | uint8_t octave = note / NOTES_PER_OCT; 117 | uint8_t note_in_oct = note % NOTES_PER_OCT; 118 | return notes_oct[note_in_oct] * (1 << octave); 119 | } 120 | 121 | static float frequency_offset_semitones(float frequency, uint8_t semitones) { 122 | return frequency * (1.0f + ((1.0f / 12.0f) * semitones)); 123 | } 124 | 125 | static float frequency_get_seventh_of_a_semitone(float frequency) { 126 | return frequency * ((1.0f / 12.0f) / 7.0f); 127 | } 128 | 129 | static UnpackedRow get_current_row(const Song* song, SongState* song_state, uint8_t channel) { 130 | const Pattern* pattern = &song->patterns[song_state->pattern_index]; 131 | const Row row = pattern->channels[channel].rows[song_state->row_index]; 132 | return (UnpackedRow){ 133 | .note = record_get_note(row), 134 | .effect = record_get_effect(row), 135 | .data = record_get_effect_data(row), 136 | }; 137 | } 138 | 139 | static int16_t advance_order_and_get_next_pattern_index(const Song* song, SongState* song_state) { 140 | song_state->order_list_index++; 141 | if(song_state->order_list_index >= song->order_list_size) { 142 | return -1; 143 | } else { 144 | return song->order_list[song_state->order_list_index]; 145 | } 146 | } 147 | 148 | typedef struct { 149 | int16_t pattern; 150 | int16_t row; 151 | bool change_pattern; 152 | bool change_row; 153 | } Location; 154 | 155 | static void tracker_send_position_message(Tracker* tracker) { 156 | if(tracker->callback != NULL) { 157 | tracker->callback( 158 | (TrackerMessage){ 159 | .type = TrackerPositionChanged, 160 | .data = 161 | { 162 | .position = 163 | { 164 | .order_list_index = tracker->song_state.order_list_index, 165 | .row = tracker->song_state.row_index, 166 | }, 167 | }, 168 | }, 169 | tracker->context); 170 | } 171 | } 172 | 173 | static void tracker_send_end_message(Tracker* tracker) { 174 | if(tracker->callback != NULL) { 175 | tracker->callback((TrackerMessage){.type = TrackerEndOfSong}, tracker->context); 176 | } 177 | } 178 | 179 | static void advance_to_pattern(Tracker* tracker, Location advance) { 180 | if(advance.change_pattern) { 181 | if(advance.pattern < 0 || advance.pattern >= tracker->song->patterns_count) { 182 | tracker->playing = false; 183 | tracker_send_end_message(tracker); 184 | } else { 185 | tracker->song_state.pattern_index = advance.pattern; 186 | tracker->song_state.row_index = 0; 187 | } 188 | } 189 | 190 | if(advance.change_row) { 191 | if(advance.row < 0) advance.row = 0; 192 | if(advance.row >= PATTERN_SIZE) advance.row = PATTERN_SIZE - 1; 193 | tracker->song_state.row_index = advance.row; 194 | } 195 | 196 | tracker_send_position_message(tracker); 197 | } 198 | 199 | static void tracker_interrupt_body(Tracker* tracker) { 200 | if(!tracker->playing) { 201 | tracker_speaker_stop(); 202 | return; 203 | } 204 | 205 | const uint8_t channel_index = 0; 206 | SongState* song_state = &tracker->song_state; 207 | ChannelState* channel_state = &song_state->channels[channel_index]; 208 | const Song* song = tracker->song; 209 | UnpackedRow row = get_current_row(song, song_state, channel_index); 210 | 211 | // load frequency from note at tick 0 212 | if(song_state->tick == 0) { 213 | bool invalidate_row = false; 214 | // handle "on first tick" effects 215 | if(row.effect == EffectBreakPattern) { 216 | int16_t next_row_index = row.data; 217 | int16_t next_pattern_index = 218 | advance_order_and_get_next_pattern_index(song, song_state); 219 | advance_to_pattern( 220 | tracker, 221 | (Location){ 222 | .pattern = next_pattern_index, 223 | .row = next_row_index, 224 | .change_pattern = true, 225 | .change_row = true, 226 | }); 227 | 228 | invalidate_row = true; 229 | } 230 | 231 | if(row.effect == EffectJumpToOrder) { 232 | song_state->order_list_index = row.data; 233 | int16_t next_pattern_index = song->order_list[song_state->order_list_index]; 234 | 235 | advance_to_pattern( 236 | tracker, 237 | (Location){ 238 | .pattern = next_pattern_index, 239 | .change_pattern = true, 240 | }); 241 | 242 | invalidate_row = true; 243 | } 244 | 245 | // tracker state can be affected by effects 246 | if(!tracker->playing) { 247 | tracker_speaker_stop(); 248 | return; 249 | } 250 | 251 | if(invalidate_row) { 252 | row = get_current_row(song, song_state, channel_index); 253 | 254 | if(row.effect == EffectSetSpeed) { 255 | song_state->tick_limit = row.data; 256 | } 257 | } 258 | 259 | // handle note effects 260 | if(row.note == NOTE_OFF) { 261 | channel_state->play = false; 262 | } else if((row.note > NOTE_NONE) && (row.note < NOTE_OFF)) { 263 | channel_state->play = true; 264 | 265 | // reset vibrato 266 | channel_state->vibrato.speed = 0; 267 | channel_state->vibrato.depth = 0; 268 | channel_state->vibrato.value = 0; 269 | channel_state->vibrato.direction = 0; 270 | 271 | // reset pwm 272 | channel_state->pwm = PWM_DEFAULT; 273 | 274 | if(row.effect == EffectSlideToNote) { 275 | channel_state->frequency_target = note_to_freq(row.note); 276 | } else { 277 | channel_state->frequency = note_to_freq(row.note); 278 | channel_state->frequency_target = FREQUENCY_UNSET; 279 | } 280 | } 281 | } 282 | 283 | if(channel_state->play) { 284 | float frequency, pwm; 285 | 286 | if((row.effect == EffectSlideUp || row.effect == EffectSlideDown) && 287 | row.data != EFFECT_DATA_NONE) { 288 | // apply slide effect 289 | channel_state->frequency += (row.effect == EffectSlideUp ? 1 : -1) * row.data; 290 | } else if(row.effect == EffectSlideToNote) { 291 | // apply slide to note effect, if target frequency is set 292 | if(channel_state->frequency_target > 0) { 293 | if(channel_state->frequency_target > channel_state->frequency) { 294 | channel_state->frequency += row.data; 295 | if(channel_state->frequency > channel_state->frequency_target) { 296 | channel_state->frequency = channel_state->frequency_target; 297 | channel_state->frequency_target = FREQUENCY_UNSET; 298 | } 299 | } else if(channel_state->frequency_target < channel_state->frequency) { 300 | channel_state->frequency -= row.data; 301 | if(channel_state->frequency < channel_state->frequency_target) { 302 | channel_state->frequency = channel_state->frequency_target; 303 | channel_state->frequency_target = FREQUENCY_UNSET; 304 | } 305 | } 306 | } 307 | } 308 | 309 | frequency = channel_state->frequency; 310 | pwm = channel_state->pwm; 311 | 312 | // apply arpeggio effect 313 | if(row.effect == EffectArpeggio) { 314 | if(row.data != EFFECT_DATA_NONE) { 315 | if((song_state->tick % 3) == 1) { 316 | uint8_t note_offset = EFFECT_DATA_GET_X(row.data); 317 | frequency = frequency_offset_semitones(frequency, note_offset); 318 | } else if((song_state->tick % 3) == 2) { 319 | uint8_t note_offset = EFFECT_DATA_GET_Y(row.data); 320 | frequency = frequency_offset_semitones(frequency, note_offset); 321 | } 322 | } 323 | } else if(row.effect == EffectVibrato) { 324 | // apply vibrato effect, data = speed, depth 325 | uint8_t vibrato_speed = EFFECT_DATA_GET_X(row.data); 326 | uint8_t vibrato_depth = EFFECT_DATA_GET_Y(row.data); 327 | 328 | // update vibrato parameters if speed or depth is non-zero 329 | if(vibrato_speed != 0) channel_state->vibrato.speed = vibrato_speed; 330 | if(vibrato_depth != 0) channel_state->vibrato.depth = vibrato_depth; 331 | 332 | // update vibrato value 333 | channel_state->vibrato.value += 334 | channel_state->vibrato.direction * channel_state->vibrato.speed; 335 | 336 | // change direction if value is at the limit 337 | if(channel_state->vibrato.value > channel_state->vibrato.depth) { 338 | channel_state->vibrato.direction = -1; 339 | } else if(channel_state->vibrato.value < -channel_state->vibrato.depth) { 340 | channel_state->vibrato.direction = 1; 341 | } else if(channel_state->vibrato.direction == 0) { 342 | // set initial direction, if it is not set 343 | channel_state->vibrato.direction = 1; 344 | } 345 | 346 | frequency += 347 | (frequency_get_seventh_of_a_semitone(frequency) * channel_state->vibrato.value); 348 | } else if(row.effect == EffectPWM) { 349 | pwm = (pwm - PWM_MIN) / EFFECT_DATA_1_MAX * row.data + PWM_MIN; 350 | } 351 | 352 | tracker_speaker_play(frequency, pwm); 353 | } else { 354 | tracker_speaker_stop(); 355 | } 356 | 357 | song_state->tick++; 358 | if(song_state->tick >= song_state->tick_limit) { 359 | song_state->tick = 0; 360 | 361 | // next note 362 | song_state->row_index = (song_state->row_index + 1); 363 | 364 | if(song_state->row_index >= PATTERN_SIZE) { 365 | int16_t next_pattern_index = 366 | advance_order_and_get_next_pattern_index(song, song_state); 367 | advance_to_pattern( 368 | tracker, 369 | (Location){ 370 | .pattern = next_pattern_index, 371 | .change_pattern = true, 372 | }); 373 | } else { 374 | tracker_send_position_message(tracker); 375 | } 376 | } 377 | } 378 | 379 | static void tracker_interrupt_cb(void* context) { 380 | Tracker* tracker = (Tracker*)context; 381 | tracker_debug_set(true); 382 | tracker_interrupt_body(tracker); 383 | tracker_debug_set(false); 384 | } 385 | 386 | /********************************************************************* 387 | * Tracker Interface 388 | *********************************************************************/ 389 | 390 | Tracker* tracker_alloc() { 391 | Tracker* tracker = malloc(sizeof(Tracker)); 392 | return tracker; 393 | } 394 | 395 | void tracker_free(Tracker* tracker) { 396 | tracker_song_state_clear(tracker); 397 | free(tracker); 398 | } 399 | 400 | void tracker_set_message_callback(Tracker* tracker, TrackerMessageCallback callback, void* context) { 401 | furi_check(tracker->playing == false); 402 | tracker->callback = callback; 403 | tracker->context = context; 404 | } 405 | 406 | void tracker_set_song(Tracker* tracker, const Song* song) { 407 | furi_check(tracker->playing == false); 408 | tracker->song = song; 409 | tracker_song_state_init(tracker); 410 | } 411 | 412 | void tracker_set_order_index(Tracker* tracker, uint8_t order_index) { 413 | furi_check(tracker->playing == false); 414 | furi_check(order_index < tracker->song->order_list_size); 415 | tracker->song_state.order_list_index = order_index; 416 | tracker->song_state.pattern_index = tracker->song->order_list[order_index]; 417 | } 418 | 419 | void tracker_set_row(Tracker* tracker, uint8_t row) { 420 | furi_check(tracker->playing == false); 421 | furi_check(row < PATTERN_SIZE); 422 | tracker->song_state.row_index = row; 423 | } 424 | 425 | void tracker_start(Tracker* tracker) { 426 | furi_check(tracker->song != NULL); 427 | 428 | tracker->playing = true; 429 | tracker_send_position_message(tracker); 430 | tracker_debug_init(); 431 | tracker_speaker_init(); 432 | tracker_interrupt_init(tracker->song->ticks_per_second, tracker_interrupt_cb, tracker); 433 | } 434 | 435 | void tracker_stop(Tracker* tracker) { 436 | tracker_interrupt_deinit(); 437 | tracker_speaker_deinit(); 438 | tracker_debug_deinit(); 439 | 440 | tracker->playing = false; 441 | } -------------------------------------------------------------------------------- /zero_tracker.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "zero_tracker.h" 6 | #include "tracker_engine/tracker.h" 7 | #include "view/tracker_view.h" 8 | 9 | // Channel p_0_channels[] = { 10 | // { 11 | // .rows = 12 | // { 13 | // // 1/4 14 | // ROW_MAKE(NOTE_C3, EffectArpeggio, EFFECT_DATA_2(4, 7)), 15 | // ROW_MAKE(0, EffectArpeggio, EFFECT_DATA_2(4, 7)), 16 | // ROW_MAKE(NOTE_C4, EffectSlideToNote, 0x20), 17 | // ROW_MAKE(0, EffectSlideToNote, 0x20), 18 | // // 19 | // ROW_MAKE(0, EffectSlideToNote, 0x20), 20 | // ROW_MAKE(0, EffectSlideToNote, 0x20), 21 | // ROW_MAKE(0, EffectSlideToNote, 0x20), 22 | // ROW_MAKE(0, EffectSlideToNote, 0x20), 23 | // // 24 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 25 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 26 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 27 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 28 | // // 29 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 30 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 31 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 32 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 33 | // // 2/4 34 | // ROW_MAKE(NOTE_C3, EffectSlideDown, 0x20), 35 | // ROW_MAKE(0, EffectSlideDown, 0x20), 36 | // ROW_MAKE(NOTE_C4, EffectVibrato, EFFECT_DATA_2(3, 3)), 37 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 38 | // // 39 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 40 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 41 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 42 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 43 | // // 44 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 45 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 46 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 47 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 48 | // // 49 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 50 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 51 | // ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)), 52 | // ROW_MAKE(NOTE_OFF, EffectVibrato, EFFECT_DATA_2(3, 3)), 53 | // // 3/4 54 | // ROW_MAKE(NOTE_C3, EffectArpeggio, EFFECT_DATA_2(4, 7)), 55 | // ROW_MAKE(0, EffectArpeggio, EFFECT_DATA_2(4, 7)), 56 | // ROW_MAKE(NOTE_OFF, 0, 0), 57 | // ROW_MAKE(0, 0, 0), 58 | // // 59 | // ROW_MAKE(0, 0, 0), 60 | // ROW_MAKE(0, 0, 0), 61 | // ROW_MAKE(0, 0, 0), 62 | // ROW_MAKE(0, 0, 0), 63 | // // 64 | // ROW_MAKE(NOTE_C2, EffectPWM, 60), 65 | // ROW_MAKE(0, EffectPWM, 32), 66 | // ROW_MAKE(0, EffectPWM, 12), 67 | // ROW_MAKE(NOTE_OFF, 0, 0), 68 | // // 69 | // ROW_MAKE(0, 0, 0), 70 | // ROW_MAKE(0, 0, 0), 71 | // ROW_MAKE(0, 0, 0), 72 | // ROW_MAKE(0, 0, 0), 73 | // // 4/4 74 | // ROW_MAKE(NOTE_C3, EffectSlideDown, 0x20), 75 | // ROW_MAKE(0, EffectSlideDown, 0x20), 76 | // ROW_MAKE(0, EffectSlideDown, 0x20), 77 | // ROW_MAKE(NOTE_OFF, 0, 0), 78 | // // 79 | // ROW_MAKE(0, 0, 0), 80 | // ROW_MAKE(0, 0, 0), 81 | // ROW_MAKE(0, 0, 0), 82 | // ROW_MAKE(0, 0, 0), 83 | // // 84 | // ROW_MAKE(NOTE_C2, EffectPWM, 60), 85 | // ROW_MAKE(0, EffectPWM, 32), 86 | // ROW_MAKE(0, EffectPWM, 12), 87 | // ROW_MAKE(NOTE_OFF, 0, 0), 88 | // // 89 | // ROW_MAKE(0, 0, 0), 90 | // ROW_MAKE(0, 0, 0), 91 | // ROW_MAKE(0, 0, 0), 92 | // ROW_MAKE(0, 0, 0), 93 | // }, 94 | // }, 95 | // }; 96 | 97 | Channel p_0_channels[] = { 98 | { 99 | .rows = 100 | { 101 | // 102 | ROW_MAKE(NOTE_A4, EffectArpeggio, EFFECT_DATA_2(4, 7)), 103 | ROW_MAKE(NOTE_C3, 0, 0), 104 | ROW_MAKE(NOTE_F2, 0, 0), 105 | ROW_MAKE(NOTE_C3, 0, 0), 106 | // 107 | ROW_MAKE(NOTE_E4, 0, 0), 108 | ROW_MAKE(NOTE_C3, 0, 0), 109 | ROW_MAKE(NOTE_E4, EffectPWM, 50), 110 | ROW_MAKE(NOTE_OFF, 0, 0), 111 | // 112 | ROW_MAKE(NOTE_A4, 0, 0), 113 | ROW_MAKE(0, EffectPWM, 55), 114 | ROW_MAKE(0, EffectPWM, 45), 115 | ROW_MAKE(NOTE_OFF, 0, 0), 116 | // 117 | ROW_MAKE(NOTE_E5, 0, 0), 118 | ROW_MAKE(0, EffectPWM, 55), 119 | ROW_MAKE(0, EffectPWM, 45), 120 | ROW_MAKE(NOTE_OFF, 0, 0), 121 | // 122 | ROW_MAKE(NOTE_D5, 0, 0), 123 | ROW_MAKE(NOTE_C3, EffectSlideDown, 0x30), 124 | ROW_MAKE(NOTE_F2, 0, 0), 125 | ROW_MAKE(NOTE_C3, 0, 0), 126 | // 127 | ROW_MAKE(NOTE_C5, 0, 0), 128 | ROW_MAKE(NOTE_C3, 0, 0), 129 | ROW_MAKE(NOTE_C5, 0, 0), 130 | ROW_MAKE(NOTE_OFF, 0, 0), 131 | // 132 | ROW_MAKE(NOTE_A4, 0, 0), 133 | ROW_MAKE(0, 0, 0), 134 | ROW_MAKE(0, 0, 0), 135 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 136 | // 137 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 138 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 139 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 140 | ROW_MAKE(NOTE_OFF, 0, 0), 141 | // 142 | ROW_MAKE(NOTE_B4, EffectArpeggio, EFFECT_DATA_2(4, 7)), 143 | ROW_MAKE(NOTE_D3, 0, 0), 144 | ROW_MAKE(NOTE_G2, 0, 0), 145 | ROW_MAKE(NOTE_D3, 0, 0), 146 | // 147 | ROW_MAKE(NOTE_E4, 0, 0), 148 | ROW_MAKE(NOTE_D3, 0, 0), 149 | ROW_MAKE(NOTE_E4, EffectPWM, 50), 150 | ROW_MAKE(NOTE_OFF, 0, 0), 151 | // 152 | ROW_MAKE(NOTE_A4, 0, 0), 153 | ROW_MAKE(0, EffectPWM, 55), 154 | ROW_MAKE(0, EffectPWM, 45), 155 | ROW_MAKE(NOTE_OFF, 0, 0), 156 | // 157 | ROW_MAKE(NOTE_E5, 0, 0), 158 | ROW_MAKE(0, EffectPWM, 55), 159 | ROW_MAKE(0, EffectPWM, 45), 160 | ROW_MAKE(NOTE_OFF, 0, 0), 161 | // 162 | ROW_MAKE(NOTE_D5, 0, 0), 163 | ROW_MAKE(NOTE_D3, EffectSlideDown, 0x3F), 164 | ROW_MAKE(NOTE_G2, 0, 0), 165 | ROW_MAKE(NOTE_D3, 0, 0), 166 | // 167 | ROW_MAKE(NOTE_C5, 0, 0), 168 | ROW_MAKE(NOTE_D3, 0, 0), 169 | ROW_MAKE(NOTE_C5, 0, 0), 170 | ROW_MAKE(NOTE_OFF, 0, 0), 171 | // 172 | ROW_MAKE(NOTE_A4, 0, 0), 173 | ROW_MAKE(0, 0, 0), 174 | ROW_MAKE(0, 0, 0), 175 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 176 | // 177 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 178 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 179 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 180 | ROW_MAKE(NOTE_OFF, 0, 0), 181 | }, 182 | }, 183 | }; 184 | 185 | Channel p_1_channels[] = { 186 | { 187 | .rows = 188 | { 189 | // 190 | ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)), 191 | ROW_MAKE(NOTE_E3, 0, 0), 192 | ROW_MAKE(NOTE_A2, 0, 0), 193 | ROW_MAKE(NOTE_E3, 0, 0), 194 | // 195 | ROW_MAKE(NOTE_B4, 0, 0), 196 | ROW_MAKE(NOTE_E3, 0, 0), 197 | ROW_MAKE(NOTE_B4, EffectPWM, 50), 198 | ROW_MAKE(NOTE_OFF, 0, 0), 199 | // 200 | ROW_MAKE(NOTE_G4, 0, 0), 201 | ROW_MAKE(0, EffectPWM, 55), 202 | ROW_MAKE(0, EffectPWM, 45), 203 | ROW_MAKE(NOTE_OFF, 0, 0), 204 | // 205 | ROW_MAKE(NOTE_C5, 0, 0), 206 | ROW_MAKE(0, EffectPWM, 55), 207 | ROW_MAKE(0, EffectPWM, 45), 208 | ROW_MAKE(NOTE_OFF, 0, 0), 209 | // 210 | ROW_MAKE(NOTE_C6, 0, 0), 211 | ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30), 212 | ROW_MAKE(NOTE_A2, 0, 0), 213 | ROW_MAKE(NOTE_E3, 0, 0), 214 | // 215 | ROW_MAKE(NOTE_B4, 0, 0), 216 | ROW_MAKE(NOTE_E3, 0, 0), 217 | ROW_MAKE(NOTE_B4, EffectPWM, 50), 218 | ROW_MAKE(NOTE_OFF, 0, 0), 219 | // 220 | ROW_MAKE(NOTE_G4, 0, 0), 221 | ROW_MAKE(0, 0, 0), 222 | ROW_MAKE(0, 0, 0), 223 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 224 | // 225 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 226 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 227 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 228 | ROW_MAKE(NOTE_OFF, 0, 0), 229 | // 230 | ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)), 231 | ROW_MAKE(NOTE_E3, 0, 0), 232 | ROW_MAKE(NOTE_A2, 0, 0), 233 | ROW_MAKE(NOTE_E3, 0, 0), 234 | // 235 | ROW_MAKE(NOTE_B4, 0, 0), 236 | ROW_MAKE(NOTE_E3, 0, 0), 237 | ROW_MAKE(NOTE_B4, EffectPWM, 50), 238 | ROW_MAKE(NOTE_OFF, 0, 0), 239 | // 240 | ROW_MAKE(NOTE_G4, 0, 0), 241 | ROW_MAKE(0, EffectPWM, 55), 242 | ROW_MAKE(0, EffectPWM, 45), 243 | ROW_MAKE(NOTE_OFF, 0, 0), 244 | // 245 | ROW_MAKE(NOTE_D5, 0, 0), 246 | ROW_MAKE(0, EffectPWM, 55), 247 | ROW_MAKE(0, EffectPWM, 45), 248 | ROW_MAKE(NOTE_OFF, 0, 0), 249 | // 250 | ROW_MAKE(NOTE_C6, 0, 0), 251 | ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30), 252 | ROW_MAKE(NOTE_A2, 0, 0), 253 | ROW_MAKE(NOTE_E3, 0, 0), 254 | // 255 | ROW_MAKE(NOTE_B4, 0, 0), 256 | ROW_MAKE(NOTE_E3, 0, 0), 257 | ROW_MAKE(NOTE_B4, EffectPWM, 50), 258 | ROW_MAKE(NOTE_OFF, 0, 0), 259 | // 260 | ROW_MAKE(NOTE_G4, 0, 0), 261 | ROW_MAKE(0, 0, 0), 262 | ROW_MAKE(0, 0, 0), 263 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 264 | // 265 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)), 266 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 267 | ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)), 268 | ROW_MAKE(NOTE_OFF, 0, 0), 269 | }, 270 | }, 271 | }; 272 | 273 | Channel p_2_channels[] = { 274 | { 275 | .rows = 276 | { 277 | // 278 | ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)), 279 | ROW_MAKE(NOTE_E3, 0, 0), 280 | ROW_MAKE(NOTE_A2, 0, 0), 281 | ROW_MAKE(NOTE_E3, 0, 0), 282 | // 283 | ROW_MAKE(NOTE_C5, 0, 0), 284 | ROW_MAKE(NOTE_A4, 0, 0), 285 | ROW_MAKE(NOTE_C5, 0, 0), 286 | ROW_MAKE(NOTE_A4, 0, 0), 287 | // 288 | ROW_MAKE(NOTE_C5, EffectPWM, 55), 289 | ROW_MAKE(NOTE_A4, EffectPWM, 45), 290 | ROW_MAKE(NOTE_C5, EffectPWM, 35), 291 | ROW_MAKE(NOTE_OFF, 0, 0), 292 | // 293 | ROW_MAKE(NOTE_C5, 0, 0), 294 | ROW_MAKE(NOTE_A4, 0, 0), 295 | ROW_MAKE(NOTE_C5, EffectPWM, 55), 296 | ROW_MAKE(NOTE_OFF, 0, 0), 297 | // 298 | ROW_MAKE(NOTE_D5, 0, 0), 299 | ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30), 300 | ROW_MAKE(NOTE_A2, 0, 0), 301 | ROW_MAKE(NOTE_E3, 0, 0), 302 | // 303 | ROW_MAKE(NOTE_OFF, 0, 0), 304 | ROW_MAKE(NOTE_E3, 0, 0), 305 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 306 | ROW_MAKE(NOTE_OFF, 0, 0), 307 | // 308 | ROW_MAKE(NOTE_D5, 0, 0), 309 | ROW_MAKE(NOTE_B4, 0, 0), 310 | ROW_MAKE(NOTE_D5, EffectPWM, 55), 311 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 312 | // 313 | ROW_MAKE(NOTE_D5, EffectPWM, 45), 314 | ROW_MAKE(NOTE_B4, EffectPWM, 45), 315 | ROW_MAKE(NOTE_D5, EffectPWM, 35), 316 | ROW_MAKE(NOTE_OFF, 0, 0), 317 | // 318 | ROW_MAKE(NOTE_D5, EffectArpeggio, EFFECT_DATA_2(4, 7)), 319 | ROW_MAKE(NOTE_E3, 0, 0), 320 | ROW_MAKE(NOTE_A2, 0, 0), 321 | ROW_MAKE(NOTE_E3, 0, 0), 322 | // 323 | ROW_MAKE(NOTE_E5, 0, 0), 324 | ROW_MAKE(NOTE_C5, 0, 0), 325 | ROW_MAKE(NOTE_E5, 0, 0), 326 | ROW_MAKE(NOTE_C5, 0, 0), 327 | // 328 | ROW_MAKE(NOTE_E5, EffectPWM, 55), 329 | ROW_MAKE(NOTE_C5, EffectPWM, 45), 330 | ROW_MAKE(NOTE_E5, EffectPWM, 35), 331 | ROW_MAKE(NOTE_OFF, 0, 0), 332 | // 333 | ROW_MAKE(NOTE_E5, 0, 0), 334 | ROW_MAKE(NOTE_C5, 0, 0), 335 | ROW_MAKE(NOTE_E5, EffectPWM, 55), 336 | ROW_MAKE(NOTE_OFF, 0, 0), 337 | // 338 | ROW_MAKE(NOTE_D5, 0, 0), 339 | ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30), 340 | ROW_MAKE(NOTE_A2, 0, 0), 341 | ROW_MAKE(NOTE_E3, 0, 0), 342 | // 343 | ROW_MAKE(NOTE_OFF, 0, 0), 344 | ROW_MAKE(NOTE_E3, 0, 0), 345 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 346 | ROW_MAKE(NOTE_OFF, 0, 0), 347 | // 348 | ROW_MAKE(NOTE_D5, 0, 0), 349 | ROW_MAKE(NOTE_B4, 0, 0), 350 | ROW_MAKE(NOTE_D5, EffectPWM, 55), 351 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 352 | // 353 | ROW_MAKE(NOTE_D5, EffectPWM, 45), 354 | ROW_MAKE(NOTE_B4, EffectPWM, 45), 355 | ROW_MAKE(NOTE_D5, EffectPWM, 35), 356 | ROW_MAKE(NOTE_OFF, 0, 0), 357 | }, 358 | }, 359 | }; 360 | 361 | Channel p_3_channels[] = { 362 | { 363 | .rows = 364 | { 365 | // 366 | ROW_MAKE(NOTE_Ds5, EffectArpeggio, EFFECT_DATA_2(4, 6)), 367 | ROW_MAKE(NOTE_C5, 0, 0), 368 | ROW_MAKE(NOTE_Ds5, 0, 0), 369 | ROW_MAKE(NOTE_C5, EffectPWM, 55), 370 | // 371 | ROW_MAKE(NOTE_Ds5, EffectPWM, 45), 372 | ROW_MAKE(NOTE_C5, EffectPWM, 35), 373 | ROW_MAKE(NOTE_Ds5, EffectPWM, 30), 374 | ROW_MAKE(NOTE_OFF, 0, 0), 375 | // 376 | ROW_MAKE(NOTE_D5, 0, 0), 377 | ROW_MAKE(NOTE_B4, 0, 0), 378 | ROW_MAKE(NOTE_D5, 0, 0), 379 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 380 | // 381 | ROW_MAKE(NOTE_D5, EffectPWM, 45), 382 | ROW_MAKE(NOTE_B4, EffectPWM, 35), 383 | ROW_MAKE(NOTE_D5, EffectPWM, 30), 384 | ROW_MAKE(NOTE_OFF, 0, 0), 385 | // 386 | ROW_MAKE(NOTE_Cs5, EffectArpeggio, EFFECT_DATA_2(4, 6)), 387 | ROW_MAKE(NOTE_As4, 0, 0), 388 | ROW_MAKE(NOTE_Cs5, 0, 0), 389 | ROW_MAKE(NOTE_As4, EffectPWM, 55), 390 | // 391 | ROW_MAKE(NOTE_Cs5, EffectPWM, 45), 392 | ROW_MAKE(NOTE_As4, EffectPWM, 35), 393 | ROW_MAKE(NOTE_Cs5, EffectPWM, 30), 394 | ROW_MAKE(NOTE_OFF, 0, 0), 395 | // 396 | ROW_MAKE(NOTE_C5, 0, 0), 397 | ROW_MAKE(NOTE_A4, 0, 0), 398 | ROW_MAKE(NOTE_C5, 0, 0), 399 | ROW_MAKE(NOTE_A4, EffectPWM, 55), 400 | // 401 | ROW_MAKE(NOTE_C5, EffectPWM, 45), 402 | ROW_MAKE(NOTE_A4, EffectPWM, 35), 403 | ROW_MAKE(NOTE_C5, EffectPWM, 30), 404 | ROW_MAKE(NOTE_OFF, 0, 0), 405 | // 406 | ROW_MAKE(NOTE_B4, EffectArpeggio, EFFECT_DATA_2(4, 6)), 407 | ROW_MAKE(NOTE_Gs4, 0, 0), 408 | ROW_MAKE(NOTE_B4, 0, 0), 409 | ROW_MAKE(NOTE_Gs4, EffectPWM, 55), 410 | // 411 | ROW_MAKE(NOTE_B4, EffectPWM, 45), 412 | ROW_MAKE(NOTE_Gs4, EffectPWM, 35), 413 | ROW_MAKE(NOTE_B4, EffectPWM, 30), 414 | ROW_MAKE(NOTE_OFF, 0, 0), 415 | // 416 | ROW_MAKE(NOTE_C5, 0, 0), 417 | ROW_MAKE(NOTE_A4, 0, 0), 418 | ROW_MAKE(NOTE_C5, 0, 0), 419 | ROW_MAKE(NOTE_A4, EffectPWM, 55), 420 | // 421 | ROW_MAKE(NOTE_C5, EffectPWM, 45), 422 | ROW_MAKE(NOTE_A4, EffectPWM, 35), 423 | ROW_MAKE(NOTE_C5, EffectPWM, 30), 424 | ROW_MAKE(NOTE_OFF, 0, 0), 425 | // 426 | ROW_MAKE(NOTE_Cs5, EffectArpeggio, EFFECT_DATA_2(4, 6)), 427 | ROW_MAKE(NOTE_As4, 0, 0), 428 | ROW_MAKE(NOTE_Cs5, 0, 0), 429 | ROW_MAKE(NOTE_As4, EffectPWM, 55), 430 | // 431 | ROW_MAKE(NOTE_Cs5, EffectPWM, 45), 432 | ROW_MAKE(NOTE_As4, EffectPWM, 35), 433 | ROW_MAKE(NOTE_Cs5, EffectPWM, 30), 434 | ROW_MAKE(NOTE_OFF, 0, 0), 435 | // 436 | ROW_MAKE(NOTE_D5, 0, 0), 437 | ROW_MAKE(NOTE_B4, 0, 0), 438 | ROW_MAKE(NOTE_D5, 0, 0), 439 | ROW_MAKE(NOTE_B4, EffectPWM, 55), 440 | // 441 | ROW_MAKE(NOTE_D5, EffectPWM, 45), 442 | ROW_MAKE(NOTE_B4, EffectPWM, 35), 443 | ROW_MAKE(NOTE_D5, EffectPWM, 30), 444 | ROW_MAKE(NOTE_OFF, 0, 0), 445 | }, 446 | }, 447 | }; 448 | Pattern patterns[] = { 449 | {.channels = p_0_channels}, 450 | {.channels = p_1_channels}, 451 | {.channels = p_2_channels}, 452 | {.channels = p_3_channels}, 453 | }; 454 | 455 | uint8_t order_list[] = { 456 | 0, 457 | 1, 458 | 0, 459 | 2, 460 | 0, 461 | 1, 462 | 0, 463 | 3, 464 | }; 465 | 466 | Song song = { 467 | .channels_count = 1, 468 | .patterns_count = sizeof(patterns) / sizeof(patterns[0]), 469 | .patterns = patterns, 470 | 471 | .order_list_size = sizeof(order_list) / sizeof(order_list[0]), 472 | .order_list = order_list, 473 | 474 | .ticks_per_second = 60, 475 | }; 476 | 477 | void tracker_message(TrackerMessage message, void* context) { 478 | FuriMessageQueue* queue = context; 479 | furi_assert(queue); 480 | furi_message_queue_put(queue, &message, 0); 481 | } 482 | 483 | int32_t zero_tracker_app(void* p) { 484 | UNUSED(p); 485 | 486 | NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); 487 | notification_message(notification, &sequence_display_backlight_enforce_on); 488 | 489 | Gui* gui = furi_record_open(RECORD_GUI); 490 | ViewDispatcher* view_dispatcher = view_dispatcher_alloc(); 491 | TrackerView* tracker_view = tracker_view_alloc(); 492 | tracker_view_set_song(tracker_view, &song); 493 | view_dispatcher_add_view(view_dispatcher, 0, tracker_view_get_view(tracker_view)); 494 | view_dispatcher_attach_to_gui(view_dispatcher, gui, ViewDispatcherTypeFullscreen); 495 | view_dispatcher_switch_to_view(view_dispatcher, 0); 496 | 497 | FuriMessageQueue* queue = furi_message_queue_alloc(8, sizeof(TrackerMessage)); 498 | Tracker* tracker = tracker_alloc(); 499 | tracker_set_message_callback(tracker, tracker_message, queue); 500 | tracker_set_song(tracker, &song); 501 | tracker_start(tracker); 502 | 503 | while(1) { 504 | TrackerMessage message; 505 | FuriStatus status = furi_message_queue_get(queue, &message, portMAX_DELAY); 506 | if(status == FuriStatusOk) { 507 | if(message.type == TrackerPositionChanged) { 508 | uint8_t order_list_index = message.data.position.order_list_index; 509 | uint8_t row = message.data.position.row; 510 | uint8_t pattern = song.order_list[order_list_index]; 511 | tracker_view_set_position(tracker_view, order_list_index, row); 512 | FURI_LOG_I("Tracker", "O:%d P:%d R:%d", order_list_index, pattern, row); 513 | } else if(message.type == TrackerEndOfSong) { 514 | FURI_LOG_I("Tracker", "End of song"); 515 | break; 516 | } 517 | } 518 | } 519 | 520 | tracker_stop(tracker); 521 | tracker_free(tracker); 522 | furi_message_queue_free(queue); 523 | 524 | furi_delay_ms(500); 525 | 526 | view_dispatcher_remove_view(view_dispatcher, 0); 527 | tracker_view_free(tracker_view); 528 | view_dispatcher_free(view_dispatcher); 529 | 530 | notification_message(notification, &sequence_display_backlight_enforce_auto); 531 | 532 | furi_record_close(RECORD_NOTIFICATION); 533 | furi_record_close(RECORD_GUI); 534 | 535 | return 0; 536 | } --------------------------------------------------------------------------------