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