Bläddra i källkod

Add music_tracker from https://github.com/DrZlo13/flipper-zero-music-tracker

git-subtree-dir: music_tracker
git-subtree-mainline: 5e7d42c674cdbe7c621e15c62f23b8099fd2a84e
git-subtree-split: 90da4c9c1ac3bd152756ed8daa7e53b48c997b6a
Willy-JL 2 år sedan
förälder
incheckning
ce3273baf1

+ 27 - 0
music_tracker/.github/workflows/build_dev.yml

@@ -0,0 +1,27 @@
+name: "FAP: Build and lint"
+on: [push, pull_request]
+jobs:
+  ufbt-build-action:
+    runs-on: ubuntu-latest
+    name: 'ufbt: Build for Dev branch'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.0
+        id: build-app
+        with:
+          # Set to 'release' to build for latest published release version
+          sdk-channel: dev
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: zero_tracker.fap.zip
+          path: ${{ steps.build-app.outputs.fap-artifacts }}
+      # You can remove this step if you don't want to check source code formatting
+      - name: Lint sources
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.0
+        with:
+          # skip SDK setup, we already did it in previous step
+          skip-setup: true
+          task: lint

+ 1 - 0
music_tracker/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/DrZlo13/flipper-zero-music-tracker master

+ 4 - 0
music_tracker/README.md

@@ -0,0 +1,4 @@
+# Flipper Zero music tracker
+-=-=- MVP Stage: minimum viable player -=-=-
+
+[>Get latest build<](https://nightly.link/DrZlo13/flipper-zero-music-tracker/workflows/build_dev/master/zero_tracker.fap.zip)

+ 14 - 0
music_tracker/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="zero_tracker",
+    name="Zero Tracker",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="zero_tracker_app",
+    requires=[
+        "gui",
+    ],
+    stack_size=4 * 1024,
+    order=20,
+    fap_icon="zero_tracker.png",
+    fap_category="Misc",
+    fap_icon_assets="icons",
+)

+ 0 - 0
music_tracker/icons/.gitignore


+ 102 - 0
music_tracker/tracker_engine/speaker_hal.c

@@ -0,0 +1,102 @@
+#include "speaker_hal.h"
+
+#define FURI_HAL_SPEAKER_TIMER TIM16
+#define FURI_HAL_SPEAKER_CHANNEL LL_TIM_CHANNEL_CH1
+#define FURI_HAL_SPEAKER_PRESCALER 500
+
+void tracker_speaker_play(float frequency, float pwm) {
+    uint32_t autoreload = (SystemCoreClock / FURI_HAL_SPEAKER_PRESCALER / frequency) - 1;
+    if(autoreload < 2) {
+        autoreload = 2;
+    } else if(autoreload > UINT16_MAX) {
+        autoreload = UINT16_MAX;
+    }
+
+    if(pwm < 0) pwm = 0;
+    if(pwm > 1) pwm = 1;
+
+    uint32_t compare_value = pwm * autoreload;
+
+    if(compare_value == 0) {
+        compare_value = 1;
+    }
+
+    if(LL_TIM_OC_GetCompareCH1(FURI_HAL_SPEAKER_TIMER) != compare_value) {
+        LL_TIM_OC_SetCompareCH1(FURI_HAL_SPEAKER_TIMER, compare_value);
+    }
+
+    if(LL_TIM_GetAutoReload(FURI_HAL_SPEAKER_TIMER) != autoreload) {
+        LL_TIM_SetAutoReload(FURI_HAL_SPEAKER_TIMER, autoreload);
+        if(LL_TIM_GetCounter(FURI_HAL_SPEAKER_TIMER) > autoreload) {
+            LL_TIM_SetCounter(FURI_HAL_SPEAKER_TIMER, 0);
+        }
+    }
+
+    LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+}
+
+void tracker_speaker_stop() {
+    LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+}
+
+void tracker_speaker_init() {
+    furi_hal_speaker_start(200.0f, 0.01f);
+    tracker_speaker_stop();
+}
+
+void tracker_speaker_deinit() {
+    furi_hal_speaker_stop();
+}
+
+static FuriHalInterruptISR tracker_isr;
+static void* tracker_isr_context;
+static void tracker_interrupt_cb(void* context) {
+    UNUSED(context);
+
+    if(LL_TIM_IsActiveFlag_UPDATE(TIM2)) {
+        LL_TIM_ClearFlag_UPDATE(TIM2);
+
+        if(tracker_isr) {
+            tracker_isr(tracker_isr_context);
+        }
+    }
+}
+
+void tracker_interrupt_init(float freq, FuriHalInterruptISR isr, void* context) {
+    tracker_isr = isr;
+    tracker_isr_context = context;
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, tracker_interrupt_cb, NULL);
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    // Prescaler to get 1kHz clock
+    TIM_InitStruct.Prescaler = SystemCoreClock / 1000000 - 1;
+    TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+    // Auto reload to get freq Hz interrupt
+    TIM_InitStruct.Autoreload = (1000000 / freq) - 1;
+    TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
+    LL_TIM_Init(TIM2, &TIM_InitStruct);
+    LL_TIM_EnableIT_UPDATE(TIM2);
+    LL_TIM_EnableAllOutputs(TIM2);
+    LL_TIM_EnableCounter(TIM2);
+}
+
+void tracker_interrupt_deinit() {
+    FURI_CRITICAL_ENTER();
+    LL_TIM_DeInit(TIM2);
+    FURI_CRITICAL_EXIT();
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL);
+}
+
+void tracker_debug_init() {
+    furi_hal_gpio_init(&gpio_ext_pc3, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+}
+
+void tracker_debug_set(bool value) {
+    furi_hal_gpio_write(&gpio_ext_pc3, value);
+}
+
+void tracker_debug_deinit() {
+    furi_hal_gpio_init(&gpio_ext_pc3, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+}

+ 19 - 0
music_tracker/tracker_engine/speaker_hal.h

@@ -0,0 +1,19 @@
+#include <furi_hal.h>
+
+void tracker_speaker_init();
+
+void tracker_speaker_deinit();
+
+void tracker_speaker_play(float frequency, float pwm);
+
+void tracker_speaker_stop();
+
+void tracker_interrupt_init(float freq, FuriHalInterruptISR isr, void* context);
+
+void tracker_interrupt_deinit();
+
+void tracker_debug_init();
+
+void tracker_debug_set(bool value);
+
+void tracker_debug_deinit();

+ 441 - 0
music_tracker/tracker_engine/tracker.c

@@ -0,0 +1,441 @@
+#include "tracker.h"
+#include <stdbool.h>
+#include "speaker_hal.h"
+
+// SongState song_state = {
+//     .tick = 0,
+//     .tick_limit = 2,
+//     .row = 0,
+// };
+
+typedef struct {
+    uint8_t speed;
+    uint8_t depth;
+    int8_t direction;
+    int8_t value;
+} IntegerOscillator;
+
+typedef struct {
+    float frequency;
+    float frequency_target;
+    float pwm;
+    bool play;
+    IntegerOscillator vibrato;
+} ChannelState;
+
+typedef struct {
+    ChannelState* channels;
+    uint8_t tick;
+    uint8_t tick_limit;
+
+    uint8_t pattern_index;
+    uint8_t row_index;
+    uint8_t order_list_index;
+} SongState;
+
+typedef struct {
+    uint8_t note;
+    uint8_t effect;
+    uint8_t data;
+} UnpackedRow;
+
+struct Tracker {
+    const Song* song;
+    bool playing;
+    TrackerMessageCallback callback;
+    void* context;
+    SongState song_state;
+};
+
+static void channels_state_init(ChannelState* channel) {
+    channel->frequency = 0;
+    channel->frequency_target = FREQUENCY_UNSET;
+    channel->pwm = PWM_DEFAULT;
+    channel->play = false;
+    channel->vibrato.speed = 0;
+    channel->vibrato.depth = 0;
+    channel->vibrato.direction = 0;
+    channel->vibrato.value = 0;
+}
+
+static void tracker_song_state_init(Tracker* tracker) {
+    tracker->song_state.tick = 0;
+    tracker->song_state.tick_limit = 2;
+    tracker->song_state.row_index = 0;
+    tracker->song_state.order_list_index = 0;
+    tracker->song_state.pattern_index = tracker->song->order_list[0];
+
+    if(tracker->song_state.channels != NULL) {
+        free(tracker->song_state.channels);
+    }
+
+    tracker->song_state.channels = malloc(sizeof(ChannelState) * tracker->song->channels_count);
+    for(uint8_t i = 0; i < tracker->song->channels_count; i++) {
+        channels_state_init(&tracker->song_state.channels[i]);
+    }
+}
+
+static void tracker_song_state_clear(Tracker* tracker) {
+    if(tracker->song_state.channels != NULL) {
+        free(tracker->song_state.channels);
+        tracker->song_state.channels = NULL;
+    }
+}
+
+static uint8_t record_get_note(Row note) {
+    return note & ROW_NOTE_MASK;
+}
+
+static uint8_t record_get_effect(Row note) {
+    return (note >> 6) & ROW_EFFECT_MASK;
+}
+
+static uint8_t record_get_effect_data(Row note) {
+    return (note >> 10) & ROW_EFFECT_DATA_MASK;
+}
+
+#define NOTES_PER_OCT 12
+const float notes_oct[NOTES_PER_OCT] = {
+    130.813f,
+    138.591f,
+    146.832f,
+    155.563f,
+    164.814f,
+    174.614f,
+    184.997f,
+    195.998f,
+    207.652f,
+    220.00f,
+    233.082f,
+    246.942f,
+};
+
+static float note_to_freq(uint8_t note) {
+    if(note == NOTE_NONE) return 0.0f;
+    note = note - NOTE_C2;
+    uint8_t octave = note / NOTES_PER_OCT;
+    uint8_t note_in_oct = note % NOTES_PER_OCT;
+    return notes_oct[note_in_oct] * (1 << octave);
+}
+
+static float frequency_offset_semitones(float frequency, uint8_t semitones) {
+    return frequency * (1.0f + ((1.0f / 12.0f) * semitones));
+}
+
+static float frequency_get_seventh_of_a_semitone(float frequency) {
+    return frequency * ((1.0f / 12.0f) / 7.0f);
+}
+
+static UnpackedRow get_current_row(const Song* song, SongState* song_state, uint8_t channel) {
+    const Pattern* pattern = &song->patterns[song_state->pattern_index];
+    const Row row = pattern->channels[channel].rows[song_state->row_index];
+    return (UnpackedRow){
+        .note = record_get_note(row),
+        .effect = record_get_effect(row),
+        .data = record_get_effect_data(row),
+    };
+}
+
+static int16_t advance_order_and_get_next_pattern_index(const Song* song, SongState* song_state) {
+    song_state->order_list_index++;
+    if(song_state->order_list_index >= song->order_list_size) {
+        return -1;
+    } else {
+        return song->order_list[song_state->order_list_index];
+    }
+}
+
+typedef struct {
+    int16_t pattern;
+    int16_t row;
+    bool change_pattern;
+    bool change_row;
+} Location;
+
+static void tracker_send_position_message(Tracker* tracker) {
+    if(tracker->callback != NULL) {
+        tracker->callback(
+            (TrackerMessage){
+                .type = TrackerPositionChanged,
+                .data =
+                    {
+                        .position =
+                            {
+                                .order_list_index = tracker->song_state.order_list_index,
+                                .row = tracker->song_state.row_index,
+                            },
+                    },
+            },
+            tracker->context);
+    }
+}
+
+static void tracker_send_end_message(Tracker* tracker) {
+    if(tracker->callback != NULL) {
+        tracker->callback((TrackerMessage){.type = TrackerEndOfSong}, tracker->context);
+    }
+}
+
+static void advance_to_pattern(Tracker* tracker, Location advance) {
+    if(advance.change_pattern) {
+        if(advance.pattern < 0 || advance.pattern >= tracker->song->patterns_count) {
+            tracker->playing = false;
+            tracker_send_end_message(tracker);
+        } else {
+            tracker->song_state.pattern_index = advance.pattern;
+            tracker->song_state.row_index = 0;
+        }
+    }
+
+    if(advance.change_row) {
+        if(advance.row < 0) advance.row = 0;
+        if(advance.row >= PATTERN_SIZE) advance.row = PATTERN_SIZE - 1;
+        tracker->song_state.row_index = advance.row;
+    }
+
+    tracker_send_position_message(tracker);
+}
+
+static void tracker_interrupt_body(Tracker* tracker) {
+    if(!tracker->playing) {
+        tracker_speaker_stop();
+        return;
+    }
+
+    const uint8_t channel_index = 0;
+    SongState* song_state = &tracker->song_state;
+    ChannelState* channel_state = &song_state->channels[channel_index];
+    const Song* song = tracker->song;
+    UnpackedRow row = get_current_row(song, song_state, channel_index);
+
+    // load frequency from note at tick 0
+    if(song_state->tick == 0) {
+        bool invalidate_row = false;
+        // handle "on first tick" effects
+        if(row.effect == EffectBreakPattern) {
+            int16_t next_row_index = row.data;
+            int16_t next_pattern_index =
+                advance_order_and_get_next_pattern_index(song, song_state);
+            advance_to_pattern(
+                tracker,
+                (Location){
+                    .pattern = next_pattern_index,
+                    .row = next_row_index,
+                    .change_pattern = true,
+                    .change_row = true,
+                });
+
+            invalidate_row = true;
+        }
+
+        if(row.effect == EffectJumpToOrder) {
+            song_state->order_list_index = row.data;
+            int16_t next_pattern_index = song->order_list[song_state->order_list_index];
+
+            advance_to_pattern(
+                tracker,
+                (Location){
+                    .pattern = next_pattern_index,
+                    .change_pattern = true,
+                });
+
+            invalidate_row = true;
+        }
+
+        // tracker state can be affected by effects
+        if(!tracker->playing) {
+            tracker_speaker_stop();
+            return;
+        }
+
+        if(invalidate_row) {
+            row = get_current_row(song, song_state, channel_index);
+
+            if(row.effect == EffectSetSpeed) {
+                song_state->tick_limit = row.data;
+            }
+        }
+
+        // handle note effects
+        if(row.note == NOTE_OFF) {
+            channel_state->play = false;
+        } else if((row.note > NOTE_NONE) && (row.note < NOTE_OFF)) {
+            channel_state->play = true;
+
+            // reset vibrato
+            channel_state->vibrato.speed = 0;
+            channel_state->vibrato.depth = 0;
+            channel_state->vibrato.value = 0;
+            channel_state->vibrato.direction = 0;
+
+            // reset pwm
+            channel_state->pwm = PWM_DEFAULT;
+
+            if(row.effect == EffectSlideToNote) {
+                channel_state->frequency_target = note_to_freq(row.note);
+            } else {
+                channel_state->frequency = note_to_freq(row.note);
+                channel_state->frequency_target = FREQUENCY_UNSET;
+            }
+        }
+    }
+
+    if(channel_state->play) {
+        float frequency, pwm;
+
+        if((row.effect == EffectSlideUp || row.effect == EffectSlideDown) &&
+           row.data != EFFECT_DATA_NONE) {
+            // apply slide effect
+            channel_state->frequency += (row.effect == EffectSlideUp ? 1 : -1) * row.data;
+        } else if(row.effect == EffectSlideToNote) {
+            // apply slide to note effect, if target frequency is set
+            if(channel_state->frequency_target > 0) {
+                if(channel_state->frequency_target > channel_state->frequency) {
+                    channel_state->frequency += row.data;
+                    if(channel_state->frequency > channel_state->frequency_target) {
+                        channel_state->frequency = channel_state->frequency_target;
+                        channel_state->frequency_target = FREQUENCY_UNSET;
+                    }
+                } else if(channel_state->frequency_target < channel_state->frequency) {
+                    channel_state->frequency -= row.data;
+                    if(channel_state->frequency < channel_state->frequency_target) {
+                        channel_state->frequency = channel_state->frequency_target;
+                        channel_state->frequency_target = FREQUENCY_UNSET;
+                    }
+                }
+            }
+        }
+
+        frequency = channel_state->frequency;
+        pwm = channel_state->pwm;
+
+        // apply arpeggio effect
+        if(row.effect == EffectArpeggio) {
+            if(row.data != EFFECT_DATA_NONE) {
+                if((song_state->tick % 3) == 1) {
+                    uint8_t note_offset = EFFECT_DATA_GET_X(row.data);
+                    frequency = frequency_offset_semitones(frequency, note_offset);
+                } else if((song_state->tick % 3) == 2) {
+                    uint8_t note_offset = EFFECT_DATA_GET_Y(row.data);
+                    frequency = frequency_offset_semitones(frequency, note_offset);
+                }
+            }
+        } else if(row.effect == EffectVibrato) {
+            // apply vibrato effect, data = speed, depth
+            uint8_t vibrato_speed = EFFECT_DATA_GET_X(row.data);
+            uint8_t vibrato_depth = EFFECT_DATA_GET_Y(row.data);
+
+            // update vibrato parameters if speed or depth is non-zero
+            if(vibrato_speed != 0) channel_state->vibrato.speed = vibrato_speed;
+            if(vibrato_depth != 0) channel_state->vibrato.depth = vibrato_depth;
+
+            // update vibrato value
+            channel_state->vibrato.value +=
+                channel_state->vibrato.direction * channel_state->vibrato.speed;
+
+            // change direction if value is at the limit
+            if(channel_state->vibrato.value > channel_state->vibrato.depth) {
+                channel_state->vibrato.direction = -1;
+            } else if(channel_state->vibrato.value < -channel_state->vibrato.depth) {
+                channel_state->vibrato.direction = 1;
+            } else if(channel_state->vibrato.direction == 0) {
+                // set initial direction, if it is not set
+                channel_state->vibrato.direction = 1;
+            }
+
+            frequency +=
+                (frequency_get_seventh_of_a_semitone(frequency) * channel_state->vibrato.value);
+        } else if(row.effect == EffectPWM) {
+            pwm = (pwm - PWM_MIN) / EFFECT_DATA_1_MAX * row.data + PWM_MIN;
+        }
+
+        tracker_speaker_play(frequency, pwm);
+    } else {
+        tracker_speaker_stop();
+    }
+
+    song_state->tick++;
+    if(song_state->tick >= song_state->tick_limit) {
+        song_state->tick = 0;
+
+        // next note
+        song_state->row_index = (song_state->row_index + 1);
+
+        if(song_state->row_index >= PATTERN_SIZE) {
+            int16_t next_pattern_index =
+                advance_order_and_get_next_pattern_index(song, song_state);
+            advance_to_pattern(
+                tracker,
+                (Location){
+                    .pattern = next_pattern_index,
+                    .change_pattern = true,
+                });
+        } else {
+            tracker_send_position_message(tracker);
+        }
+    }
+}
+
+static void tracker_interrupt_cb(void* context) {
+    Tracker* tracker = (Tracker*)context;
+    tracker_debug_set(true);
+    tracker_interrupt_body(tracker);
+    tracker_debug_set(false);
+}
+
+/*********************************************************************
+ * Tracker Interface
+ *********************************************************************/
+
+Tracker* tracker_alloc() {
+    Tracker* tracker = malloc(sizeof(Tracker));
+    return tracker;
+}
+
+void tracker_free(Tracker* tracker) {
+    tracker_song_state_clear(tracker);
+    free(tracker);
+}
+
+void tracker_set_message_callback(Tracker* tracker, TrackerMessageCallback callback, void* context) {
+    furi_check(tracker->playing == false);
+    tracker->callback = callback;
+    tracker->context = context;
+}
+
+void tracker_set_song(Tracker* tracker, const Song* song) {
+    furi_check(tracker->playing == false);
+    tracker->song = song;
+    tracker_song_state_init(tracker);
+}
+
+void tracker_set_order_index(Tracker* tracker, uint8_t order_index) {
+    furi_check(tracker->playing == false);
+    furi_check(order_index < tracker->song->order_list_size);
+    tracker->song_state.order_list_index = order_index;
+    tracker->song_state.pattern_index = tracker->song->order_list[order_index];
+}
+
+void tracker_set_row(Tracker* tracker, uint8_t row) {
+    furi_check(tracker->playing == false);
+    furi_check(row < PATTERN_SIZE);
+    tracker->song_state.row_index = row;
+}
+
+void tracker_start(Tracker* tracker) {
+    furi_check(tracker->song != NULL);
+
+    tracker->playing = true;
+    tracker_send_position_message(tracker);
+    tracker_debug_init();
+    tracker_speaker_init();
+    tracker_interrupt_init(tracker->song->ticks_per_second, tracker_interrupt_cb, tracker);
+}
+
+void tracker_stop(Tracker* tracker) {
+    tracker_interrupt_deinit();
+    tracker_speaker_deinit();
+    tracker_debug_deinit();
+
+    tracker->playing = false;
+}

+ 38 - 0
music_tracker/tracker_engine/tracker.h

@@ -0,0 +1,38 @@
+#pragma once
+#include "tracker_notes.h"
+#include "tracker_song.h"
+
+typedef enum {
+    TrackerPositionChanged,
+    TrackerEndOfSong,
+} TrackerMessageType;
+
+typedef struct {
+    TrackerMessageType type;
+    union tracker_message_data {
+        struct {
+            uint8_t order_list_index;
+            uint8_t row;
+        } position;
+    } data;
+} TrackerMessage;
+
+typedef void (*TrackerMessageCallback)(TrackerMessage message, void* context);
+
+typedef struct Tracker Tracker;
+
+Tracker* tracker_alloc();
+
+void tracker_free(Tracker* tracker);
+
+void tracker_set_message_callback(Tracker* tracker, TrackerMessageCallback callback, void* context);
+
+void tracker_set_song(Tracker* tracker, const Song* song);
+
+void tracker_set_order_index(Tracker* tracker, uint8_t order_index);
+
+void tracker_set_row(Tracker* tracker, uint8_t row);
+
+void tracker_start(Tracker* tracker);
+
+void tracker_stop(Tracker* tracker);

+ 64 - 0
music_tracker/tracker_engine/tracker_notes.h

@@ -0,0 +1,64 @@
+#pragma once
+
+#define NOTE_NONE 0
+#define NOTE_C2 1
+#define NOTE_Cs2 2
+#define NOTE_D2 3
+#define NOTE_Ds2 4
+#define NOTE_E2 5
+#define NOTE_F2 6
+#define NOTE_Fs2 7
+#define NOTE_G2 8
+#define NOTE_Gs2 9
+#define NOTE_A2 10
+#define NOTE_As2 11
+#define NOTE_B2 12
+#define NOTE_C3 13
+#define NOTE_Cs3 14
+#define NOTE_D3 15
+#define NOTE_Ds3 16
+#define NOTE_E3 17
+#define NOTE_F3 18
+#define NOTE_Fs3 19
+#define NOTE_G3 20
+#define NOTE_Gs3 21
+#define NOTE_A3 22
+#define NOTE_As3 23
+#define NOTE_B3 24
+#define NOTE_C4 25
+#define NOTE_Cs4 26
+#define NOTE_D4 27
+#define NOTE_Ds4 28
+#define NOTE_E4 29
+#define NOTE_F4 30
+#define NOTE_Fs4 31
+#define NOTE_G4 32
+#define NOTE_Gs4 33
+#define NOTE_A4 34
+#define NOTE_As4 35
+#define NOTE_B4 36
+#define NOTE_C5 37
+#define NOTE_Cs5 38
+#define NOTE_D5 39
+#define NOTE_Ds5 40
+#define NOTE_E5 41
+#define NOTE_F5 42
+#define NOTE_Fs5 43
+#define NOTE_G5 44
+#define NOTE_Gs5 45
+#define NOTE_A5 46
+#define NOTE_As5 47
+#define NOTE_B5 48
+#define NOTE_C6 49
+#define NOTE_Cs6 50
+#define NOTE_D6 51
+#define NOTE_Ds6 52
+#define NOTE_E6 53
+#define NOTE_F6 54
+#define NOTE_Fs6 55
+#define NOTE_G6 56
+#define NOTE_Gs6 57
+#define NOTE_A6 58
+#define NOTE_As6 59
+#define NOTE_B6 60
+#define NOTE_OFF 63

+ 109 - 0
music_tracker/tracker_engine/tracker_song.h

@@ -0,0 +1,109 @@
+#pragma once
+#include <stdint.h>
+
+/**
+ * @brief Row
+ * 
+ * AH       AL
+ * FEDCBA98 76543210
+ * nnnnnnee eedddddd
+ * -------- --------
+ * nnnnnn            = [0] do nothing, [1..60] note number, [61] note off, [62..63] not used
+ *       ee ee       = [0..F] effect 
+ *            111222 = [0..63] or [0..7, 0..7] effect data 
+ */
+typedef uint16_t Row;
+
+#define ROW_NOTE_MASK 0x3F
+#define ROW_EFFECT_MASK 0x0F
+#define ROW_EFFECT_DATA_MASK 0x3F
+
+typedef enum {
+    // 0xy, x - first semitones offset, y - second semitones offset. 0 - no offset .. 7 - +7 semitones...
+    // Play the arpeggio chord with three notes. The first note is the base note, the second and third are offset by x and y.
+    // Each note plays one tick.
+    EffectArpeggio = 0x00,
+
+    // 1xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest.
+    // Slide the note pitch up by xx Hz every tick.
+    EffectSlideUp = 0x01,
+
+    // 2xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest.
+    // Slide the note pitch down by xx Hz every tick.
+    EffectSlideDown = 0x02,
+
+    // 3xx, xx - effect speed, 0 - no effect, 1 - slowest, 0x3F - fastest.
+    // Slide the already playing note pitch towards another one by xx Hz every tick.
+    // The note value is saved until the note is playing, so you don't have to repeat the note value to continue sliding.
+    EffectSlideToNote = 0x03,
+
+    // 4xy, x - vibrato speed (0..7), y - vibrato depth (0..7).
+    // Vibrato effect. The pitch of the note increases by x Hz each tick to a positive vibrato depth, then decreases to a negative depth.
+    // Value 1 of depth means 1/7 of a semitone (about 14.28 ct), so value 7 means full semitone.
+    // Note will play without vibrato on the first tick at the beginning of the effect.
+    // 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.
+    EffectVibrato = 0x04,
+
+    // Effect05 = 0x05,
+    // Effect06 = 0x06,
+    // Effect07 = 0x07,
+    // Effect08 = 0x08,
+    // Effect09 = 0x09,
+    // Effect0A = 0x0A,
+
+    // Bxx, xx - pattern number
+    // Jump to the order xx in the pattern order table at first tick of current row.
+    // So if you want to jump to the pattern after note 4, you should put this effect on the 5th note.
+    EffectJumpToOrder = 0x0B,
+
+    // Cxx, xx - pwm value
+    // Set the PWM value to xx for current row.
+    EffectPWM = 0x0C,
+
+    // Bxx, xx - row number
+    // Jump to the row xx in next pattern at first tick of current row.
+    // So if you want to jump to the pattern after note 4, you should put this effect on the 5th note.
+    EffectBreakPattern = 0x0D,
+
+    // Effect0E = 0x0E,
+
+    // Fxx, xx - song speed, 0 - 1 tick per note, 1 - 2 ticks per note, 0x3F - 64 ticks per note.
+    // Set the speed of the song in terms of ticks per note.
+    // Will be applied at the first tick of current row.
+    EffectSetSpeed = 0x0F,
+} Effect;
+
+#define EFFECT_DATA_2(x, y) ((x) | ((y) << 3))
+#define EFFECT_DATA_GET_X(data) ((data)&0x07)
+#define EFFECT_DATA_GET_Y(data) (((data) >> 3) & 0x07)
+#define EFFECT_DATA_NONE 0
+#define EFFECT_DATA_1_MAX 0x3F
+#define EFFECT_DATA_2_MAX 0x07
+
+#define FREQUENCY_UNSET -1.0f
+
+#define PWM_MIN 0.01f
+#define PWM_MAX 0.5f
+#define PWM_DEFAULT PWM_MAX
+
+#define PATTERN_SIZE 64
+
+#define ROW_MAKE(note, effect, data) \
+    ((Row)(((note)&0x3F) | (((effect)&0xF) << 6) | (((data)&0x3F) << 10)))
+
+typedef struct {
+    Row rows[PATTERN_SIZE];
+} Channel;
+
+typedef struct {
+    Channel* channels;
+} Pattern;
+
+typedef struct {
+    uint8_t channels_count;
+    uint8_t patterns_count;
+    Pattern* patterns;
+    uint8_t order_list_size;
+    uint8_t* order_list;
+    uint16_t ticks_per_second;
+} Song;

+ 182 - 0
music_tracker/view/tracker_view.c

@@ -0,0 +1,182 @@
+#include "tracker_view.h"
+#include <gui/elements.h>
+#include <furi.h>
+
+typedef struct {
+    const Song* song;
+    uint8_t order_list_index;
+    uint8_t row;
+} TrackerViewModel;
+
+struct TrackerView {
+    View* view;
+    void* back_context;
+    TrackerViewCallback back_callback;
+};
+
+static Channel* get_current_channel(TrackerViewModel* model) {
+    uint8_t channel_id = 0;
+    uint8_t pattern_id = model->song->order_list[model->order_list_index];
+    Pattern* pattern = &model->song->patterns[pattern_id];
+    return &pattern->channels[channel_id];
+}
+
+static const char* get_note_from_id(uint8_t note) {
+#define NOTE_COUNT 12
+    const char* notes[NOTE_COUNT] = {
+        "C ",
+        "C#",
+        "D ",
+        "D#",
+        "E ",
+        "F ",
+        "F#",
+        "G ",
+        "G#",
+        "A ",
+        "A#",
+        "B ",
+    };
+    return notes[(note) % NOTE_COUNT];
+#undef NOTE_COUNT
+}
+
+static uint8_t get_octave_from_id(uint8_t note) {
+    return ((note) / 12) + 2;
+}
+
+static uint8_t get_first_row_id(uint8_t row) {
+    return (row / 10) * 10;
+}
+
+static void
+    draw_row(Canvas* canvas, uint8_t i, Channel* channel, uint8_t row, FuriString* buffer) {
+    uint8_t x = 12 * (i + 1);
+    uint8_t first_row_id = get_first_row_id(row);
+    uint8_t current_row_id = first_row_id + i;
+
+    if((current_row_id) >= 64) {
+        return;
+    }
+
+    Row current_row = channel->rows[current_row_id];
+    uint8_t note = current_row & ROW_NOTE_MASK;
+    uint8_t effect = (current_row >> 6) & ROW_EFFECT_MASK;
+    uint8_t data = (current_row >> 10) & ROW_EFFECT_DATA_MASK;
+
+    if(current_row_id == row) {
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_line(canvas, x - 9, 1, x - 9, 62);
+        canvas_draw_box(canvas, x - 8, 0, 9, 64);
+        canvas_draw_line(canvas, x + 1, 1, x + 1, 62);
+        canvas_set_color(canvas, ColorWhite);
+    }
+
+    furi_string_printf(buffer, "%02X", current_row_id);
+    canvas_draw_str(canvas, x, 61, furi_string_get_cstr(buffer));
+
+    if(note > 0 && note < NOTE_OFF) {
+        furi_string_printf(
+            buffer, "%s%d", get_note_from_id(note - 1), get_octave_from_id(note - 1));
+        canvas_draw_str(canvas, x, 44, furi_string_get_cstr(buffer));
+    } else if(note == NOTE_OFF) {
+        canvas_draw_str(canvas, x, 44, "OFF");
+    } else {
+        canvas_draw_str(canvas, x, 44, "---");
+    }
+
+    if(effect == 0 && data == 0) {
+        canvas_draw_str(canvas, x, 21, "-");
+        canvas_draw_str(canvas, x, 12, "--");
+    } else {
+        furi_string_printf(buffer, "%X", effect);
+        canvas_draw_str(canvas, x, 21, furi_string_get_cstr(buffer));
+
+        if(effect == EffectArpeggio || effect == EffectVibrato) {
+            uint8_t data_x = EFFECT_DATA_GET_X(data);
+            uint8_t data_y = EFFECT_DATA_GET_Y(data);
+            furi_string_printf(buffer, "%d%d", data_x, data_y);
+            canvas_draw_str(canvas, x, 12, furi_string_get_cstr(buffer));
+        } else {
+            furi_string_printf(buffer, "%02X", data);
+            canvas_draw_str(canvas, x, 12, furi_string_get_cstr(buffer));
+        }
+    }
+
+    if(current_row_id == row) {
+        canvas_set_color(canvas, ColorBlack);
+    }
+}
+
+static void tracker_view_draw_callback(Canvas* canvas, void* _model) {
+    TrackerViewModel* model = _model;
+    if(model->song == NULL) {
+        return;
+    }
+
+    canvas_set_font_direction(canvas, CanvasDirectionBottomToTop);
+    canvas_set_font(canvas, FontKeyboard);
+
+    Channel* channel = get_current_channel(model);
+    FuriString* buffer = furi_string_alloc();
+
+    for(uint8_t i = 0; i < 10; i++) {
+        draw_row(canvas, i, channel, model->row, buffer);
+    }
+    furi_string_free(buffer);
+}
+
+static bool tracker_view_input_callback(InputEvent* event, void* context) {
+    TrackerView* tracker_view = context;
+
+    if(tracker_view->back_callback) {
+        if(event->type == InputTypeShort && event->key == InputKeyBack) {
+            tracker_view->back_callback(tracker_view->back_context);
+            return true;
+        }
+    }
+    return false;
+}
+
+TrackerView* tracker_view_alloc() {
+    TrackerView* tracker_view = malloc(sizeof(TrackerView));
+    tracker_view->view = view_alloc();
+    view_allocate_model(tracker_view->view, ViewModelTypeLocking, sizeof(TrackerViewModel));
+    view_set_context(tracker_view->view, tracker_view);
+    view_set_draw_callback(tracker_view->view, (ViewDrawCallback)tracker_view_draw_callback);
+    view_set_input_callback(tracker_view->view, (ViewInputCallback)tracker_view_input_callback);
+    return tracker_view;
+}
+
+void tracker_view_free(TrackerView* tracker_view) {
+    view_free(tracker_view->view);
+    free(tracker_view);
+}
+
+View* tracker_view_get_view(TrackerView* tracker_view) {
+    return tracker_view->view;
+}
+
+void tracker_view_set_back_callback(
+    TrackerView* tracker_view,
+    TrackerViewCallback callback,
+    void* context) {
+    tracker_view->back_callback = callback;
+    tracker_view->back_context = context;
+}
+
+void tracker_view_set_song(TrackerView* tracker_view, const Song* song) {
+    with_view_model(
+        tracker_view->view, TrackerViewModel * model, { model->song = song; }, true);
+}
+
+void tracker_view_set_position(TrackerView* tracker_view, uint8_t order_list_index, uint8_t row) {
+    with_view_model(
+        tracker_view->view,
+        TrackerViewModel * model,
+        {
+            model->order_list_index = order_list_index;
+            model->row = row;
+        },
+        true);
+}

+ 29 - 0
music_tracker/view/tracker_view.h

@@ -0,0 +1,29 @@
+#include <gui/view.h>
+#include "../tracker_engine/tracker.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct TrackerView TrackerView;
+
+TrackerView* tracker_view_alloc();
+
+void tracker_view_free(TrackerView* tracker_view);
+
+View* tracker_view_get_view(TrackerView* tracker_view);
+
+typedef void (*TrackerViewCallback)(void* context);
+
+void tracker_view_set_back_callback(
+    TrackerView* tracker_view,
+    TrackerViewCallback callback,
+    void* context);
+
+void tracker_view_set_song(TrackerView* tracker_view, const Song* song);
+
+void tracker_view_set_position(TrackerView* tracker_view, uint8_t order_list_index, uint8_t row);
+
+#ifdef __cplusplus
+}
+#endif

+ 536 - 0
music_tracker/zero_tracker.c

@@ -0,0 +1,536 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification_messages.h>
+#include "zero_tracker.h"
+#include "tracker_engine/tracker.h"
+#include "view/tracker_view.h"
+
+// Channel p_0_channels[] = {
+//     {
+//         .rows =
+//             {
+//                 // 1/4
+//                 ROW_MAKE(NOTE_C3, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+//                 ROW_MAKE(0, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+//                 ROW_MAKE(NOTE_C4, EffectSlideToNote, 0x20),
+//                 ROW_MAKE(0, EffectSlideToNote, 0x20),
+//                 //
+//                 ROW_MAKE(0, EffectSlideToNote, 0x20),
+//                 ROW_MAKE(0, EffectSlideToNote, 0x20),
+//                 ROW_MAKE(0, EffectSlideToNote, 0x20),
+//                 ROW_MAKE(0, EffectSlideToNote, 0x20),
+//                 //
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+//                 //
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+//                 // 2/4
+//                 ROW_MAKE(NOTE_C3, EffectSlideDown, 0x20),
+//                 ROW_MAKE(0, EffectSlideDown, 0x20),
+//                 ROW_MAKE(NOTE_C4, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 //
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 //
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 //
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 ROW_MAKE(NOTE_OFF, EffectVibrato, EFFECT_DATA_2(3, 3)),
+//                 // 3/4
+//                 ROW_MAKE(NOTE_C3, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+//                 ROW_MAKE(0, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+//                 ROW_MAKE(NOTE_OFF, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 //
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 //
+//                 ROW_MAKE(NOTE_C2, EffectPWM, 60),
+//                 ROW_MAKE(0, EffectPWM, 32),
+//                 ROW_MAKE(0, EffectPWM, 12),
+//                 ROW_MAKE(NOTE_OFF, 0, 0),
+//                 //
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 // 4/4
+//                 ROW_MAKE(NOTE_C3, EffectSlideDown, 0x20),
+//                 ROW_MAKE(0, EffectSlideDown, 0x20),
+//                 ROW_MAKE(0, EffectSlideDown, 0x20),
+//                 ROW_MAKE(NOTE_OFF, 0, 0),
+//                 //
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 //
+//                 ROW_MAKE(NOTE_C2, EffectPWM, 60),
+//                 ROW_MAKE(0, EffectPWM, 32),
+//                 ROW_MAKE(0, EffectPWM, 12),
+//                 ROW_MAKE(NOTE_OFF, 0, 0),
+//                 //
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//                 ROW_MAKE(0, 0, 0),
+//             },
+//     },
+// };
+
+Channel p_0_channels[] = {
+    {
+        .rows =
+            {
+                //
+                ROW_MAKE(NOTE_A4, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_C3, 0, 0),
+                ROW_MAKE(NOTE_F2, 0, 0),
+                ROW_MAKE(NOTE_C3, 0, 0),
+                //
+                ROW_MAKE(NOTE_E4, 0, 0),
+                ROW_MAKE(NOTE_C3, 0, 0),
+                ROW_MAKE(NOTE_E4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_E5, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_C3, EffectSlideDown, 0x30),
+                ROW_MAKE(NOTE_F2, 0, 0),
+                ROW_MAKE(NOTE_C3, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_C3, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                //
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_D3, 0, 0),
+                ROW_MAKE(NOTE_G2, 0, 0),
+                ROW_MAKE(NOTE_D3, 0, 0),
+                //
+                ROW_MAKE(NOTE_E4, 0, 0),
+                ROW_MAKE(NOTE_D3, 0, 0),
+                ROW_MAKE(NOTE_E4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_E5, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_D3, EffectSlideDown, 0x3F),
+                ROW_MAKE(NOTE_G2, 0, 0),
+                ROW_MAKE(NOTE_D3, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_D3, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                //
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+            },
+    },
+};
+
+Channel p_1_channels[] = {
+    {
+        .rows =
+            {
+                //
+                ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_G4, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C6, 0, 0),
+                ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_G4, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                //
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_G4, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(0, EffectPWM, 55),
+                ROW_MAKE(0, EffectPWM, 45),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C6, 0, 0),
+                ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 50),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_G4, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, 0, 0),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                //
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(1, 1)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(0, EffectVibrato, EFFECT_DATA_2(2, 2)),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+            },
+    },
+};
+
+Channel p_2_channels[] = {
+    {
+        .rows =
+            {
+                //
+                ROW_MAKE(NOTE_C5, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, EffectPWM, 55),
+                ROW_MAKE(NOTE_A4, EffectPWM, 45),
+                ROW_MAKE(NOTE_C5, EffectPWM, 35),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(NOTE_C5, EffectPWM, 55),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_D5, EffectPWM, 55),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_D5, EffectPWM, 45),
+                ROW_MAKE(NOTE_B4, EffectPWM, 45),
+                ROW_MAKE(NOTE_D5, EffectPWM, 35),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, EffectArpeggio, EFFECT_DATA_2(4, 7)),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_E5, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_E5, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                //
+                ROW_MAKE(NOTE_E5, EffectPWM, 55),
+                ROW_MAKE(NOTE_C5, EffectPWM, 45),
+                ROW_MAKE(NOTE_E5, EffectPWM, 35),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_E5, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_E5, EffectPWM, 55),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_E3, EffectSlideDown, 0x30),
+                ROW_MAKE(NOTE_A2, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                //
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                ROW_MAKE(NOTE_E3, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_D5, EffectPWM, 55),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_D5, EffectPWM, 45),
+                ROW_MAKE(NOTE_B4, EffectPWM, 45),
+                ROW_MAKE(NOTE_D5, EffectPWM, 35),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+            },
+    },
+};
+
+Channel p_3_channels[] = {
+    {
+        .rows =
+            {
+                //
+                ROW_MAKE(NOTE_Ds5, EffectArpeggio, EFFECT_DATA_2(4, 6)),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_Ds5, 0, 0),
+                ROW_MAKE(NOTE_C5, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_Ds5, EffectPWM, 45),
+                ROW_MAKE(NOTE_C5, EffectPWM, 35),
+                ROW_MAKE(NOTE_Ds5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_D5, EffectPWM, 45),
+                ROW_MAKE(NOTE_B4, EffectPWM, 35),
+                ROW_MAKE(NOTE_D5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_Cs5, EffectArpeggio, EFFECT_DATA_2(4, 6)),
+                ROW_MAKE(NOTE_As4, 0, 0),
+                ROW_MAKE(NOTE_Cs5, 0, 0),
+                ROW_MAKE(NOTE_As4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_Cs5, EffectPWM, 45),
+                ROW_MAKE(NOTE_As4, EffectPWM, 35),
+                ROW_MAKE(NOTE_Cs5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_C5, EffectPWM, 45),
+                ROW_MAKE(NOTE_A4, EffectPWM, 35),
+                ROW_MAKE(NOTE_C5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_B4, EffectArpeggio, EFFECT_DATA_2(4, 6)),
+                ROW_MAKE(NOTE_Gs4, 0, 0),
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_Gs4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_B4, EffectPWM, 45),
+                ROW_MAKE(NOTE_Gs4, EffectPWM, 35),
+                ROW_MAKE(NOTE_B4, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, 0, 0),
+                ROW_MAKE(NOTE_C5, 0, 0),
+                ROW_MAKE(NOTE_A4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_C5, EffectPWM, 45),
+                ROW_MAKE(NOTE_A4, EffectPWM, 35),
+                ROW_MAKE(NOTE_C5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_Cs5, EffectArpeggio, EFFECT_DATA_2(4, 6)),
+                ROW_MAKE(NOTE_As4, 0, 0),
+                ROW_MAKE(NOTE_Cs5, 0, 0),
+                ROW_MAKE(NOTE_As4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_Cs5, EffectPWM, 45),
+                ROW_MAKE(NOTE_As4, EffectPWM, 35),
+                ROW_MAKE(NOTE_Cs5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+                //
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, 0, 0),
+                ROW_MAKE(NOTE_D5, 0, 0),
+                ROW_MAKE(NOTE_B4, EffectPWM, 55),
+                //
+                ROW_MAKE(NOTE_D5, EffectPWM, 45),
+                ROW_MAKE(NOTE_B4, EffectPWM, 35),
+                ROW_MAKE(NOTE_D5, EffectPWM, 30),
+                ROW_MAKE(NOTE_OFF, 0, 0),
+            },
+    },
+};
+Pattern patterns[] = {
+    {.channels = p_0_channels},
+    {.channels = p_1_channels},
+    {.channels = p_2_channels},
+    {.channels = p_3_channels},
+};
+
+uint8_t order_list[] = {
+    0,
+    1,
+    0,
+    2,
+    0,
+    1,
+    0,
+    3,
+};
+
+Song song = {
+    .channels_count = 1,
+    .patterns_count = sizeof(patterns) / sizeof(patterns[0]),
+    .patterns = patterns,
+
+    .order_list_size = sizeof(order_list) / sizeof(order_list[0]),
+    .order_list = order_list,
+
+    .ticks_per_second = 60,
+};
+
+void tracker_message(TrackerMessage message, void* context) {
+    FuriMessageQueue* queue = context;
+    furi_assert(queue);
+    furi_message_queue_put(queue, &message, 0);
+}
+
+int32_t zero_tracker_app(void* p) {
+    UNUSED(p);
+
+    NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+    notification_message(notification, &sequence_display_backlight_enforce_on);
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    ViewDispatcher* view_dispatcher = view_dispatcher_alloc();
+    TrackerView* tracker_view = tracker_view_alloc();
+    tracker_view_set_song(tracker_view, &song);
+    view_dispatcher_add_view(view_dispatcher, 0, tracker_view_get_view(tracker_view));
+    view_dispatcher_attach_to_gui(view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_switch_to_view(view_dispatcher, 0);
+
+    FuriMessageQueue* queue = furi_message_queue_alloc(8, sizeof(TrackerMessage));
+    Tracker* tracker = tracker_alloc();
+    tracker_set_message_callback(tracker, tracker_message, queue);
+    tracker_set_song(tracker, &song);
+    tracker_start(tracker);
+
+    while(1) {
+        TrackerMessage message;
+        FuriStatus status = furi_message_queue_get(queue, &message, portMAX_DELAY);
+        if(status == FuriStatusOk) {
+            if(message.type == TrackerPositionChanged) {
+                uint8_t order_list_index = message.data.position.order_list_index;
+                uint8_t row = message.data.position.row;
+                uint8_t pattern = song.order_list[order_list_index];
+                tracker_view_set_position(tracker_view, order_list_index, row);
+                FURI_LOG_I("Tracker", "O:%d P:%d R:%d", order_list_index, pattern, row);
+            } else if(message.type == TrackerEndOfSong) {
+                FURI_LOG_I("Tracker", "End of song");
+                break;
+            }
+        }
+    }
+
+    tracker_stop(tracker);
+    tracker_free(tracker);
+    furi_message_queue_free(queue);
+
+    furi_delay_ms(500);
+
+    view_dispatcher_remove_view(view_dispatcher, 0);
+    tracker_view_free(tracker_view);
+    view_dispatcher_free(view_dispatcher);
+
+    notification_message(notification, &sequence_display_backlight_enforce_auto);
+
+    furi_record_close(RECORD_NOTIFICATION);
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

+ 0 - 0
music_tracker/zero_tracker.h


BIN
music_tracker/zero_tracker.png