diff --git a/include/rew/common/directory_sink.h b/include/rew/common/directory_sink.h
new file mode 100644
index 0000000000000000000000000000000000000000..0a398e5948530311db4c7473de3f31da28ac4bea
--- /dev/null
+++ b/include/rew/common/directory_sink.h
@@ -0,0 +1,56 @@
+#ifndef REW_COMMON_DIRECTORY_SINK_H
+#define REW_COMMON_DIRECTORY_SINK_H
+
+#include <fstream>
+#include "named_raw_file.h"
+#include "input.h"
+
+REW_NAMESPACE {
+    class DirectorySink: public rew::Input<rew::NamedRawFile> {
+    public:
+        DirectorySink(const std::string& destination):destination(destination) {
+            
+        }
+        virtual ~DirectorySink() = default;
+
+        void process(const rew::NamedRawFile* data, const size_t length) override {
+            for (size_t i = 0; i < length; i++) {
+                const auto& namedFile = data[i];
+
+                auto terminator = namedFile.length;
+                for (size_t i = 0; i < namedFile.length; i++) {
+                    if (namedFile.data[i] == '\0') {
+                        terminator = i;
+                        break;
+                    }
+                }
+
+                if (terminator != namedFile.length) {
+                    const auto sptr = reinterpret_cast<const char*>(namedFile.data.get());
+                    const auto name = std::string(sptr, terminator);
+
+                    const auto contents = reinterpret_cast<const char*>(namedFile.data.get() + terminator + 1);
+                    const auto total = namedFile.length - terminator - 1;
+                    std::string path = destination + DELIMITER + name;
+
+                    std::cout << "Writing file: " << path << " of size: " << total << " bytes!" << std::endl;
+                    std::fstream file(path, std::ios::out | std::ios::binary);
+                    if (!file) {
+                        std::cerr << "Failed to open file: " << path << " for writing!" << std::endl;
+                    }
+                    else {
+                        file.write(contents, total);
+                    }
+                }
+                else {
+                    std::cerr << "File index: " << namedFile.index << " successfully inflated but contains bad filename!" << std::endl;
+                }
+            }
+        }
+
+    private:
+        std::string destination;
+    };
+}
+
+#endif
diff --git a/report/images/decoder-gui.png b/report/images/decoder-gui.png
new file mode 100644
index 0000000000000000000000000000000000000000..a02f2850ecaf2f04a5d70f11b8aeb6f2a5fad90d
Binary files /dev/null and b/report/images/decoder-gui.png differ
diff --git a/src/rew/decoder-cli/main.cpp b/src/rew/decoder-cli/main.cpp
index 44c6e4280473b4c4d391652858414320fc4e24ae..5e6468e805ee6e8287f602902f745fca4d038c3e 100644
--- a/src/rew/decoder-cli/main.cpp
+++ b/src/rew/decoder-cli/main.cpp
@@ -15,10 +15,11 @@
 #include <thread>
 #include <future>
 #include <rew/decoder/decoder.h>
-#include <rew/common/named_raw_file.h>
+#include <rew/common/directory_sink.h>
 #include <rew/decoder/physical_audio_source.h>
 #include <rew/decoder/tcp_http_server.h>
 
+
 ///=====================================================================================================================
 std::string getFullPath(const std::string& path) {
 #ifdef _WIN32
@@ -89,53 +90,6 @@ argagg::parser argparser{ {
     }
  } };
 
-///=====================================================================================================================
-class CustomSink: public rew::Input<rew::NamedRawFile> {
-public:
-    CustomSink(const std::string& destination):destination(destination) {
-        
-    }
-    virtual ~CustomSink() = default;
-
-    void process(const rew::NamedRawFile* data, const size_t length) override {
-        for (size_t i = 0; i < length; i++) {
-            const auto& namedFile = data[i];
-
-            auto terminator = namedFile.length;
-            for (size_t i = 0; i < namedFile.length; i++) {
-                if (namedFile.data[i] == '\0') {
-                    terminator = i;
-                    break;
-                }
-            }
-
-            if (terminator != namedFile.length) {
-                const auto sptr = reinterpret_cast<const char*>(namedFile.data.get());
-                const auto name = std::string(sptr, terminator);
-
-                const auto contents = reinterpret_cast<const char*>(namedFile.data.get() + terminator + 1);
-                const auto total = namedFile.length - terminator - 1;
-                std::string path = destination + DELIMITER + name;
-
-                std::cout << "Writing file: " << path << " of size: " << total << " bytes!" << std::endl;
-                std::fstream file(path, std::ios::out | std::ios::binary);
-                if (!file) {
-                    std::cerr << "Failed to open file: " << path << " for writing!" << std::endl;
-                }
-                else {
-                    file.write(contents, total);
-                }
-            }
-            else {
-                std::cerr << "File index: " << namedFile.index << " successfully inflated but contains bad filename!" << std::endl;
-            }
-        }
-    }
-
-private:
-    std::string destination;
-};
-
 ///=====================================================================================================================
 int main (const int argc, char* argv[]) {
     static bool terminate = false;
@@ -185,7 +139,7 @@ int main (const int argc, char* argv[]) {
             if (!folderIsValid(outputPath))
                 throw std::runtime_error("Output path: \"" + outputPath + "\" is not a folder!");
 
-            sink = std::make_shared<CustomSink>(outputPath);
+            sink = std::make_shared<rew::DirectorySink>(outputPath);
         }
 
         else if (args["serve"]) {
diff --git a/src/rew/decoder-gui/main.cpp b/src/rew/decoder-gui/main.cpp
index e5f20176be3ceaffb28f7f4ca2c77315cfd477b4..5f92fd2e0262701830d65f1f6412ce29fa4ae82f 100644
--- a/src/rew/decoder-gui/main.cpp
+++ b/src/rew/decoder-gui/main.cpp
@@ -13,11 +13,11 @@
 #include <iostream>
 #include <thread>
 #include <future>
+#include <functional>
 #include <rew/decoder/decoder.h>
-#include <rew/common/named_raw_file.h>
+#include <rew/common/directory_sink.h>
 #include <rew/decoder/physical_audio_source.h>
-#include "rew/encoder/audio_sink.h"
-#include "rew/encoder/wav_writer.h"
+#include <rew/decoder/tcp_http_server.h>
 
 #ifdef _MSC_VER
 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
@@ -127,6 +127,386 @@ private:
     std::string destination;
 };
 
+///=====================================================================================================================
+static int onClosing(uiWindow *w, void *data) {
+	uiQuit();
+	return 1;
+}
+
+///=====================================================================================================================
+static int onShouldQuit(void *data) {
+	uiWindow *mainwin = uiWindow(data);
+
+	uiControlDestroy(uiControl(mainwin));
+	return 1;
+}
+
+///=====================================================================================================================
+class Context;
+static struct Globals {
+    std::shared_ptr<Context> context;
+    bool sourceDevice = false;
+    bool sourceFile = false;
+    bool destinationFile = false;
+    bool destinationServe = false;
+    std::string sourceFileStr;
+    std::string destinationFileStr;
+} globals;
+
+///=====================================================================================================================
+class Context: public std::enable_shared_from_this<Context> {
+public:
+    Context(Globals& globals, const std::function<void()>& callback)
+        : globals(globals), 
+          callback(callback) {
+
+        if (globals.destinationFile) {
+            std::cout << "destination: " << globals.destinationFileStr << std::endl;
+            const auto outputPath = getFullPath(globals.destinationFileStr);
+
+            if (!pathIsValid(outputPath))
+                throw std::runtime_error("Output path: \"" + outputPath + "\" does not exist!");
+
+            if (!folderIsValid(outputPath))
+                throw std::runtime_error("Output path: \"" + outputPath + "\" is not a folder!");
+
+            sink = std::make_shared<rew::DirectorySink>(outputPath);
+        }
+
+        else if (globals.destinationServe) {
+            std::cout << "Starting server" << std::endl;
+            server = std::make_shared<rew::TcpHttpServer>("localhost", 8080);
+            sink = server;
+            server->start();
+        }
+
+        else {
+            throw std::runtime_error("Please select a destination");
+        }
+
+        if (globals.sourceFile) {
+            std::cout << "source: " << globals.sourceFileStr << std::endl;
+            const auto inputPath = getFullPath(globals.sourceFileStr);
+
+            if (!pathIsValid(inputPath))
+                throw std::runtime_error("Input file: \"" + inputPath + "\" does not exist!");
+
+            auto wav = std::make_shared<rew::WavReader>();
+            auto source = std::make_shared<rew::AudioSource>(wav);
+
+            decoder = std::make_shared<rew::Decoder>(source, sink, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, DEFAULT_SAMPLE_LENGTH_MS);
+
+            thread = std::thread([=]() -> void {
+                source->open(inputPath);
+                source->process();
+                source->close();
+
+                if (this->callback && !this->server) {
+                    this->stopped = true;
+                    this->callback();
+                }
+            });
+        }
+
+        else if (globals.sourceDevice) {
+            audio = std::make_shared<rew::PhysicalAudioSource>();
+
+            decoder = std::make_shared<rew::Decoder>(audio, sink, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, DEFAULT_SAMPLE_LENGTH_MS);
+
+            thread = std::thread([=]() -> void {
+                audio->start();
+                audio->process();
+
+                if (this->callback && !this->server) {
+                    this->stopped = true;
+                    this->callback();
+                }
+            });
+        }
+
+        else {
+            throw std::runtime_error("Please select a source");
+        }
+    }
+
+    ~Context() {
+        stop();
+    }
+
+    void stop() {
+        if (thread.joinable()) {
+            if (server) server->stop();
+            if (audio) audio->close();
+            thread.join();
+        }
+    }
+
+    inline bool isStopped() const {
+        return stopped;
+    }
+
+private:
+    Globals& globals;
+    std::shared_ptr<rew::Input<rew::NamedRawFile>> sink;
+    std::shared_ptr<rew::PhysicalAudioSource> audio;
+    std::shared_ptr<rew::TcpHttpServer> server;
+    std::shared_ptr<rew::Decoder> decoder;
+    std::thread thread;
+    std::function<void()> callback;
+    bool stopped = false;
+};
+
+///=====================================================================================================================
+static void widgetsSelectSource(uiWindow* mainwin, uiBox* box) {
+    const auto group = uiNewGroup("Select source");
+	uiGroupSetMargined(group, 1);
+    uiBoxAppend(box, uiControl(group), 0);
+
+    uiBoxAppend(box, uiControl(uiNewHorizontalSeparator()), 0);
+
+    const auto cboxDevice = uiNewCheckbox("From audio input device");
+    const auto cboxFile = uiNewCheckbox("From audio file");
+
+    static struct CBoxData {
+        Globals& globals;
+        uiCheckbox* device;
+        uiCheckbox* file;
+    } cboxData{globals, cboxDevice, cboxFile};
+
+    uiBoxAppend(box, uiControl(cboxDevice), 0);
+    uiCheckboxOnToggled(cboxDevice, [](uiCheckbox* cbox, void* data) -> void {
+        auto& d = *reinterpret_cast<CBoxData*>(data);
+        d.globals.sourceDevice = uiCheckboxChecked(cbox);
+        uiCheckboxSetChecked(d.file, !d.globals.sourceDevice);
+    }, &cboxData);
+
+    const auto combo = uiNewCombobox();
+    const auto devices = rew::PhysicalAudioSource::getDevices();
+    for (const auto& device : devices) {
+        if (device.inputChannels == 0) continue;
+        uiComboboxAppend(combo, device.name.c_str());
+    }
+    uiBoxAppend(box, uiControl(combo), 0);
+    uiComboboxOnSelected(combo, [](uiCombobox* combo, void* data) -> void {
+        auto& d = *reinterpret_cast<CBoxData*>(data);
+        d.globals.sourceDevice = true;
+        uiCheckboxSetChecked(d.device, d.globals.sourceDevice);
+        uiCheckboxSetChecked(d.file, !d.globals.sourceDevice);
+    }, &cboxData);
+
+    uiBoxAppend(box, uiControl(cboxFile), 0);
+    uiCheckboxOnToggled(cboxFile, [](uiCheckbox* cbox, void* data) -> void {
+        auto& d = *reinterpret_cast<CBoxData*>(data);
+        d.globals.sourceFile = uiCheckboxChecked(cbox);
+        uiCheckboxSetChecked(d.device, !d.globals.sourceFile);
+    }, &cboxData);
+
+    const auto grid = uiNewGrid();
+	uiGridSetPadded(grid, 1);
+	uiBoxAppend(box, uiControl(grid), 0);
+
+    const auto fileButton = uiNewButton("Select Folder");
+	const auto fileEntry = uiNewEntry();
+
+    static struct SelectFileData {
+        uiWindow* mainwin;
+        Globals& globals;
+        uiCheckbox* device;
+        uiCheckbox* file;
+        uiEntry* entry;
+    } selectFileData{mainwin, globals, cboxDevice, cboxFile, fileEntry};
+
+    const auto onOpenFileClicked = [](uiButton *b, void *data) -> void {
+        auto& d = *reinterpret_cast<SelectFileData*>(data);
+
+        const auto filename = uiOpenFile(d.mainwin);
+        if (filename == NULL) {
+            uiEntrySetText(d.entry, "");
+            return;
+        }
+
+        if (!pathIsValid(filename)) {
+            uiMsgBoxError(d.mainwin, 
+                "Select file error", 
+                "You must select a valid file!");
+
+            uiEntrySetText(d.entry, "");
+            return;
+        }
+
+        uiEntrySetText(d.entry, filename);
+        d.globals.sourceFileStr = std::string(filename);
+        uiFreeText(filename);
+
+        d.globals.sourceFile = true;
+        uiCheckboxSetChecked(d.device, !d.globals.sourceFile);
+        uiCheckboxSetChecked(d.file, d.globals.sourceFile);
+    };
+
+    uiEntryOnChanged(fileEntry, [](uiEntry* e, void* data) -> void {
+        auto& globals = *reinterpret_cast<Globals*>(data);
+        globals.sourceFileStr = std::string(uiEntryText(e));
+    }, &globals);
+
+	uiEntrySetReadOnly(fileEntry, 0);
+	uiButtonOnClicked(fileButton, onOpenFileClicked, &selectFileData);
+	uiGridAppend(grid, uiControl(fileButton),
+		0, 0, 1, 1,
+		0, uiAlignFill, 0, uiAlignFill);
+	uiGridAppend(grid, uiControl(fileEntry),
+		1, 0, 1, 1,
+        1, uiAlignFill, 0, uiAlignFill);
+}
+
+///=====================================================================================================================
+static void widgetsSelectDestination(uiWindow* mainwin, uiBox* box) {
+    const auto group = uiNewGroup("Select destination");
+	uiGroupSetMargined(group, 1);
+    uiBoxAppend(box, uiControl(group), 0);
+
+    uiBoxAppend(box, uiControl(uiNewHorizontalSeparator()), 0);
+
+    const auto cboxServe = uiNewCheckbox("Serve on http://localhost:8080/");
+    const auto cboxFile = uiNewCheckbox("Save to folder");
+
+    static struct CBoxData {
+        Globals& globals;
+        uiCheckbox* serve;
+        uiCheckbox* file;
+    } cboxData{globals, cboxServe, cboxFile};
+
+    uiBoxAppend(box, uiControl(cboxServe), 0);
+    uiCheckboxOnToggled(cboxServe, [](uiCheckbox* cbox, void* data) -> void {
+        auto& d = *reinterpret_cast<CBoxData*>(data);
+        d.globals.destinationServe = uiCheckboxChecked(cbox);
+        uiCheckboxSetChecked(d.file, !d.globals.destinationServe);
+    }, &cboxData);
+
+    uiBoxAppend(box, uiControl(cboxFile), 0);
+    uiCheckboxOnToggled(cboxFile, [](uiCheckbox* cbox, void* data) -> void {
+        auto& d = *reinterpret_cast<CBoxData*>(data);
+        d.globals.destinationFile = uiCheckboxChecked(cbox);
+        uiCheckboxSetChecked(d.serve, !d.globals.destinationFile);
+    }, &cboxData);
+
+    const auto grid = uiNewGrid();
+	uiGridSetPadded(grid, 1);
+	uiBoxAppend(box, uiControl(grid), 0);
+
+    const auto fileButton = uiNewButton("Select Folder");
+	const auto fileEntry = uiNewEntry();
+
+    static struct SelectFileData {
+        uiWindow* mainwin;
+        Globals& globals;
+        uiCheckbox* serve;
+        uiCheckbox* file;
+        uiEntry* entry;
+    } selectFileData{mainwin, globals, cboxServe, cboxFile, fileEntry};
+
+    const auto onOpenFileClicked = [](uiButton *b, void *data) -> void {
+        auto& d = *reinterpret_cast<SelectFileData*>(data);
+
+        const auto filename = uiOpenFile(d.mainwin);
+        if (filename == NULL) {
+            uiEntrySetText(d.entry, "");
+            return;
+        }
+
+        if (!folderIsValid(filename)) {
+            uiMsgBoxError(d.mainwin, 
+                "Select folder error", 
+                "You must select a valid folder, not a file!");
+
+            uiEntrySetText(d.entry, "");
+            return;
+        }
+
+        uiEntrySetText(d.entry, filename);
+        d.globals.destinationFileStr = std::string(filename);
+        uiFreeText(filename);
+
+        d.globals.destinationFile = true;
+        uiCheckboxSetChecked(d.serve, !d.globals.destinationFile);
+        uiCheckboxSetChecked(d.file, d.globals.destinationFile);
+    };
+
+    uiEntryOnChanged(fileEntry, [](uiEntry* e, void* data) -> void {
+        auto& globals = *reinterpret_cast<Globals*>(data);
+        globals.destinationFileStr = std::string(uiEntryText(e));
+    }, &globals);
+
+	uiEntrySetReadOnly(fileEntry, 0);
+	uiButtonOnClicked(fileButton, onOpenFileClicked, &selectFileData);
+	uiGridAppend(grid, uiControl(fileButton),
+		0, 0, 1, 1,
+		0, uiAlignFill, 0, uiAlignFill);
+	uiGridAppend(grid, uiControl(fileEntry),
+		1, 0, 1, 1,
+        1, uiAlignFill, 0, uiAlignFill);
+}
+
+///=====================================================================================================================
+static void widgets(uiWindow* mainwin) {
+    const auto box = uiNewVerticalBox();
+	uiBoxSetPadded(box, 1);
+    uiWindowSetChild(mainwin, uiControl(box));
+
+    widgetsSelectSource(mainwin, box);
+    widgetsSelectDestination(mainwin, box);
+
+    uiBoxAppend(box, uiControl(uiNewHorizontalSeparator()), 0);
+
+    auto status = uiNewLabel("Press start to begin");
+    uiBoxAppend(box, uiControl(status), 0);
+
+    auto progressbar = uiNewProgressBar();
+	uiProgressBarSetValue(progressbar, 0);
+    uiBoxAppend(box, uiControl(progressbar), 0);
+
+    auto buttonGrid = uiNewGrid();
+    uiGridSetPadded(buttonGrid, 1);
+
+    static struct StatusData {
+        uiWindow* mainwin;
+        Globals& globals;
+        uiProgressBar* progress;
+        uiLabel* label;
+    } statusData{mainwin, globals, progressbar, status};
+
+    auto button = uiNewButton("Start");
+    uiButtonOnClicked(button, [](uiButton *b, void *data) -> void {
+        const auto callback = [=]() {
+            auto& d = *reinterpret_cast<StatusData*>(data);
+
+            uiProgressBarSetValue(d.progress, 0);
+            uiButtonSetText(b, "Start");
+        };
+
+        auto& d = *reinterpret_cast<StatusData*>(data);
+
+        if (!d.globals.context || d.globals.context->isStopped()) {
+            try {
+                d.globals.context = std::make_unique<Context>(d.globals, callback);
+                uiProgressBarSetValue(d.progress, -1);
+                uiButtonSetText(b, "Stop");
+            } catch (std::exception& e) {
+                uiMsgBoxError(d.mainwin, "Error", e.what());
+            }
+        } else {
+            d.globals.context.reset();
+            uiProgressBarSetValue(d.progress, 0);
+            uiButtonSetText(b, "Start");
+        }
+
+    }, &statusData);
+    uiGridAppend(buttonGrid, uiControl(button),
+		1, 0, 1, 1,
+        1, uiAlignCenter, 0, uiAlignFill);
+
+    uiBoxAppend(box, uiControl(buttonGrid), 0);
+}
+
 ///=====================================================================================================================
 int main (const int argc, char* argv[]) {
 #ifdef _WIN32
@@ -134,6 +514,26 @@ int main (const int argc, char* argv[]) {
 #endif
 
     try {
+        uiInitOptions o = {0};
+        const char* err;
+        if ((err = uiInit(&o)) != nullptr) {
+            const auto e = std::string(err);
+            uiFreeInitError(err);
+            throw std::runtime_error(e);
+        }
+
+        const auto mainwin = uiNewWindow("Decoder", 640, 400, false);
+        uiWindowSetMargined(mainwin, 1);
+	    uiWindowOnClosing(mainwin, onClosing, NULL);
+        uiOnShouldQuit(onShouldQuit, mainwin);
+
+        widgets(mainwin);
+
+        uiControlShow(uiControl(mainwin));
+        uiMain();
+        uiUninit();
+        return 0;
+
         return EXIT_SUCCESS;
     }
     catch (const std::exception& e) {