LTVA1 3 лет назад
Сommit
9e9568f1cc
9 измененных файлов с 820 добавлено и 0 удалено
  1. 12 0
      application.fam
  2. BIN
      wav_10px.png
  3. 84 0
      wav_parser.c
  4. 51 0
      wav_parser.h
  5. 323 0
      wav_player.c
  6. 81 0
      wav_player_hal.c
  7. 23 0
      wav_player_hal.h
  8. 201 0
      wav_player_view.c
  9. 45 0
      wav_player_view.h

+ 12 - 0
application.fam

@@ -0,0 +1,12 @@
+App(
+    appid="My_WAV_Player",
+    name="WAV Player",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="wav_player_app",
+    cdefines=["APP_MY_WAV_PLAYER"],
+    stack_size=4 * 1024,
+    order=60,
+    fap_icon="wav_10px.png",
+    fap_category="Music",
+    fap_icon_assets="images",
+)


+ 84 - 0
wav_parser.c

@@ -0,0 +1,84 @@
+#include "wav_parser.h"
+
+#define TAG "WavParser"
+
+const char* format_text(FormatTag tag) {
+    switch(tag) {
+    case FormatTagPCM:
+        return "PCM";
+    case FormatTagIEEE_FLOAT:
+        return "IEEE FLOAT";
+    default:
+        return "Unknown";
+    }
+};
+
+struct WavParser {
+    WavHeaderChunk header;
+    WavFormatChunk format;
+    WavDataChunk data;
+    size_t wav_data_start;
+    size_t wav_data_end;
+};
+
+WavParser* wav_parser_alloc() {
+    return malloc(sizeof(WavParser));
+}
+
+void wav_parser_free(WavParser* parser) {
+    free(parser);
+}
+
+bool wav_parser_parse(WavParser* parser, Stream* stream) {
+    stream_read(stream, (uint8_t*)&parser->header, sizeof(WavHeaderChunk));
+    stream_read(stream, (uint8_t*)&parser->format, sizeof(WavFormatChunk));
+    stream_read(stream, (uint8_t*)&parser->data, sizeof(WavDataChunk));
+
+    if(memcmp(parser->header.riff, "RIFF", 4) != 0 ||
+       memcmp(parser->header.wave, "WAVE", 4) != 0) {
+        FURI_LOG_E(TAG, "WAV: wrong header");
+        return false;
+    }
+
+    if(memcmp(parser->format.fmt, "fmt ", 4) != 0) {
+        FURI_LOG_E(TAG, "WAV: wrong format");
+        return false;
+    }
+
+    if(parser->format.tag != FormatTagPCM || memcmp(parser->data.data, "data", 4) != 0) {
+        FURI_LOG_E(
+            TAG,
+            "WAV: non-PCM format %u, next '%lu'",
+            parser->format.tag,
+            (uint32_t)parser->data.data);
+        return false;
+    }
+
+    FURI_LOG_I(
+        TAG,
+        "Format tag: %s, ch: %u, smplrate: %lu, bps: %lu, bits: %u",
+        format_text(parser->format.tag),
+        parser->format.channels,
+        parser->format.sample_rate,
+        parser->format.byte_per_sec,
+        parser->format.bits_per_sample);
+
+    parser->wav_data_start = stream_tell(stream);
+    parser->wav_data_end = parser->wav_data_start + parser->data.size;
+
+    FURI_LOG_I(TAG, "data: %u - %u", parser->wav_data_start, parser->wav_data_end);
+
+    return true;
+}
+
+size_t wav_parser_get_data_start(WavParser* parser) {
+    return parser->wav_data_start;
+}
+
+size_t wav_parser_get_data_end(WavParser* parser) {
+    return parser->wav_data_end;
+}
+
+size_t wav_parser_get_data_len(WavParser* parser) {
+    return parser->wav_data_end - parser->wav_data_start;
+}

+ 51 - 0
wav_parser.h

@@ -0,0 +1,51 @@
+#pragma once
+#include <toolbox/stream/stream.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    FormatTagPCM = 0x0001,
+    FormatTagIEEE_FLOAT = 0x0003,
+} FormatTag;
+
+typedef struct {
+    uint8_t riff[4];
+    uint32_t size;
+    uint8_t wave[4];
+} WavHeaderChunk;
+
+typedef struct {
+    uint8_t fmt[4];
+    uint32_t size;
+    uint16_t tag;
+    uint16_t channels;
+    uint32_t sample_rate;
+    uint32_t byte_per_sec;
+    uint16_t block_align;
+    uint16_t bits_per_sample;
+} WavFormatChunk;
+
+typedef struct {
+    uint8_t data[4];
+    uint32_t size;
+} WavDataChunk;
+
+typedef struct WavParser WavParser;
+
+WavParser* wav_parser_alloc();
+
+void wav_parser_free(WavParser* parser);
+
+bool wav_parser_parse(WavParser* parser, Stream* stream);
+
+size_t wav_parser_get_data_start(WavParser* parser);
+
+size_t wav_parser_get_data_end(WavParser* parser);
+
+size_t wav_parser_get_data_len(WavParser* parser);
+
+#ifdef __cplusplus
+}
+#endif

+ 323 - 0
wav_player.c

@@ -0,0 +1,323 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <cli/cli.h>
+#include <gui/gui.h>
+#include <stm32wbxx_ll_dma.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <toolbox/stream/file_stream.h>
+#include "wav_player_hal.h"
+#include "wav_parser.h"
+#include "wav_player_view.h"
+#include <math.h>
+#include <My_WAV_Player_icons.h>
+
+#include <My_WAV_Player_icons.h>
+
+#define TAG "WavPlayer"
+
+#define WAVPLAYER_FOLDER "/ext/wav_player"
+
+static bool open_wav_stream(Stream* stream) {
+    DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
+    bool result = false;
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, WAVPLAYER_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, ".wav", &I_music_10px);
+    browser_options.base_path = WAVPLAYER_FOLDER;
+    browser_options.hide_ext = false;
+
+    bool ret = dialog_file_browser_show(dialogs, path, path, &browser_options);
+
+    furi_record_close(RECORD_DIALOGS);
+    if(ret) {
+        if(!file_stream_open(stream, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            FURI_LOG_E(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(path));
+        } else {
+            result = true;
+        }
+    }
+    furi_string_free(path);
+    return result;
+}
+
+typedef enum {
+    WavPlayerEventHalfTransfer,
+    WavPlayerEventFullTransfer,
+    WavPlayerEventCtrlVolUp,
+    WavPlayerEventCtrlVolDn,
+    WavPlayerEventCtrlMoveL,
+    WavPlayerEventCtrlMoveR,
+    WavPlayerEventCtrlOk,
+    WavPlayerEventCtrlBack,
+} WavPlayerEventType;
+
+typedef struct {
+    WavPlayerEventType type;
+} WavPlayerEvent;
+
+static void wav_player_dma_isr(void* ctx) {
+    FuriMessageQueue* event_queue = ctx;
+
+    // half of transfer
+    if(LL_DMA_IsActiveFlag_HT1(DMA1)) {
+        LL_DMA_ClearFlag_HT1(DMA1);
+        // fill first half of buffer
+        WavPlayerEvent event = {.type = WavPlayerEventHalfTransfer};
+        furi_message_queue_put(event_queue, &event, 0);
+    }
+
+    // transfer complete
+    if(LL_DMA_IsActiveFlag_TC1(DMA1)) {
+        LL_DMA_ClearFlag_TC1(DMA1);
+        // fill second half of buffer
+        WavPlayerEvent event = {.type = WavPlayerEventFullTransfer};
+        furi_message_queue_put(event_queue, &event, 0);
+    }
+}
+
+typedef struct {
+    Storage* storage;
+    Stream* stream;
+    WavParser* parser;
+    uint16_t* sample_buffer;
+    uint8_t* tmp_buffer;
+
+    size_t samples_count_half;
+    size_t samples_count;
+
+    FuriMessageQueue* queue;
+
+    float volume;
+    bool play;
+
+    WavPlayerView* view;
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notification;
+} WavPlayerApp;
+
+static WavPlayerApp* app_alloc() {
+    WavPlayerApp* app = malloc(sizeof(WavPlayerApp));
+    app->samples_count_half = 1024 * 4;
+    app->samples_count = app->samples_count_half * 2;
+    app->storage = furi_record_open(RECORD_STORAGE);
+    app->stream = file_stream_alloc(app->storage);
+    app->parser = wav_parser_alloc();
+    app->sample_buffer = malloc(sizeof(uint16_t) * app->samples_count);
+    app->tmp_buffer = malloc(sizeof(uint8_t) * app->samples_count);
+    app->queue = furi_message_queue_alloc(10, sizeof(WavPlayerEvent));
+
+    app->volume = 10.0f;
+    app->play = true;
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->view = wav_player_view_alloc();
+
+    view_dispatcher_add_view(app->view_dispatcher, 0, wav_player_view_get_view(app->view));
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+    notification_message(app->notification, &sequence_display_backlight_enforce_on);
+
+    return app;
+}
+
+static void app_free(WavPlayerApp* app) {
+    view_dispatcher_remove_view(app->view_dispatcher, 0);
+    view_dispatcher_free(app->view_dispatcher);
+    wav_player_view_free(app->view);
+    furi_record_close(RECORD_GUI);
+
+    furi_message_queue_free(app->queue);
+    free(app->tmp_buffer);
+    free(app->sample_buffer);
+    wav_parser_free(app->parser);
+    stream_free(app->stream);
+    furi_record_close(RECORD_STORAGE);
+
+    notification_message(app->notification, &sequence_display_backlight_enforce_auto);
+    furi_record_close(RECORD_NOTIFICATION);
+    free(app);
+}
+
+// TODO: that works only with 8-bit 2ch audio
+static bool fill_data(WavPlayerApp* app, size_t index) {
+    uint16_t* sample_buffer_start = &app->sample_buffer[index];
+    size_t count = stream_read(app->stream, app->tmp_buffer, app->samples_count);
+
+    for(size_t i = count; i < app->samples_count; i++) {
+        app->tmp_buffer[i] = 0;
+    }
+
+    for(size_t i = 0; i < app->samples_count; i += 2) {
+        float data = app->tmp_buffer[i];
+        data -= UINT8_MAX / 2; // to signed
+        data /= UINT8_MAX / 2; // scale -1..1
+
+        data *= app->volume; // volume
+        data = tanhf(data); // hyperbolic tangent limiter
+
+        data *= UINT8_MAX / 2; // scale -128..127
+        data += UINT8_MAX / 2; // to unsigned
+
+        if(data < 0) {
+            data = 0;
+        }
+
+        if(data > 255) {
+            data = 255;
+        }
+
+        sample_buffer_start[i / 2] = data;
+    }
+
+    wav_player_view_set_data(app->view, sample_buffer_start, app->samples_count_half);
+
+    return count != app->samples_count;
+}
+
+static void ctrl_callback(WavPlayerCtrl ctrl, void* ctx) {
+    FuriMessageQueue* event_queue = ctx;
+    WavPlayerEvent event;
+
+    switch(ctrl) {
+    case WavPlayerCtrlVolUp:
+        event.type = WavPlayerEventCtrlVolUp;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    case WavPlayerCtrlVolDn:
+        event.type = WavPlayerEventCtrlVolDn;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    case WavPlayerCtrlMoveL:
+        event.type = WavPlayerEventCtrlMoveL;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    case WavPlayerCtrlMoveR:
+        event.type = WavPlayerEventCtrlMoveR;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    case WavPlayerCtrlOk:
+        event.type = WavPlayerEventCtrlOk;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    case WavPlayerCtrlBack:
+        event.type = WavPlayerEventCtrlBack;
+        furi_message_queue_put(event_queue, &event, 0);
+        break;
+    default:
+        break;
+    }
+}
+
+static void app_run(WavPlayerApp* app) {
+    if(!open_wav_stream(app->stream)) return;
+    if(!wav_parser_parse(app->parser, app->stream)) return;
+
+    wav_player_view_set_volume(app->view, app->volume);
+    wav_player_view_set_start(app->view, wav_parser_get_data_start(app->parser));
+    wav_player_view_set_current(app->view, stream_tell(app->stream));
+    wav_player_view_set_end(app->view, wav_parser_get_data_end(app->parser));
+    wav_player_view_set_play(app->view, app->play);
+
+    wav_player_view_set_context(app->view, app->queue);
+    wav_player_view_set_ctrl_callback(app->view, ctrl_callback);
+
+    bool eof = fill_data(app, 0);
+    eof = fill_data(app, app->samples_count_half);
+
+    wav_player_speaker_init();
+    wav_player_dma_init((uint32_t)app->sample_buffer, app->samples_count);
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, wav_player_dma_isr, app->queue);
+
+    if(furi_hal_speaker_acquire(1000)) {
+        wav_player_dma_start();
+        wav_player_speaker_start();
+
+        WavPlayerEvent event;
+
+        while(1) {
+            if(furi_message_queue_get(app->queue, &event, FuriWaitForever) == FuriStatusOk) {
+                if(event.type == WavPlayerEventHalfTransfer) {
+                    eof = fill_data(app, 0);
+                    wav_player_view_set_current(app->view, stream_tell(app->stream));
+                    if(eof) {
+                        stream_seek(
+                            app->stream,
+                            wav_parser_get_data_start(app->parser),
+                            StreamOffsetFromStart);
+                    }
+
+                } else if(event.type == WavPlayerEventFullTransfer) {
+                    eof = fill_data(app, app->samples_count_half);
+                    wav_player_view_set_current(app->view, stream_tell(app->stream));
+                    if(eof) {
+                        stream_seek(
+                            app->stream,
+                            wav_parser_get_data_start(app->parser),
+                            StreamOffsetFromStart);
+                    }
+                } else if(event.type == WavPlayerEventCtrlVolUp) {
+                    if(app->volume < 9.9) app->volume += 0.4;
+                    wav_player_view_set_volume(app->view, app->volume);
+                } else if(event.type == WavPlayerEventCtrlVolDn) {
+                    if(app->volume > 0.01) app->volume -= 0.4;
+                    wav_player_view_set_volume(app->view, app->volume);
+                } else if(event.type == WavPlayerEventCtrlMoveL) {
+                    int32_t seek =
+                        stream_tell(app->stream) - wav_parser_get_data_start(app->parser);
+                    seek =
+                        MIN(seek, (int32_t)(wav_parser_get_data_len(app->parser) / (size_t)100));
+                    stream_seek(app->stream, -seek, StreamOffsetFromCurrent);
+                    wav_player_view_set_current(app->view, stream_tell(app->stream));
+                } else if(event.type == WavPlayerEventCtrlMoveR) {
+                    int32_t seek = wav_parser_get_data_end(app->parser) - stream_tell(app->stream);
+                    seek =
+                        MIN(seek, (int32_t)(wav_parser_get_data_len(app->parser) / (size_t)100));
+                    stream_seek(app->stream, seek, StreamOffsetFromCurrent);
+                    wav_player_view_set_current(app->view, stream_tell(app->stream));
+                } else if(event.type == WavPlayerEventCtrlOk) {
+                    app->play = !app->play;
+                    wav_player_view_set_play(app->view, app->play);
+
+                    if(!app->play) {
+                        wav_player_speaker_stop();
+                    } else {
+                        wav_player_speaker_start();
+                    }
+                } else if(event.type == WavPlayerEventCtrlBack) {
+                    break;
+                }
+            }
+        }
+
+        wav_player_speaker_stop();
+        wav_player_dma_stop();
+        furi_hal_speaker_release();
+    }
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
+}
+
+int32_t wav_player_app(void* p) {
+    UNUSED(p);
+    WavPlayerApp* app = app_alloc();
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    if(!storage_simply_mkdir(storage, WAVPLAYER_FOLDER)) {
+        FURI_LOG_E(TAG, "Could not create folder %s", WAVPLAYER_FOLDER);
+    }
+    furi_record_close(RECORD_STORAGE);
+
+    app_run(app);
+    app_free(app);
+    return 0;
+}

+ 81 - 0
wav_player_hal.c

@@ -0,0 +1,81 @@
+#include "wav_player_hal.h"
+#include <stm32wbxx_ll_tim.h>
+#include <stm32wbxx_ll_dma.h>
+
+//#define FURI_HAL_SPEAKER_TIMER TIM16
+
+#define FURI_HAL_SPEAKER_TIMER TIM16
+
+#define SAMPLE_RATE_TIMER TIM2
+
+#define FURI_HAL_SPEAKER_CHANNEL LL_TIM_CHANNEL_CH1
+#define DMA_INSTANCE DMA1, LL_DMA_CHANNEL_1
+
+void wav_player_speaker_init() {
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    //TIM_InitStruct.Prescaler = 4;
+    TIM_InitStruct.Prescaler = 1;
+    TIM_InitStruct.Autoreload =
+        255; //in this fork used purely as PWM timer, the DMA now is triggered by SAMPLE_RATE_TIMER
+    LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct);
+
+    LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    TIM_OC_InitStruct.CompareValue = 127;
+    LL_TIM_OC_Init(FURI_HAL_SPEAKER_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct);
+
+    TIM_InitStruct.Prescaler = 0;
+    TIM_InitStruct.Autoreload = 1451; //64 000 000 / 1451 ~= 44100 Hz
+    LL_TIM_Init(SAMPLE_RATE_TIMER, &TIM_InitStruct);
+
+    //LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    TIM_OC_InitStruct.CompareValue = 127;
+    LL_TIM_OC_Init(SAMPLE_RATE_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct);
+}
+
+void wav_player_speaker_start() {
+    LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_EnableCounter(FURI_HAL_SPEAKER_TIMER);
+
+    LL_TIM_EnableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_EnableCounter(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_speaker_stop() {
+    LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER);
+
+    LL_TIM_DisableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_DisableCounter(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_dma_init(uint32_t address, size_t size) {
+    uint32_t dma_dst = (uint32_t) & (FURI_HAL_SPEAKER_TIMER->CCR1);
+
+    LL_DMA_ConfigAddresses(DMA_INSTANCE, address, dma_dst, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetDataLength(DMA_INSTANCE, size);
+
+    LL_DMA_SetPeriphRequest(DMA_INSTANCE, LL_DMAMUX_REQ_TIM2_UP);
+    LL_DMA_SetDataTransferDirection(DMA_INSTANCE, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetChannelPriorityLevel(DMA_INSTANCE, LL_DMA_PRIORITY_VERYHIGH);
+    LL_DMA_SetMode(DMA_INSTANCE, LL_DMA_MODE_CIRCULAR);
+    LL_DMA_SetPeriphIncMode(DMA_INSTANCE, LL_DMA_PERIPH_NOINCREMENT);
+    LL_DMA_SetMemoryIncMode(DMA_INSTANCE, LL_DMA_MEMORY_INCREMENT);
+    LL_DMA_SetPeriphSize(DMA_INSTANCE, LL_DMA_PDATAALIGN_HALFWORD);
+    LL_DMA_SetMemorySize(DMA_INSTANCE, LL_DMA_MDATAALIGN_HALFWORD);
+
+    LL_DMA_EnableIT_TC(DMA_INSTANCE);
+    LL_DMA_EnableIT_HT(DMA_INSTANCE);
+}
+
+void wav_player_dma_start() {
+    LL_DMA_EnableChannel(DMA_INSTANCE);
+    LL_TIM_EnableDMAReq_UPDATE(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_dma_stop() {
+    LL_DMA_DisableChannel(DMA_INSTANCE);
+}

+ 23 - 0
wav_player_hal.h

@@ -0,0 +1,23 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void wav_player_speaker_init();
+
+void wav_player_speaker_start();
+
+void wav_player_speaker_stop();
+
+void wav_player_dma_init(uint32_t address, size_t size);
+
+void wav_player_dma_start();
+
+void wav_player_dma_stop();
+
+#ifdef __cplusplus
+}
+#endif

+ 201 - 0
wav_player_view.c

@@ -0,0 +1,201 @@
+#include "wav_player_view.h"
+
+#define DATA_COUNT 116
+
+struct WavPlayerView {
+    View* view;
+    WavPlayerCtrlCallback callback;
+    void* context;
+};
+
+typedef struct {
+    bool play;
+    float volume;
+    size_t start;
+    size_t end;
+    size_t current;
+    uint8_t data[DATA_COUNT];
+} WavPlayerViewModel;
+
+float map(float x, float in_min, float in_max, float out_min, float out_max) {
+    return (x - in_min) * (out_max - out_min + 1) / (in_max - in_min + 1) + out_min;
+}
+
+static void wav_player_view_draw_callback(Canvas* canvas, void* _model) {
+    WavPlayerViewModel* model = _model;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    uint8_t x_pos = 0;
+    uint8_t y_pos = 0;
+
+    // volume
+    x_pos = 123;
+    y_pos = 0;
+    const float volume = (64 / 10.0f) * model->volume;
+    canvas_draw_frame(canvas, x_pos, y_pos, 4, 64);
+    canvas_draw_box(canvas, x_pos, y_pos + (64 - volume), 4, volume);
+
+    // play / pause
+    x_pos = 58;
+    y_pos = 55;
+    if(!model->play) {
+        canvas_draw_line(canvas, x_pos, y_pos, x_pos + 8, y_pos + 4);
+        canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos + 8, y_pos + 4);
+        canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos, y_pos);
+    } else {
+        canvas_draw_box(canvas, x_pos, y_pos, 3, 9);
+        canvas_draw_box(canvas, x_pos + 4, y_pos, 3, 9);
+    }
+
+    x_pos = 78;
+    y_pos = 55;
+    canvas_draw_line(canvas, x_pos, y_pos, x_pos + 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos + 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos, y_pos);
+
+    x_pos = 82;
+    y_pos = 55;
+    canvas_draw_line(canvas, x_pos, y_pos, x_pos + 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos + 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos, y_pos);
+
+    x_pos = 40;
+    y_pos = 55;
+    canvas_draw_line(canvas, x_pos, y_pos, x_pos - 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos - 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos, y_pos);
+
+    x_pos = 44;
+    y_pos = 55;
+    canvas_draw_line(canvas, x_pos, y_pos, x_pos - 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos - 4, y_pos + 4);
+    canvas_draw_line(canvas, x_pos, y_pos + 8, x_pos, y_pos);
+
+    // len
+    x_pos = 4;
+    y_pos = 47;
+    const uint8_t play_len = 116;
+    uint8_t play_pos = map(model->current, model->start, model->end, 0, play_len - 4);
+
+    canvas_draw_frame(canvas, x_pos, y_pos, play_len, 4);
+    canvas_draw_box(canvas, x_pos + play_pos, y_pos - 2, 4, 8);
+    canvas_draw_box(canvas, x_pos, y_pos, play_pos, 4);
+
+    // osc
+    x_pos = 4;
+    y_pos = 0;
+    for(size_t i = 1; i < DATA_COUNT; i++) {
+        canvas_draw_line(canvas, x_pos + i - 1, model->data[i - 1], x_pos + i, model->data[i]);
+    }
+}
+
+static bool wav_player_view_input_callback(InputEvent* event, void* context) {
+    WavPlayerView* wav_player_view = context;
+    bool consumed = false;
+
+    if(wav_player_view->callback) {
+        if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
+            if(event->key == InputKeyUp) {
+                wav_player_view->callback(WavPlayerCtrlVolUp, wav_player_view->context);
+                consumed = true;
+            } else if(event->key == InputKeyDown) {
+                wav_player_view->callback(WavPlayerCtrlVolDn, wav_player_view->context);
+                consumed = true;
+            } else if(event->key == InputKeyLeft) {
+                wav_player_view->callback(WavPlayerCtrlMoveL, wav_player_view->context);
+                consumed = true;
+            } else if(event->key == InputKeyRight) {
+                wav_player_view->callback(WavPlayerCtrlMoveR, wav_player_view->context);
+                consumed = true;
+            } else if(event->key == InputKeyOk) {
+                wav_player_view->callback(WavPlayerCtrlOk, wav_player_view->context);
+                consumed = true;
+            } else if(event->key == InputKeyBack) {
+                wav_player_view->callback(WavPlayerCtrlBack, wav_player_view->context);
+                consumed = true;
+            }
+        }
+    }
+
+    return consumed;
+}
+
+WavPlayerView* wav_player_view_alloc() {
+    WavPlayerView* wav_view = malloc(sizeof(WavPlayerView));
+    wav_view->view = view_alloc();
+    view_set_context(wav_view->view, wav_view);
+    view_allocate_model(wav_view->view, ViewModelTypeLocking, sizeof(WavPlayerViewModel));
+    view_set_draw_callback(wav_view->view, wav_player_view_draw_callback);
+    view_set_input_callback(wav_view->view, wav_player_view_input_callback);
+
+    return wav_view;
+}
+
+void wav_player_view_free(WavPlayerView* wav_view) {
+    furi_assert(wav_view);
+    view_free(wav_view->view);
+    free(wav_view);
+}
+
+View* wav_player_view_get_view(WavPlayerView* wav_view) {
+    furi_assert(wav_view);
+    return wav_view->view;
+}
+
+void wav_player_view_set_volume(WavPlayerView* wav_view, float volume) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->volume = volume; }, true);
+}
+
+void wav_player_view_set_start(WavPlayerView* wav_view, size_t start) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->start = start; }, true);
+}
+
+void wav_player_view_set_end(WavPlayerView* wav_view, size_t end) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->end = end; }, true);
+}
+
+void wav_player_view_set_current(WavPlayerView* wav_view, size_t current) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->current = current; }, true);
+}
+
+void wav_player_view_set_play(WavPlayerView* wav_view, bool play) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->play = play; }, true);
+}
+
+void wav_player_view_set_data(WavPlayerView* wav_view, uint16_t* data, size_t data_count) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view,
+        WavPlayerViewModel * model,
+        {
+            size_t inc = (data_count / DATA_COUNT) - 1;
+
+            for(size_t i = 0; i < DATA_COUNT; i++) {
+                model->data[i] = *data / 6;
+                if(model->data[i] > 42) model->data[i] = 42;
+                data += inc;
+            }
+        },
+        true);
+}
+
+void wav_player_view_set_ctrl_callback(WavPlayerView* wav_view, WavPlayerCtrlCallback callback) {
+    furi_assert(wav_view);
+    wav_view->callback = callback;
+}
+
+void wav_player_view_set_context(WavPlayerView* wav_view, void* context) {
+    furi_assert(wav_view);
+    wav_view->context = context;
+}

+ 45 - 0
wav_player_view.h

@@ -0,0 +1,45 @@
+#pragma once
+#include <gui/view.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct WavPlayerView WavPlayerView;
+
+typedef enum {
+    WavPlayerCtrlVolUp,
+    WavPlayerCtrlVolDn,
+    WavPlayerCtrlMoveL,
+    WavPlayerCtrlMoveR,
+    WavPlayerCtrlOk,
+    WavPlayerCtrlBack,
+} WavPlayerCtrl;
+
+typedef void (*WavPlayerCtrlCallback)(WavPlayerCtrl ctrl, void* context);
+
+WavPlayerView* wav_player_view_alloc();
+
+void wav_player_view_free(WavPlayerView* wav_view);
+
+View* wav_player_view_get_view(WavPlayerView* wav_view);
+
+void wav_player_view_set_volume(WavPlayerView* wav_view, float volume);
+
+void wav_player_view_set_start(WavPlayerView* wav_view, size_t start);
+
+void wav_player_view_set_end(WavPlayerView* wav_view, size_t end);
+
+void wav_player_view_set_current(WavPlayerView* wav_view, size_t current);
+
+void wav_player_view_set_play(WavPlayerView* wav_view, bool play);
+
+void wav_player_view_set_data(WavPlayerView* wav_view, uint16_t* data, size_t data_count);
+
+void wav_player_view_set_ctrl_callback(WavPlayerView* wav_view, WavPlayerCtrlCallback callback);
+
+void wav_player_view_set_context(WavPlayerView* wav_view, void* context);
+
+#ifdef __cplusplus
+}
+#endif