diff --git a/README.md b/README.md index 57bda1c8dc21357ce2c11312c73a5a5518257852..36fa546727bc88364b4a2e67c6399e6d35789463 100644 --- a/README.md +++ b/README.md @@ -23,82 +23,6 @@ website, encode it into an 48Khz audio, transmit over large distanced using FM transmitter, and then decode it on the other side and serve the files to the user. -## Project structure - -This project contains the following sub-projects (libraries) that together -create the final application. The four libraries together form a client and -a server applications. Where the server forms of the emitter library and the -encoder library, and the client only needs the decoder and the server library. - -* **encoder** - This is a shared library which takes a raw stream of bytes, - for example a tar file, and encodes it into an audio file. -* **decoder** - This is a shared library which takes a raw stream of 48Khz - audio samples and constructs the raw bytes back, that were created using - the encoder. -* **emitter** - This is a shared library that consumes the raw data created - by the encoder and either saves it as an mp3 file, or emitts the data to - the computer sound output. -* **server** - This is a shared library that simply serves the decoded data - to the user. - -## Third party libraries - -This project also uses the following third party libraries: - -* **[asio](https://think-async.com/)** - C++11 library for creating a http server. -* **[zlib](https://zlib.net/)** - C library for compression and decompression. -* **[Catch2](https://github.com/catchorg/Catch2)** - C++14 library for unit tests only. -* **[argagg](https://github.com/vietjtnguyen/argagg)** - For parsing command line arguments. -* **[dirent](https://github.com/tronkko/dirent)** - Only needed on Windows as a replacement for Unix dirent.h header. - -## Milestones - -### 1. Basic project structure (Done) - -Create a basic project either using [Conan](https://conan.io/), [vcpkg](https://docs.microsoft.com/en-us/cpp/vcpkg?view=vs-2017), or simple git submodules for the third party libraries. Use CMake for the project configuration and C++ as the target language. - -*Completed! The vcpkg has been chosen as the package manager for the third party libraries, later to be replaced by simple git submodules due to lack of proper Linux support (no static libraries support). The libraries as listed in the third party libraries section above has been chosen for their simplicity and large documentation. The C++ version 2017 has been chosen as the target language for this project. The project structure and configuration works both in Linux and Windows, the OSX has not been tested at this point.* - -### 2. Encoder and Decoder phase 1 (Todo) - -Create a basic encoder which takes a path to the source folder (a folder which contains the html files) and encodes them into a raw stream of audio samples. The audio samples will be targeted to 48Khz as this is the highest common sample rate used in personal computers in domestic use [1]. The next part of this milestone is to create a decoder, which takes raw data from the encoder and assembles back the raw input data. A proper unit tests must be crated during this step to create as the baseline for the next phase. - -*Completed! The Goertzel algorithm was used instead of the Fast Fourier Transform. The Goertzel alows to target a specific frequency, in this case two frequencies (for low and high bits). The algorithm is faster than the FFT which allows us to use this software on slower hardware. The decoding part has been programmed into blocks, similar to the GNU Radio framework.* - -### 3. Encoder and Decoder phase 2 (Todo) - -Create a data link layer, in which the source input (in the encoder) is split into chunks, a data packet(s). These data packets must be small in order for them to be easily transmitted and received. The higher the packet size is, the higher the chance of the packet loss [cittation needed]. The packets will additionally contain a fixed count, the total number of packets, alongside with an index number. The decoder must be able to reject the incorrect packets and verify the integrity of the data (using a checksum). - -*Partially completed, there are still some things to do when splitting data into data packets.* - -### 4. Testing with FM hardware (Todo) - -Testing the encoder and decoder with a real hardware. If an issue is raised, the phase 1 and 2 of the encoder and decoder must be further adjusted. - -### 5. Noise filtering (Todo) - -Implement unit testing with artificial and real noise in the audio data stream. - -### 6. Encoder CLI (Todo) - -Create a simple emitter that takes the encoded raw input as audio samples (in 48Khz) and emits them to the audio output of the machine. The encoded data must be transmited in a loop until the user decides to stop the transmission. The [PortAudio](http://www.portaudio.com/) library will be used for this process to access the hardware audio streams. - -### 7. Decoder CLI + HTTP Server (Todo) - -Create a simple server which takes the packets from the decoder and servers the files to the user. As an alternative, create a Windows driver (a service) which will act as an additional network device. This device will act as the internet gateway and will serve the files. Additionally, the server can cache the old files and the user will be able to view the previously transmitted version of the website. This server will also listed to the hardware audio input stream using The [PortAudio](http://www.portaudio.com/). The data will be fed to the decoder library. - -### 8. Documentation and user installation (Todo) - -Write down simple documentation and create an installation (exe or msi). - -### 9. Experiment with multiple baudrates (Todo) - -Experiment with multiple data streams encoded on top of each other. For example, a higher baud rate with a lower amplitude on top of lower baudrate with higher amplitude. If the user has the perfect signal, the application will choose the higher baudrate as the source, which results in faster loading times for the website. - -### 10. Experiment with transmitting data back (Todo) - -This application only works in one way, transmitting the data to the user. An experiment can be conducted to transmit data back to the server. Essentially creating a WiFi out of FM radio transmitters and receivers. - ## Compiling You will need CMake 3.1 or newer with a relevant compiler (GCC, Clang, or Visual Studio 2017). The compiler must support C++17 @@ -116,7 +40,3 @@ cmake --build . --target ALL_BUILD --config MinSizeRel ``` ctest --verbose -C "MinSizeRel" ``` - -## References - -[1] [AES5-2008 (r2013): AES recommended practice for professional digital audio - Preferred sampling frequencies for applications employing pulse-code modulation](http://www.aes.org/publications/standards/search.cfm?docID=14) (revision of AES5-2003), Audio Engineering Society, 2014-06-16 \ No newline at end of file diff --git a/report/images/encoder-gui.png b/report/images/encoder-gui.png new file mode 100644 index 0000000000000000000000000000000000000000..06c5e2396aa140283b054a13ba2c87e23409cee5 Binary files /dev/null and b/report/images/encoder-gui.png differ diff --git a/src/rew/decoder-gui/main.cpp b/src/rew/decoder-gui/main.cpp index 5f92fd2e0262701830d65f1f6412ce29fa4ae82f..f1d6865a30f90bd0d89cbb63ef2201982cb24b94 100644 --- a/src/rew/decoder-gui/main.cpp +++ b/src/rew/decoder-gui/main.cpp @@ -80,53 +80,6 @@ bool pathIsValid(const std::string& path) { return stat(path.c_str(), &info) == 0; } -///===================================================================================================================== -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; -}; - ///===================================================================================================================== static int onClosing(uiWindow *w, void *data) { uiQuit(); @@ -393,7 +346,7 @@ static void widgetsSelectDestination(uiWindow* mainwin, uiBox* box) { uiGridSetPadded(grid, 1); uiBoxAppend(box, uiControl(grid), 0); - const auto fileButton = uiNewButton("Select Folder"); + const auto fileButton = uiNewButton("Select File"); const auto fileEntry = uiNewEntry(); static struct SelectFileData { @@ -509,10 +462,6 @@ static void widgets(uiWindow* mainwin) { ///===================================================================================================================== int main (const int argc, char* argv[]) { -#ifdef _WIN32 - SetConsoleCtrlHandler(ctrlHandler, TRUE); -#endif - try { uiInitOptions o = {0}; const char* err; diff --git a/src/rew/encoder-cli/main.cpp b/src/rew/encoder-cli/main.cpp index 98b64337142edae8e42ef3e60f6ec7ff7f047254..7df740a57c0bbf75229552aeb8dc4298ba0f1bf0 100644 --- a/src/rew/encoder-cli/main.cpp +++ b/src/rew/encoder-cli/main.cpp @@ -170,7 +170,7 @@ int main(const int argc, char* argv[]) { // Create our source which will go through the directory auto source = std::make_shared<DirectoryTraverser>(); - // Create the output wav file + // Create the output audio sink auto audioSink = std::make_shared<rew::PhysicalAudioSink>(); // Finally combine everything together into a single encoder diff --git a/src/rew/encoder-gui/main.cpp b/src/rew/encoder-gui/main.cpp index 5cb1ac16e957dcd387cf2896802be36da5ac6c29..e658d40947908280284deccb6435fd95a3cff322 100644 --- a/src/rew/encoder-gui/main.cpp +++ b/src/rew/encoder-gui/main.cpp @@ -10,6 +10,7 @@ #include <stdlib.h> #endif #include <iostream> +#include <thread> #include "folder.h" #include <rew/encoder/encoder.h> #include <rew/encoder/physical_audio_sink.h> @@ -54,6 +55,20 @@ bool pathIsValid(const std::string& path) { return stat(path.c_str(), &info) == 0; } +///===================================================================================================================== +static int onClosing(uiWindow *w, void *data) { + uiQuit(); + return 1; +} + +///===================================================================================================================== +static int onShouldQuit(void *data) { + uiWindow *mainwin = uiWindow(data); + + uiControlDestroy(uiControl(mainwin)); + return 1; +} + ///===================================================================================================================== std::vector<std::string> recursivelyParseFolder(const std::string& path) { std::vector<std::string> files; @@ -118,10 +133,363 @@ private: } }; +///===================================================================================================================== +class Context; +static struct Globals { + std::shared_ptr<Context> context; + bool destinationFile = false; + bool destinationStream = false; + std::string sourceFileStr; + std::string destinationFileStr; + std::string extensions = "html"; +} globals; + +///===================================================================================================================== +class Context: public std::enable_shared_from_this<Context> { +public: + Context(Globals& globals, const std::function<void()>& callback) + : globals(globals), + callback(callback) { + + const auto inputPath = getFullPath(globals.sourceFileStr); + + if (!pathIsValid(inputPath)) + throw std::runtime_error("Input path: \"" + inputPath + "\" does not exist!"); + + if (!folderIsValid(inputPath)) + throw std::runtime_error("Input path: \"" + inputPath + "\" is not a folder!"); + + // Create our source which will go through the directory + source = std::make_shared<DirectoryTraverser>(); + + loader = std::make_shared<rew::FileLoader>(); + source->connect(loader); + + if (globals.destinationFile) { + const auto outputPath = globals.destinationFileStr; + + // Create the output wav file + wav = std::make_shared<rew::AudioSink>(std::make_shared<rew::WavWriter>()); + + // Finally combine everything together into a single encoder + encoder = std::make_shared<rew::Encoder>(loader, wav, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, + DEFAULT_SAMPLE_LENGTH_MS); + + thread = std::thread([=]() -> void { + wav->open(outputPath); + source->process(inputPath); + wav->close(); + + if (this->callback) { + this->stopped = true; + this->callback(); + } + }); + } + + else if (globals.destinationStream) { + audio = std::make_shared<rew::PhysicalAudioSink>(); + + // Finally combine everything together into a single encoder + encoder = std::make_shared<rew::Encoder>(loader, audio, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, + DEFAULT_SAMPLE_LENGTH_MS); + + thread = std::thread([=]() -> void { + audio->start(); + source->process(inputPath); + audio->join(); + audio->close(); + + if (this->callback) { + this->stopped = true; + this->callback(); + } + }); + } + + else { + throw std::runtime_error("Please select a destination"); + } + } + + ~Context() { + stop(); + } + + void stop() { + if (thread.joinable()) { + if (audio) audio->close(); + thread.join(); + } + } + + inline bool isStopped() const { + return stopped; + } + +private: + Globals& globals; + std::shared_ptr<rew::FileLoader> loader; + std::shared_ptr<DirectoryTraverser> source; + std::shared_ptr<rew::PhysicalAudioSink> audio; + std::shared_ptr<rew::AudioSink> wav; + std::shared_ptr<rew::Encoder> encoder; + 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 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; + uiEntry* entry; + } selectFileData{mainwin, globals, 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!"); + + uiEntrySetText(d.entry, ""); + return; + } + + uiEntrySetText(d.entry, filename); + d.globals.sourceFileStr = std::string(filename); + uiFreeText(filename); + }; + + 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); + + auto rb = uiNewRadioButtons(); + uiRadioButtonsAppend(rb, "Html only"); + uiRadioButtonsAppend(rb, "Html with images"); + uiRadioButtonsAppend(rb, "Everything"); + uiBoxAppend(box, uiControl(rb), 0); + + uiRadioButtonsOnSelected(rb, [](uiRadioButtons* rb, void* data) -> void { + auto& globals = *reinterpret_cast<Globals*>(data); + switch (uiRadioButtonsSelected(rb)) { + case 0: globals.extensions = "html"; break; + case 1: globals.extensions = "html jpg jpeg png bmp svg tif tiff"; break; + case 2: globals.extensions = ""; break; + default: break; + } + }, &globals); +} + +///===================================================================================================================== +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 cboxDevice = uiNewCheckbox("To audio output device"); + const auto cboxFile = uiNewCheckbox("To 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.destinationStream = uiCheckboxChecked(cbox); + uiCheckboxSetChecked(d.file, !d.globals.destinationStream); + }, &cboxData); + + const auto combo = uiNewCombobox(); + const auto devices = rew::PhysicalAudioSink::getDevices(); + for (const auto& device : devices) { + if (device.outputChannels == 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.destinationStream = true; + uiCheckboxSetChecked(d.device, d.globals.destinationStream); + uiCheckboxSetChecked(d.file, !d.globals.destinationStream); + }, &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.device, !d.globals.destinationFile); + }, &cboxData); + + const auto grid = uiNewGrid(); + uiGridSetPadded(grid, 1); + uiBoxAppend(box, uiControl(grid), 0); + + const auto fileButton = uiNewButton("Select File"); + 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 = uiSaveFile(d.mainwin); + if (filename == NULL) { + uiEntrySetText(d.entry, ""); + return; + } + + uiEntrySetText(d.entry, filename); + d.globals.destinationFileStr = std::string(filename); + uiFreeText(filename); + + d.globals.destinationFile = true; + uiCheckboxSetChecked(d.device, !d.globals.destinationFile); + uiCheckboxSetChecked(d.file, d.globals.destinationFile); + }; + + 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 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[]) { 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("Encoder", 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; }