From 24a6adf64551a23be94926b202da8551dea1df4e Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Fri, 17 Feb 2023 12:21:07 -0500 Subject: [PATCH] 24_3LE format encoding bug. --- .vscode/launch.json | 2 +- CMakeLists.txt | 4 +- README.md | 2 +- debian/control | 2 +- docs/Installing.md | 4 +- docs/download.md | 4 +- docs/index.md | 2 +- src/AlsaDriver.cpp | 478 ++++++++++------ src/AlsaDriver.hpp | 3 +- src/AlsaDriverTest.cpp | 3 + src/AudioDriver.hpp | 2 + src/AudioHost.cpp | 2 +- src/mustache.hpp | 1190 ++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1522 insertions(+), 176 deletions(-) create mode 100644 src/mustache.hpp diff --git a/.vscode/launch.json b/.vscode/launch.json index f2c3d92..a2aca1b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -79,7 +79,7 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", - "args": [ "[alsa_midi_test]" ], + "args": [ "[alsa_test]" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/CMakeLists.txt b/CMakeLists.txt index eba0631..6eab60c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.0.18 + VERSION 1.0.19 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "v1.0.18") +set (DISPLAY_VERSION "v1.0.19") set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/README.md b/README.md index 37022fe..16f3537 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-Download: v1.0.18 +Download: v1.0.19 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). diff --git a/debian/control b/debian/control index 85e3207..a22a797 100644 --- a/debian/control +++ b/debian/control @@ -9,6 +9,6 @@ Package: pipedal Pre-Depends: hostapd;authbind Priority: optional Section: sound -Version: 1.0.18 +Version: 1.0.19 Installed-Size: 15147 diff --git a/docs/Installing.md b/docs/Installing.md index e4ca367..8c2eb7c 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,14 +13,14 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Ubuntu/Raspberry Pi OS (64-bit) v1.0.18](https://github.com/rerdavies/pipedal/releases/download/v1.0.18/pipedal_1.0.18_arm64.deb) +- [Ubuntu/Raspberry Pi OS (64-bit) v1.0.19](https://github.com/rerdavies/pipedal/releases/download/v1.0.19/pipedal_1.0.19_arm64.deb) Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.0.18_arm64.deb + sudo apt-get install ./pipedal_1.0.19_arm64.deb ``` On Raspberry Pi OS, if you have a graphical desktop installed, you can also install the package by double-clicking on the downloaded package in the File Manager. diff --git a/docs/download.md b/docs/download.md index 330fa98..f54549e 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,14 +4,14 @@ Download the most recent Debian (.deb) package for your platform: -- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v1.0.18/pipedal_1.0.18_arm64.deb) v1.0.18 +- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v1.0.19/pipedal_1.0.19_arm64.deb) v1.0.19 Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.0.18_arm64.deb + sudo apt-get install ./pipedal_1.0.19_arm64.deb ``` Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. diff --git a/docs/index.md b/docs/index.md index ed0af00..446d3ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v1.0.18 +v1.0.19   diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index dbba000..24d818f 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -55,12 +55,8 @@ namespace pipedal char name[40]; snd_pcm_format_t pcm_format; }; - [[noreturn]] static void AlsaError(const std::string &message) - { - throw PiPedalStateException(message); - } - static bool SetPreferredAlsaFormat( + bool SetPreferredAlsaFormat( const char *streamType, snd_pcm_t *handle, snd_pcm_hw_params_t *hwParams, @@ -78,7 +74,48 @@ namespace pipedal return false; } - static void SetPreferredAlsaFormat( + static AudioFormat leFormats[]{ + {"16-bit little-endian", SND_PCM_FORMAT_S16_LE}, + + {"32-bit float little-endian", SND_PCM_FORMAT_FLOAT_LE}, + {"32-bit integer little-endian", SND_PCM_FORMAT_S32_LE}, + {"24-bit little-endian", SND_PCM_FORMAT_S24_LE}, + {"24-bit little-endian in 3bytes format", SND_PCM_FORMAT_S24_3LE}, + {"16-bit little-endian", SND_PCM_FORMAT_S16_LE}, + + }; + static AudioFormat beFormats[]{ + {"32-bit float big-endian", SND_PCM_FORMAT_FLOAT_BE}, + {"32-bit integer big-endian", SND_PCM_FORMAT_S32_BE}, + {"24-bit big-endian", SND_PCM_FORMAT_S24_BE}, + {"24-bit big-endian in 3bytes format", SND_PCM_FORMAT_S24_3BE}, + {"16-bit big-endian", SND_PCM_FORMAT_S16_BE}, + }; + [[noreturn]] static void AlsaError(const std::string &message) + { + throw PiPedalStateException(message); + } + + std::string GetAlsaFormatDescription(snd_pcm_format_t format) + { + for (size_t i = 0; i < sizeof(beFormats) / sizeof(beFormats[0]); ++i) + { + if (beFormats[i].pcm_format == format) + { + return beFormats[i].name; + } + } + for (size_t i = 0; i < sizeof(leFormats) / sizeof(leFormats[0]); ++i) + { + if (leFormats[i].pcm_format == format) + { + return leFormats[i].name; + } + } + return "Unknown format."; + } + + void SetPreferredAlsaFormat( const std::string &alsa_device_name, const char *streamType, snd_pcm_t *handle, @@ -86,22 +123,6 @@ namespace pipedal { int err; - static AudioFormat leFormats[]{ - {"32-bit float little-endian", SND_PCM_FORMAT_FLOAT_LE}, - {"32-bit integer little-endian", SND_PCM_FORMAT_S32_LE}, - {"24-bit little-endian", SND_PCM_FORMAT_S24_LE}, - {"24-bit little-endian in 3bytes format", SND_PCM_FORMAT_S24_3LE}, - {"16-bit little-endian", SND_PCM_FORMAT_S16_LE}, - - }; - static AudioFormat beFormats[]{ - {"32-bit float big-endian", SND_PCM_FORMAT_FLOAT_BE}, - {"32-bit integer big-endian", SND_PCM_FORMAT_S32_BE}, - {"24-bit big-endian", SND_PCM_FORMAT_S24_BE}, - {"24-bit big-endian in 3bytes format", SND_PCM_FORMAT_S24_3BE}, - {"16-bit big-endian", SND_PCM_FORMAT_S16_BE}, - }; - if (std::endian::native == std::endian::big) { if (SetPreferredAlsaFormat(streamType, handle, hwParams, beFormats, sizeof(beFormats) / sizeof(beFormats[0]))) @@ -140,6 +161,8 @@ namespace pipedal uint32_t user_threshold = 0; bool soft_mode = false; + snd_pcm_format_t captureFormat = snd_pcm_format_t::SND_PCM_FORMAT_UNKNOWN; + uint32_t playbackSampleSize = 0; uint32_t captureSampleSize = 0; uint32_t playbackFrameSize = 0; @@ -208,6 +231,7 @@ namespace pipedal JackServerSettings jackServerSettings; std::string alsa_device_name; + snd_pcm_t *playbackHandle = nullptr; snd_pcm_t *captureHandle = nullptr; @@ -270,7 +294,7 @@ namespace pipedal snd_pcm_sw_params_free(playbackSwParams); playbackSwParams = nullptr; } - for (auto *midiState: this->midiStates) + for (auto *midiState : this->midiStates) { if (midiState != nullptr) { @@ -344,16 +368,18 @@ namespace pipedal snd_pcm_uframes_t effectivePeriodSize = this->bufferSize; + int dir = 0; if ((err = snd_pcm_hw_params_set_period_size_near(handle, hwParams, &effectivePeriodSize, - 0)) < 0) + &dir)) < 0) { AlsaError(SS("Can't set period size to " << this->bufferSize << " (" << alsa_device_name << "/" << streamType << ")")); } this->bufferSize = effectivePeriodSize; *periods = this->numberOfBuffers; - snd_pcm_hw_params_set_periods_min(handle, hwParams, periods, NULL); + dir = 0; + snd_pcm_hw_params_set_periods_min(handle, hwParams, periods, &dir); if (*periods < this->numberOfBuffers) *periods = this->numberOfBuffers; if (snd_pcm_hw_params_set_periods_near(handle, hwParams, @@ -508,10 +534,10 @@ namespace pipedal return (b0 << 8) | (b1); } - void EndianSwap(float*p,float v_) + void EndianSwap(float *p, float v_) { - int32_t v = EndianSwap(*(int32_t*)&v_); - *(int32_t*)p = v; + int32_t v = EndianSwap(*(int32_t *)&v_); + *(int32_t *)p = v; } void CopyCaptureFloatBe(size_t frames) @@ -527,7 +553,7 @@ namespace pipedal int32_t v = EndianSwap(*p); ++p; - *(int32_t*)(buffers[channel]+frame) = v; + *(int32_t *)(buffers[channel] + frame) = v; } } } @@ -599,16 +625,16 @@ namespace pipedal } void CopyCaptureS24_3Le(size_t frames) { - uint8_t *p = (uint8_t*)rawCaptureBuffer; + uint8_t *p = (uint8_t *)rawCaptureBuffer; std::vector &buffers = this->captureBuffers; int channels = this->captureChannels; - constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); + constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) { for (int channel = 0; channel < channels; ++channel) { - int32_t v = (p[0] << 8)+(p[1] << 16) | (p[2] << 24); + int32_t v = (p[0] << 8) + (p[1] << 16) | (p[2] << 24); p += 3; buffers[channel][frame] = scale * v; } @@ -616,16 +642,16 @@ namespace pipedal } void CopyCaptureS24_3Be(size_t frames) { - uint8_t *p = (uint8_t*)rawCaptureBuffer; + uint8_t *p = (uint8_t *)rawCaptureBuffer; std::vector &buffers = this->captureBuffers; int channels = this->captureChannels; - constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); + constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) { for (int channel = 0; channel < channels; ++channel) { - int32_t v = (p[2] << 8)+(p[1] << 16) | (p[0] << 24); + int32_t v = (p[2] << 8) + (p[1] << 16) | (p[0] << 24); p += 3; buffers[channel][frame] = scale * v; } @@ -819,10 +845,12 @@ namespace pipedal v = 1.0f; else if (v < -1.0f) v = -1.0f; - int32_t iValue = (int32_t)(scale * v); + int32_t iValue = (int32_t)(scale * v); p[0] = (uint8_t)(iValue >> 24); p[1] = (uint8_t)(iValue >> 16); p[2] = (uint8_t)(iValue >> 8); + + p += 3; } } } @@ -842,10 +870,12 @@ namespace pipedal v = 1.0f; else if (v < -1.0f) v = -1.0f; - int32_t iValue = (int32_t)(scale * v); + int32_t iValue = (int32_t)(scale * v); p[0] = (uint8_t)(iValue >> 8); p[1] = (uint8_t)(iValue >> 16); p[2] = (uint8_t)(iValue >> 24); + + p += 3; } } } @@ -876,11 +906,15 @@ namespace pipedal for (int channel = 0; channel < channels; ++channel) { float v = buffers[channel][frame]; - EndianSwap(p,v); + EndianSwap(p, v); p++; } } } + + public: + void TestFormatEncodeDecode(snd_pcm_format_t captureFormat); + private: void AllocateBuffers(std::vector &buffers, size_t n) { buffers.resize(n); @@ -907,16 +941,152 @@ namespace pipedal this->channelSelection = channelSelection; open = true; - try { + try + { OpenMidi(jackServerSettings, channelSelection); OpenAudio(jackServerSettings, channelSelection); - } catch (const std::exception&e) + } + catch (const std::exception &e) { Close(); throw; } } + void PrepareCaptureFunctions(snd_pcm_format_t captureFormat) + { + this->captureFormat = captureFormat; + + switch (captureFormat) + { + case SND_PCM_FORMAT_FLOAT_LE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureFloatLe; + break; + case SND_PCM_FORMAT_S24_3LE: + copyInputFn = &AlsaDriverImpl::CopyCaptureS24_3Le; + captureSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_LE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureS32Le; + break; + case SND_PCM_FORMAT_S24_LE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureS24Le; + break; + case SND_PCM_FORMAT_S16_LE: + captureSampleSize = 2; + copyInputFn = &AlsaDriverImpl::CopyCaptureS16Le; + break; + case SND_PCM_FORMAT_FLOAT_BE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureFloatBe; + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_3BE: + captureSampleSize = 3; + copyInputFn = &AlsaDriverImpl::CopyCaptureS24_3Be; + break; + case SND_PCM_FORMAT_S32_BE: + copyInputFn = &AlsaDriverImpl::CopyCaptureS32Be; + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_BE: + copyInputFn = &AlsaDriverImpl::CopyCaptureS24Be; + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_BE: + copyInputFn = &AlsaDriverImpl::CopyCaptureS16Be; + captureSampleSize = 2; + break; + default: + break; + } + if (copyInputFn == nullptr) + { + throw PiPedalStateException(SS("Audio input format not supported. (" << captureFormat << ")")); + } + + captureFrameSize = captureSampleSize * captureChannels; + rawCaptureBuffer = new uint8_t[captureFrameSize * bufferSize]; + memset(rawCaptureBuffer, 0, captureFrameSize * bufferSize); + + AllocateBuffers(captureBuffers, captureChannels); + } + + virtual std::string GetConfigurationDescription() + { + std::string result = SS( + "ALSA, " + << this->alsa_device_name + << ", " << GetAlsaFormatDescription(this->captureFormat) + << ", " << this->sampleRate + << ", " << this->bufferSize << "x" << this->numberOfBuffers + << ", in: " << this->InputBufferCount() << "/" << this->captureChannels + << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + return result; + } + void PreparePlaybackFunctions(snd_pcm_format_t playbackFormat) + { + copyOutputFn = nullptr; + switch (playbackFormat) + { + case SND_PCM_FORMAT_FLOAT_LE: + playbackSampleSize = 4; + copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatLe; + break; + case SND_PCM_FORMAT_S24_3LE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24_3Le; + playbackSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_LE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Le; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_LE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24Le; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_LE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS16Le; + playbackSampleSize = 2; + break; + case SND_PCM_FORMAT_FLOAT_BE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatBe; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_3BE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24_3Be; + playbackSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_BE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Be; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_BE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24Be; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_BE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS16Be; + playbackSampleSize = 2; + break; + default: + break; + } + if (copyOutputFn == nullptr) + { + throw PiPedalStateException(SS("Unsupported audio output format. (" << playbackFormat << ")")); + } + + playbackFrameSize = playbackSampleSize * playbackChannels; + rawPlaybackBuffer = new uint8_t[playbackFrameSize * bufferSize]; + memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize); + + AllocateBuffers(playbackBuffers, playbackChannels); + } + void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) { int err; @@ -1034,121 +1204,12 @@ namespace pipedal snd_pcm_hw_params_get_format(captureHwParams, &captureFormat); copyInputFn = nullptr; - switch (captureFormat) - { - case SND_PCM_FORMAT_FLOAT_LE: - captureSampleSize = 4; - copyInputFn = &AlsaDriverImpl::CopyCaptureFloatLe; - break; - case SND_PCM_FORMAT_S24_3LE: - copyInputFn = &AlsaDriverImpl::CopyCaptureS24_3Le; - captureSampleSize = 3; - break; - case SND_PCM_FORMAT_S32_LE: - captureSampleSize = 4; - copyInputFn = &AlsaDriverImpl::CopyCaptureS32Le; - break; - case SND_PCM_FORMAT_S24_LE: - captureSampleSize = 4; - copyInputFn = &AlsaDriverImpl::CopyCaptureS24Le; - break; - case SND_PCM_FORMAT_S16_LE: - captureSampleSize = 2; - copyInputFn = &AlsaDriverImpl::CopyCaptureS16Le; - break; - case SND_PCM_FORMAT_FLOAT_BE: - captureSampleSize = 4; - copyInputFn = &AlsaDriverImpl::CopyCaptureFloatBe; - captureSampleSize = 4; - break; - case SND_PCM_FORMAT_S24_3BE: - captureSampleSize = 3; - copyInputFn = &AlsaDriverImpl::CopyCaptureS24_3Be; - break; - case SND_PCM_FORMAT_S32_BE: - copyInputFn = &AlsaDriverImpl::CopyCaptureS32Be; - captureSampleSize = 4; - break; - case SND_PCM_FORMAT_S24_BE: - copyInputFn = &AlsaDriverImpl::CopyCaptureS24Be; - captureSampleSize = 4; - break; - case SND_PCM_FORMAT_S16_BE: - copyInputFn = &AlsaDriverImpl::CopyCaptureS16Be; - captureSampleSize = 2; - break; - default: - break; - } - if (copyInputFn == nullptr) - { - throw PiPedalStateException(SS("Audio input format not supported. (" << captureFormat << ")")); - } - captureFrameSize = captureSampleSize * captureChannels; - rawCaptureBuffer = new uint8_t[captureFrameSize * bufferSize]; - memset(rawCaptureBuffer, 0, captureFrameSize * bufferSize); - - AllocateBuffers(captureBuffers, captureChannels); + PrepareCaptureFunctions(captureFormat); snd_pcm_format_t playbackFormat; snd_pcm_hw_params_get_format(playbackHwParams, &playbackFormat); - copyOutputFn = nullptr; - switch (playbackFormat) - { - case SND_PCM_FORMAT_FLOAT_LE: - playbackSampleSize = 4; - copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatLe; - break; - case SND_PCM_FORMAT_S24_3LE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24_3Le; - playbackSampleSize = 3; - break; - case SND_PCM_FORMAT_S32_LE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Le; - playbackSampleSize = 4; - break; - case SND_PCM_FORMAT_S24_LE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24Le; - playbackSampleSize = 4; - break; - case SND_PCM_FORMAT_S16_LE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS16Le; - playbackSampleSize = 2; - break; - case SND_PCM_FORMAT_FLOAT_BE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatBe; - playbackSampleSize = 4; - break; - case SND_PCM_FORMAT_S24_3BE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24_3Be; - playbackSampleSize = 3; - break; - case SND_PCM_FORMAT_S32_BE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Be; - playbackSampleSize = 4; - break; - case SND_PCM_FORMAT_S24_BE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS24Be; - playbackSampleSize = 4; - break; - case SND_PCM_FORMAT_S16_BE: - copyOutputFn = &AlsaDriverImpl::CopyPlaybackS16Be; - playbackSampleSize = 2; - break; - default: - break; - } - if (copyOutputFn == nullptr) - { - throw PiPedalStateException(SS("Unsupported audio output format. (" << playbackFormat << ")")); - } - - playbackFrameSize = playbackSampleSize * playbackChannels; - rawPlaybackBuffer = new uint8_t[playbackFrameSize * bufferSize]; - memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize); - - AllocateBuffers(playbackBuffers, playbackChannels); + PreparePlaybackFunctions(playbackFormat); } catch (const std::exception &e) { @@ -1444,7 +1505,9 @@ namespace pipedal if (sourceIndex >= captureBuffers.size()) { Lv2Log::error(SS("Invalid audio input port: " << x)); - } else { + } + else + { this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; } } @@ -1458,7 +1521,9 @@ namespace pipedal if (sourceIndex >= playbackBuffers.size()) { Lv2Log::error(SS("Invalid audio output port: " << x)); - } else { + } + else + { this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; } } @@ -1836,6 +1901,16 @@ namespace pipedal activePlaybackBuffers.clear(); FreeBuffers(this->playbackBuffers); FreeBuffers(this->captureBuffers); + if (rawCaptureBuffer) + { + delete[] rawCaptureBuffer; + rawCaptureBuffer = nullptr; + } + if (rawPlaybackBuffer) + { + delete[] rawPlaybackBuffer; + rawPlaybackBuffer = nullptr; + } } virtual void Close() { @@ -1858,7 +1933,6 @@ namespace pipedal { return cpuUse.GetCpuOverhead(); } - }; AudioDriver *CreateAlsaDriver(AudioDriverHost *driverHost) @@ -1994,7 +2068,7 @@ namespace pipedal static void ExpectEvent(AlsaDriverImpl::MidiState &m, int event, const std::vector message) { MidiEvent e; - m.GetMidiInputEvent(&e,event); + m.GetMidiInputEvent(&e, event); AlsaAssert(e.size == message.size()); for (size_t i = 0; i < message.size(); ++i) { @@ -2002,6 +2076,83 @@ namespace pipedal } } + void AlsaDriverImpl::TestFormatEncodeDecode(snd_pcm_format_t captureFormat) + { + this->alsa_device_name = "Test"; + this->numberOfBuffers = 3; + this->bufferSize = 64; + this->user_threshold = this->bufferSize; + this->sampleRate = 44100; + this->captureChannels = 2; + this->playbackChannels = 2; + + PrepareCaptureFunctions(captureFormat); + PreparePlaybackFunctions(captureFormat); + + // make sure encode decode round-trips with reasonable accuracy. + + for (size_t i= 0; i < bufferSize; ++i) + { + for (size_t c = 0; c < captureChannels; ++c) + { + // provide a rich set of approximately readable bits in the output. + float value = 1.0f*i/bufferSize + +1.0f*(i)/(128.0*256.0); + + // only 16-bits of precision in data for 16-bit formats + if (captureFormat != snd_pcm_format_t::SND_PCM_FORMAT_S16_BE && captureFormat != snd_pcm_format_t::SND_PCM_FORMAT_S16_LE) + { + value += 1.0f*(c)/(128.0*256.0*256.0); + } + this->playbackBuffers[c][i] = value; + } + } + + (this->*copyOutputFn)(bufferSize); + + assert(captureFrameSize == playbackFrameSize); + memcpy(this->rawCaptureBuffer,this->rawPlaybackBuffer,captureFrameSize*bufferSize); + + (this->*copyInputFn)(bufferSize); + + for (size_t i= 0; i < bufferSize; ++i) + { + for (size_t c = 0; c < captureChannels; ++c) + { + float error = + this->captureBuffers[c][i] - this->playbackBuffers[c][i]; + + assert(std::abs(error) < 4e-5); + } + } + + + } + + void AlsaFormatEncodeDecodeTest(AudioDriverHost*testDriverHost) + { + static snd_pcm_format_t formats[] = { + snd_pcm_format_t::SND_PCM_FORMAT_S16_LE, + snd_pcm_format_t::SND_PCM_FORMAT_S16_BE, + snd_pcm_format_t::SND_PCM_FORMAT_S32_LE, + snd_pcm_format_t::SND_PCM_FORMAT_S32_BE, + snd_pcm_format_t::SND_PCM_FORMAT_S24_3BE, + snd_pcm_format_t::SND_PCM_FORMAT_S24_3LE, + snd_pcm_format_t::SND_PCM_FORMAT_FLOAT_BE, + snd_pcm_format_t::SND_PCM_FORMAT_FLOAT_LE, + }; + + for (auto format: formats) + { + // Check audio encode/decode. + std::unique_ptr alsaDriver { + (AlsaDriverImpl*)new AlsaDriverImpl(testDriverHost) + }; + + alsaDriver->TestFormatEncodeDecode(format); + } + + } void MidiDecoderTest() { @@ -2011,25 +2162,24 @@ namespace pipedal // Running status decoding. { - static uint8_t m0[] = {0x80, 0x1, 0x2, 0x3,0x4,0x5}; + static uint8_t m0[] = {0x80, 0x1, 0x2, 0x3, 0x4, 0x5}; midiState.NextEventBuffer(); midiState.WriteBuffer(m0, sizeof(m0)); AlsaAssert(midiState.GetMidiInputEventCount() == 2); AlsaAssert(midiState.GetMidiInputEvent(&event, 0)); - ExpectEvent(midiState,0, {0x80,0x1,0x2}); - ExpectEvent(midiState,1, {0x80,0x3,0x4}); + ExpectEvent(midiState, 0, {0x80, 0x1, 0x2}); + ExpectEvent(midiState, 1, {0x80, 0x3, 0x4}); - static uint8_t m1[] = {0x06,0xC0,0x1,0x2}; + static uint8_t m1[] = {0x06, 0xC0, 0x1, 0x2}; midiState.NextEventBuffer(); - midiState.WriteBuffer(m1,sizeof(m1)); + midiState.WriteBuffer(m1, sizeof(m1)); AlsaAssert(midiState.GetMidiInputEventCount() == 3); - ExpectEvent(midiState,0,{0x80,0x05,0x06}); - ExpectEvent(midiState,1,{0xC0,0x1}); - ExpectEvent(midiState,2,{0xC0,0x2}); + ExpectEvent(midiState, 0, {0x80, 0x05, 0x06}); + ExpectEvent(midiState, 1, {0xC0, 0x1}); + ExpectEvent(midiState, 2, {0xC0, 0x2}); } - // SYSEX. { static uint8_t m0[] = {0xF0, 0x76, 0xF7, 0xA}; diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp index 984aa04..d9a6aa2 100644 --- a/src/AlsaDriver.hpp +++ b/src/AlsaDriver.hpp @@ -37,7 +37,8 @@ namespace pipedal { AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost); - + // test only. + void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost); void MidiDecoderTest(); } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index a04decd..160071d 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -61,6 +61,9 @@ public: void Test() { + + AlsaFormatEncodeDecodeTest(this); + JackServerSettings serverSettings("hw:M2",48000,32,3); JackConfiguration jackConfiguration; diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index b7fd10c..0442ca2 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -84,6 +84,8 @@ namespace pipedal { virtual void Deactivate() = 0; virtual void Close() = 0; + virtual std::string GetConfigurationDescription() = 0; + }; }; \ No newline at end of file diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index e6b29b0..cf1a42d 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -1203,7 +1203,7 @@ public: active = true; audioDriver->Activate(); - Lv2Log::info("Audio started."); + Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription())); } catch (const std::exception &e) { diff --git a/src/mustache.hpp b/src/mustache.hpp new file mode 100644 index 0000000..047c849 --- /dev/null +++ b/src/mustache.hpp @@ -0,0 +1,1190 @@ +/* + * Boost Software License - Version 1.0 + * + * Mustache + * Copyright 2015-2020 Kevin Wojniak + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef KAINJOW_MUSTACHE_HPP +#define KAINJOW_MUSTACHE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#define KAINJOW_MUSTACHE_VERSION_MAJOR 5 +#define KAINJOW_MUSTACHE_VERSION_MINOR 0 +#define KAINJOW_MUSTACHE_VERSION_PATCH 0 + +namespace kainjow { +namespace mustache { + +template +string_type trim(const string_type& s) { + auto it = s.begin(); + while (it != s.end() && std::isspace(*it)) { + it++; + } + auto rit = s.rbegin(); + while (rit.base() != it && std::isspace(*rit)) { + rit++; + } + return {it, rit.base()}; +} + +template +string_type html_escape(const string_type& s) { + string_type ret; + ret.reserve(s.size()*2); + for (const auto ch : s) { + switch (ch) { + case '&': + ret.append({'&','a','m','p',';'}); + break; + case '<': + ret.append({'&','l','t',';'}); + break; + case '>': + ret.append({'&','g','t',';'}); + break; + case '\"': + ret.append({'&','q','u','o','t',';'}); + break; + case '\'': + ret.append({'&','a','p','o','s',';'}); + break; + default: + ret.append(1, ch); + break; + } + } + return ret; +} + +template +std::vector split(const string_type& s, typename string_type::value_type delim) { + std::vector elems; + std::basic_stringstream ss(s); + string_type item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } + return elems; +} + +template +class basic_renderer { +public: + using type1 = std::function; + using type2 = std::function; + + string_type operator()(const string_type& text) const { + return type1_(text); + } + + string_type operator()(const string_type& text, bool escaped) const { + return type2_(text, escaped); + } + +private: + basic_renderer(const type1& t1, const type2& t2) + : type1_(t1) + , type2_(t2) + {} + + const type1& type1_; + const type2& type2_; + + template + friend class basic_mustache; +}; + +template +class basic_lambda_t { +public: + using type1 = std::function; + using type2 = std::function& render)>; + + basic_lambda_t(const type1& t) : type1_(new type1(t)) {} + basic_lambda_t(const type2& t) : type2_(new type2(t)) {} + + bool is_type1() const { return static_cast(type1_); } + bool is_type2() const { return static_cast(type2_); } + + const type1& type1_value() const { return *type1_; } + const type2& type2_value() const { return *type2_; } + + // Copying + basic_lambda_t(const basic_lambda_t& l) { + if (l.type1_) { + type1_.reset(new type1(*l.type1_)); + } else if (l.type2_) { + type2_.reset(new type2(*l.type2_)); + } + } + + string_type operator()(const string_type& text) const { + return (*type1_)(text); + } + + string_type operator()(const string_type& text, const basic_renderer& render) const { + return (*type2_)(text, render); + } + +private: + std::unique_ptr type1_; + std::unique_ptr type2_; +}; + +template +class basic_data; +template +using basic_object = std::unordered_map>; +template +using basic_list = std::vector>; +template +using basic_partial = std::function; +template +using basic_lambda = typename basic_lambda_t::type1; +template +using basic_lambda2 = typename basic_lambda_t::type2; + +template +class basic_data { +public: + enum class type { + object, + string, + list, + bool_true, + bool_false, + partial, + lambda, + lambda2, + invalid, + }; + + // Construction + basic_data() : basic_data(type::object) { + } + basic_data(const string_type& string) : type_{type::string} { + str_.reset(new string_type(string)); + } + basic_data(const typename string_type::value_type* string) : type_{type::string} { + str_.reset(new string_type(string)); + } + basic_data(const basic_object& obj) : type_{type::object} { + obj_.reset(new basic_object(obj)); + } + basic_data(const basic_list& l) : type_{type::list} { + list_.reset(new basic_list(l)); + } + basic_data(type t) : type_{t} { + switch (type_) { + case type::object: + obj_.reset(new basic_object); + break; + case type::string: + str_.reset(new string_type); + break; + case type::list: + list_.reset(new basic_list); + break; + default: + break; + } + } + basic_data(const string_type& name, const basic_data& var) : basic_data{} { + set(name, var); + } + basic_data(const basic_partial& p) : type_{type::partial} { + partial_.reset(new basic_partial(p)); + } + basic_data(const basic_lambda& l) : type_{type::lambda} { + lambda_.reset(new basic_lambda_t(l)); + } + basic_data(const basic_lambda2& l) : type_{type::lambda2} { + lambda_.reset(new basic_lambda_t(l)); + } + basic_data(const basic_lambda_t& l) { + if (l.is_type1()) { + type_ = type::lambda; + } else if (l.is_type2()) { + type_ = type::lambda2; + } + lambda_.reset(new basic_lambda_t(l)); + } + basic_data(bool b) : type_{b ? type::bool_true : type::bool_false} { + } + + // Copying + basic_data(const basic_data& dat) : type_(dat.type_) { + if (dat.obj_) { + obj_.reset(new basic_object(*dat.obj_)); + } else if (dat.str_) { + str_.reset(new string_type(*dat.str_)); + } else if (dat.list_) { + list_.reset(new basic_list(*dat.list_)); + } else if (dat.partial_) { + partial_.reset(new basic_partial(*dat.partial_)); + } else if (dat.lambda_) { + lambda_.reset(new basic_lambda_t(*dat.lambda_)); + } + } + + // Move + basic_data(basic_data&& dat) : type_{dat.type_} { + if (dat.obj_) { + obj_ = std::move(dat.obj_); + } else if (dat.str_) { + str_ = std::move(dat.str_); + } else if (dat.list_) { + list_ = std::move(dat.list_); + } else if (dat.partial_) { + partial_ = std::move(dat.partial_); + } else if (dat.lambda_) { + lambda_ = std::move(dat.lambda_); + } + dat.type_ = type::invalid; + } + basic_data& operator= (basic_data&& dat) { + if (this != &dat) { + obj_.reset(); + str_.reset(); + list_.reset(); + partial_.reset(); + lambda_.reset(); + if (dat.obj_) { + obj_ = std::move(dat.obj_); + } else if (dat.str_) { + str_ = std::move(dat.str_); + } else if (dat.list_) { + list_ = std::move(dat.list_); + } else if (dat.partial_) { + partial_ = std::move(dat.partial_); + } else if (dat.lambda_) { + lambda_ = std::move(dat.lambda_); + } + type_ = dat.type_; + dat.type_ = type::invalid; + } + return *this; + } + + // Type info + bool is_object() const { + return type_ == type::object; + } + bool is_string() const { + return type_ == type::string; + } + bool is_list() const { + return type_ == type::list; + } + bool is_bool() const { + return is_true() || is_false(); + } + bool is_true() const { + return type_ == type::bool_true; + } + bool is_false() const { + return type_ == type::bool_false; + } + bool is_partial() const { + return type_ == type::partial; + } + bool is_lambda() const { + return type_ == type::lambda; + } + bool is_lambda2() const { + return type_ == type::lambda2; + } + bool is_invalid() const { + return type_ == type::invalid; + } + + // Object data + bool is_empty_object() const { + return is_object() && obj_->empty(); + } + bool is_non_empty_object() const { + return is_object() && !obj_->empty(); + } + void set(const string_type& name, const basic_data& var) { + if (is_object()) { + auto it = obj_->find(name); + if (it != obj_->end()) { + obj_->erase(it); + } + obj_->insert(std::pair{name, var}); + } + } + const basic_data* get(const string_type& name) const { + if (!is_object()) { + return nullptr; + } + const auto& it = obj_->find(name); + if (it == obj_->end()) { + return nullptr; + } + return &it->second; + } + + // List data + void push_back(const basic_data& var) { + if (is_list()) { + list_->push_back(var); + } + } + const basic_list& list_value() const { + return *list_; + } + bool is_empty_list() const { + return is_list() && list_->empty(); + } + bool is_non_empty_list() const { + return is_list() && !list_->empty(); + } + basic_data& operator<< (const basic_data& data) { + push_back(data); + return *this; + } + + // String data + const string_type& string_value() const { + return *str_; + } + + basic_data& operator[] (const string_type& key) { + return (*obj_)[key]; + } + + const basic_partial& partial_value() const { + return (*partial_); + } + + const basic_lambda& lambda_value() const { + return lambda_->type1_value(); + } + + const basic_lambda2& lambda2_value() const { + return lambda_->type2_value(); + } + +private: + type type_; + std::unique_ptr> obj_; + std::unique_ptr str_; + std::unique_ptr> list_; + std::unique_ptr> partial_; + std::unique_ptr> lambda_; +}; + +template +class delimiter_set { +public: + string_type begin; + string_type end; + delimiter_set() + : begin(default_begin) + , end(default_end) + {} + bool is_default() const { return begin == default_begin && end == default_end; } + static const string_type default_begin; + static const string_type default_end; +}; + +template +const string_type delimiter_set::default_begin(2, '{'); +template +const string_type delimiter_set::default_end(2, '}'); + +template +class basic_context { +public: + virtual ~basic_context() = default; + virtual void push(const basic_data* data) = 0; + virtual void pop() = 0; + + virtual const basic_data* get(const string_type& name) const = 0; + virtual const basic_data* get_partial(const string_type& name) const = 0; +}; + +template +class context : public basic_context { +public: + context(const basic_data* data) { + push(data); + } + + context() { + } + + virtual void push(const basic_data* data) override { + items_.insert(items_.begin(), data); + } + + virtual void pop() override { + items_.erase(items_.begin()); + } + + virtual const basic_data* get(const string_type& name) const override { + // process {{.}} name + if (name.size() == 1 && name.at(0) == '.') { + return items_.front(); + } + if (name.find('.') == string_type::npos) { + // process normal name without having to split which is slower + for (const auto& item : items_) { + const auto var = item->get(name); + if (var) { + return var; + } + } + return nullptr; + } + // process x.y-like name + const auto names = split(name, '.'); + for (const auto& item : items_) { + auto var = item; + for (const auto& n : names) { + var = var->get(n); + if (!var) { + break; + } + } + if (var) { + return var; + } + } + return nullptr; + } + + virtual const basic_data* get_partial(const string_type& name) const override { + for (const auto& item : items_) { + const auto var = item->get(name); + if (var) { + return var; + } + } + return nullptr; + } + + context(const context&) = delete; + context& operator= (const context&) = delete; + +private: + std::vector*> items_; +}; + +template +class line_buffer_state { +public: + string_type data; + bool contained_section_tag = false; + + bool is_empty_or_contains_only_whitespace() const { + for (const auto ch : data) { + // don't look at newlines + if (ch != ' ' && ch != '\t') { + return false; + } + } + return true; + } + + void clear() { + data.clear(); + contained_section_tag = false; + } +}; + +template +class context_internal { +public: + basic_context& ctx; + delimiter_set delim_set; + line_buffer_state line_buffer; + + context_internal(basic_context& a_ctx) + : ctx(a_ctx) + { + } +}; + +enum class tag_type { + text, + variable, + unescaped_variable, + section_begin, + section_end, + section_begin_inverted, + comment, + partial, + set_delimiter, +}; + +template +class mstch_tag /* gcc doesn't allow "tag tag;" so rename the class :( */ { +public: + string_type name; + tag_type type = tag_type::text; + std::shared_ptr section_text; + std::shared_ptr> delim_set; + bool is_section_begin() const { + return type == tag_type::section_begin || type == tag_type::section_begin_inverted; + } + bool is_section_end() const { + return type == tag_type::section_end; + } +}; + +template +class context_pusher { +public: + context_pusher(context_internal& ctx, const basic_data* data) + : ctx_(ctx) + { + ctx.ctx.push(data); + } + ~context_pusher() { + ctx_.ctx.pop(); + } + context_pusher(const context_pusher&) = delete; + context_pusher& operator= (const context_pusher&) = delete; +private: + context_internal& ctx_; +}; + +template +class component { +private: + using string_size_type = typename string_type::size_type; + +public: + string_type text; + mstch_tag tag; + std::vector children; + string_size_type position = string_type::npos; + + enum class walk_control { + walk, // "continue" is reserved :/ + stop, + skip, + }; + using walk_callback = std::function; + + component() {} + component(const string_type& t, string_size_type p) : text(t), position(p) {} + + bool is_text() const { + return tag.type == tag_type::text; + } + + bool is_newline() const { + return is_text() && ((text.size() == 2 && text[0] == '\r' && text[1] == '\n') || + (text.size() == 1 && (text[0] == '\n' || text[0] == '\r'))); + } + + bool is_non_newline_whitespace() const { + return is_text() && !is_newline() && text.size() == 1 && (text[0] == ' ' || text[0] == '\t'); + } + + void walk_children(const walk_callback& callback) { + for (auto& child : children) { + if (child.walk(callback) != walk_control::walk) { + break; + } + } + } + +private: + walk_control walk(const walk_callback& callback) { + walk_control control{callback(*this)}; + if (control == walk_control::stop) { + return control; + } else if (control == walk_control::skip) { + return walk_control::walk; + } + for (auto& child : children) { + control = child.walk(callback); + if (control == walk_control::stop) { + return control; + } + } + return control; + } +}; + +template +class parser { +public: + parser(const string_type& input, context_internal& ctx, component& root_component, string_type& error_message) + { + parse(input, ctx, root_component, error_message); + } + +private: + void parse(const string_type& input, context_internal& ctx, component& root_component, string_type& error_message) const { + using string_size_type = typename string_type::size_type; + using streamstring = std::basic_ostringstream; + + const string_type brace_delimiter_end_unescaped(3, '}'); + const string_size_type input_size{input.size()}; + + bool current_delimiter_is_brace{ctx.delim_set.is_default()}; + + std::vector*> sections{&root_component}; + std::vector section_starts; + string_type current_text; + string_size_type current_text_position = string_type::npos; + + current_text.reserve(input_size); + + const auto process_current_text = [¤t_text, ¤t_text_position, §ions]() { + if (!current_text.empty()) { + const component comp{current_text, current_text_position}; + sections.back()->children.push_back(comp); + current_text.clear(); + current_text_position = string_type::npos; + } + }; + + const std::vector whitespace{ + string_type(1, '\r') + string_type(1, '\n'), + string_type(1, '\n'), + string_type(1, '\r'), + string_type(1, ' '), + string_type(1, '\t'), + }; + + for (string_size_type input_position = 0; input_position != input_size;) { + bool parse_tag = false; + + if (input.compare(input_position, ctx.delim_set.begin.size(), ctx.delim_set.begin) == 0) { + process_current_text(); + + // Tag start delimiter + parse_tag = true; + } else { + bool parsed_whitespace = false; + for (const auto& whitespace_text : whitespace) { + if (input.compare(input_position, whitespace_text.size(), whitespace_text) == 0) { + process_current_text(); + + const component comp{whitespace_text, input_position}; + sections.back()->children.push_back(comp); + input_position += whitespace_text.size(); + + parsed_whitespace = true; + break; + } + } + + if (!parsed_whitespace) { + if (current_text.empty()) { + current_text_position = input_position; + } + current_text.append(1, input[input_position]); + input_position++; + } + } + + if (!parse_tag) { + continue; + } + + // Find the next tag start delimiter + const string_size_type tag_location_start = input_position; + + // Find the next tag end delimiter + string_size_type tag_contents_location{tag_location_start + ctx.delim_set.begin.size()}; + const bool tag_is_unescaped_var{current_delimiter_is_brace && tag_location_start != (input_size - 2) && input.at(tag_contents_location) == ctx.delim_set.begin.at(0)}; + const string_type& current_tag_delimiter_end{tag_is_unescaped_var ? brace_delimiter_end_unescaped : ctx.delim_set.end}; + const auto current_tag_delimiter_end_size = current_tag_delimiter_end.size(); + if (tag_is_unescaped_var) { + ++tag_contents_location; + } + const string_size_type tag_location_end{input.find(current_tag_delimiter_end, tag_contents_location)}; + if (tag_location_end == string_type::npos) { + streamstring ss; + ss << "Unclosed tag at " << tag_location_start; + error_message.assign(ss.str()); + return; + } + + // Parse tag + const string_type tag_contents{trim(string_type{input, tag_contents_location, tag_location_end - tag_contents_location})}; + component comp; + if (!tag_contents.empty() && tag_contents[0] == '=') { + if (!parse_set_delimiter_tag(tag_contents, ctx.delim_set)) { + streamstring ss; + ss << "Invalid set delimiter tag at " << tag_location_start; + error_message.assign(ss.str()); + return; + } + current_delimiter_is_brace = ctx.delim_set.is_default(); + comp.tag.type = tag_type::set_delimiter; + comp.tag.delim_set.reset(new delimiter_set(ctx.delim_set)); + } + if (comp.tag.type != tag_type::set_delimiter) { + parse_tag_contents(tag_is_unescaped_var, tag_contents, comp.tag); + } + comp.position = tag_location_start; + sections.back()->children.push_back(comp); + + // Start next search after this tag + input_position = tag_location_end + current_tag_delimiter_end_size; + + // Push or pop sections + if (comp.tag.is_section_begin()) { + sections.push_back(§ions.back()->children.back()); + section_starts.push_back(input_position); + } else if (comp.tag.is_section_end()) { + if (sections.size() == 1) { + streamstring ss; + ss << "Unopened section \"" << comp.tag.name << "\" at " << comp.position; + error_message.assign(ss.str()); + return; + } + sections.back()->tag.section_text.reset(new string_type(input.substr(section_starts.back(), tag_location_start - section_starts.back()))); + sections.pop_back(); + section_starts.pop_back(); + } + } + + process_current_text(); + + // Check for sections without an ending tag + root_component.walk_children([&error_message](component& comp) -> typename component::walk_control { + if (!comp.tag.is_section_begin()) { + return component::walk_control::walk; + } + if (comp.children.empty() || !comp.children.back().tag.is_section_end() || comp.children.back().tag.name != comp.tag.name) { + streamstring ss; + ss << "Unclosed section \"" << comp.tag.name << "\" at " << comp.position; + error_message.assign(ss.str()); + return component::walk_control::stop; + } + comp.children.pop_back(); // remove now useless end section component + return component::walk_control::walk; + }); + if (!error_message.empty()) { + return; + } + } + + bool is_set_delimiter_valid(const string_type& delimiter) const { + // "Custom delimiters may not contain whitespace or the equals sign." + for (const auto ch : delimiter) { + if (ch == '=' || std::isspace(ch)) { + return false; + } + } + return true; + } + + bool parse_set_delimiter_tag(const string_type& contents, delimiter_set& delimiter_set) const { + // Smallest legal tag is "=X X=" + if (contents.size() < 5) { + return false; + } + if (contents.back() != '=') { + return false; + } + const auto contents_substr = trim(contents.substr(1, contents.size() - 2)); + const auto spacepos = contents_substr.find(' '); + if (spacepos == string_type::npos) { + return false; + } + const auto nonspace = contents_substr.find_first_not_of(' ', spacepos + 1); + assert(nonspace != string_type::npos); + const string_type begin = contents_substr.substr(0, spacepos); + const string_type end = contents_substr.substr(nonspace, contents_substr.size() - nonspace); + if (!is_set_delimiter_valid(begin) || !is_set_delimiter_valid(end)) { + return false; + } + delimiter_set.begin = begin; + delimiter_set.end = end; + return true; + } + + void parse_tag_contents(bool is_unescaped_var, const string_type& contents, mstch_tag& tag) const { + if (is_unescaped_var) { + tag.type = tag_type::unescaped_variable; + tag.name = contents; + } else if (contents.empty()) { + tag.type = tag_type::variable; + tag.name.clear(); + } else { + switch (contents.at(0)) { + case '#': + tag.type = tag_type::section_begin; + break; + case '^': + tag.type = tag_type::section_begin_inverted; + break; + case '/': + tag.type = tag_type::section_end; + break; + case '>': + tag.type = tag_type::partial; + break; + case '&': + tag.type = tag_type::unescaped_variable; + break; + case '!': + tag.type = tag_type::comment; + break; + default: + tag.type = tag_type::variable; + break; + } + if (tag.type == tag_type::variable) { + tag.name = contents; + } else { + string_type name{contents}; + name.erase(name.begin()); + tag.name = trim(name); + } + } + } +}; + +template +class basic_mustache { +public: + using string_type = StringType; + + basic_mustache(const string_type& input) + : basic_mustache() { + context ctx; + context_internal context{ctx}; + parser parser{input, context, root_component_, error_message_}; + } + + bool is_valid() const { + return error_message_.empty(); + } + + const string_type& error_message() const { + return error_message_; + } + + using escape_handler = std::function; + void set_custom_escape(const escape_handler& escape_fn) { + escape_ = escape_fn; + } + + template + stream_type& render(const basic_data& data, stream_type& stream) { + render(data, [&stream](const string_type& str) { + stream << str; + }); + return stream; + } + + string_type render(const basic_data& data) { + std::basic_ostringstream ss; + return render(data, ss).str(); + } + + template + stream_type& render(basic_context& ctx, stream_type& stream) { + context_internal context{ctx}; + render([&stream](const string_type& str) { + stream << str; + }, context); + return stream; + } + + string_type render(basic_context& ctx) { + std::basic_ostringstream ss; + return render(ctx, ss).str(); + } + + using render_handler = std::function; + void render(const basic_data& data, const render_handler& handler) { + if (!is_valid()) { + return; + } + context ctx{&data}; + context_internal context{ctx}; + render(handler, context); + } + + basic_mustache() + : escape_(html_escape) + { + } + +private: + using string_size_type = typename string_type::size_type; + + + basic_mustache(const string_type& input, context_internal& ctx) + : basic_mustache() { + parser parser{input, ctx, root_component_, error_message_}; + } + + string_type render(context_internal& ctx) { + std::basic_ostringstream ss; + render([&ss](const string_type& str) { + ss << str; + }, ctx); + return ss.str(); + } + + void render(const render_handler& handler, context_internal& ctx, bool root_renderer = true) { + root_component_.walk_children([&handler, &ctx, this](component& comp) -> typename component::walk_control { + return render_component(handler, ctx, comp); + }); + // process the last line, but only for the top-level renderer + if (root_renderer) { + render_current_line(handler, ctx, nullptr); + } + } + + void render_current_line(const render_handler& handler, context_internal& ctx, const component* comp) const { + // We're at the end of a line, so check the line buffer state to see + // if the line had tags in it, and also if the line is now empty or + // contains whitespace only. if this situation is true, skip the line. + bool output = true; + if (ctx.line_buffer.contained_section_tag && ctx.line_buffer.is_empty_or_contains_only_whitespace()) { + output = false; + } + if (output) { + handler(ctx.line_buffer.data); + if (comp) { + handler(comp->text); + } + } + ctx.line_buffer.clear(); + } + + void render_result(context_internal& ctx, const string_type& text) const { + ctx.line_buffer.data.append(text); + } + + typename component::walk_control render_component(const render_handler& handler, context_internal& ctx, component& comp) { + if (comp.is_text()) { + if (comp.is_newline()) { + render_current_line(handler, ctx, &comp); + } else { + render_result(ctx, comp.text); + } + return component::walk_control::walk; + } + + const mstch_tag& tag{comp.tag}; + const basic_data* var = nullptr; + switch (tag.type) { + case tag_type::variable: + case tag_type::unescaped_variable: + if ((var = ctx.ctx.get(tag.name)) != nullptr) { + if (!render_variable(handler, var, ctx, tag.type == tag_type::variable)) { + return component::walk_control::stop; + } + } + break; + case tag_type::section_begin: + if ((var = ctx.ctx.get(tag.name)) != nullptr) { + if (var->is_lambda() || var->is_lambda2()) { + if (!render_lambda(handler, var, ctx, render_lambda_escape::optional, *comp.tag.section_text, true)) { + return component::walk_control::stop; + } + } else if (!var->is_false() && !var->is_empty_list()) { + render_section(handler, ctx, comp, var); + } + } + return component::walk_control::skip; + case tag_type::section_begin_inverted: + if ((var = ctx.ctx.get(tag.name)) == nullptr || var->is_false() || var->is_empty_list()) { + render_section(handler, ctx, comp, var); + } + return component::walk_control::skip; + case tag_type::partial: + if ((var = ctx.ctx.get_partial(tag.name)) != nullptr && (var->is_partial() || var->is_string())) { + const auto& partial_result = var->is_partial() ? var->partial_value()() : var->string_value(); + basic_mustache tmpl{partial_result}; + tmpl.set_custom_escape(escape_); + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + } else { + tmpl.render(handler, ctx, false); + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + } + } + if (!tmpl.is_valid()) { + return component::walk_control::stop; + } + } + break; + case tag_type::set_delimiter: + ctx.delim_set = *comp.tag.delim_set; + break; + default: + break; + } + + return component::walk_control::walk; + } + + enum class render_lambda_escape { + escape, + unescape, + optional, + }; + + bool render_lambda(const render_handler& handler, const basic_data* var, context_internal& ctx, render_lambda_escape escape, const string_type& text, bool parse_with_same_context) { + const typename basic_renderer::type2 render2 = [this, &ctx, parse_with_same_context, escape](const string_type& text, bool escaped) { + const auto process_template = [this, &ctx, escape, escaped](basic_mustache& tmpl) -> string_type { + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + return {}; + } + context_internal render_ctx{ctx.ctx}; // start a new line_buffer + const auto str = tmpl.render(render_ctx); + if (!tmpl.is_valid()) { + error_message_ = tmpl.error_message(); + return {}; + } + bool do_escape = false; + switch (escape) { + case render_lambda_escape::escape: + do_escape = true; + break; + case render_lambda_escape::unescape: + do_escape = false; + break; + case render_lambda_escape::optional: + do_escape = escaped; + break; + } + return do_escape ? escape_(str) : str; + }; + if (parse_with_same_context) { + basic_mustache tmpl{text, ctx}; + tmpl.set_custom_escape(escape_); + return process_template(tmpl); + } + basic_mustache tmpl{text}; + tmpl.set_custom_escape(escape_); + return process_template(tmpl); + }; + const typename basic_renderer::type1 render = [&render2](const string_type& text) { + return render2(text, false); + }; + if (var->is_lambda2()) { + const basic_renderer renderer{render, render2}; + render_result(ctx, var->lambda2_value()(text, renderer)); + } else { + render_current_line(handler, ctx, nullptr); + render_result(ctx, render(var->lambda_value()(text))); + } + return error_message_.empty(); + } + + bool render_variable(const render_handler& handler, const basic_data* var, context_internal& ctx, bool escaped) { + if (var->is_string()) { + const auto& varstr = var->string_value(); + render_result(ctx, escaped ? escape_(varstr) : varstr); + } else if (var->is_lambda()) { + const render_lambda_escape escape_opt = escaped ? render_lambda_escape::escape : render_lambda_escape::unescape; + return render_lambda(handler, var, ctx, escape_opt, {}, false); + } else if (var->is_lambda2()) { + using streamstring = std::basic_ostringstream; + streamstring ss; + ss << "Lambda with render argument is not allowed for regular variables"; + error_message_ = ss.str(); + return false; + } + return true; + } + + void render_section(const render_handler& handler, context_internal& ctx, component& incomp, const basic_data* var) { + const auto callback = [&handler, &ctx, this](component& comp) -> typename component::walk_control { + return render_component(handler, ctx, comp); + }; + if (var && var->is_non_empty_list()) { + for (const auto& item : var->list_value()) { + // account for the section begin tag + ctx.line_buffer.contained_section_tag = true; + + const context_pusher ctxpusher{ctx, &item}; + incomp.walk_children(callback); + + // ctx may have been cleared. account for the section end tag + ctx.line_buffer.contained_section_tag = true; + } + } else if (var) { + // account for the section begin tag + ctx.line_buffer.contained_section_tag = true; + + const context_pusher ctxpusher{ctx, var}; + incomp.walk_children(callback); + + // ctx may have been cleared. account for the section end tag + ctx.line_buffer.contained_section_tag = true; + } else { + // account for the section begin tag + ctx.line_buffer.contained_section_tag = true; + + incomp.walk_children(callback); + + // ctx may have been cleared. account for the section end tag + ctx.line_buffer.contained_section_tag = true; + } + } + +private: + string_type error_message_; + component root_component_; + escape_handler escape_; +}; + +using mustache = basic_mustache; +using data = basic_data; +using object = basic_object; +using list = basic_list; +using partial = basic_partial; +using renderer = basic_renderer; +using lambda = basic_lambda; +using lambda2 = basic_lambda2; +using lambda_t = basic_lambda_t; + +using mustachew = basic_mustache; +using dataw = basic_data; + +} // namespace mustache +} // namespace kainjow + +#endif // KAINJOW_MUSTACHE_HPP \ No newline at end of file