Procházet zdrojové kódy

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

git-subtree-dir: wav_player
git-subtree-mainline: d0e11cbb1a3a8a2fa2b3fc557010b9ea468d1d1e
git-subtree-split: cdafdd51fe2fd57ce1c003f624f24c26351c7d28
Willy-JL před 2 roky
rodič
revize
9f2b960952

+ 1 - 0
wav_player/.gitsubtree

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

+ 4 - 0
wav_player/README.md

@@ -0,0 +1,4 @@
+# WAV player
+ A Flipper Zero application for playing wav files. My fork adds support for correct playback speed (for files with different sample rates) and for mono files (original wav player only plays stereo). ~~You still need to convert your file to unsigned 8-bit PCM format for it to played correctly on flipper~~. Now supports 16-bit (ordinary) wav files too, both mono and stereo!
+
+Original app by https://github.com/DrZlo13.

+ 11 - 0
wav_player/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="wav_player",
+    name="WAV Player",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="wav_player_app",
+    stack_size=4 * 1024,
+    order=46,
+    fap_icon="wav_10px.png",
+    fap_category="Media",
+    fap_icon_assets="images",
+)

binární
wav_player/images/music_10px.png


binární
wav_player/wav_10px.png


+ 88 - 0
wav_player/wav_parser.c

@@ -0,0 +1,88 @@
+#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, WavPlayerApp* app) {
+    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);
+
+    app->sample_rate = parser->format.sample_rate;
+    app->num_channels = parser->format.channels;
+    app->bits_per_sample = 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;
+}

+ 89 - 0
wav_player/wav_parser.h

@@ -0,0 +1,89 @@
+#pragma once
+#include <toolbox/stream/stream.h>
+
+#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_view.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;
+
+typedef struct {
+    Storage* storage;
+    Stream* stream;
+    WavParser* parser;
+    uint16_t* sample_buffer;
+    uint8_t* tmp_buffer;
+
+    uint32_t sample_rate;
+
+    uint16_t num_channels;
+    uint16_t bits_per_sample;
+
+    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;
+
+WavParser* wav_parser_alloc();
+
+void wav_parser_free(WavParser* parser);
+
+bool wav_parser_parse(WavParser* parser, Stream* stream, WavPlayerApp* app);
+
+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

+ 462 - 0
wav_player/wav_player.c

@@ -0,0 +1,462 @@
+#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 <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);
+    }
+}
+
+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) {
+    if(app->num_channels == 1 && app->bits_per_sample == 8) {
+        uint16_t* sample_buffer_start = &app->sample_buffer[index];
+        size_t count = stream_read(app->stream, app->tmp_buffer, app->samples_count_half);
+
+        for(size_t i = count; i < app->samples_count_half; i++) {
+            app->tmp_buffer[i] = 0;
+        }
+
+        //for(size_t i = 0; i < app->samples_count; i += 2)
+        for(size_t i = 0; i < app->samples_count_half; i++) //now works with mono!
+        {
+            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;
+            }
+
+            //uint8_t data = app->tmp_buffer[i];
+
+            sample_buffer_start[i] = data;
+        }
+
+        wav_player_view_set_data(app->view, sample_buffer_start, app->samples_count_half);
+
+        return count != app->samples_count_half;
+    }
+
+    if(app->num_channels == 1 && app->bits_per_sample == 16) {
+        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) {
+            int16_t int_16 =
+                (((int16_t)app->tmp_buffer[i + 1] << 8) + (int16_t)app->tmp_buffer[i]);
+
+            float data = ((float)int_16 / 256.0 + 127.0);
+            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;
+    }
+
+    if(app->num_channels == 2 && app->bits_per_sample == 16) {
+        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 = 0; i < app->samples_count; i += 4) {
+            int16_t L = (((int16_t)app->tmp_buffer[i + 1] << 8) + (int16_t)app->tmp_buffer[i]);
+            int16_t R = (((int16_t)app->tmp_buffer[i + 3] << 8) + (int16_t)app->tmp_buffer[i + 2]);
+            int32_t int_16 = L / 2 + R / 2; // (L + R) / 2
+
+            float data = ((float)int_16 / 256.0 + 127.0);
+            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 / 4] = data;
+        }
+
+        count = stream_read(app->stream, app->tmp_buffer, app->samples_count);
+
+        for(size_t i = 0; i < app->samples_count; i += 4) {
+            int16_t L = (((int16_t)app->tmp_buffer[i + 1] << 8) + (int16_t)app->tmp_buffer[i]);
+            int16_t R = (((int16_t)app->tmp_buffer[i + 3] << 8) + (int16_t)app->tmp_buffer[i + 2]);
+            int32_t int_16 = L / 2 + R / 2; // (L + R) / 2
+
+            float data = ((float)int_16 / 256.0 + 127.0);
+            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 / 4 + app->samples_count / 4] = data;
+        }
+
+        wav_player_view_set_data(app->view, sample_buffer_start, app->samples_count_half);
+
+        return count != app->samples_count;
+    }
+
+    if(app->num_channels == 2 && app->bits_per_sample == 8) {
+        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] + app->tmp_buffer[i + 1]) / 2; // (L + R) / 2
+            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;
+            }
+
+            //uint8_t data = app->tmp_buffer[i];
+
+            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;
+    }
+
+    return true;
+}
+
+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, app)) 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);
+
+    if(furi_hal_speaker_acquire(1000)) {
+        wav_player_speaker_init(app->sample_rate);
+        wav_player_dma_init((uint32_t)app->sample_buffer, app->samples_count);
+
+        furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, wav_player_dma_isr, app->queue);
+
+        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) {
+                    wav_player_view_set_chans(app->view, app->num_channels);
+                    wav_player_view_set_bits(app->view, app->bits_per_sample);
+
+                    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) {
+                    wav_player_view_set_chans(app->view, app->num_channels);
+                    wav_player_view_set_bits(app->view, app->bits_per_sample);
+
+                    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) % 2 ?
+                            ((int32_t)(wav_parser_get_data_len(app->parser) / (size_t)100) - 1) :
+                            (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) % 2 ?
+                            ((int32_t)(wav_parser_get_data_len(app->parser) / (size_t)100) - 1) :
+                            (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();
+    }
+
+    // Reset GPIO pin and bus states
+    wav_player_hal_deinit();
+
+    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;
+}

+ 111 - 0
wav_player/wav_player_hal.c

@@ -0,0 +1,111 @@
+#include "wav_player_hal.h"
+#include <stm32wbxx_ll_tim.h>
+#include <stm32wbxx_ll_dma.h>
+
+#include <stm32wbxx_ll_gpio.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_resources.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(uint32_t sample_rate) {
+    // Enable bus
+    furi_hal_bus_enable(FuriHalBusTIM2);
+
+    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
+
+    TIM_InitStruct.Autoreload = 64000000 / sample_rate - 1; //to support various sample rates
+
+    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);
+
+    //=========================================================
+    //configuring PA6 pin as TIM16 output
+
+    furi_hal_gpio_init_ex(
+        &gpio_ext_pa6,
+        GpioModeAltFunctionPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFn14TIM16);
+}
+
+void wav_player_hal_deinit() {
+    furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+
+    // Disable bus
+    furi_hal_bus_disable(FuriHalBusTIM2);
+}
+
+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);
+}

+ 25 - 0
wav_player/wav_player_hal.h

@@ -0,0 +1,25 @@
+#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();
+
+void wav_player_hal_deinit();
+
+#ifdef __cplusplus
+}
+#endif

+ 202 - 0
wav_player/wav_player_view.c

@@ -0,0 +1,202 @@
+#include "wav_player_view.h"
+
+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;
+
+    /*char buffer[20];
+    snprintf(buffer, sizeof(buffer), "%d", model->num_channels);
+    canvas_draw_str(canvas, 0, 10, buffer);
+    snprintf(buffer, sizeof(buffer), "%d", model->bits_per_sample);
+    canvas_draw_str(canvas, 0, 20, buffer);*/
+
+    // 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_chans(WavPlayerView* wav_view, uint16_t chn) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->num_channels = chn; }, true);
+}
+
+void wav_player_view_set_bits(WavPlayerView* wav_view, uint16_t bit) {
+    furi_assert(wav_view);
+    with_view_model(
+        wav_view->view, WavPlayerViewModel * model, { model->bits_per_sample = bit; }, 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;
+}

+ 82 - 0
wav_player/wav_player_view.h

@@ -0,0 +1,82 @@
+#pragma once
+#include <gui/view.h>
+
+#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>
+
+#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);
+
+#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];
+
+    uint16_t bits_per_sample;
+    uint16_t num_channels;
+} WavPlayerViewModel;
+
+
+
+
+
+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_bits(WavPlayerView* wav_view, uint16_t bit);
+void wav_player_view_set_chans(WavPlayerView* wav_view, uint16_t chn);
+
+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