this.handleMidiSelection()} >
- Select MIDI input channels
+ Select MIDI input
{this.midiSummary()}
@@ -767,9 +768,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
(this.state.showMidiSelectDialog) &&
(
this.handleSelectMidiDialogResult(selectedChannels)}
- selectedChannels={this.state.jackSettings.inputMidiPorts}
- availableChannels={this.state.jackConfiguration.inputMidiPorts}
+ onClose={(selectedChannels: AlsaMidiDeviceInfo[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
+ selectedChannels={this.state.jackSettings.inputMidiDevices}
+ availableChannels={this.state.jackConfiguration.inputMidiDevices}
/>
)
diff --git a/src/AdminMain.cpp b/src/AdminMain.cpp
index c26db87..3b68132 100644
--- a/src/AdminMain.cpp
+++ b/src/AdminMain.cpp
@@ -204,10 +204,16 @@ bool setJackConfiguration(JackServerSettings serverSettings)
{
bool success = true;
+#if JACK_HOST
+
serverSettings.Write();
silentSysExec("/usr/bin/systemctl unmask jack");
silentSysExec("/usr/bin/systemctl enable jack");
+#else
+ // otherwise,
+ // the config was written before invoking admin main. but we still need to restart the service.
+#endif
std::thread delayedRestartThread(delayedRestartProc);
delayedRestartThread.detach();
diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp
new file mode 100644
index 0000000..a49bf3c
--- /dev/null
+++ b/src/AlsaDriver.cpp
@@ -0,0 +1,1686 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "pch.h"
+#include
+#include
+#include "ss.hpp"
+#include "AlsaDriver.hpp"
+#include "JackServerSettings.hpp"
+#include
+
+#include "CpuUse.hpp"
+
+#include
+
+#include "Lv2Log.hpp"
+#include
+
+#undef ALSADRIVER_CONFIG_DBG
+
+#ifdef ALSADRIVER_CONFIG_DBG
+#include
+#endif
+
+using namespace pipedal;
+
+namespace pipedal
+{
+
+ struct AudioFormat
+ {
+ char name[40];
+ snd_pcm_format_t pcm_format;
+ };
+ [[noreturn]] static void AlsaError(const std::string &message)
+ {
+ throw PiPedalStateException(message);
+ }
+
+ static bool SetPreferredAlsaFormat(
+ const char *streamType,
+ snd_pcm_t *handle,
+ snd_pcm_hw_params_t *hwParams,
+ AudioFormat *formats,
+ size_t nItems)
+ {
+ for (size_t i = 0; i < nItems; ++i)
+ {
+ int err = snd_pcm_hw_params_set_format(handle, hwParams, formats[i].pcm_format);
+ if (err == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static void SetPreferredAlsaFormat(
+ const std::string &alsa_device_name,
+ const char *streamType,
+ snd_pcm_t *handle,
+ snd_pcm_hw_params_t *hwParams)
+ {
+ int err;
+
+ static AudioFormat leFormats[]{
+ {"32-bit float little-endian", SND_PCM_FORMAT_FLOAT_LE},
+ {"24-bit little-endian in 3bytes format", SND_PCM_FORMAT_S24_3LE},
+ {"32-bit integer little-endian", SND_PCM_FORMAT_S32_LE},
+ {"24-bit little-endian", SND_PCM_FORMAT_S24_LE},
+ {"16-bit little-endian", SND_PCM_FORMAT_S16_LE},
+
+ };
+ static AudioFormat beFormats[]{
+ {"32-bit float big-endian", SND_PCM_FORMAT_FLOAT_BE},
+ {"24-bit big-endian in 3bytes format", SND_PCM_FORMAT_S24_3BE},
+ {"32-bit integer big-endian", SND_PCM_FORMAT_S32_BE},
+ {"24-bit big-endian", SND_PCM_FORMAT_S24_BE},
+ {"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])))
+ return;
+ if (SetPreferredAlsaFormat(streamType, handle, hwParams, leFormats, sizeof(leFormats) / sizeof(leFormats[0])))
+ return;
+ }
+ else
+ {
+ if (SetPreferredAlsaFormat(streamType, handle, hwParams, leFormats, sizeof(leFormats) / sizeof(leFormats[0])))
+ return;
+ if (SetPreferredAlsaFormat(streamType, handle, hwParams, beFormats, sizeof(beFormats) / sizeof(beFormats[0])))
+ return;
+ }
+ AlsaError(SS("No supported audio formats (" << alsa_device_name << "/" << streamType << ")"));
+ }
+
+ class AlsaDriverImpl : public AudioDriver
+ {
+ private:
+ pipedal::CpuUse cpuUse;
+
+#ifdef ALSADRIVER_CONFIG_DBG
+ snd_output_t *snd_output = nullptr;
+ snd_pcm_status_t *snd_status = nullptr;
+
+#endif
+ uint32_t sampleRate = 0;
+
+ uint32_t bufferSize;
+ uint32_t numberOfBuffers;
+
+ int playbackChannels = 0;
+ int captureChannels = 0;
+
+ uint32_t user_threshold = 0;
+ bool soft_mode = false;
+
+ uint32_t playbackSampleSize = 0;
+ uint32_t captureSampleSize = 0;
+ uint32_t playbackFrameSize = 0;
+ uint32_t captureFrameSize = 0;
+
+ using CopyFunction = void (AlsaDriverImpl::*)(size_t frames);
+
+ CopyFunction copyInputFn;
+ CopyFunction copyOutputFn;
+
+ bool inputSwapped = false;
+ bool outputSwapped = false;
+
+ std::vector activeCaptureBuffers;
+ std::vector activePlaybackBuffers;
+
+ std::vector captureBuffers;
+ std::vector playbackBuffers;
+
+ uint8_t *rawCaptureBuffer = nullptr;
+ uint8_t *rawPlaybackBuffer = nullptr;
+
+ AudioDriverHost *driverHost = nullptr;
+
+ public:
+ AlsaDriverImpl(AudioDriverHost *driverHost)
+ : driverHost(driverHost)
+ {
+#ifdef ALSADRIVER_CONFIG_DBG
+ snd_output_stdio_attach(&snd_output, stdout, 0);
+ snd_pcm_status_malloc(&snd_status);
+#endif
+ }
+ virtual ~AlsaDriverImpl()
+ {
+ Close();
+#ifdef ALSADRIVER_CONFIG_DBG
+ snd_output_close(snd_output);
+ snd_pcm_status_free(snd_status);
+#endif
+ }
+
+ private:
+ void OnShutdown()
+ {
+ Lv2Log::info("Jack Audio Server has shut down.");
+ }
+
+ static void
+ jack_shutdown_fn(void *arg)
+ {
+ ((AlsaDriverImpl *)arg)->OnShutdown();
+ }
+
+ static int xrun_callback_fn(void *arg)
+ {
+ ((AudioDriverHost *)arg)->OnUnderrun();
+ return 0;
+ }
+
+ virtual uint32_t GetSampleRate()
+ {
+ return this->sampleRate;
+ }
+
+ JackServerSettings jackServerSettings;
+
+ std::string alsa_device_name;
+ snd_pcm_t *playbackHandle = nullptr;
+ snd_pcm_t *captureHandle = nullptr;
+
+ unsigned int periods = 0;
+
+ snd_pcm_hw_params_t *captureHwParams;
+ snd_pcm_sw_params_t *captureSwParams;
+ snd_pcm_hw_params_t *playbackHwParams;
+ snd_pcm_sw_params_t *playbackSwParams;
+
+ bool capture_and_playback_not_synced = false;
+
+ std::mutex terminateSync;
+
+ bool terminateAudio_ = false;
+
+ void terminateAudio(bool terminate)
+ {
+ std::lock_guard lock{terminateSync};
+ this->terminateAudio_ = terminate;
+ }
+
+ bool terminateAudio()
+ {
+ std::lock_guard lock{terminateSync};
+ return this->terminateAudio_;
+ }
+
+ private:
+ void AlsaCleanup()
+ {
+
+ if (captureHandle)
+ {
+ snd_pcm_close(captureHandle);
+ captureHandle = nullptr;
+ }
+ if (playbackHandle)
+ {
+ snd_pcm_close(playbackHandle);
+ playbackHandle = nullptr;
+ }
+ if (captureHwParams)
+ {
+ snd_pcm_hw_params_free(captureHwParams);
+ captureHwParams = nullptr;
+ }
+ if (captureSwParams)
+ {
+ snd_pcm_sw_params_free(captureSwParams);
+ captureSwParams = nullptr;
+ }
+ if (playbackHwParams)
+ {
+ snd_pcm_hw_params_free(playbackHwParams);
+ playbackHwParams = nullptr;
+ }
+ if (playbackSwParams)
+ {
+ snd_pcm_sw_params_free(playbackSwParams);
+ playbackSwParams = nullptr;
+ }
+ }
+
+ std::string discover_alsa_using_apps()
+ {
+ return ""; // xxx fix me.
+ }
+
+ void AlsaConfigureStream(
+ const std::string &alsa_device_name,
+ const char *streamType,
+ snd_pcm_t *handle,
+ snd_pcm_hw_params_t *hwParams,
+ snd_pcm_sw_params_t *swParams,
+ int *channels,
+ unsigned int *periods)
+ {
+ int err;
+ snd_pcm_uframes_t stop_th;
+
+ if ((err = snd_pcm_hw_params_any(handle, hwParams)) < 0)
+ {
+ AlsaError(SS("No playback configurations available (" << snd_strerror(err) << ")"));
+ }
+
+ err = snd_pcm_hw_params_set_access(handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED);
+ if (err < 0)
+ {
+ AlsaError("snd_pcm_hw_params_set_access failed.");
+ }
+
+ SetPreferredAlsaFormat(alsa_device_name, streamType, handle, hwParams);
+
+ unsigned int sampleRate = (unsigned int)this->sampleRate;
+ err = snd_pcm_hw_params_set_rate_near(handle, hwParams,
+ &sampleRate, NULL);
+ this->sampleRate = sampleRate;
+ if (err < 0)
+ {
+ AlsaError(SS("Can't set sample rate to " << this->sampleRate << " (" << alsa_device_name << "/" << streamType << ")"));
+ }
+ if (!*channels)
+ {
+ /*if not user-specified, try to find the maximum
+ * number of channels */
+ unsigned int channels_max;
+ err = snd_pcm_hw_params_get_channels_max(hwParams,
+ &channels_max);
+ *channels = channels_max;
+
+ if (*channels > 1024)
+ {
+ // The default PCM device has unlimited channels.
+ // report 2 channels
+ *channels = 2;
+ }
+ }
+
+ if ((err = snd_pcm_hw_params_set_channels(handle, hwParams,
+ *channels)) < 0)
+ {
+ AlsaError(SS("Can't set channel count to " << *channels << " (" << alsa_device_name << "/" << streamType << ")"));
+ }
+
+ snd_pcm_uframes_t effectivePeriodSize = this->bufferSize;
+
+ if ((err = snd_pcm_hw_params_set_period_size_near(handle, hwParams,
+ &effectivePeriodSize,
+ 0)) < 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);
+ if (*periods < this->numberOfBuffers)
+ *periods = this->numberOfBuffers;
+ if (snd_pcm_hw_params_set_periods_near(handle, hwParams,
+ periods, NULL) < 0)
+ {
+ AlsaError(SS("Can't set number of periods to " << (*periods) << " (" << alsa_device_name << "/" << streamType << ")"));
+ }
+
+ if (*periods < this->numberOfBuffers)
+ {
+ AlsaError(SS("Got smaller periods " << *periods << " than " << this->numberOfBuffers));
+ }
+
+ snd_pcm_uframes_t bSize;
+
+ // if ((err = snd_pcm_hw_params_set_buffer_size(handle, hwParams,
+ // *periods *
+ // this->bufferSize)) < 0)
+ // {
+ // AlsaError(SS("Can't set buffer length to " << (*periods * this->bufferSize)));
+ // }
+
+ if ((err = snd_pcm_hw_params(handle, hwParams)) < 0)
+ {
+ AlsaError(SS("Cannot set hardware parameters for " << alsa_device_name));
+ }
+
+ snd_pcm_sw_params_current(handle, swParams);
+
+ if (handle == this->captureHandle)
+ {
+ if ((err = snd_pcm_sw_params_set_start_threshold(handle, swParams,
+ 0)) < 0)
+ {
+ AlsaError(SS("Cannot set start mode for " << alsa_device_name));
+ }
+ }
+ else
+ {
+ if ((err = snd_pcm_sw_params_set_start_threshold(handle, swParams,
+ 0x7fffffff)) < 0)
+ {
+ AlsaError(SS("Cannot set start mode for " << alsa_device_name));
+ }
+ }
+
+ stop_th = *periods * this->bufferSize;
+ if (this->soft_mode)
+ {
+ stop_th = (snd_pcm_uframes_t)-1;
+ }
+
+ if ((err = snd_pcm_sw_params_set_stop_threshold(
+ handle, swParams, stop_th)) < 0)
+ {
+ AlsaError(SS("ALSA: cannot set stop mode for " << alsa_device_name));
+ }
+
+ if ((err = snd_pcm_sw_params_set_silence_threshold(
+ handle, swParams, 0)) < 0)
+ {
+ AlsaError(SS("Cannot set silence threshold for " << alsa_device_name));
+ }
+
+ if (handle == this->playbackHandle)
+ err = snd_pcm_sw_params_set_avail_min(
+ handle, swParams,
+ this->bufferSize * (*periods - this->numberOfBuffers + 1));
+ else
+ err = snd_pcm_sw_params_set_avail_min(
+ handle, swParams, this->bufferSize);
+
+ if (err < 0)
+ {
+ AlsaError(SS("Cannot set avail min for " << alsa_device_name));
+ }
+
+ // err = snd_pcm_sw_params_set_tstamp_mode(handle, swParams, SND_PCM_TSTAMP_ENABLE);
+ // if (err < 0)
+ // {
+ // Lv2Log::info(SS(
+ // "Could not enable ALSA time stamp mode for " << alsa_device_name << " (err " << err << ")"));
+ // }
+
+#if SND_LIB_MAJOR >= 1 && SND_LIB_MINOR >= 1
+ err = snd_pcm_sw_params_set_tstamp_type(handle, swParams, SND_PCM_TSTAMP_TYPE_MONOTONIC);
+ if (err < 0)
+ {
+ Lv2Log::info(SS(
+ "Could not use monotonic ALSA time stamps for " << alsa_device_name << "(err " << err << ")"));
+ }
+#endif
+
+ if ((err = snd_pcm_sw_params(handle, swParams)) < 0)
+ {
+ AlsaError(SS("Cannot set software parameters for " << alsa_device_name));
+ }
+ err = snd_pcm_prepare(handle);
+ if (err < 0)
+ {
+ AlsaError(SS("ALSA prepare failed. " << snd_strerror(err)));
+ }
+ }
+ void SetAlsaParameters(uint32_t bufferSize, uint32_t numberOfBuffers, uint32_t sampleRate)
+ {
+ this->bufferSize = bufferSize;
+ this->numberOfBuffers = numberOfBuffers;
+ this->sampleRate = sampleRate;
+
+ if (this->captureHandle)
+ {
+ AlsaConfigureStream(
+ this->alsa_device_name,
+ "capture",
+ captureHandle,
+ captureHwParams,
+ captureSwParams,
+ &captureChannels,
+ &this->periods);
+ }
+ if (this->playbackHandle)
+ {
+ AlsaConfigureStream(
+ this->alsa_device_name,
+ "playback",
+ playbackHandle,
+ playbackHwParams,
+ playbackSwParams,
+ &playbackChannels,
+ &this->periods);
+ }
+
+#ifdef ALSADRIVER_CONFIG_DBG
+ snd_pcm_dump(captureHandle, snd_output);
+ snd_pcm_dump(playbackHandle, snd_output);
+#endif
+ }
+ void CopyCaptureFloatLe(size_t frames)
+ {
+ float *p = (float *)rawCaptureBuffer;
+
+ std::vector &buffers = this->captureBuffers;
+ int channels = this->captureChannels;
+ for (size_t frame = 0; frame < frames; ++frame)
+ {
+ for (int channel = 0; channel < channels; ++channel)
+ {
+ float v = *p++;
+ buffers[channel][frame] = v;
+ }
+ }
+ }
+
+ void CopyCaptureS32Le(size_t frames)
+ {
+ int32_t *p = (int32_t *)rawCaptureBuffer;
+
+ std::vector &buffers = this->captureBuffers;
+ int channels = this->captureChannels;
+ constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L);
+ for (size_t frame = 0; frame < frames; ++frame)
+ {
+ for (int channel = 0; channel < channels; ++channel)
+ {
+ int32_t v = *p++;
+ buffers[channel][frame] = scale * v;
+ }
+ }
+ }
+ void CopyPlaybackS32Le(size_t frames)
+ {
+ int32_t *p = (int32_t *)rawPlaybackBuffer;
+
+ std::vector &buffers = this->playbackBuffers;
+ int channels = this->playbackChannels;
+ constexpr float scale = std::numeric_limits::max();
+ for (size_t frame = 0; frame < frames; ++frame)
+ {
+ for (int channel = 0; channel < channels; ++channel)
+ {
+ float v = buffers[channel][frame];
+ if (v > 1.0f)
+ v = 1.0f;
+ if (v < -1.0f)
+ v = -1.0f;
+ *p++ = (int32_t)(scale * v);
+ }
+ }
+ }
+ void CopyPlaybackFloatLe(size_t frames)
+ {
+ float *p = (float *)rawPlaybackBuffer;
+
+ std::vector &buffers = this->playbackBuffers;
+ int channels = this->captureChannels;
+ for (size_t frame = 0; frame < frames; ++frame)
+ {
+ for (int channel = 0; channel < channels; ++channel)
+ {
+ float v = buffers[channel][frame];
+ *p++ = v;
+ }
+ }
+ }
+ void AllocateBuffers(std::vector &buffers, size_t n)
+ {
+ buffers.resize(n);
+ for (size_t i = 0; i < n; ++i)
+ {
+ buffers[i] = new float[this->bufferSize];
+ for (size_t j = 0; j < this->bufferSize; ++j)
+ {
+ buffers[i][j] = 0;
+ }
+ }
+ }
+
+ JackChannelSelection channelSelection;
+ bool open = false;
+ virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
+ {
+ terminateAudio_ = false;
+ if (open)
+ {
+ throw PiPedalStateException("Already open.");
+ }
+ this->jackServerSettings = jackServerSettings;
+ this->channelSelection = channelSelection;
+
+ open = true;
+
+ OpenMidi(jackServerSettings, channelSelection);
+ OpenAudio(jackServerSettings, channelSelection);
+ }
+
+ void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
+ {
+ int err;
+
+ alsa_device_name = jackServerSettings.GetAlsaDevice();
+
+ this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers();
+ this->bufferSize = jackServerSettings.GetBufferSize();
+ this->user_threshold = jackServerSettings.GetBufferSize();
+
+ try
+ {
+
+ err = snd_pcm_open(&playbackHandle, alsa_device_name.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (err < 0)
+ {
+ switch (errno)
+ {
+ case EBUSY:
+ {
+ std::string apps = discover_alsa_using_apps();
+ std::string message;
+ if (apps.size() != 0)
+ {
+ message =
+ SS("Device " << alsa_device_name << " in use. The following applications are using your soundcard: " << apps
+ << ". Stop them as neccesary before trying to restart pipedald.");
+ }
+ else
+ {
+ message =
+ SS("Device " << alsa_device_name << " in use. Stop the application using it before trying to restart pipedald. ");
+ }
+ Lv2Log::error(message);
+ throw PiPedalStateException(std::move(message));
+ }
+ break;
+ case EPERM:
+ throw PiPedalStateException(SS("Permission denied opening device '" << alsa_device_name << "'"));
+ default:
+ throw PiPedalStateException(SS("Unexepected error (" << errno << ") opening device '" << alsa_device_name << "'"));
+ }
+ }
+ if (this->playbackHandle)
+ {
+ snd_pcm_nonblock(playbackHandle, 0);
+ }
+
+ err = snd_pcm_open(&captureHandle, alsa_device_name.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
+
+ if (err < 0)
+ {
+ switch (errno)
+ {
+ case EBUSY:
+ {
+ std::string apps = discover_alsa_using_apps();
+ std::string message;
+ if (apps.size() != 0)
+ {
+ message =
+ SS("Device " << alsa_device_name << " in use. The following applications are using your soundcard: " << apps
+ << ". Stop them as neccesary before trying to restart pipedald.");
+ }
+ else
+ {
+ message =
+ SS("Device " << alsa_device_name << " in use. Stop the application using it before trying to restart pipedald. ");
+ }
+ Lv2Log::error(message);
+ throw PiPedalStateException(std::move(message));
+ }
+ break;
+ case EPERM:
+ throw PiPedalStateException(SS("Permission denied opening device '" << alsa_device_name << "'"));
+ default:
+ throw PiPedalStateException(SS("Unexepected error (" << errno << ") opening device '" << alsa_device_name << "'"));
+ }
+ }
+ if (this->captureHandle)
+ {
+ snd_pcm_nonblock(captureHandle, 0);
+ }
+
+ if ((err = snd_pcm_hw_params_malloc(&captureHwParams)) < 0)
+ {
+ throw PiPedalStateException("Failed to allocate captureHwParams");
+ }
+ if ((err = snd_pcm_sw_params_malloc(&captureSwParams)) < 0)
+ {
+ throw PiPedalStateException("Failed to allocate captureSwParams");
+ }
+ if ((err = snd_pcm_hw_params_malloc(&playbackHwParams)) < 0)
+ {
+ throw PiPedalStateException("Failed to allocate playbackHwParams");
+ }
+ if ((err = snd_pcm_sw_params_malloc(&playbackSwParams)) < 0)
+ {
+ throw PiPedalStateException("Failed to allocate playbackSwParams");
+ }
+
+ SetAlsaParameters(jackServerSettings.GetBufferSize(), jackServerSettings.GetNumberOfBuffers(), jackServerSettings.GetSampleRate());
+ capture_and_playback_not_synced = false;
+
+ if (captureHandle && playbackHandle)
+ {
+ if (snd_pcm_link(playbackHandle,
+ captureHandle) != 0)
+ {
+ capture_and_playback_not_synced = true;
+ }
+ }
+
+ snd_pcm_format_t captureFormat;
+ snd_pcm_hw_params_get_format(captureHwParams, &captureFormat);
+
+ switch (captureFormat)
+ {
+ case SND_PCM_FORMAT_FLOAT_LE:
+ captureSampleSize = 4;
+ copyInputFn = &AlsaDriverImpl::CopyCaptureFloatLe;
+ break;
+ case SND_PCM_FORMAT_S24_3LE:
+ captureSampleSize = 3;
+ break;
+ case SND_PCM_FORMAT_S32_LE:
+ captureSampleSize = 4;
+ copyInputFn = &AlsaDriverImpl::CopyCaptureS32Le;
+ break;
+ case SND_PCM_FORMAT_S24_LE:
+ captureSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S16_LE:
+ captureSampleSize = 2;
+ break;
+ case SND_PCM_FORMAT_FLOAT_BE:
+ captureSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S24_3BE:
+ captureSampleSize = 3;
+ break;
+ case SND_PCM_FORMAT_S32_BE:
+ captureSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S24_BE:
+ captureSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S16_BE:
+ captureSampleSize = 2;
+ break;
+ default:
+ throw PiPedalStateException("Invalid format.");
+ }
+ captureFrameSize = captureSampleSize * captureChannels;
+ rawCaptureBuffer = new uint8_t[captureFrameSize * bufferSize];
+ memset(rawCaptureBuffer, 0, captureFrameSize * bufferSize);
+
+ AllocateBuffers(captureBuffers, captureChannels);
+
+ snd_pcm_format_t playbackFormat;
+ snd_pcm_hw_params_get_format(playbackHwParams, &playbackFormat);
+
+ switch (playbackFormat)
+ {
+ case SND_PCM_FORMAT_FLOAT_LE:
+ playbackSampleSize = 4;
+ copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatLe;
+ break;
+ case SND_PCM_FORMAT_S24_3LE:
+ playbackSampleSize = 3;
+ break;
+ case SND_PCM_FORMAT_S32_LE:
+ copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Le;
+ playbackSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S24_LE:
+ playbackSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S16_LE:
+ playbackSampleSize = 2;
+ break;
+ case SND_PCM_FORMAT_FLOAT_BE:
+ playbackSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S24_3BE:
+ playbackSampleSize = 3;
+ break;
+ case SND_PCM_FORMAT_S32_BE:
+ playbackSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S24_BE:
+ playbackSampleSize = 4;
+ break;
+ case SND_PCM_FORMAT_S16_BE:
+ playbackSampleSize = 2;
+ break;
+ default:
+ throw PiPedalStateException("Invalid format.");
+ }
+ playbackFrameSize = playbackSampleSize * playbackChannels;
+ rawPlaybackBuffer = new uint8_t[playbackFrameSize * bufferSize];
+ memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize);
+
+ AllocateBuffers(playbackBuffers, playbackChannels);
+ }
+ catch (const std::exception &e)
+ {
+ AlsaCleanup();
+ throw;
+ }
+ }
+
+ void FillOutputBuffer()
+ {
+ memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize);
+ while (true)
+ {
+ auto avail = snd_pcm_avail(this->playbackHandle);
+ if (avail < 0)
+ {
+ int err = snd_pcm_prepare(playbackHandle);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err)));
+ }
+ continue;
+ }
+ if (avail == 0)
+ break;
+ if (avail > this->bufferSize)
+ avail = this->bufferSize;
+
+ ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, avail);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err)));
+ }
+ }
+ }
+ void XrunRecoverOutputOverrun(snd_pcm_t *handle, int err)
+ {
+ if (err == -EPIPE)
+ {
+ err = snd_pcm_prepare(handle);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
+ }
+ }
+ FillOutputBuffer();
+ }
+
+ void DumpStatus(snd_pcm_t *handle)
+ {
+#ifdef ALSADRIVER_CONFIG_DBG
+ snd_pcm_status(handle, snd_status);
+ snd_pcm_status_dump(snd_status, snd_output);
+#endif
+ }
+
+ void XrunRecoverInputUnderrun(snd_pcm_t *handle, int err)
+ {
+ if (err == -EPIPE)
+ { /* underrun */
+
+ this->driverHost->OnUnderrun();
+
+ // DumpStatus(handle);
+ err = snd_pcm_drop(handle);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
+ }
+ err = snd_pcm_prepare(handle);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
+ }
+ FillOutputBuffer();
+
+ err = snd_pcm_start(handle);
+ // if (err < 0)
+ // {
+ // throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
+ // }
+ return;
+ }
+ else if (err == -ESTRPIPE)
+ {
+ audioRunning = false;
+ while ((err = snd_pcm_resume(handle)) == -EAGAIN)
+ {
+ sleep(1);
+ }
+ if (err < 0)
+ {
+ err = snd_pcm_prepare(handle);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Can't recover from ALSA suspend. (" << snd_strerror(err) << ")"));
+ }
+ }
+ return;
+ }
+ throw PiPedalStateException(SS("ALSA error:" << snd_strerror(err)));
+ }
+
+ std::jthread *audioThread;
+ bool audioRunning;
+
+ bool block = false;
+
+ snd_pcm_sframes_t ReadBuffer(snd_pcm_t *handle, uint8_t *buffer, snd_pcm_uframes_t frames)
+ {
+ // transcode to jack format.
+ // expand running status if neccessary.
+ // deal with regular and sysex messages split across
+ // buffer boundaries.
+ snd_pcm_sframes_t framesRead;
+
+ auto state = snd_pcm_state(handle);
+ auto frame_bytes = this->captureFrameSize;
+ do
+ {
+ framesRead = snd_pcm_readi(handle, buffer, frames);
+ if (framesRead < 0)
+ {
+ return framesRead;
+ }
+ if (framesRead > 0)
+ {
+ buffer += framesRead * frame_bytes;
+ frames -= framesRead;
+ }
+ if (framesRead == 0)
+ {
+ snd_pcm_wait(captureHandle, 1);
+ }
+ } while (frames > 0);
+ return framesRead;
+ }
+
+ long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames)
+ {
+ long framesRead;
+ auto frame_bytes = this->captureFrameSize;
+
+ while (frames > 0)
+ {
+ framesRead = snd_pcm_writei(handle, buf, frames);
+ if (framesRead == -EAGAIN)
+ continue;
+ if (framesRead < 0)
+ return framesRead;
+ buf += framesRead * frame_bytes;
+ frames -= framesRead;
+ }
+ return 0;
+ }
+ void AudioThread()
+ {
+ try
+ {
+#if defined(__WIN32)
+ // bump thread prioriy two levels to
+ // ensure that the service thread doesn't
+ // get bogged down by UIwork. Doesn't have to be realtime, but it
+ // MUST run at higher priority than UI threads.
+ xxx; // TO DO.
+#elif defined(__linux__)
+ int min = sched_get_priority_min(SCHED_RR);
+ int max = sched_get_priority_max(SCHED_RR);
+
+ struct sched_param param;
+ memset(¶m, 0, sizeof(param));
+ param.sched_priority = 79;
+
+ int result = sched_setscheduler(0, SCHED_RR, ¶m);
+ if (result == 0)
+ {
+ Lv2Log::debug("Service thread priority successfully boosted.");
+ }
+ else
+ {
+ Lv2Log::error(SS("Failed to set ALSA AudioThread priority. (" << strerror(result) << ")"));
+ }
+#else
+ xxx; // TODO!
+#endif
+
+ bool ok = true;
+
+ auto playbackState = snd_pcm_state(playbackHandle);
+
+ FillOutputBuffer();
+
+ int err;
+ if ((err = snd_pcm_start(captureHandle)) < 0)
+ {
+ throw PiPedalStateException("Unable to start ALSA capture.");
+ }
+
+ cpuUse.SetStartTime(cpuUse.Now());
+ while (true)
+ {
+ cpuUse.UpdateCpuUse();
+
+ if (terminateAudio())
+ {
+ break;
+ }
+
+ // snd_pcm_wait(captureHandle, 1);
+
+ ssize_t framesRead;
+ if ((framesRead = ReadBuffer(captureHandle, this->rawCaptureBuffer, bufferSize)) < 0)
+ {
+ this->driverHost->OnUnderrun();
+ auto state = snd_pcm_state(playbackHandle);
+ XrunRecoverInputUnderrun(captureHandle, framesRead);
+ continue;
+ }
+ else
+ {
+ cpuUse.AddSample(ProfileCategory::Read);
+ if (framesRead == 0)
+ continue;
+ if (framesRead != bufferSize)
+ {
+ throw PiPedalStateException("Invalid read.");
+ }
+
+ (this->*copyInputFn)(framesRead);
+ cpuUse.AddSample(ProfileCategory::Driver);
+
+ this->driverHost->OnProcess(framesRead);
+
+ cpuUse.AddSample(ProfileCategory::Execute);
+
+ (this->*copyOutputFn)(framesRead);
+ cpuUse.AddSample(ProfileCategory::Driver);
+ // process.
+
+ ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, framesRead);
+
+ if (err < 0)
+ {
+ this->driverHost->OnUnderrun();
+ XrunRecoverOutputOverrun(playbackHandle, err);
+ }
+ cpuUse.AddSample(ProfileCategory::Write);
+ }
+ }
+ }
+ catch (const std::exception &e)
+ {
+ Lv2Log::error(e.what());
+ Lv2Log::error("ALSA audio thread terminated abnormally.");
+ }
+ this->driverHost->OnAudioStopped();
+ }
+
+ bool alsaActive = false;
+
+ static int IndexFromPortName(const std::string &s)
+ {
+ auto pos = s.find_last_of('_');
+ if (pos == std::string::npos)
+ {
+ throw std::invalid_argument("Bad port name.");
+ }
+ const char *p = s.c_str() + (pos + 1);
+
+ int v = atoi(p);
+ if (v < 0)
+ {
+ throw std::invalid_argument("Bad port name.");
+ }
+ return v;
+ }
+
+ bool activated = false;
+ virtual void Activate()
+ {
+ if (activated)
+ {
+ throw PiPedalStateException("Already activated.");
+ }
+ activated = true;
+
+ this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size());
+
+ int ix = 0;
+ for (auto &x : channelSelection.GetInputAudioPorts())
+ {
+ int sourceIndex = IndexFromPortName(x);
+ if (sourceIndex >= captureBuffers.size())
+ {
+ throw PiPedalArgumentException("Invalid input port.");
+ }
+ this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex];
+ }
+
+ this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size());
+
+ ix = 0;
+ for (auto &x : channelSelection.GetOutputAudioPorts())
+ {
+ int sourceIndex = IndexFromPortName(x);
+ if (sourceIndex >= playbackBuffers.size())
+ {
+ throw PiPedalArgumentException("Invalid output port.");
+ }
+ this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex];
+ }
+
+ audioThread = new std::jthread([this]()
+ { AudioThread(); });
+ }
+
+ virtual void Deactivate()
+ {
+ if (!activated)
+ {
+ return;
+ }
+ activated = false;
+ terminateAudio(true);
+ if (audioThread)
+ {
+ this->audioThread->join();
+ this->audioThread = 0;
+ }
+ Lv2Log::debug("Audio thread joined.");
+ }
+
+ static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
+ static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
+
+ public:
+ class MidiState
+ {
+ private:
+ snd_rawmidi_t *hIn = nullptr;
+ snd_rawmidi_params_t *hInParams = nullptr;
+ std::string deviceName;
+
+ // running status state.
+ uint8_t runningStatus = 0;
+ int dataLength = 0;
+ int dataIndex = 0;
+ size_t statusBytesRemaining = 0;
+ size_t data0;
+ size_t data1;
+
+ size_t eventCount = 0;
+ MidiEvent events[MAX_MIDI_EVENT];
+ size_t bufferCount = 0;
+ uint8_t buffer[MIDI_BUFFER_SIZE];
+
+ uint8_t readBuffer[1024];
+
+ ssize_t sysexStartIndex = -1;
+
+ void checkError(int result, const char *message)
+ {
+ if (result < 0)
+ {
+ throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName));
+ }
+ }
+
+ public:
+ void Open(const AlsaMidiDeviceInfo &device)
+ {
+ bufferCount = 0;
+ eventCount = 0;
+ sysexStartIndex = -1;
+ runningStatus = 0;
+ dataIndex = 0;
+ dataLength = 0;
+
+ this->deviceName = device.description_;
+
+ int err = snd_rawmidi_open(&hIn, nullptr, device.name_.c_str(), SND_RAWMIDI_NONBLOCK);
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS("Can't open midi device " << deviceName << ". (" << snd_strerror(err)));
+ }
+
+ err = snd_rawmidi_params_malloc(&hInParams);
+ checkError(err, "snd_rawmidi_params_malloc failed.");
+
+ err = snd_rawmidi_params_set_buffer_size(hIn, hInParams, 2048);
+ checkError(err, "snd_rawmidi_params_set_buffer_size failed.");
+
+ err = snd_rawmidi_params_set_no_active_sensing(hIn, hInParams, 1);
+ checkError(err, "snd_rawmidi_params_set_no_active_sensing failed.");
+ }
+ void Close()
+ {
+ if (hIn)
+ {
+ snd_rawmidi_close(hIn);
+ hIn = nullptr;
+ }
+ if (hInParams)
+ {
+ snd_rawmidi_params_free(hInParams);
+ hInParams = 0;
+ }
+ }
+
+ int GetDataLength(uint8_t cc)
+ {
+ static int sDataLength[] = {0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, -1};
+ return sDataLength[cc >> 4];
+ }
+
+ size_t GetMidiInputEventCount()
+ {
+ return eventCount;
+ }
+
+ void NextEventBuffer()
+ {
+ // xxx preserve unflushed sysex data.
+ if (sysexStartIndex != -1)
+ {
+ int end = bufferCount;
+ bufferCount = 0;
+ eventCount = 0;
+ for (int i = sysexStartIndex; i < end; ++i)
+ {
+ buffer[bufferCount++] = buffer[i];
+ }
+ sysexStartIndex = 0;
+ }
+ else
+ {
+ bufferCount = 0;
+ eventCount = 0;
+ }
+ }
+ bool GetMidiInputEvent(MidiEvent *event, size_t nFrame)
+ {
+ if (nFrame >= eventCount)
+ return false;
+ *event = this->events[nFrame];
+ return true;
+ }
+
+ void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1)
+ {
+ if (cc == 0)
+ return;
+
+ // check for overrun.
+ if (bufferCount + 1 + dataLength >= sizeof(buffer))
+ {
+ bufferCount = sizeof(buffer);
+ return;
+ }
+ if (eventCount >= MAX_MIDI_EVENT)
+ {
+ return;
+ }
+
+ auto *event = &(this->events[eventCount++]);
+
+ event->time = 0;
+ event->buffer = &buffer[bufferCount];
+ event->size = dataLength + 1;
+
+ buffer[bufferCount++] = cc;
+ if (dataLength >= 1)
+ {
+ buffer[bufferCount++] = d0;
+ if (dataLength >= 2)
+ {
+ buffer[bufferCount++] = d1;
+ }
+ }
+ }
+
+ void FillBuffer()
+ {
+ while (true)
+ {
+ ssize_t nRead = snd_rawmidi_read(hIn, readBuffer, sizeof(readBuffer));
+ if (nRead == -EAGAIN)
+ return;
+ if (nRead < 0)
+ {
+ checkError(nRead, SS(this->deviceName << "MIDI event read failed. (" << snd_strerror(nRead)).c_str());
+ }
+ WriteBuffer(readBuffer, nRead); // expose write to test code.
+ }
+ }
+
+ void FlushSysex()
+ {
+ if (sysexStartIndex != -1)
+ {
+ if (this->eventCount != MAX_MIDI_EVENT)
+ {
+ auto *event = &(events[eventCount++]);
+ event->size = this->bufferCount - sysexStartIndex;
+ event->buffer = &(this->buffer[this->sysexStartIndex]);
+ event->time = 0;
+ }
+ sysexStartIndex = -1;
+ }
+ }
+
+ int GetSystemCommonLength(uint8_t cc)
+ {
+ static int sizes[] = {-1, 1, 2, 1, -1, -1, 0, 0};
+ return sizes[(cc >> 4) & 0x07];
+ }
+ void WriteBuffer(uint8_t *readBuffer, size_t nRead)
+ {
+ for (ssize_t i = 0; i < nRead; ++i)
+ {
+ uint8_t v = readBuffer[i];
+
+ if (v >= 0x80)
+ {
+ if (v >= 0xF0)
+ {
+ if (v == 0xF0)
+ {
+ if (bufferCount == sizeof(buffer))
+ {
+ break;
+ }
+ sysexStartIndex = bufferCount;
+
+ buffer[bufferCount++] = 0xF0;
+
+ runningStatus = 0; // discard subsequent data.
+ dataLength = -2; // indefinitely.
+ dataIndex = -1;
+ }
+ else if (v >= 0xF8)
+ {
+ // don't overwrite running status.
+ // don't break sysexes on a running status message.
+ // LV2 standard is ambiguous how realtime messages are handled, so just discard them.
+ continue;
+ }
+ else
+ {
+ FlushSysex();
+ int length = GetSystemCommonLength(v);
+ if (length == -1)
+ break; // ignore illegal messages.
+ runningStatus = v;
+ dataLength = length;
+ dataIndex = 0;
+ }
+ }
+ else
+ {
+ FlushSysex();
+ int dataLength = GetDataLength(v);
+ runningStatus = v;
+ if (dataLength == -1)
+ {
+ this->dataLength = dataLength;
+ dataIndex = -1;
+ }
+ else
+ {
+ this->dataLength = dataLength;
+ dataIndex = 0;
+ }
+ }
+ }
+ else
+ {
+ if (sysexStartIndex != -1)
+ {
+ if (bufferCount != sizeof(buffer))
+ {
+ buffer[bufferCount++] = v;
+ }
+ }
+ else
+ {
+ switch (dataIndex)
+ {
+ default:
+ // discard.
+ break;
+ case 0:
+ data0 = v;
+ dataIndex = 1;
+ break;
+ case 1:
+ data1 = v;
+ dataIndex = 2;
+ break;
+ }
+ }
+ }
+ if (dataIndex == dataLength)
+ {
+ MidiPut(runningStatus, data0, data1);
+ dataIndex = 0;
+ }
+ }
+ }
+ };
+
+ std::vector midiStates;
+
+ void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
+ {
+ const auto &devices = channelSelection.GetInputMidiDevices();
+
+ midiStates.resize(devices.size());
+
+ for (size_t i = 0; i < devices.size(); ++i)
+ {
+ const auto &device = devices[i];
+ MidiState *state = new MidiState();
+ midiStates[i] = state;
+ state->Open(device);
+ }
+ }
+
+ virtual size_t MidiInputBufferCount() const
+ {
+ return this->midiStates.size();
+ }
+ virtual void *GetMidiInputBuffer(size_t channel, size_t nFrames)
+ {
+ return (void *)midiStates[channel];
+ }
+
+ virtual size_t GetMidiInputEventCount(void *portBuffer)
+ {
+ MidiState *state = (MidiState *)portBuffer;
+ return state->GetMidiInputEventCount();
+ }
+ virtual bool GetMidiInputEvent(MidiEvent *event, void *portBuf, size_t nFrame)
+ {
+ MidiState *state = (MidiState *)portBuf;
+ return state->GetMidiInputEvent(event, nFrame);
+ }
+
+ virtual void FillMidiBuffers()
+ {
+ for (size_t i = 0; i < this->midiStates.size(); ++i)
+ {
+ auto *state = midiStates[i];
+ state->NextEventBuffer();
+ state->FillBuffer();
+ }
+ }
+
+ virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
+ virtual float *GetInputBuffer(size_t channel, size_t nFrames)
+ {
+ return activeCaptureBuffers[channel];
+ }
+
+ virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); }
+ virtual float *GetOutputBuffer(size_t channel, size_t nFrames)
+ {
+ return activePlaybackBuffers[channel];
+ }
+
+ void FreeBuffers(std::vector &buffer)
+ {
+ for (size_t i = 0; i < buffer.size(); ++i)
+ {
+ // delete[] buffer[i];
+ buffer[i] = 0;
+ }
+ buffer.clear();
+ }
+ void DeleteBuffers()
+ {
+ activeCaptureBuffers.clear();
+ activePlaybackBuffers.clear();
+ FreeBuffers(this->playbackBuffers);
+ FreeBuffers(this->captureBuffers);
+ }
+ virtual void Close()
+ {
+ if (!open)
+ {
+ return;
+ }
+ open = false;
+ Deactivate();
+ AlsaCleanup();
+ DeleteBuffers();
+ }
+
+ virtual float CpuUse()
+ {
+ return cpuUse.GetCpuUse();
+ }
+
+ virtual float CpuOverhead()
+ {
+ return cpuUse.GetCpuOverhead();
+ }
+
+ };
+
+ AudioDriver *CreateAlsaDriver(AudioDriverHost *driverHost)
+ {
+ return new AlsaDriverImpl(driverHost);
+ }
+
+ bool GetAlsaChannels(const JackServerSettings &jackServerSettings,
+ std::vector &inputAudioPorts,
+ std::vector &outputAudioPorts)
+ {
+
+ snd_pcm_t *playbackHandle = nullptr;
+ snd_pcm_t *captureHandle = nullptr;
+ snd_pcm_hw_params_t *playbackHwParams = nullptr;
+ snd_pcm_hw_params_t *captureHwParams = nullptr;
+ std::string alsaDeviceName = jackServerSettings.GetAlsaDevice();
+ bool result = false;
+
+ try
+ {
+ int err;
+ for (int retry = 0; retry < 5; ++retry)
+ {
+ err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (err == -EBUSY)
+ {
+ sleep(1);
+ continue;
+ }
+ break;
+ }
+ if (err < 0)
+ {
+ throw PiPedalStateException(SS(alsaDeviceName << " not found. "
+ << "(" << snd_strerror(err) << ")"));
+ }
+
+ err = snd_pcm_open(&captureHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
+ if (err < 0)
+ throw PiPedalStateException(SS(alsaDeviceName << " not found."));
+
+ if (snd_pcm_hw_params_malloc(&playbackHwParams) < 0)
+ {
+ throw PiPedalLogicException("Out of memory.");
+ }
+ if (snd_pcm_hw_params_malloc(&captureHwParams) < 0)
+ {
+ throw PiPedalLogicException("Out of memory.");
+ }
+
+ snd_pcm_hw_params_any(playbackHandle, playbackHwParams);
+ snd_pcm_hw_params_any(captureHandle, captureHwParams);
+
+ SetPreferredAlsaFormat(alsaDeviceName, "capture", captureHandle, captureHwParams);
+ SetPreferredAlsaFormat(alsaDeviceName, "output", playbackHandle, playbackHwParams);
+
+ unsigned int sampleRate = jackServerSettings.GetSampleRate();
+ err = snd_pcm_hw_params_set_rate_near(playbackHandle, playbackHwParams, &sampleRate, 0);
+ if (err < 0)
+ {
+ throw PiPedalLogicException("Sample rate not supported.");
+ }
+ sampleRate = jackServerSettings.GetSampleRate();
+ err = snd_pcm_hw_params_set_rate_near(captureHandle, captureHwParams, &sampleRate, 0);
+ if (err < 0)
+ {
+ throw PiPedalLogicException("Sample rate not supported.");
+ }
+
+ unsigned int playbackChannels, captureChannels;
+
+ err = snd_pcm_hw_params_get_channels_max(playbackHwParams, &playbackChannels);
+ if (err < 0)
+ {
+ throw PiPedalLogicException("No outut channels.");
+ }
+ err = snd_pcm_hw_params_get_channels_max(captureHwParams, &captureChannels);
+ if (err < 0)
+ {
+ throw PiPedalLogicException("No input channels.");
+ }
+
+ inputAudioPorts.clear();
+ for (unsigned int i = 0; i < playbackChannels; ++i)
+ {
+ inputAudioPorts.push_back(SS("system::playback_" << i));
+ }
+
+ outputAudioPorts.clear();
+ for (unsigned int i = 0; i < captureChannels; ++i)
+ {
+ outputAudioPorts.push_back(SS("system::capture_" << i));
+ }
+
+ result = true;
+ }
+ catch (const std::exception &e)
+ {
+ Lv2Log::warning(SS("Unable to read ALSA configuration for " << alsaDeviceName << ". " << e.what() << "."));
+ result = false;
+ throw;
+ }
+ if (playbackHwParams)
+ snd_pcm_hw_params_free(playbackHwParams);
+ if (captureHwParams)
+ snd_pcm_hw_params_free(captureHwParams);
+
+ if (playbackHandle)
+ {
+ snd_pcm_close(playbackHandle);
+ }
+ if (captureHandle)
+ snd_pcm_close(captureHandle);
+ return result;
+ }
+
+ static void AlsaAssert(bool value)
+ {
+ if (!value)
+ throw PiPedalStateException("Assert failed.");
+ }
+
+ static void ExpectEvent(AlsaDriverImpl::MidiState &m, int event, const std::vector message)
+ {
+ MidiEvent e;
+ m.GetMidiInputEvent(&e,event);
+ AlsaAssert(e.size == message.size());
+ for (size_t i = 0; i < message.size(); ++i)
+ {
+ AlsaAssert(message[i] == e.buffer[i]);
+ }
+ }
+
+ void MidiDecoderTest()
+ {
+
+ AlsaDriverImpl::MidiState midiState;
+
+ MidiEvent event;
+
+ // Running status decoding.
+ {
+ 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});
+
+ static uint8_t m1[] = {0x06,0xC0,0x1,0x2};
+ midiState.NextEventBuffer();
+ 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});
+ }
+
+
+ // SYSEX.
+ {
+ static uint8_t m0[] = {0xF0, 0x76, 0xF7, 0xA};
+ midiState.NextEventBuffer();
+ midiState.WriteBuffer(m0, 4);
+ AlsaAssert(midiState.GetMidiInputEventCount() == 2);
+ AlsaAssert(midiState.GetMidiInputEvent(&event, 0));
+ AlsaAssert(event.size == 2);
+ AlsaAssert(event.buffer[0] == 0xF0);
+ AlsaAssert(event.buffer[1] == 0x76);
+ }
+
+ // SPLIT SYSEX
+ {
+ static uint8_t m0[] = {0xF0, 0x76, 0x3B};
+ midiState.NextEventBuffer();
+ midiState.WriteBuffer(m0, sizeof(m0));
+ AlsaAssert(midiState.GetMidiInputEventCount() == 0);
+ static uint8_t m1[] = {0x77, 0xF7};
+ midiState.NextEventBuffer();
+ midiState.WriteBuffer(m1, sizeof(m1));
+ AlsaAssert(midiState.GetMidiInputEventCount() == 2);
+
+ AlsaAssert(midiState.GetMidiInputEvent(&event, 0));
+ AlsaAssert(event.size == 0x4);
+ AlsaAssert(event.buffer[0] == 0xF0);
+ AlsaAssert(event.buffer[1] == 0x76);
+ AlsaAssert(event.buffer[2] == 0x3B);
+ AlsaAssert(event.buffer[3] == 0x77);
+ }
+ }
+} // namespace
diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp
new file mode 100644
index 0000000..984aa04
--- /dev/null
+++ b/src/AlsaDriver.hpp
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include "AudioDriver.hpp"
+#include "JackServerSettings.hpp"
+
+namespace pipedal {
+
+ bool GetAlsaChannels(const JackServerSettings &jackServerSettings,
+ std::vector&inputAudioPorts,
+ std::vector&outputAudioPorts
+ );
+
+
+ AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost);
+
+
+ void MidiDecoderTest();
+}
+
diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp
new file mode 100644
index 0000000..a04decd
--- /dev/null
+++ b/src/AlsaDriverTest.cpp
@@ -0,0 +1,278 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "pch.h"
+#include "catch.hpp"
+#include
+#include
+#include
+
+#include "AlsaDriver.hpp"
+#include "JackDriver.hpp"
+
+using namespace pipedal;
+using namespace std;
+
+
+class AlsaTester: private AudioDriverHost {
+public:
+ enum class TestType { Oscillator, LatencyMonitor, NullTest};
+private:
+ AudioDriver *audioDriver = nullptr;
+ TestType testType;
+
+public:
+ AlsaTester(TestType testType)
+ : testType(testType)
+ {
+ // audioDriver = CreateAlsaDriver(this);
+ // audioDriver = CreateJackDriver(this);
+
+ }
+ ~AlsaTester()
+ {
+ delete audioDriver;
+ delete[] inputBuffers;
+ delete[] outputBuffers;
+ }
+
+ bool useJack = false;
+
+ void Test()
+ {
+ JackServerSettings serverSettings("hw:M2",48000,32,3);
+
+ JackConfiguration jackConfiguration;
+ if (useJack)
+ {
+ jackConfiguration.JackInitialize();
+ } else {
+ jackConfiguration.AlsaInitialize(serverSettings);
+ }
+
+ JackChannelSelection channelSelection(
+ jackConfiguration.GetInputAudioPorts(),
+ jackConfiguration.GetOutputAudioPorts(),
+ jackConfiguration.GetInputMidiDevices());
+
+#if JACK_HOST
+ if (useJack)
+ {
+ audioDriver = CreateJackDriver(this);
+
+ } else {
+ audioDriver = CreateAlsaDriver(this);
+ }
+#else
+ audioDriver = CreateAlsaDriver(this);
+#endif
+
+
+ oscillator.Init(440,jackConfiguration.GetSampleRate());
+ latencyMonitor.Init(jackConfiguration.GetSampleRate());
+ audioDriver->Open(serverSettings,channelSelection);
+
+ inputBuffers = new float*[channelSelection.GetInputAudioPorts().size()];
+ outputBuffers = new float*[channelSelection.GetOutputAudioPorts().size()];
+
+ audioDriver->Activate();
+
+ for (int i = 0; i < 10; ++i)
+ {
+ sleep(1);
+ if (testType == TestType::LatencyMonitor)
+ {
+ auto latency = this->latencyMonitor.GetLatency();
+ double ms = 1000.0*latency/jackConfiguration.GetSampleRate();
+
+ cout << "Latency: " << latency << " samples " << ms << "ms" << " xruns: " << GetXruns() << " Cpu: " << audioDriver->CpuUse() << "%" << endl;
+ }
+ }
+
+ audioDriver->Deactivate();
+ audioDriver->Close();
+
+ }
+
+ float**inputBuffers = nullptr;
+ float**outputBuffers = nullptr;
+
+ class Oscillator {
+ private:
+ double dx = 0;
+ double x = 0;
+ double dx2 = 0;
+ double x2 = 0;
+ public:
+
+ void Init(float frequency, size_t sampleRate)
+ {
+ dx = frequency*3.141592736*2/sampleRate;
+ dx2 = 0.5*3.141592736*2/sampleRate;
+ }
+
+ float Next() {
+ float result = (float)std::cos(x);
+ float env = (float)std::cos(x2);
+ x += dx;
+ x2 += dx2;
+ return result*env;
+ }
+ };
+
+ class LatencyMonitor {
+ enum class State {
+ Idle,
+ Waiting,
+ };
+ State state = State::Idle;
+ uint64_t t;
+ uint64_t idle_samples;
+ uint64_t waiting_samples;
+ size_t current_latency = 0;
+ size_t latency = 0;
+ std::mutex sync;
+ public:
+ void Init(uint64_t sampleRate)
+ {
+ idle_samples = (uint64_t)sampleRate*2;
+ waiting_samples = (uint64_t)sampleRate*2;
+
+ state = State::Idle;
+ t = idle_samples;
+ latency = 0;
+ }
+
+ size_t GetLatency() {
+ std::lock_guard lock { sync};
+ return latency;
+ }
+ float Next(float input)
+ {
+ switch(state)
+ {
+ default:
+ case State::Idle:
+ {
+ if (t-- == 0) {
+ state = State::Waiting;
+ current_latency = 0;
+ }
+ return 0.01;
+ }
+ break;
+ case State::Waiting:
+ {
+ if (std::abs(input) > 0.1 || current_latency > 500) {
+ {
+ std::lock_guard lock { sync};
+ latency = current_latency;
+ }
+ state = State::Idle;
+ t = idle_samples;
+ } else {
+ ++current_latency;
+ }
+ return current_latency < 100 ? 0.25 : 0.0;
+ }
+ break;
+ }
+ }
+ };
+
+
+ Oscillator oscillator;
+ LatencyMonitor latencyMonitor;
+
+
+ virtual void OnAudioStopped() {
+ }
+ virtual void OnProcess(size_t nFrames) {
+ if (testType == TestType::NullTest) return;
+
+
+ size_t inputs = audioDriver->InputBufferCount();
+ size_t outputs = audioDriver->OutputBufferCount();
+
+ for (size_t i = 0; i < inputs; ++i)
+ {
+ inputBuffers[i] = audioDriver->GetInputBuffer(i,nFrames);
+ }
+ for (size_t i = 0; i < outputs; ++i)
+ {
+ outputBuffers[i] = audioDriver->GetOutputBuffer(i,nFrames);
+ }
+ if (this->testType == TestType::Oscillator)
+ {
+ for (size_t i = 0; i < nFrames; ++i)
+ {
+ float v = oscillator.Next()*0.25f;
+ for (size_t c = 0; c < outputs; ++c)
+ {
+ outputBuffers[c][i] = v;
+ }
+ }
+ }
+ else {
+ for (size_t i = 0; i < nFrames; ++i)
+ {
+ float v = latencyMonitor.Next(inputBuffers[0][i]);
+ for (size_t c = 0; c < outputs; ++c)
+ {
+ outputBuffers[c][i] = v;
+ }
+ }
+
+ }
+
+ }
+ std::mutex sync;
+ uint64_t xruns;
+
+ uint64_t GetXruns() {
+ lock_guard lock { sync};
+ return xruns;
+ }
+ virtual void OnUnderrun() {
+ lock_guard lock { sync };
+ ++xruns;
+
+ }
+};
+
+
+TEST_CASE( "alsa_test", "[alsa_test]" ) {
+
+ AlsaTester alsaDriver(AlsaTester::TestType::Oscillator);
+
+ alsaDriver.Test();
+}
+
+
+TEST_CASE( "alsa_midi_test", "[alsa_midi_test]" ) {
+ AlsaTester alsaDriver(AlsaTester::TestType::Oscillator);
+
+ MidiDecoderTest();
+}
+
diff --git a/src/AudioConfig.hpp b/src/AudioConfig.hpp
new file mode 100644
index 0000000..7b57e1f
--- /dev/null
+++ b/src/AudioConfig.hpp
@@ -0,0 +1,32 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#define JACK_HOST 0
+#define ALSA_HOST 1
+
+#if JACK_HOST && ALSA_HOST
+#error Choose either JACK or ALSA
+#endif
diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp
new file mode 100644
index 0000000..b7fd10c
--- /dev/null
+++ b/src/AudioDriver.hpp
@@ -0,0 +1,89 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#pragma once
+
+#include "JackConfiguration.hpp"
+#include
+
+
+
+
+namespace pipedal {
+
+
+ using ProcessCallback = std::function;
+
+
+
+ struct MidiEvent
+ {
+ /* BINARY COMPATIBLE WITH jack_midi_event_t;! (in case we ever want to resurrect Jack support) */
+ uint32_t time; /**< Sample index at which event is valid */
+ size_t size; /**< Number of bytes of data in \a buffer */
+ uint8_t *buffer; /**< Raw MIDI data */
+
+ };
+
+
+ class AudioDriverHost {
+ public:
+ virtual void OnProcess(size_t nFrames) = 0;
+ virtual void OnUnderrun() = 0;
+ virtual void OnAudioStopped() = 0;
+
+
+ };
+ class AudioDriver {
+ public:
+
+ virtual ~AudioDriver() { }
+
+ virtual float CpuUse() = 0;
+ virtual float CpuOverhead() = 0;
+
+ virtual uint32_t GetSampleRate() = 0;
+
+ virtual size_t MidiInputBufferCount() const = 0;
+ virtual void*GetMidiInputBuffer(size_t channel, size_t nFrames) = 0;
+ virtual size_t GetMidiInputEventCount(void*buffer) = 0;
+ virtual bool GetMidiInputEvent(MidiEvent*event, void*portBuf, size_t nFrame) = 0;
+
+ virtual void FillMidiBuffers() = 0;
+
+ virtual size_t InputBufferCount() const = 0;
+ virtual float*GetInputBuffer(size_t channel, size_t nFrames) = 0;
+
+
+ virtual size_t OutputBufferCount() const = 0;
+ virtual float*GetOutputBuffer(size_t channel, size_t nFrames) = 0;
+
+ virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0;
+
+ virtual void Activate() = 0;
+ virtual void Deactivate() = 0;
+ virtual void Close() = 0;
+
+ };
+
+};
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3eb5259..a89e7cd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -94,6 +94,7 @@ add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY -DBOOST_BIND_GLOBAL_PLACEHOLDERS
set (PIPEDAL_SOURCES
+ CpuUse.hpp CpuUse.cpp
P2pConfigFiles.hpp
AvahiService.cpp AvahiService.hpp
DeviceIdFile.cpp DeviceIdFile.hpp
@@ -153,6 +154,11 @@ set (PIPEDAL_SOURCES
InheritPriorityMutex.hpp InheritPriorityMutex.cpp
UnixSocket.cpp UnixSocket.hpp
+ JackDriver.cpp JackDriver.hpp
+ AlsaDriver.cpp AlsaDriver.hpp
+ AudioDriver.hpp
+ AudioConfig.hpp
+
)
@@ -165,7 +171,9 @@ set (PIPEDAL_INCLUDES
)
set(PIPEDAL_LIBS libpipedald pthread atomic stdc++fs asound avahi-common avahi-client systemd
- ${LILV_0_LIBRARIES} ${JACK_LIBRARIES} ${LIBNL3_LIBRARIES} )
+ ${LILV_0_LIBRARIES}
+ # ${JACK_LIBRARIES}
+ ${LIBNL3_LIBRARIES} )
##########################
@@ -195,6 +203,7 @@ target_link_libraries(pipedald PRIVATE
#################################
add_executable(pipedaltest testMain.cpp
jsonTest.cpp
+ AlsaDriverTest.cpp
AvahiServiceTest.cpp
WifiChannelsTest.cpp
PiPedalAlsaTest.cpp
@@ -259,6 +268,27 @@ add_executable(pipedalconfig
target_link_libraries(pipedalconfig PRIVATE pthread atomic uuid stdc++fs
)
+add_executable(pipedal_latency_test
+ PrettyPrinter.hpp
+ CommandLineParser.hpp
+ PiLatencyMain.cpp
+ PiPedalAlsa.hpp PiPedalAlsa.cpp
+ json.hpp json.cpp
+ HtmlHelper.cpp HtmlHelper.hpp
+ asan_options.cpp
+ Lv2Log.cpp Lv2Log.hpp
+ AlsaDriver.cpp AlsaDriver.hpp
+ JackConfiguration.hpp JackConfiguration.cpp
+ JackServerSettings.hpp JackServerSettings.cpp
+ CpuUse.hpp
+ CpuUse.cpp
+
+
+ )
+
+target_link_libraries(pipedal_latency_test PRIVATE pthread asound
+ )
+
add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
UnixSocket.cpp UnixSocket.hpp
DeviceIdFile.cpp DeviceIdFile.hpp
@@ -281,7 +311,6 @@ add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
)
target_link_libraries(pipedaladmind PRIVATE pthread atomic stdc++fs systemd)
-
add_executable(processcopyrights copyrightMain.cpp
CommandLineParser.hpp
PiPedalException.hpp
@@ -323,7 +352,7 @@ add_custom_target (
)
-install (TARGETS pipedalconfig pipedald pipedaladmind DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
+install (TARGETS pipedalconfig pipedald pipedaladmind pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
EXPORT pipedalTargets)
diff --git a/src/CommandLineParser.hpp b/src/CommandLineParser.hpp
index 1917453..6ce4276 100644
--- a/src/CommandLineParser.hpp
+++ b/src/CommandLineParser.hpp
@@ -157,16 +157,53 @@ namespace pipedal
}
}
+
+ void AddOption(const std::string &shortOption, const std::string &longOption, bool *pResult)
+ {
+ if (shortOption.length() != 0)
+ {
+ auto option = "-" + longOption;
+ options.push_back(new BooleanOption(option, pResult, true));
+ options.push_back(new BooleanOption(option + "+", pResult, true));
+ options.push_back(new BooleanOption(option + "-", pResult, false));
+ }
+ if (longOption.length() != 0)
+ {
+ auto option = "--" + longOption;
+ options.push_back(new BooleanOption(option, pResult, true));
+ options.push_back(new BooleanOption(option + "+", pResult, true));
+ options.push_back(new BooleanOption(option + "-", pResult, false));
+ }
+ }
+
void AddOption(const std::string &option, bool *pResult)
{
options.push_back(new BooleanOption(option, pResult, true));
options.push_back(new BooleanOption(option + "+", pResult, true));
options.push_back(new BooleanOption(option + "-", pResult, false));
}
+
+ void AddOption(const std::string &shortOption, const std::string &longOption, std::string *pResult)
+ {
+ options.push_back(new StringOption("-" + shortOption, pResult));
+ options.push_back(new StringOption("--" + longOption, pResult));
+ }
+
+
void AddOption(const std::string &option, std::string *pResult)
{
options.push_back(new StringOption(option, pResult));
}
+
+ template
+ void AddOption(const std::string &shortOption, const std::string &longOption, T *pResult)
+ {
+ if (shortOption.length() != 0)
+ options.push_back(new Option("-" + shortOption, pResult));
+ if (longOption.length() != 0)
+ options.push_back(new Option("--" + longOption, pResult));
+ }
+
template
void AddOption(const std::string &option, T *pResult)
{
diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp
index ebaa2cf..972b0e8 100644
--- a/src/ConfigMain.cpp
+++ b/src/ConfigMain.cpp
@@ -36,6 +36,17 @@
#include "DeviceIdFile.hpp"
#include
#include
+#include "AudioConfig.hpp"
+
+
+#if JACK_HOST
+#define INSTALL_JACK_SERVICE 1
+#else
+#define INSTALL_JACK_SERVICE 0
+#endif
+
+#define UNINSTALL_JACK_SERVICE 1
+
#define SS(x) (((std::stringstream &)(std::stringstream() << x)).str())
@@ -110,6 +121,14 @@ void EnableService()
}
void DisableService()
{
+ #if INSTALL_JACK_SERVICE || UNINSTALL_JACK_SERVICE
+ if (sysExec(SYSTEMCTL_BIN " disable " JACK_SERVICE ".service") != EXIT_SUCCESS)
+ {
+ #if INSTALL_JACK_SERVICE
+ cout << "Error: Failed to disable the " JACK_SERVICE " service.";
+ #endif
+ }
+ #endif
if (sysExec(SYSTEMCTL_BIN " disable " NATIVE_SERVICE ".service") != EXIT_SUCCESS)
{
cout << "Error: Failed to disable the " NATIVE_SERVICE " service.";
@@ -145,10 +164,14 @@ void StopService(bool excludeShutdownService = false)
}
#endif
}
+#if INSTALL_JACK_SERVICE || UNINSTALL_JACK_SERVICE
if (sysExec(SYSTEMCTL_BIN " stop " JACK_SERVICE ".service") != EXIT_SUCCESS)
{
+ #if INSTALL_JACK_SERVICE
throw PiPedalException("Failed to stop the " JACK_SERVICE " service.");
+ #endif
}
+#endif
}
void StartService(bool excludeShutdownService = false)
@@ -166,6 +189,7 @@ void StartService(bool excludeShutdownService = false)
{
throw PiPedalException("Failed to start the " NATIVE_SERVICE " service.");
}
+#if INSTALL_JACK_SERVICE
JackServerSettings serverSettings;
serverSettings.ReadJackConfiguration();
if (serverSettings.IsValid())
@@ -175,6 +199,7 @@ void StartService(bool excludeShutdownService = false)
throw PiPedalException("Failed to start the " JACK_SERVICE " service.");
}
}
+#endif
}
void RestartService(bool excludeShutdownService)
{
@@ -258,12 +283,15 @@ static void RemoveLine(const std::string &path, const std::string lineToRemove)
const std::string PAM_LINE = "JACK_PROMISCUOUS_SERVER DEFAULT=" JACK_SERVICE_GROUP_NAME;
void UninstallPamEnv()
{
+#if UNINSTALL_JACK_SERVICE
RemoveLine(
"/etc/security/pam_env.conf",
PAM_LINE);
+#endif
}
void InstallPamEnv()
{
+#if INSTALL_JACK_SERVICE
std::string newLine = PAM_LINE;
std::vector lines;
std::filesystem::path path = "/etc/security/pam_env.conf";
@@ -313,6 +341,7 @@ void InstallPamEnv()
{
cout << "Failed to update " << path << ". " << e.what();
}
+#endif
}
void InstallLimits()
@@ -322,12 +351,12 @@ void InstallLimits()
throw PiPedalException("Failed to create audio service group.");
}
- std::filesystem::path limitsConfig = "/usr/security/pipedal.conf";
+ std::filesystem::path limitsConfig = "/etc/security/limits.d/audio.conf";
if (!std::filesystem::exists(limitsConfig))
{
ofstream output(limitsConfig);
- output << "# Realtime priority group used by pipedal/jack daemon"
+ output << "# Realtime priority group used by pipedal audio (and also by jack)"
<< "\n";
output << "@audio - rtprio 95"
<< "\n";
@@ -336,15 +365,6 @@ void InstallLimits()
}
}
-void UninstallLimits()
-{
- std::filesystem::path limitsConfig = "/usr/security/pipedal.conf";
-
- if (std::filesystem::exists(limitsConfig))
- {
- std::filesystem::remove(limitsConfig);
- }
-}
void MaybeStartJackService()
{
@@ -363,6 +383,8 @@ void InstallJackService()
{
InstallLimits();
InstallPamEnv();
+
+#if INSTALL_JACK_SERVICE
if (sysExec(GROUPADD_BIN " -f " JACK_SERVICE_GROUP_NAME) != EXIT_SUCCESS)
{
throw PiPedalException("Failed to create jack service group.");
@@ -391,6 +413,8 @@ void InstallJackService()
WriteTemplateFile(map, GetServiceFileName(JACK_SERVICE));
MaybeStartJackService();
+#endif
+
}
int SudoExec(char **argv)
@@ -463,7 +487,7 @@ void Uninstall()
{
}
UninstallPamEnv();
- UninstallLimits();
+ // UninstallLimits();
sysExec(SYSTEMCTL_BIN " daemon-reload");
}
catch (const std::exception &e)
diff --git a/src/CpuUse.cpp b/src/CpuUse.cpp
new file mode 100644
index 0000000..63d2032
--- /dev/null
+++ b/src/CpuUse.cpp
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "CpuUse.hpp"
+
+using namespace pipedal;
+using namespace std;
+
+
+CpuUse::CpuUseAverager::CpuUseAverager()
+{
+ for (size_t i = 0; i < NUM_SAMPLES; ++i)
+ {
+ samples[i] = 0;
+ }
+ sampleTotal = 0;
+}
\ No newline at end of file
diff --git a/src/CpuUse.hpp b/src/CpuUse.hpp
new file mode 100644
index 0000000..e2dedac
--- /dev/null
+++ b/src/CpuUse.hpp
@@ -0,0 +1,167 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace pipedal
+{
+
+ enum ProfileCategory
+ {
+ Init,
+ Read,
+ Driver,
+ Execute,
+ Write,
+
+ MaxCategory
+ };
+ constexpr size_t NUM_PROFILE_CATEGORIES = (size_t)ProfileCategory::MaxCategory;
+
+ class CpuUse
+ {
+
+ private:
+ using ClockT = std::chrono::steady_clock;
+ using TimeT = ClockT::time_point;
+ using DurationT = ClockT::duration;
+ using SampleT = DurationT::rep;
+
+ public:
+ class CpuUseAverager
+ {
+ private:
+ static constexpr size_t NUM_SAMPLES = 20; // average over N samples.
+ SampleT sampleTotal = 0;
+ SampleT samples[NUM_SAMPLES];
+ size_t sampleIndex = 0;
+
+ public:
+
+ CpuUseAverager();
+
+ SampleT GetAverage() const
+ {
+ return sampleTotal / NUM_SAMPLES;
+ }
+ SampleT GetTotal() const {
+ return sampleTotal;
+ }
+ void AddSample(DurationT duration)
+ {
+ SampleT sample = duration.count();
+ sampleTotal = sampleTotal + sample-samples[sampleIndex];
+ samples[sampleIndex] = sample;
+ ++sampleIndex;
+ if (sampleIndex >= NUM_SAMPLES)
+ {
+ sampleIndex = 0;
+ }
+ }
+ };
+ private:
+
+ std::mutex sync;
+
+ TimeT lastSample;
+
+ CpuUseAverager profileTimes[NUM_PROFILE_CATEGORIES];
+ float currentCpuUse = 0;
+ float currentOverhead = 0;
+
+ CpuUseAverager &GetCategory(ProfileCategory category) {
+ return profileTimes[(size_t)category];
+ }
+
+ public:
+
+ TimeT Now() {
+ return ClockT::now();
+ }
+ void SetStartTime(TimeT time)
+ {
+ lastSample = time;
+ }
+ void AddSample(ProfileCategory category, TimeT time)
+ {
+ profileTimes[(size_t)category].AddSample((time-lastSample));
+ lastSample = time;
+ }
+ void AddSample(ProfileCategory category)
+ {
+ AddSample(category,Now());
+ }
+ void AddSample(ProfileCategory category, TimeT startTime, TimeT endTime)
+ {
+ profileTimes[(size_t)category].AddSample((endTime-startTime));
+ }
+
+ void UpdateCpuUse() {
+
+ SampleT readTime = GetCategory(ProfileCategory::Read).GetTotal();
+ SampleT writeTime = GetCategory(ProfileCategory::Driver).GetTotal();
+
+ SampleT processingTime = GetCategory(ProfileCategory::Execute).GetTotal() + GetCategory(ProfileCategory::Driver).GetTotal();
+
+ // most of the waiting occurs in the write. Assume that the difference between readTime and write Time is how long we spent waiting (i.e. free time)
+
+ SampleT waitTime;
+ SampleT overheadTime;
+ if (readTime > writeTime)
+ {
+ waitTime = readTime-writeTime;
+ overheadTime = writeTime;
+ } else {
+ waitTime = writeTime-readTime;
+ overheadTime = readTime;
+ }
+
+ SampleT totalTime = writeTime+ readTime + processingTime;
+ SampleT maxTime = waitTime+processingTime;
+
+
+ float result = 100.0f*(processingTime)/(maxTime);
+ float overhead = 100.0F*(overheadTime*2)/totalTime;
+ {
+ std::lock_guard lock { sync};
+ currentCpuUse = result;
+ currentOverhead = overhead;
+ }
+ }
+ float GetCpuUse()
+ {
+ std::lock_guard lock { sync};
+ return currentCpuUse;
+ }
+ float GetCpuOverhead()
+ {
+ std::lock_guard lock { sync};
+ return currentOverhead;
+ }
+ };
+
+}
diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp
index b211ac7..b39dd33 100644
--- a/src/JackConfiguration.cpp
+++ b/src/JackConfiguration.cpp
@@ -19,50 +19,24 @@
#include "pch.h"
#include "JackConfiguration.hpp"
+#include "AudioConfig.hpp"
+#include "AlsaDriver.hpp"
#include "Lv2Log.hpp"
#include "PiPedalException.hpp"
+
+#if JACK_HOST
#include
#include
#include
+#endif
+
#include
#include "defer.hpp"
#include
using namespace pipedal;
-std::string JackStatusToString(jack_status_t status)
-{
- switch (status)
- {
- default:
- return "Unknown Jack Audio error.";
- case JackStatus::JackFailure:
- return "Jack Audio operation failed.";
- case JackStatus::JackInvalidOption:
- return "Invalid Jack Audio operation.";
- case JackStatus::JackNameNotUnique:
- return "Jack Audio name in use.";
- case JackStatus::JackServerStarted:
- return "Jack Audio Server started.";
- case JackStatus::JackServerFailed:
- return "Jack Audio Server failed.";
- case JackStatus::JackServerError:
- return "Can't connect to the Jack Audio Server.";
- case JackStatus::JackNoSuchClient:
- return "Jack Audio client not found.";
- case JackLoadFailure:
- return "Jack Audio client failed to load.";
- case JackStatus::JackInitFailure:
- return "Failed to initialize the Jack Audio client.";
- case JackStatus::JackShmFailure:
- return "Jack Audio failed to access shared memory.";
- case JackStatus::JackVersionError:
- return "Jack Audio Server version error.";
- case JackStatus::JackClientZombie:
- return "JackClientZombie error.";
- }
-};
JackConfiguration::JackConfiguration()
{
@@ -73,10 +47,12 @@ JackConfiguration::~JackConfiguration()
{
}
+#if JACK_HOST
static void AddPorts(jack_client_t *client, std::vector *output, const char *port_name_pattern,
const char *type_name_pattern,
unsigned long flags)
{
+ #if JACK_HOST
const char**ports = jack_get_ports(client, port_name_pattern, type_name_pattern, flags);
if (ports != nullptr)
{
@@ -88,9 +64,14 @@ static void AddPorts(jack_client_t *client, std::vector *output, co
jack_free(ports);
}
+ #else
+ throw PiPedalStateException("JACK_HOST not enabled at compile time.");
+ #endif
}
+#endif
namespace pipedal {
+#if JACK_HOST
std::string GetJackErrorMessage(jack_status_t status)
{
std::stringstream s;
@@ -122,9 +103,43 @@ namespace pipedal {
return s.str();
}
+#endif
}
-void JackConfiguration::Initialize()
+
+void JackConfiguration::AlsaInitialize(
+ const JackServerSettings &jackServerSettings)
{
+ this->isValid_ = false;
+ this->errorStatus_ = "";
+
+ this->inputMidiDevices_ = GetAlsaMidiInputDevices();
+ if (jackServerSettings.IsValid())
+ {
+ this->blockLength_ = jackServerSettings.GetBufferSize();
+ this->sampleRate_ = jackServerSettings.GetSampleRate();
+
+ try {
+ if (GetAlsaChannels(jackServerSettings,
+ inputAudioPorts_,outputAudioPorts_))
+ {
+ this->isValid_ = true;
+ }
+ } catch (const std::exception& /*ignore*/)
+ {
+
+ }
+
+ }
+ if (!this->isValid_)
+ {
+ this->isValid_ = false;
+ this->errorStatus_ = "Not configured.";
+ }
+
+}
+void JackConfiguration::JackInitialize()
+{
+ #if JACK_HOST
jack_status_t status;
jack_client_t *client = nullptr;
@@ -161,8 +176,6 @@ void JackConfiguration::Initialize()
AddPorts(client, &this->inputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput);
AddPorts(client, &this->outputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput);
- AddPorts(client, &this->inputMidiPorts_, "system", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput);
- AddPorts(client, &this->outputMidiPorts_, "system", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput);
isValid_ = true;
this->errorStatus_ = "";
jack_client_close(client);
@@ -172,6 +185,9 @@ void JackConfiguration::Initialize()
jack_client_close(client);
this->errorStatus_ = e.what();
}
+ #else
+ throw PiPedalStateException("JACK_HOST not enabled at compile time.");
+ #endif
}
JackChannelSelection JackChannelSelection::MakeDefault(const JackConfiguration&config){
@@ -211,12 +227,59 @@ static std::vector makeValid(const std::vector & selec
}
return result;
}
+static std::vector makeValid(const std::vector & selected, const std::vector &available)
+{
+ // Matching rule:
+ // same name AND same description (unchanged)
+ // different name and same description: use the new device name. (i.e. same device plugged into different USB port)
+ // otherwise discard.
+
+ std::vector sourceDevices = available;
+ std::vector result;
+ std::vector partialMatch;
+
+ for (auto & sel : selected)
+ {
+ size_t matchIndex = -1;
+ for (size_t i = 0; i < sourceDevices.size(); ++i)
+ {
+ const AlsaMidiDeviceInfo &candidate = sourceDevices[i];
+ if (candidate.description_ == sel.description_)
+ {
+ if (candidate.name_ == sel.name_)
+ {
+ result.push_back(candidate);
+ } else {
+ partialMatch.push_back(candidate);
+ }
+ sourceDevices.erase(sourceDevices.begin()+i);
+ break;
+ }
+ }
+ }
+ for (auto &sel : partialMatch)
+ {
+ for (size_t i = 0; i < sourceDevices.size(); ++i)
+ {
+ const AlsaMidiDeviceInfo &candidate = sourceDevices[i];
+ if (candidate.description_ == sel.description_)
+ {
+ result.push_back(candidate); // same description, usb address has changed. includeit.
+ sourceDevices.erase(sourceDevices.begin()+i);
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+
JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfiguration&config) const
{
JackChannelSelection result;
result.inputAudioPorts_ = makeValid(this->inputAudioPorts_,config.GetInputAudioPorts());
result.outputAudioPorts_ = makeValid(this->outputAudioPorts_,config.GetOutputAudioPorts());
- result.inputMidiPorts_ = makeValid(this->inputMidiPorts_, config.GetInputMidiPorts());
+ result.inputMidiDevices_ = makeValid(this->inputMidiDevices_, config.GetInputMidiDevices());
if (!result.isValid())
{
return this->MakeDefault(config);
@@ -226,7 +289,7 @@ JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfi
JSON_MAP_BEGIN(JackChannelSelection)
JSON_MAP_REFERENCE(JackChannelSelection,inputAudioPorts)
JSON_MAP_REFERENCE(JackChannelSelection,outputAudioPorts)
- JSON_MAP_REFERENCE(JackChannelSelection,inputMidiPorts)
+ JSON_MAP_REFERENCE(JackChannelSelection,inputMidiDevices)
JSON_MAP_END()
@@ -240,7 +303,6 @@ JSON_MAP_BEGIN(JackConfiguration)
JSON_MAP_REFERENCE(JackConfiguration,maxAllowedMidiDelta)
JSON_MAP_REFERENCE(JackConfiguration,inputAudioPorts)
JSON_MAP_REFERENCE(JackConfiguration,outputAudioPorts)
- JSON_MAP_REFERENCE(JackConfiguration,inputMidiPorts)
- JSON_MAP_REFERENCE(JackConfiguration,outputMidiPorts)
+ JSON_MAP_REFERENCE(JackConfiguration,inputMidiDevices)
JSON_MAP_END()
diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp
index 10777a3..10b9225 100644
--- a/src/JackConfiguration.hpp
+++ b/src/JackConfiguration.hpp
@@ -22,6 +22,9 @@
#include
#include
#include "json.hpp"
+#include "JackServerSettings.hpp"
+#include "PiPedalAlsa.hpp"
+
namespace pipedal
{
@@ -42,11 +45,13 @@ namespace pipedal
std::vector inputAudioPorts_;
std::vector outputAudioPorts_;
- std::vector inputMidiPorts_;
- std::vector outputMidiPorts_;
+ std::vector inputMidiDevices_;
public:
JackConfiguration();
- void Initialize();
+
+ void AlsaInitialize(const JackServerSettings &jackServerSettings); // from also config settings.
+
+ void JackInitialize(); // from jack server instance.
~JackConfiguration();
bool isValid() const { return isValid_;}
bool isRestarting() const { return isRestarting_; }
@@ -59,8 +64,7 @@ namespace pipedal
const std::vector &GetInputAudioPorts() const { return inputAudioPorts_; }
const std::vector &GetOutputAudioPorts() const { return outputAudioPorts_; }
- const std::vector &GetInputMidiPorts() const { return inputMidiPorts_; }
- const std::vector &GetOutputMidiPorts() const { return outputMidiPorts_; }
+ const std::vector &GetInputMidiDevices() const { return inputMidiDevices_; }
DECLARE_JSON_MAP(JackConfiguration);
@@ -69,10 +73,20 @@ namespace pipedal
private:
std::vector inputAudioPorts_;
std::vector outputAudioPorts_;
- std::vector inputMidiPorts_;
+ std::vector inputMidiDevices_;
public:
JackChannelSelection()
{
+ }
+ JackChannelSelection(
+ std::vector inputAudioPorts,
+ std::vector outputAudioPorts,
+ std::vector inputMidiDevices
+ ) : inputAudioPorts_(inputAudioPorts),
+ outputAudioPorts_(outputAudioPorts),
+ inputMidiDevices_(inputMidiDevices)
+ {
+
}
bool isValid() const {
@@ -83,14 +97,14 @@ namespace pipedal
{
return inputAudioPorts_;
}
- const std::vector& getOutputAudioPorts() const
+ const std::vector& GetOutputAudioPorts() const
{
return outputAudioPorts_;
}
- const std::vector& GetInputMidiPorts() const
+ const std::vector& GetInputMidiDevices() const
{
- return inputMidiPorts_;
+ return inputMidiDevices_;
}
JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const;
diff --git a/src/JackDriver.cpp b/src/JackDriver.cpp
new file mode 100644
index 0000000..d0b4764
--- /dev/null
+++ b/src/JackDriver.cpp
@@ -0,0 +1,512 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* Status of Jack support.
+
+PiPedal was originally written for Jack, but subsequently ported to Alsa because running
+Jack as a systemd daemon proved to be unsupportable,.
+
+
+
+*/
+
+#include "pch.h"
+#include "JackDriver.hpp"
+
+#include
+#include
+#include
+#include
+#include "Lv2Log.hpp"
+
+#if JACK_HOST
+
+namespace pipedal {
+
+
+std::string JackStatusToString(jack_status_t status)
+{
+ switch (status)
+ {
+ default:
+ return "Unknown Jack Audio error.";
+ case JackStatus::JackFailure:
+ return "Jack Audio operation failed.";
+ case JackStatus::JackInvalidOption:
+ return "Invalid Jack Audio operation.";
+ case JackStatus::JackNameNotUnique:
+ return "Jack Audio name in use.";
+ case JackStatus::JackServerStarted:
+ return "Jack Audio Server started.";
+ case JackStatus::JackServerFailed:
+ return "Jack Audio Server failed.";
+ case JackStatus::JackServerError:
+ return "Can't connect to the Jack Audio Server.";
+ case JackStatus::JackNoSuchClient:
+ return "Jack Audio client not found.";
+ case JackLoadFailure:
+ return "Jack Audio client failed to load.";
+ case JackStatus::JackInitFailure:
+ return "Failed to initialize the Jack Audio client.";
+ case JackStatus::JackShmFailure:
+ return "Jack Audio failed to access shared memory.";
+ case JackStatus::JackVersionError:
+ return "Jack Audio Server version error.";
+ case JackStatus::JackClientZombie:
+ return "JackClientZombie error.";
+ }
+};
+
+/*
+
+ This code relies on the fact that jack_midi_event_t and pipedal::MidiEvent are binary-compatible.
+ (the default build does not require Jack Audio headers to be present).
+
+
+ Test to make sure this is still true.
+*/
+
+
+static_assert(sizeof(jack_midi_event_t) == sizeof(pipedal::MidiEvent));
+
+#define CHECK_FIELD(fieldName) \
+ static_assert(offsetof(jack_midi_event_t,fieldName) == offsetof(pipedal::MidiEvent,fieldName)); \
+ static_assert(sizeof(jack_midi_event_t::fieldName) == sizeof(pipedal::MidiEvent::fieldName));
+
+CHECK_FIELD(size)
+CHECK_FIELD(buffer)
+CHECK_FIELD(time)
+
+
+// private import from JackConfiguration.cpp
+std::string GetJackErrorMessage(jack_status_t status);
+
+
+// forward decl
+static int process_fn(jack_nframes_t nframes, void *arg);
+
+static void jack_error_fn(const char *msg)
+{
+ Lv2Log::error("Jack - %s", msg);
+}
+static void jack_info_fn(const char *msg)
+{
+ Lv2Log::info("Jack - %s", msg);
+}
+
+
+class JackDriverImpl : public AudioDriver {
+private:
+ jack_client_t *client = nullptr;
+ std::vector inputPorts;
+ std::vector outputPorts;
+ std::vector midiInputPorts;
+ uint32_t sampleRate = 0;
+
+ AudioDriverHost *driverHost = nullptr;
+
+public:
+ JackDriverImpl(AudioDriverHost*driverHost)
+ : driverHost(driverHost)
+ {
+ jack_set_error_function(jack_error_fn);
+ jack_set_info_function(jack_info_fn);
+
+ }
+ virtual ~JackDriverImpl()
+ {
+ jack_set_error_function(nullptr);
+ jack_set_info_function(nullptr);
+
+ }
+private:
+
+ void OnShutdown()
+ {
+ Lv2Log::info("Jack Audio Server has shut down.");
+ }
+
+ static void
+ jack_shutdown_fn(void *arg)
+ {
+ ((JackDriverImpl *)arg)->OnShutdown();
+ }
+
+
+ static int jackInstanceId;
+
+ static int xrun_callback_fn(void *arg)
+ {
+ ((AudioDriverHost *)arg)->OnUnderrun();
+ return 0;
+ }
+
+ virtual uint32_t GetSampleRate() {
+ return this->sampleRate;
+ }
+
+ bool open = false;
+ virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection &channelSelection) {
+ if (open) throw PiPedalStateException("Already open");
+ open = true;
+
+ this->channelSelection = channelSelection;
+ jack_status_t status;
+ // need a unique instance name every timme.
+ std::stringstream s;
+ s << "PiPedal";
+ if (jackInstanceId != 0) {
+ s << jackInstanceId;
+ }
+ ++jackInstanceId;
+
+ std::string instanceName = s.str();
+
+ client = jack_client_open(instanceName.c_str(), JackNullOption, &status);
+
+ if (client == nullptr || status & JackFailure)
+ {
+ if (client)
+ {
+ jack_client_close(client);
+ client = nullptr;
+ }
+ std::string error = GetJackErrorMessage(status);
+ Lv2Log::error(error);
+ throw PiPedalStateException(error.c_str());
+ }
+
+ if (status & JackServerStarted)
+ {
+ Lv2Log::info("Jack server started.");
+ }
+
+ jack_set_process_callback(client, process_fn, this->driverHost);
+
+ jack_on_shutdown(client, jack_shutdown_fn, this);
+
+ jack_set_xrun_callback(client, xrun_callback_fn, this->driverHost);
+
+ this->sampleRate = jack_get_sample_rate(client);
+
+
+ auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
+ this->inputPorts.clear();
+ this->outputPorts.clear();
+ this->midiInputPorts.clear();
+
+ this->inputPorts.resize(selectedInputPorts.size());
+ for (int i = 0; i < selectedInputPorts.size(); ++i)
+ {
+ std::stringstream name;
+ name << "input" << (i + 1);
+ this->inputPorts[i] =
+ jack_port_register(
+ client, name.str().c_str(),
+ JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsInput, 0);
+ if (this->inputPorts[i] == nullptr)
+ {
+ Lv2Log::error("Can't allocate Jack Audio input ports.");
+ throw PiPedalStateException("Failed to allocate Jack Audio port.");
+ }
+ }
+ auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
+ this->outputPorts.resize(selectedOutputPorts.size());
+ for (int i = 0; i < selectedOutputPorts.size(); ++i)
+ {
+ std::stringstream name;
+ name << "output" << (i + 1);
+ this->outputPorts[i] =
+ jack_port_register(client, name.str().c_str(),
+ JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+ if (this->outputPorts[i] == nullptr)
+ {
+ Lv2Log::error("Can't allocate Jack Audio output port.");
+ throw PiPedalStateException("Failed to allocate Jack Audio port.");
+ }
+ }
+ auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
+
+ this->midiInputPorts.resize(selectedMidiPorts.size());
+ for (int i = 0; i < selectedMidiPorts.size(); ++i)
+ {
+ std::stringstream name;
+ name << "midiIn" << (i + 1);
+ this->midiInputPorts[i] =
+ jack_port_register(client, name.str().c_str(),
+ JACK_DEFAULT_MIDI_TYPE,
+ JackPortIsInput, 0);
+ if (this->midiInputPorts[i] == nullptr)
+ {
+ std::stringstream s;
+ s << "can't register Jack port " << name.str().c_str();
+ Lv2Log::error(s.str());
+ throw PiPedalStateException(s.str().c_str());
+ }
+ }
+ }
+
+ bool jackActive = false;
+
+ JackChannelSelection channelSelection;
+ virtual void Activate()
+ {
+ int activateResult = jack_activate(client);
+ if (activateResult == 0)
+ {
+ jackActive = true;
+
+ auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
+
+ for (int i = 0; i < selectedInputPorts.size(); ++i)
+ {
+ auto result = jack_connect(client, selectedInputPorts[i].c_str(), jack_port_name(this->inputPorts[i]));
+ if (result)
+ {
+ Lv2Log::error("Can't connect input port %s", selectedInputPorts[i].c_str());
+ throw PiPedalStateException("Jack Audio port connection failed.");
+ }
+ }
+
+ auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
+
+ for (int i = 0; i < selectedOutputPorts.size(); ++i)
+ {
+ auto result = jack_connect(client, jack_port_name(this->outputPorts[i]), selectedOutputPorts[i].c_str());
+ if (result)
+ {
+ Lv2Log::error("Can't connect output port %s", selectedOutputPorts[i].c_str());
+ throw PiPedalStateException("Jack Audio port connection failed.");
+ }
+ }
+
+ auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
+
+ for (int i = 0; i < selectedMidiPorts.size(); ++i)
+ {
+ auto result = jack_connect(client, selectedMidiPorts[i].name_.c_str(), jack_port_name(this->midiInputPorts[i]));
+ if (result)
+ {
+ Lv2Log::error("Can't connect midi input port %s", selectedMidiPorts[i].name_.c_str());
+ throw PiPedalStateException("Jack Audio midi port connection failed.");
+ }
+ }
+
+ Lv2Log::info("Jack configuration complete.");
+ }
+ else
+ {
+ Lv2Log::error("Failed to activate Jack Audio client. (%d)", (int)activateResult);
+ throw PiPedalStateException("Failed to activate Jack Audio client.");
+ }
+ }
+
+ void FillMidiBuffers()
+ {
+ // do nothing. Jack does this for us.
+ }
+ virtual void Deactivate()
+ {
+ if (!jackActive)
+ {
+ return;
+ }
+ jackActive = false;
+ // disconnect ports.
+ for (size_t i = 0; i < this->inputPorts.size(); ++i)
+ {
+ auto port = inputPorts[i];
+ if (port != nullptr)
+ {
+ jack_disconnect(client, channelSelection.GetInputAudioPorts()[i].c_str(), jack_port_name(port));
+ }
+ }
+ for (size_t i = 0; i < outputPorts.size(); ++i)
+ {
+ auto port = outputPorts[i];
+ if (port != nullptr)
+ {
+ jack_disconnect(client, jack_port_name(port),channelSelection.GetOutputAudioPorts()[i].c_str());
+ }
+ }
+ for (size_t i = 0; i < midiInputPorts.size(); ++i)
+ {
+ auto port = midiInputPorts[i];
+ if (port != nullptr)
+ {
+ jack_disconnect(client, channelSelection.GetInputMidiDevices()[i].name_.c_str(), jack_port_name(port));
+ }
+ }
+
+ if (jackActive)
+ {
+ jack_deactivate(client);
+ jackActive = false;
+ }
+
+ jack_set_process_callback(client, nullptr, nullptr);
+ jack_on_shutdown(client, nullptr, nullptr);
+ #if JACK_SESSION_CALLBACK // (deprecated, and not actually useful)
+ jack_set_session_callback(client, nullptr, nullptr);
+ #endif
+ jack_set_xrun_callback(client, nullptr, nullptr);
+
+ }
+
+ virtual size_t MidiInputBufferCount() const { return this->midiInputPorts.size(); }
+ virtual void*GetMidiInputBuffer(size_t channel, size_t nFrames) {
+ return (void*)jack_port_get_buffer(this->midiInputPorts[channel], nFrames);
+ }
+
+ virtual size_t GetMidiInputEventCount(void *portBuffer)
+ {
+ return jack_midi_get_event_count(portBuffer);
+ }
+ virtual bool GetMidiInputEvent(MidiEvent*event, void*portBuf, size_t nFrame)
+ {
+ return jack_midi_event_get((jack_midi_event_t*)event,portBuf,(uint32_t)nFrame);
+ }
+
+
+
+
+ virtual size_t InputBufferCount() const { return inputPorts.size(); }
+ virtual float*GetInputBuffer(size_t channel, size_t nFrames) {
+ return (float*)jack_port_get_buffer(this->inputPorts[channel], nFrames);
+ }
+
+ virtual size_t OutputBufferCount() const { return outputPorts.size(); }
+ virtual float*GetOutputBuffer(size_t channel, size_t nFrames)
+ {
+ return (float*)jack_port_get_buffer(this->outputPorts[channel], nFrames);
+ }
+
+ virtual void Close()
+ {
+ if (!open) return;
+ open = false;
+
+ // unregister ports.
+ for (size_t i = 0; i < this->inputPorts.size(); ++i)
+ {
+ auto port = inputPorts[i];
+ if (port)
+ {
+ jack_port_unregister(client, port);
+ }
+ }
+ inputPorts.clear();
+
+ for (size_t i = 0; i < this->outputPorts.size(); ++i)
+ {
+ auto port = outputPorts[i];
+ if (port)
+ {
+ jack_port_unregister(client, port);
+ }
+ }
+ outputPorts.clear();
+ for (size_t i = 0; i < this->midiInputPorts.size(); ++i)
+ {
+ auto port = midiInputPorts[i];
+ if (port)
+ {
+ jack_port_unregister(client, port);
+ }
+ }
+ midiInputPorts.resize(0);
+ if (client)
+ {
+ jack_client_close(client);
+ client = nullptr;
+ }
+
+ }
+
+ virtual float CpuOverhead() { return 0; }
+ virtual float CpuUse()
+ {
+
+ if (client)
+ {
+ return jack_cpu_load(this->client);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ static int process_fn(jack_nframes_t nframes, void *arg)
+ {
+ ((AudioDriverHost *)arg)->OnProcess(nframes);
+ return 0;
+ }
+
+ void OnSessionCallback(jack_session_event_t *event)
+ {
+ #if JACK_SESSION_CALLBACK // deprecated and not actually useful.
+ char retval[100];
+ Lv2Log::info("path %s, uuid %s, type: %s\n", event->session_dir, event->client_uuid, event->type == JackSessionSave ? "save" : "quit");
+
+ snprintf(retval, 100, "jack_simple_session_client %s", event->client_uuid);
+ event->command_line = strdup(retval);
+
+ jack_session_reply(client, event);
+
+ if (event->type == JackSessionSaveAndQuit)
+ {
+ // simple_quit = 1;
+ }
+
+ jack_session_event_free(event);
+ #endif
+ }
+
+
+ static void
+ session_callback_fn(jack_session_event_t *event, void *arg)
+ {
+ ((JackDriverImpl *)arg)->OnSessionCallback(event);
+ };
+
+
+};
+
+AudioDriver* CreateJackDriver(AudioDriverHost*driverHost)
+{
+ return new JackDriverImpl(driverHost);
+}
+
+
+int JackDriverImpl::jackInstanceId = 0;
+
+
+} // namespace
+
+
+
+#endif
\ No newline at end of file
diff --git a/src/JackDriver.hpp b/src/JackDriver.hpp
new file mode 100644
index 0000000..b2d7bd3
--- /dev/null
+++ b/src/JackDriver.hpp
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include "AudioDriver.hpp"
+#include "AudioConfig.hpp"
+
+
+#if JACK_HOST
+namespace pipedal {
+
+ AudioDriver* CreateJackDriver(AudioDriverHost*driverHost);
+
+}
+#endif
+
diff --git a/src/JackHost.cpp b/src/JackHost.cpp
index 98ab932..b338a33 100644
--- a/src/JackHost.cpp
+++ b/src/JackHost.cpp
@@ -21,12 +21,15 @@
#include "Lv2Log.hpp"
+#include "JackDriver.hpp"
+#include "AlsaDriver.hpp"
+
+
using namespace pipedal;
-#include
-#include
-#include
-#include
+#include "AudioConfig.hpp"
+
+
#include
#include
#include
@@ -63,11 +66,6 @@ using namespace pipedal;
const int MIDI_LV2_BUFFER_SIZE = 16 * 1024;
-namespace pipedal
-{
- // private import from JackConfiguration.cpp
- std::string GetJackErrorMessage(jack_status_t status);
-}
static void GetCpuFrequency(uint64_t*freqMin,uint64_t*freqMax)
{
@@ -105,9 +103,10 @@ static std::string GetGovernor()
-class JackHostImpl : public JackHost
+class JackHostImpl : public JackHost, private AudioDriverHost
{
private:
+ AudioDriver*audioDriver = nullptr;
inherit_priority_recursive_mutex mutex;
int64_t overrunGracePeriodSamples = 0;
@@ -133,36 +132,26 @@ private:
JackChannelSelection channelSelection;
bool active = false;
- jack_client_t *client = nullptr;
std::shared_ptr currentPedalBoard;
std::vector> activePedalBoards; // pedalboards that have been sent to the audio queue.
Lv2PedalBoard *realtimeActivePedalBoard = nullptr;
- std::vector inputPorts;
- std::vector outputPorts;
- std::vector midiInputPorts;
-
std::vector midiLv2Buffers;
- uint32_t sampleRate;
+ uint32_t sampleRate = 0;
uint64_t currentSample = 0;
std::atomic underruns = 0;
std::atomic lastUnderrunTime =
std::chrono::system_clock::from_time_t(0);
- int XrunCallback()
+ virtual void OnUnderrun()
{
++this->underruns;
this->lastUnderrunTime = std::chrono::system_clock ::now();
- return 0;
}
- static int xrun_callback_fn(void *arg)
- {
- return ((JackHostImpl *)arg)->XrunCallback();
- }
virtual void Close()
{
@@ -174,84 +163,13 @@ private:
StopReaderThread();
if (active)
{
- // disconnect ports.
- for (size_t i = 0; i < this->inputPorts.size(); ++i)
- {
- auto port = inputPorts[i];
- if (port != nullptr)
- {
- jack_disconnect(client, channelSelection.GetInputAudioPorts()[i].c_str(), jack_port_name(port));
- }
- }
- for (size_t i = 0; i < outputPorts.size(); ++i)
- {
- auto port = outputPorts[i];
- if (port != nullptr)
- {
- jack_disconnect(client, jack_port_name(port),channelSelection.getOutputAudioPorts()[i].c_str());
- }
- }
- for (size_t i = 0; i < midiInputPorts.size(); ++i)
- {
- auto port = midiInputPorts[i];
- if (port != nullptr)
- {
- jack_disconnect(client, channelSelection.GetInputMidiPorts()[i].c_str(), jack_port_name(port));
- }
- }
-
- jack_deactivate(client);
-
- jack_set_process_callback(client, nullptr, nullptr);
- jack_on_shutdown(client, nullptr, nullptr);
- #if JACK_SESSION_CALLBACK // (deprecated, and not actually useful)
- jack_set_session_callback(client, nullptr, nullptr);
- #endif
- jack_set_xrun_callback(client, nullptr, nullptr);
+ audioDriver->Deactivate();
active = false;
}
- // unregister ports.
- for (size_t i = 0; i < this->inputPorts.size(); ++i)
- {
- auto port = inputPorts[i];
- if (port)
- {
- jack_port_unregister(client, port);
- }
- }
- inputPorts.clear();
- for (size_t i = 0; i < this->outputPorts.size(); ++i)
- {
- auto port = outputPorts[i];
- if (port)
- {
- jack_port_unregister(client, port);
- }
- }
- outputPorts.clear();
- for (size_t i = 0; i < this->midiInputPorts.size(); ++i)
- {
- auto port = midiInputPorts[i];
- if (port)
- {
- jack_port_unregister(client, port);
- }
- }
- midiInputPorts.resize(0);
+ audioDriver->Close();
- for (size_t i = 0; i < midiLv2Buffers.size(); ++i)
- {
- delete[] midiLv2Buffers[i];
- }
- midiLv2Buffers.resize(0);
-
- if (client)
- {
- jack_client_close(client);
- client = nullptr;
- }
// release any pdealboards owned by the process thread.
this->activePedalBoards.resize(0);
this->realtimeActivePedalBoard = nullptr;
@@ -268,20 +186,32 @@ private:
delete realtimeMonitorPortSubscriptions;
realtimeVuBuffers = nullptr;
}
+ this->inputRingBuffer.reset();
+ this->outputRingBuffer.reset();
+
+
+ for (size_t i = 0; i < midiLv2Buffers.size(); ++i)
+ {
+ delete[] midiLv2Buffers[i];
+ }
+ midiLv2Buffers.resize(0);
+
}
- void ZeroBuffer(float *buffer, jack_nframes_t nframes)
+ void ZeroBuffer(float *buffer, size_t nframes)
{
- for (jack_nframes_t i = 0; i < nframes; ++i)
+ for (size_t i = 0; i < nframes; ++i)
{
buffer[i] = 0;
}
}
- void ZeroOutputBuffers(jack_nframes_t nframes)
+ Lv2EventBufferUrids eventBufferUrids;
+
+ void ZeroOutputBuffers(size_t nframes)
{
- for (int i = 0; i < this->outputPorts.size(); ++i)
+ for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i)
{
- float *out = (float *)jack_port_get_buffer(this->outputPorts[i], nframes);
+ float *out = (float *)audioDriver->GetOutputBuffer(i, nframes);
if (out)
{
ZeroBuffer(out, nframes);
@@ -487,22 +417,26 @@ private:
{
Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids);
- for (int i = 0; i < this->midiInputPorts.size(); ++i)
+ size_t midiInputBufferCount = audioDriver->MidiInputBufferCount();
+
+ audioDriver->FillMidiBuffers();
+
+ for (size_t i = 0; i < midiInputBufferCount; ++i)
{
- jack_port_t *port = midiInputPorts[i];
- void *portBuffer = jack_port_get_buffer(midiInputPorts[i], 0);
+
+ void *portBuffer = audioDriver->GetMidiInputBuffer(i,0);
if (portBuffer)
{
uint8_t *lv2Buffer = this->midiLv2Buffers[i];
- jack_nframes_t n = jack_midi_get_event_count(portBuffer);
+ size_t n = audioDriver->GetMidiInputEventCount(portBuffer);
eventBufferWriter.Reset(lv2Buffer, MIDI_LV2_BUFFER_SIZE);
auto iterator = eventBufferWriter.begin();
- for (jack_nframes_t frame = 0; frame < n; ++frame)
+ for (size_t frame = 0; frame < n; ++frame)
{
- jack_midi_event_t event;
- if (jack_midi_event_get(&event, portBuffer, frame) == 0)
+ MidiEvent event;
+ if (audioDriver->GetMidiInputEvent(&event,portBuffer,frame))
{
eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer);
@@ -528,11 +462,26 @@ private:
#define RESET_XRUN_SAMPLES 22050ul // 1/2 a second-ish.
- int OnProcess(jack_nframes_t nframes)
+ std::mutex audioStoppedMutex;
+
+ bool IsAudioActive()
+ {
+ std::lock_guard lock { audioStoppedMutex};
+ return this->active;
+ }
+ virtual void OnAudioStopped()
+ {
+ std::lock_guard lock { audioStoppedMutex};
+ this->active = false;
+ Lv2Log::info("Audio stopped.");
+
+ }
+
+ virtual void OnProcess(size_t nframes)
{
try
{
- jack_default_audio_sample_t *in, *out;
+ float *in, *out;
pParameterRequests = nullptr;
@@ -547,9 +496,9 @@ private:
float *inputBuffers[4];
float *outputBuffers[4];
bool buffersValid = true;
- for (int i = 0; i < inputPorts.size(); ++i)
+ for (int i = 0; i < audioDriver->InputBufferCount(); ++i)
{
- float *input = (float *)jack_port_get_buffer(inputPorts[i], nframes);
+ float *input = (float *)audioDriver->GetInputBuffer(i, nframes);
if (input == nullptr)
{
buffersValid = false;
@@ -557,11 +506,11 @@ private:
}
inputBuffers[i] = input;
}
- inputBuffers[inputPorts.size()] = nullptr;
+ inputBuffers[audioDriver->InputBufferCount()] = nullptr;
- for (int i = 0; i < outputPorts.size(); ++i)
+ for (int i = 0; i < audioDriver->OutputBufferCount(); ++i)
{
- float *output = (float *)jack_port_get_buffer(outputPorts[i], nframes);
+ float *output = audioDriver->GetOutputBuffer(i,nframes);
if (output == nullptr)
{
buffersValid = false;
@@ -569,7 +518,7 @@ private:
}
outputBuffers[i] = output;
}
- outputBuffers[outputPorts.size()] = nullptr;
+ outputBuffers[audioDriver->OutputBufferCount()] = nullptr;
if (buffersValid)
{
@@ -626,61 +575,9 @@ private:
throw;
}
- return 0;
}
- static int process_fn(jack_nframes_t nframes, void *arg)
- {
- return ((JackHostImpl *)arg)->OnProcess(nframes);
- }
- void OnShutdown()
- {
- Lv2Log::info("Jack Audio Server has shut down.");
- }
-
- static void
- jack_shutdown_fn(void *arg)
- {
- ((JackHostImpl *)arg)->OnShutdown();
- }
-
- void OnSessionCallback(jack_session_event_t *event)
- {
- #if JACK_SESSION_CALLBACK // deprecated and not actually useful.
- char retval[100];
- Lv2Log::info("path %s, uuid %s, type: %s\n", event->session_dir, event->client_uuid, event->type == JackSessionSave ? "save" : "quit");
-
- snprintf(retval, 100, "jack_simple_session_client %s", event->client_uuid);
- event->command_line = strdup(retval);
-
- jack_session_reply(client, event);
-
- if (event->type == JackSessionSaveAndQuit)
- {
- // simple_quit = 1;
- }
-
- jack_session_event_free(event);
- #endif
- }
-
- static void jack_error_fn(const char *msg)
- {
- Lv2Log::error("Jack - %s", msg);
- }
- static void jack_info_fn(const char *msg)
- {
- Lv2Log::info("Jack - %s", msg);
- }
-
- static void
- session_callback_fn(jack_session_event_t *event, void *arg)
- {
- ((JackHostImpl *)arg)->OnSessionCallback(event);
- };
-
- Lv2EventBufferUrids eventBufferUrids;
-
+
public:
JackHostImpl(IHost *pHost)
: inputRingBuffer(RING_BUFFER_SIZE),
@@ -691,22 +588,29 @@ public:
hostWriter(&this->inputRingBuffer),
eventBufferUrids(pHost)
{
- jack_set_error_function(jack_error_fn);
- jack_set_info_function(jack_info_fn);
+
+ #if JACK_HOST
+ audioDriver = CreateJackDriver(this);
+ #endif
+ #if ALSA_HOST
+ audioDriver = CreateAlsaDriver(this);
+ #endif
+
}
virtual ~JackHostImpl()
{
Close();
CleanRestartThreads(true);
- jack_set_error_function(nullptr);
- jack_set_info_function(nullptr);
+
+ delete audioDriver;
+
}
virtual JackConfiguration GetServerConfiguration()
{
JackConfiguration result;
- result.Initialize();
+ result.JackInitialize();
return result;
}
@@ -980,14 +884,13 @@ public:
return result;
}
- static int jackInstanceId;
- virtual void Open(const JackChannelSelection &channelSelection)
+ virtual void Open(const JackServerSettings &jackServerSettings,const JackChannelSelection &channelSelection)
{
std::lock_guard guard(mutex);
if (channelSelection.GetInputAudioPorts().size() == 0
- || channelSelection.getOutputAudioPorts().size() == 0)
+ || channelSelection.GetOutputAudioPorts().size() == 0)
{
return;
}
@@ -1008,162 +911,32 @@ public:
StartReaderThread();
- jack_status_t status;
- try
- {
- // need a unique instance name every timme.
- std::stringstream s;
- s << "PiPedal";
- if (jackInstanceId != 0) {
- s << jackInstanceId;
- }
- ++jackInstanceId;
+ try {
- std::string instanceName = s.str();
+ audioDriver->Open(jackServerSettings,this->channelSelection);
- client = jack_client_open(instanceName.c_str(), JackNullOption, &status);
-
- if (client == nullptr || status & JackFailure)
- {
- if (client)
- {
- jack_client_close(client);
- client = nullptr;
- }
- std::string error = GetJackErrorMessage(status);
- Lv2Log::error(error);
- throw PiPedalStateException(error.c_str());
- }
-
- if (status & JackServerStarted)
- {
- Lv2Log::info("Jack server started.");
- }
-
- jack_set_process_callback(client, process_fn, this);
-
- jack_on_shutdown(client, jack_shutdown_fn, this);
-
- #if JACK_SESSION_CALLBACK
- jack_set_session_callback(client, session_callback_fn, NULL);
- #endif
-
- jack_set_xrun_callback(client, xrun_callback_fn, this);
-
- this->sampleRate = jack_get_sample_rate(client);
+ this->sampleRate = audioDriver->GetSampleRate();
this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate)*OVERRUN_GRACE_PERIOD_S);
-
-
this->vuSamplesPerUpdate = (size_t)(sampleRate * VU_UPDATE_RATE_S);
- auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
- this->inputPorts.clear();
- this->outputPorts.clear();
- this->midiInputPorts.clear();
- this->inputPorts.resize(selectedInputPorts.size());
- for (int i = 0; i < selectedInputPorts.size(); ++i)
- {
- std::stringstream name;
- name << "input" << (i + 1);
- this->inputPorts[i] =
- jack_port_register(
- client, name.str().c_str(),
- JACK_DEFAULT_AUDIO_TYPE,
- JackPortIsInput, 0);
- if (this->inputPorts[i] == nullptr)
- {
- Lv2Log::error("Can't allocate Jack Audio input ports.");
- throw PiPedalStateException("Failed to allocate Jack Audio port.");
- }
- }
- auto &selectedOutputPorts = channelSelection.getOutputAudioPorts();
- this->outputPorts.resize(selectedOutputPorts.size());
- for (int i = 0; i < selectedOutputPorts.size(); ++i)
- {
- std::stringstream name;
- name << "output" << (i + 1);
- this->outputPorts[i] =
- jack_port_register(client, name.str().c_str(),
- JACK_DEFAULT_AUDIO_TYPE,
- JackPortIsOutput, 0);
- if (this->outputPorts[i] == nullptr)
- {
- Lv2Log::error("Can't allocate Jack Audio output port.");
- throw PiPedalStateException("Failed to allocate Jack Audio port.");
- }
- }
- auto &selectedMidiPorts = channelSelection.GetInputMidiPorts();
-
- midiLv2Buffers.resize(selectedMidiPorts.size());
- for (size_t i = 0; i < selectedMidiPorts.size(); ++i)
+ midiLv2Buffers.resize(audioDriver->MidiInputBufferCount());
+ for (size_t i = 0; i < audioDriver->MidiInputBufferCount(); ++i)
{
midiLv2Buffers[i] = AllocateRealtimeBuffer(MIDI_LV2_BUFFER_SIZE);
}
- this->midiInputPorts.resize(selectedMidiPorts.size());
- for (int i = 0; i < selectedMidiPorts.size(); ++i)
- {
- std::stringstream name;
- name << "midiIn" << (i + 1);
- this->midiInputPorts[i] =
- jack_port_register(client, name.str().c_str(),
- JACK_DEFAULT_MIDI_TYPE,
- JackPortIsInput, 0);
- if (this->midiInputPorts[i] == nullptr)
- {
- std::stringstream s;
- s << "can't register Jack port " << name.str().c_str();
- Lv2Log::error(s.str());
- throw PiPedalStateException(s.str().c_str());
- }
- }
- int activateResult = jack_activate(client);
- if (activateResult == 0)
- {
- active = true;
+ active = true;
+ audioDriver->Activate();
+ Lv2Log::info("Audio started.");
- for (int i = 0; i < selectedInputPorts.size(); ++i)
- {
- auto result = jack_connect(client, selectedInputPorts[i].c_str(), jack_port_name(this->inputPorts[i]));
- if (result)
- {
- Lv2Log::error("Can't connect input port %s", selectedInputPorts[i].c_str());
- throw PiPedalStateException("Jack Audio port connection failed.");
- }
- }
- for (int i = 0; i < selectedOutputPorts.size(); ++i)
- {
- auto result = jack_connect(client, jack_port_name(this->outputPorts[i]), selectedOutputPorts[i].c_str());
- if (result)
- {
- Lv2Log::error("Can't connect output port %s", selectedOutputPorts[i].c_str());
- throw PiPedalStateException("Jack Audio port connection failed.");
- }
- }
- for (int i = 0; i < selectedMidiPorts.size(); ++i)
- {
- auto result = jack_connect(client, selectedMidiPorts[i].c_str(), jack_port_name(this->midiInputPorts[i]));
- if (result)
- {
- Lv2Log::error("Can't connect midi input port %s", selectedMidiPorts[i].c_str());
- throw PiPedalStateException("Jack Audio midi port connection failed.");
- }
- }
-
- Lv2Log::info("Jack configuration complete.");
- }
- else
- {
- Lv2Log::error("Failed to activate Jack Audio client. (%d)", (int)activateResult);
- throw PiPedalStateException("Failed to activate Jack Audio client.");
- }
}
catch (PiPedalException &e)
{
Close();
+ active = false;
throw;
}
}
@@ -1366,6 +1139,8 @@ private:
AdminClient client;
client.SetJackServerConfiguration(jackServerSettings);
+
+
//this_->Open(this_->channelSelection);
this_->restarting = false;
onComplete(true, "");
@@ -1468,16 +1243,12 @@ public:
result.temperaturemC_ = GetRaspberryPiTemperature();
- result.active_ = this->active;
+ result.active_ = IsAudioActive();
result.restarting_ = this->restarting;
- if (client != nullptr)
+ if (this->audioDriver != nullptr)
{
- result.cpuUsage_ = jack_cpu_load(this->client);
- }
- else
- {
- result.cpuUsage_ = 0;
+ result.cpuUsage_ = audioDriver->CpuUse();
}
GetCpuFrequency(&result.cpuFreqMax_,&result.cpuFreqMin_);
result.governor_ = GetGovernor();
@@ -1497,7 +1268,6 @@ public:
}
};
-int JackHostImpl::jackInstanceId = 0;
JackHost *JackHost::CreateInstance(IHost *pHost)
{
@@ -1506,6 +1276,7 @@ JackHost *JackHost::CreateInstance(IHost *pHost)
JSON_MAP_BEGIN(JackHostStatus)
JSON_MAP_REFERENCE(JackHostStatus, active)
+JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
JSON_MAP_REFERENCE(JackHostStatus, restarting)
JSON_MAP_REFERENCE(JackHostStatus, underruns)
JSON_MAP_REFERENCE(JackHostStatus, cpuUsage)
diff --git a/src/JackHost.hpp b/src/JackHost.hpp
index f19f506..689db04 100644
--- a/src/JackHost.hpp
+++ b/src/JackHost.hpp
@@ -106,6 +106,7 @@ public:
class JackHostStatus {
public:
bool active_;
+ std::string errorMessage_;
bool restarting_;
uint64_t underruns_;
float cpuUsage_ = 0;
@@ -139,7 +140,7 @@ public:
virtual void SetListenForAtomOutput(bool listen) = 0;
- virtual void Open(const JackChannelSelection & channelSelection) = 0;
+ virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection & channelSelection) = 0;
virtual void Close() = 0;
virtual uint32_t GetSampleRate() = 0;
diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp
index f09cb2c..b641741 100644
--- a/src/JackServerSettings.cpp
+++ b/src/JackServerSettings.cpp
@@ -157,6 +157,7 @@ void JackServerSettings::ReadJackConfiguration()
void JackServerSettings::Write()
{
+ #if JACK_HOST
this->valid_ = false;
std::vector precedingLines;
@@ -232,6 +233,9 @@ void JackServerSettings::Write()
catch (const std::exception &e)
{
}
+ #else
+ throw PiPedalStateException("JACK_HOST not enabled at compile time.");
+ #endif
}
JSON_MAP_BEGIN(JackServerSettings)
diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp
index ec70da2..0e81c99 100644
--- a/src/JackServerSettings.hpp
+++ b/src/JackServerSettings.hpp
@@ -33,6 +33,15 @@ namespace pipedal {
public:
JackServerSettings();
+ JackServerSettings(const std::string alsaDevice, uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers)
+ : valid_(true),
+ alsaDevice_(alsaDevice),
+ sampleRate_(sampleRate),
+ bufferSize_(bufferSize),
+ numberOfBuffers_(numberOfBuffers)
+ {
+
+ }
uint64_t GetSampleRate() const { return sampleRate_; }
uint32_t GetBufferSize() const { return bufferSize_; }
diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp
index 68d5f62..d53f3da 100644
--- a/src/Lv2Host.cpp
+++ b/src/Lv2Host.cpp
@@ -266,7 +266,7 @@ void Lv2Host::OnConfigurationChanged(const JackConfiguration &configuration, con
if (configuration.isValid())
{
this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size();
- this->numberOfAudioOutputChannels = settings.getOutputAudioPorts().size();
+ this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size();
this->maxBufferSize = configuration.GetBlockLength();
optionsFeature.Prepare(this->mapFeature, configuration.GetSampleRate(), configuration.GetBlockLength(), GetAtomBufferSize());
}
diff --git a/src/Lv2Log.cpp b/src/Lv2Log.cpp
index 55e3d2f..99c1b2c 100644
--- a/src/Lv2Log.cpp
+++ b/src/Lv2Log.cpp
@@ -31,9 +31,12 @@ using namespace pipedal;
static auto timeZero = std::chrono::system_clock::now();
+bool Lv2Log::show_time_ = true;
std::string timeTag()
{
+ if (!Lv2Log::show_time()) return "";
+
using namespace std::chrono;
auto t = std::chrono::system_clock::now()- timeZero;
@@ -47,7 +50,7 @@ std::string timeTag()
std::stringstream s;
using namespace std;
- s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_;
+ s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_ << " ";
return s.str();
}
@@ -61,23 +64,23 @@ class StdErrLogger : public Lv2Logger
virtual void onError(const char* message)
{
std::lock_guard lock(m);
- std::cerr << timeTag() << " ERR: " << message << std::endl;
+ std::cerr << timeTag() << "ERR: " << message << std::endl;
}
virtual void onWarning(const char* message)
{
std::lock_guard lock(m);
- std::cerr << timeTag() << " WRN: " << message << std::endl;
+ std::cerr << timeTag() << "WRN: " << message << std::endl;
}
virtual void onDebug(const char* message)
{
std::lock_guard lock(m);
- std::cerr << timeTag() << " DBG: " << message << std::endl;
+ std::cerr << timeTag() << "DBG: " << message << std::endl;
}
virtual void onInfo(const char* message)
{
std::lock_guard lock(m);
- std::cerr << timeTag() << " INF: " << message << std::endl;
+ std::cerr << timeTag() << "INF: " << message << std::endl;
}
};
diff --git a/src/Lv2Log.hpp b/src/Lv2Log.hpp
index b23680e..721c1a5 100644
--- a/src/Lv2Log.hpp
+++ b/src/Lv2Log.hpp
@@ -47,8 +47,10 @@ namespace pipedal
class Lv2Log
{
private:
+
static Lv2Logger *logger_;
static LogLevel log_level_;
+ static bool show_time_;
static void v_error(const char *format, va_list arglist);
static void v_warning(const char *format, va_list arglist);
@@ -68,6 +70,13 @@ namespace pipedal
{
return Lv2Log::log_level_;
}
+ static void show_time(bool show)
+ {
+ show_time_ = show;
+ }
+ static bool show_time() {
+ return show_time_;
+ }
// static void error(const char* message) {
// if (Lv2Log::log_level_ >= LogLevel::Error)
diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp
new file mode 100644
index 0000000..1052052
--- /dev/null
+++ b/src/PiLatencyMain.cpp
@@ -0,0 +1,474 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Robin E. R. Davies
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "CommandLineParser.hpp"
+#include "PrettyPrinter.hpp"
+#include
+#include "PiPedalAlsa.hpp"
+#include "Lv2Log.hpp"
+#include
+#include "AlsaDriver.hpp"
+#include
+#include
+
+using namespace pipedal;
+
+constexpr int E_NOSIGNAL = 1;
+constexpr int E_OPEN_FAILURE = 2;
+constexpr int E_XRUN = 3;
+
+constexpr uint64_t NO_SIGNAL_VALUE = 0x7000000;
+
+struct TestResult {
+ int error = 0;
+ uint64_t latency = 0;
+ float cpuOverhead = 0;
+};
+void PrintHelp()
+{
+ PrettyPrinter pp;
+ pp.width(160);
+
+ pp << "PiPedal Latency Tester\n";
+ pp << "Copyright (c) 2022 Robin Davies\n";
+ pp << "\n";
+ pp << Indent(0) << "Syntax\n\n";
+ pp << Indent(4) << "pipedal_latency_test [] \n\n";
+ pp << Indent(0) << "Options\n\n";
+ pp << Indent(15);
+
+ pp << HangingIndent() << " -l --list\t"
+ << "List available devices.\n\n";
+ pp << HangingIndent() << " -r --rate\t"
+ << "Sample rate (default 48000).\n\n";
+ pp << HangingIndent() << " -h --help\t"
+ << "Display this message.\n\n";
+
+ pp << Indent(0) << "Remarks\n\n";
+ pp << Indent(4);
+ pp << "PiPedal Latency Tester measure actual audio latency from output to input of an ALSA device. "
+ << "To run a latency test, you must connect an audio cable from left (first) output of the device "
+ "under test to the left (first) input of the device under test.\n\n"
+
+ << "PiPedal Latency Tester will measure the time it takes for a signal to propagate from output to input.\n\n"
+
+ << "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some "
+ "stress on the audio system.\n\n"
+
+ << "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n"
+ << Indent(8) << "sudo systemctl stop pipedald\n\n";
+ pp << Indent(0) << "Examples\n\n";
+ pp << Indent(4) << "pipedal_latency_test --list\n\n";
+ pp << Indent(4) << "pipedal_latency_test M2\n\n";
+}
+
+void ListDevices()
+{
+ PiPedalAlsaDevices alsaDevices;
+ auto devices = alsaDevices.GetAlsaDevices();
+
+ PrettyPrinter pp;
+ if (devices.size() == 0)
+ {
+ pp << "No devices found.\n";
+ }
+ else
+ {
+ pp << Indent(0);
+ pp << "Alsa Devices\n";
+ pp << Indent(15);
+
+ for (auto &device : devices)
+ {
+ pp << HangingIndent() << (device.id_) << "\t"
+ << device.longName_ << "\n";
+ }
+ }
+}
+
+class AlsaTester : private AudioDriverHost
+{
+public:
+ enum class TestType
+ {
+ Oscillator,
+ LatencyMonitor,
+ NullTest
+ };
+
+private:
+ AudioDriver *audioDriver = nullptr;
+
+ const std::string &deviceId;
+ uint32_t sampleRate;
+ int bufferSize;
+ int buffers;
+
+public:
+ AlsaTester(const std::string &deviceId, uint32_t sampleRate, int bufferSize, int buffers)
+ : deviceId(deviceId),
+ sampleRate(sampleRate),
+ bufferSize(bufferSize),
+ buffers(buffers)
+ {
+ }
+ ~AlsaTester()
+ {
+ delete audioDriver;
+ delete[] inputBuffers;
+ delete[] outputBuffers;
+ }
+
+ TestResult Test()
+ {
+ TestResult result;
+ try
+ {
+ JackServerSettings serverSettings(deviceId, sampleRate, bufferSize, buffers);
+
+ JackConfiguration jackConfiguration;
+ jackConfiguration.AlsaInitialize(serverSettings);
+
+ JackChannelSelection channelSelection(
+ jackConfiguration.GetInputAudioPorts(),
+ jackConfiguration.GetOutputAudioPorts(),
+ std::vector());
+
+ audioDriver = CreateAlsaDriver(this);
+
+ latencyMonitor.Init(jackConfiguration.GetSampleRate());
+ audioDriver->Open(serverSettings, channelSelection);
+
+ inputBuffers = new float *[channelSelection.GetInputAudioPorts().size()];
+ outputBuffers = new float *[channelSelection.GetOutputAudioPorts().size()];
+
+ audioDriver->Activate();
+
+ sleep(3); // let audio stabilize.
+
+ this->SetXruns(0);
+
+ sleep(7); // run for a bit.
+
+ audioDriver->Deactivate();
+ audioDriver->Close();
+
+ if (this->GetXruns() != 0)
+ {
+ result.error = E_XRUN;
+ return result;
+ }
+ result.latency = latencyMonitor.GetLatency();
+ if (result.latency == NO_SIGNAL_VALUE)
+ {
+ result.error = E_NOSIGNAL;
+ }
+ result.cpuOverhead = audioDriver->CpuOverhead();
+ }
+ catch (const std::exception &e)
+ {
+ result.error = E_OPEN_FAILURE;
+ }
+ return result;
+ }
+
+ float **inputBuffers = nullptr;
+ float **outputBuffers = nullptr;
+
+ class Oscillator
+ {
+ private:
+ double dx = 0;
+ double x = 0;
+ double dx2 = 0;
+ double x2 = 0;
+
+ public:
+ void Init(float frequency, size_t sampleRate)
+ {
+ dx = frequency * 3.141592736 * 2 / sampleRate;
+ dx2 = 0.5 * 3.141592736 * 2 / sampleRate;
+ }
+
+ float Next()
+ {
+ float result = (float)std::cos(x);
+ float env = (float)std::cos(x2);
+ x += dx;
+ x2 += dx2;
+ return result * env;
+ }
+ };
+
+ class LatencyMonitor
+ {
+ enum class State
+ {
+ Idle,
+ Waiting,
+ };
+ State state = State::Idle;
+ uint64_t t;
+ uint64_t idle_samples;
+ uint64_t waiting_samples;
+ size_t current_latency = 0;
+ size_t latency = 0;
+ std::mutex sync;
+
+ public:
+ void Init(uint64_t sampleRate)
+ {
+ idle_samples = (uint64_t)(sampleRate * 0.5);
+ waiting_samples = (uint64_t)(sampleRate * 0.5);
+
+ state = State::Idle;
+ t = idle_samples;
+ latency = 0;
+ }
+ void StartTest()
+ {
+ }
+
+ size_t GetLatency()
+ {
+ std::lock_guard lock{sync};
+ return latency;
+ }
+ float Next(float input)
+ {
+ switch (state)
+ {
+ default:
+ case State::Idle:
+ {
+ if (t-- == 0)
+ {
+ state = State::Waiting;
+ current_latency = 0;
+ }
+ return 0.01;
+ }
+ break;
+ case State::Waiting:
+ {
+ if (std::abs(input) > 0.1 || current_latency >= 2000)
+ {
+ {
+ std::lock_guard lock{sync};
+ if (latency >= 2000)
+ {
+ latency = NO_SIGNAL_VALUE;
+ }
+ else
+ {
+ latency = current_latency;
+ }
+ }
+ state = State::Idle;
+ t = idle_samples;
+ }
+ else
+ {
+ ++current_latency;
+ }
+ return current_latency < 100 ? 0.25 : 0.0;
+ }
+ break;
+ }
+ }
+ };
+
+ Oscillator oscillator;
+ LatencyMonitor latencyMonitor;
+
+ virtual void OnAudioStopped()
+ {
+ }
+ virtual void OnProcess(size_t nFrames)
+ {
+
+ size_t inputs = audioDriver->InputBufferCount();
+ size_t outputs = audioDriver->OutputBufferCount();
+
+ for (size_t i = 0; i < inputs; ++i)
+ {
+ inputBuffers[i] = audioDriver->GetInputBuffer(i, nFrames);
+ }
+ for (size_t i = 0; i < outputs; ++i)
+ {
+ outputBuffers[i] = audioDriver->GetOutputBuffer(i, nFrames);
+ }
+
+ for (size_t i = 0; i < nFrames; ++i)
+ {
+ float v = latencyMonitor.Next(inputBuffers[0][i]);
+ for (size_t c = 0; c < outputs; ++c)
+ {
+ outputBuffers[c][i] = v;
+ }
+ }
+ }
+ std::mutex sync;
+ uint64_t xruns;
+
+ uint64_t GetXruns()
+ {
+ lock_guard lock{sync};
+ return xruns;
+ }
+ void SetXruns(uint64_t value)
+ {
+ lock_guard lock{sync};
+ xruns = value;
+ }
+ virtual void OnUnderrun()
+ {
+ lock_guard lock{sync};
+ ++xruns;
+ }
+};
+
+TestResult RunLatencyTest(const std::string deviceId, uint32_t sampleRate, int bufferSize, int buffers)
+{
+ AlsaTester tester(deviceId, sampleRate, bufferSize, buffers);
+ return tester.Test();
+}
+
+static std::string msDisplay(float value)
+{
+ std::stringstream s;
+ s << fixed;
+ s.precision(1);
+ s << value << "ms";
+ return s.str();
+}
+static std::string overheadDisplay(float value)
+{
+ std::stringstream s;
+ s << setw(3) << value << "%";
+ return s.str();
+}
+
+void RunLatencyTest(const std::string &deviceId, uint32_t sampleRate)
+{
+ PrettyPrinter pp;
+ pp << "Device: " << deviceId << " Rate: " << sampleRate << "\n\n";
+
+ const int SIZE_COLUMN_WIDTH = 8;
+ const int BUFFERS_COLUMN_WIDTH = 20;
+
+ static int bufferCounts[] = {2, 3, 4};
+ static int bufferSizes[] = {16, 24, 32, 48, 64, 128};
+
+ pp << Column(SIZE_COLUMN_WIDTH) << "Buffers\n";
+ pp << "Size";
+
+ int column = SIZE_COLUMN_WIDTH;
+ for (auto bufferCount : bufferCounts)
+ {
+ pp << Column(column) << bufferCount;
+ column += BUFFERS_COLUMN_WIDTH;
+ }
+ pp << "\n";
+
+ for (auto bufferSize : bufferSizes)
+ {
+ pp << bufferSize;
+
+ int column = SIZE_COLUMN_WIDTH;
+
+ for (auto bufferCount : bufferCounts)
+ {
+ auto result = RunLatencyTest(deviceId, sampleRate, bufferSize, bufferCount);
+
+ pp.Column(column);
+ column += BUFFERS_COLUMN_WIDTH;
+ switch (result.error)
+ {
+ case E_NOSIGNAL:
+ pp << "No signal";
+ break;
+ case E_OPEN_FAILURE:
+ pp << "Failed";
+ break;
+ case E_XRUN:
+ pp << "Xrun";
+ break;
+
+ default:
+ {
+ float ms = 1000.0f * result.latency / sampleRate;
+ pp << result.latency << "/" << msDisplay(ms) ;
+ break;
+ }
+ }
+ }
+ pp << "\n";
+ }
+}
+
+int main(int argc, const char **argv)
+{
+
+ Lv2Log::log_level(LogLevel::Warning);
+
+ CommandLineParser parser;
+
+ std::string deviceName;
+ bool listDevices = false;
+ bool help = false;
+ uint32_t sampleRate = 48000;
+
+ parser.AddOption("l", "list", &listDevices);
+ parser.AddOption("h", "help", &help);
+ parser.AddOption("r", "rate", &sampleRate);
+
+ try
+ {
+ parser.Parse(argc, argv);
+
+ if (help)
+ {
+ PrintHelp();
+ }
+ else if (listDevices)
+ {
+ ListDevices();
+ }
+ else if (parser.Arguments().size() == 1)
+ {
+ RunLatencyTest(parser.Arguments()[0], sampleRate);
+ }
+ else
+ {
+ PrintHelp();
+ }
+ }
+ catch (std::exception &e)
+ {
+ cout << "Error: " << e.what() << endl;
+ return 1;
+ }
+ return 0;
+}
\ No newline at end of file
diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp
index 8e65811..c8ec346 100644
--- a/src/PiPedalAlsa.cpp
+++ b/src/PiPedalAlsa.cpp
@@ -167,9 +167,99 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices()
}
}
snd_config_update_free_global();
+
+ Lv2Log::debug("GetAlsaDevices --");
+ for (auto& device: result)
+ {
+ Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_));
+ }
return result;
}
+
+static std::vector GetAlsaDevices(const char*devname, const char*direction)
+{
+ std::vector result;
+ {
+
+ char **hints;
+ int err;
+ char **n;
+ char *name;
+ char *desc;
+ char *ioid;
+
+ /* Enumerate sound devices */
+ err = snd_device_name_hint(-1, devname, (void ***)&hints);
+ if (err != 0)
+ {
+ return result;
+ }
+
+ n = hints;
+ while (*n != NULL)
+ {
+
+ name = snd_device_name_get_hint(*n, "NAME");
+ desc = snd_device_name_get_hint(*n, "DESC");
+ ioid = snd_device_name_get_hint(*n, "IOID");
+
+ if (desc != nullptr) // skip virtual device
+ {
+ if (ioid == nullptr || strcmp(ioid,direction) == 0)
+ {
+ result.push_back(AlsaMidiDeviceInfo(name,desc));
+ }
+ }
+ if (name && strcmp("null", name) != 0)
+ free(name);
+ if (desc && strcmp("null", desc) != 0)
+ free(desc);
+ if (ioid && strcmp("null", ioid) != 0)
+ free(ioid);
+ n++;
+ }
+
+ // Free hint buffer too
+ snd_device_name_free_hint((void **)hints);
+ }
+ return result;
+}
+
+
+std::vector pipedal::GetAlsaMidiInputDevices()
+{
+ return GetAlsaDevices("rawmidi","Input");
+}
+std::vector pipedal::GetAlsaMidiOutputDevices()
+{
+ return GetAlsaDevices("rawmidi","Output");
+}
+
+AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char*name, const char*description)
+:name_(name)
+{
+ // extract just the display name from description.
+ // undocumented but e.g.: M2, M2\nM2 Raw Midi
+ const char *p = description;
+ const char *pEnd = p;
+ // undocumented but e.g.: M2, M2\nM2 Raw Midi
+ while (pEnd != nullptr && *pEnd != 0
+ && *pEnd != ','
+ && *pEnd != '\n'
+ )
+ {
+ ++pEnd;
+ }
+ if (p != pEnd)
+ {
+ description_ = std::string(p,pEnd);
+ } else {
+ description = name;
+ }
+}
+
+
JSON_MAP_BEGIN(AlsaDeviceInfo)
JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId)
JSON_MAP_REFERENCE(AlsaDeviceInfo, id)
@@ -179,3 +269,8 @@ JSON_MAP_REFERENCE(AlsaDeviceInfo, sampleRates)
JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize)
JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize)
JSON_MAP_END()
+
+JSON_MAP_BEGIN(AlsaMidiDeviceInfo)
+JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, name)
+JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, description)
+JSON_MAP_END()
diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp
index 7dc783f..58eefec 100644
--- a/src/PiPedalAlsa.hpp
+++ b/src/PiPedalAlsa.hpp
@@ -33,6 +33,16 @@ namespace pipedal {
DECLARE_JSON_MAP(AlsaDeviceInfo);
+ };
+ class AlsaMidiDeviceInfo {
+ public:
+ AlsaMidiDeviceInfo() { }
+ AlsaMidiDeviceInfo(const char*name, const char*description);
+ std::string name_;
+ std::string description_;
+
+ DECLARE_JSON_MAP(AlsaMidiDeviceInfo);
+
};
class PiPedalAlsaDevices {
@@ -45,4 +55,6 @@ namespace pipedal {
std::vector GetAlsaDevices();
};
+ std::vector GetAlsaMidiInputDevices();
+ std::vector GetAlsaMidiOutputDevices();
}
\ No newline at end of file
diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp
index 9316a0c..17f2db9 100644
--- a/src/PiPedalAlsaTest.cpp
+++ b/src/PiPedalAlsaTest.cpp
@@ -32,7 +32,14 @@ TEST_CASE( "ALSA Test", "[pipedal_alsa_test][Build][Dev]" ) {
PiPedalAlsaDevices devices;
auto result = devices.GetAlsaDevices();
- std::cout << result.size() << " ALSA devices found." << 0;
+ std::cout << result.size() << " ALSA devices found." << std::endl;
+
+ auto midiInputDevices = GetAlsaMidiInputDevices();
+ std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl;
+ auto midiOutputDevices = GetAlsaMidiOutputDevices();
+ std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl;
+
+
}
diff --git a/src/PiPedalException.hpp b/src/PiPedalException.hpp
index cbe37bb..54a9c9e 100644
--- a/src/PiPedalException.hpp
+++ b/src/PiPedalException.hpp
@@ -57,6 +57,10 @@ namespace pipedal
: PiPedalException(std::forward(what))
{
}
+ PiPedalStateException(const std::string &what)
+ : PiPedalException(what)
+ {
+ }
};
class PiPedalLogicException : public PiPedalException
{
diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp
index 0c375c1..212d287 100644
--- a/src/PiPedalModel.cpp
+++ b/src/PiPedalModel.cpp
@@ -19,6 +19,7 @@
#include "pch.h"
#include "DeviceIdFile.hpp"
+#include "AudioConfig.hpp"
#include
#include "PiPedalModel.hpp"
#include "JackHost.hpp"
@@ -49,7 +50,12 @@ std::string AtomToJson(int length, uint8_t *pData)
PiPedalModel::PiPedalModel()
{
this->pedalBoard = PedalBoard::MakeDefault();
+#if JACK_HOST
this->jackServerSettings.ReadJackConfiguration();
+#else
+ this->jackServerSettings = this->storage.GetJackServerSettings();
+#endif
+
}
@@ -108,14 +114,19 @@ PiPedalModel::~PiPedalModel()
#include
-
-void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration)
+void PiPedalModel::Init(const PiPedalConfiguration&configuration)
{
-
+ this->configuration = configuration;
storage.SetConfigRoot(configuration.GetDocRoot());
storage.SetDataRoot(configuration.GetLocalStoragePath());
storage.Initialize();
+ this->jackServerSettings.ReadJackConfiguration();
+
+}
+
+void PiPedalModel::LoadLv2PluginInfo()
+{
// Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory.
std::filesystem::path pluginClassesPath = configuration.GetDocRoot() / "plugin_classes.json";
try
@@ -149,11 +160,10 @@ void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration)
}
}
-void PiPedalModel::Load(const PiPedalConfiguration &configuration)
+void PiPedalModel::Load()
{
this->webRoot = configuration.GetWebRoot();
this->webPort = (uint16_t)configuration.GetSocketServerPort();
- this->jackServerSettings.ReadJackConfiguration();
adminClient.MonitorGovernor(storage.GetGovernorSettings());
@@ -192,8 +202,12 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
scheduler_params.sched_priority = 10;
memset(&scheduler_params,0,sizeof(sched_param));
sched_setscheduler(0,SCHED_RR,&scheduler_params);
-
- this->jackConfiguration = jackHost->GetServerConfiguration();
+#if JACK_HOST
+ this->jackConfiguration = this->jackConfiguration.JackInitialize();
+#else
+ this->jackServerSettings = storage.GetJackServerSettings();
+ this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
+#endif
if (this->jackConfiguration.isValid())
{
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
@@ -202,7 +216,7 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
this->lv2Host.OnConfigurationChanged(jackConfiguration, selection);
try
{
- jackHost->Open(selection);
+ jackHost->Open(this->jackServerSettings,selection);
std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
this->lv2PedalBoard = lv2PedalBoard;
@@ -212,6 +226,8 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
{
Lv2Log::error("Failed to load initial plugin. (%s)", e.what());
}
+ } else {
+ Lv2Log::error("Audio device not configured.");
}
}
@@ -780,15 +796,27 @@ void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSe
this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection);
if (this->jackHost->IsOpen())
{
- // do a complete reload.
this->jackHost->Close();
+#ifdef JUNK
+ // restarting is a bit dodgy. It was impossible with Jack, but
+ // now very plausible with the ALSA audio stack.
+
+ // Still bugs wrt/ restarting the circular buffers for the audio thread.
+
+ // for the meantime, just rely on the fact that the admin service will restart
+ // the process.
+ //...
+
+ // do a complete reload.
+
this->jackHost->SetPedalBoard(nullptr);
- this->jackHost->Open(channelSelection);
+ this->jackHost->Open(this->jackServerSettings,channelSelection);
std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
this->lv2PedalBoard = lv2PedalBoard;
jackHost->SetPedalBoard(lv2PedalBoard);
this->UpdateRealtimeVuSubscriptions();
+#endif
}
}
@@ -1127,6 +1155,10 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet
if (adminClient.CanUseShutdownClient())
{
+ #if ALSA_HOST
+ storage.SetJackServerSettings(jackServerSettings);
+ #endif
+
// save the current (edited) preset now in case the service shutdown isn't clean.
CurrentPreset currentPreset;
currentPreset.modified_ = this->hasPresetChanged;
diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp
index a76d314..5a93006 100644
--- a/src/PiPedalModel.hpp
+++ b/src/PiPedalModel.hpp
@@ -153,6 +153,8 @@ private: // IJackHostCallbacks
virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl);
virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson);
+
+ PiPedalConfiguration configuration;
public:
PiPedalModel();
virtual ~PiPedalModel();
@@ -164,9 +166,10 @@ public:
AdminClient&GetAdminClient() { return adminClient; }
+ void Init(const PiPedalConfiguration&configuration);
- void LoadLv2PluginInfo(const PiPedalConfiguration&configuration);
- void Load(const PiPedalConfiguration&configuration);
+ void LoadLv2PluginInfo();
+ void Load();
const Lv2Host& GetLv2Host() const { return lv2Host; }
PedalBoard GetCurrentPedalBoardCopy()
diff --git a/src/PrettyPrinter.hpp b/src/PrettyPrinter.hpp
index 6293823..f4edc34 100644
--- a/src/PrettyPrinter.hpp
+++ b/src/PrettyPrinter.hpp
@@ -52,6 +52,10 @@ namespace pipedal
{
lineBuffer.reserve(120);
}
+
+ size_t width() const { return lineWidth; }
+ void width(size_t width) { lineWidth =width; }
+
std::ostream&Stream() const { return s;}
PrettyPrinter& Indent(size_t indent)
@@ -67,6 +71,21 @@ namespace pipedal
column = 0;
}
+ PrettyPrinter& Column(int n)
+ {
+ if (column >= n) {
+ lineBuffer.push_back(' ');
+ ++column;
+ return *this;
+ }
+
+ while (column != n)
+ {
+ lineBuffer.push_back(' ');
+ ++column;
+ }
+ return *this;
+ }
PrettyPrinter& HangingIndent()
{
hangingIndent = true;
@@ -235,5 +254,13 @@ namespace pipedal
};
}
+ pp_manip Column(int n)
+ {
+ return [n] (PrettyPrinter &pp) -> PrettyPrinter& {
+ pp.Column(n);
+ return pp;
+ };
+ }
+
}
\ No newline at end of file
diff --git a/src/Storage.cpp b/src/Storage.cpp
index c8c38d8..3eb9dd0 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -19,6 +19,7 @@
#include "pch.h"
#include "Storage.hpp"
+#include "AudioConfig.hpp"
#include "PiPedalException.hpp"
#include
#include "json.hpp"
@@ -35,8 +36,6 @@ const char *BANKS_FILENAME = "index.banks";
#define WIFI_CONFIG_SETTINGS_FILENAME "wifiConfigSettings.json";
#define USER_SETTINGS_FILENAME "userSettings.json";
-
-
Storage::Storage()
{
SetConfigRoot("~/var/Config");
@@ -131,7 +130,7 @@ std::string Storage::SafeEncodeName(const std::string &name)
return s.str();
}
-std::filesystem::path ResolveHomePath(const std::filesystem::path& path)
+std::filesystem::path ResolveHomePath(const std::filesystem::path &path)
{
if (path.begin() == path.end())
return path;
@@ -161,28 +160,27 @@ std::filesystem::path ResolveHomePath(const std::filesystem::path& path)
return result;
}
-void Storage::SetConfigRoot(const std::filesystem::path& path)
+void Storage::SetConfigRoot(const std::filesystem::path &path)
{
this->configRoot = ResolveHomePath(path);
}
-void Storage::SetDataRoot(const std::filesystem::path& path)
+void Storage::SetDataRoot(const std::filesystem::path &path)
{
this->dataRoot = ResolveHomePath(path);
}
static void CopyDirectory(const std::filesystem::path &source, const std::filesystem::path &destination)
{
- for (auto &directoryEntry: std::filesystem::directory_iterator(source))
+ for (auto &directoryEntry : std::filesystem::directory_iterator(source))
{
if (directoryEntry.is_regular_file())
{
std::filesystem::path sourceFile = directoryEntry.path();
std::filesystem::path destFile = destination / sourceFile.filename();
- std::filesystem::copy_file(sourceFile,destFile);
- std::ignore = chmod(destFile.c_str(),S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
-
- }
+ std::filesystem::copy_file(sourceFile, destFile);
+ std::ignore = chmod(destFile.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
+ }
}
}
void Storage::MaybeCopyDefaultPresets()
@@ -192,18 +190,15 @@ void Storage::MaybeCopyDefaultPresets()
if (!std::filesystem::exists(presetsDirectory / "index.banks"))
{
CopyDirectory(this->configRoot / "default_presets" / "presets",
- presetsDirectory
- );
+ presetsDirectory);
}
auto pluginDirectory = this->GetPluginPresetsDirectory();
if (!std::filesystem::exists(pluginDirectory / "index.json"))
{
CopyDirectory(this->configRoot / "default_presets" / "plugin_presets",
- pluginDirectory
- );
+ pluginDirectory);
}
-
}
void Storage::Initialize()
{
@@ -233,7 +228,6 @@ void Storage::Initialize()
LoadUserSettings();
}
-
void Storage::LoadBank(int64_t instanceId)
{
auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId);
@@ -265,7 +259,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
return this->dataRoot / "currentPreset.json";
}
-
std::filesystem::path Storage::GetChannelSelectionFileName()
{
return this->dataRoot / "JackChannelSelection.json";
@@ -304,7 +297,7 @@ void Storage::LoadBankIndex()
std::string name = "Default Bank";
SaveBankFile(name, currentBank);
- int64_t selectedBank = bankIndex.addBank(-1,name);
+ int64_t selectedBank = bankIndex.addBank(-1, name);
bankIndex.selectedBank(selectedBank);
}
SaveBankIndex();
@@ -365,7 +358,7 @@ void Storage::ReIndex()
if (path.extension() == BANK_EXTENSION)
{
std::string name = SafeDecodeName(path.stem());
- bankIndex.addBank(-1,name);
+ bankIndex.addBank(-1, name);
}
}
}
@@ -387,11 +380,12 @@ void Storage::CreateBank(const std::string &name)
int64_t instanceId = bankFile.presets()[0]->instanceId();
bankFile.selectedPreset(instanceId);
SaveBankFile(name, bankFile);
- this->bankIndex.addBank(-1,name);
+ this->bankIndex.addBank(-1, name);
this->SaveBankIndex();
}
-void Storage::GetBankFile(int64_t instanceId,BankFile *pBank) const {
+void Storage::GetBankFile(int64_t instanceId, BankFile *pBank) const
+{
auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId);
auto name = indexEntry.name();
std::filesystem::path fileName = GetBankFileName(name);
@@ -407,7 +401,6 @@ void Storage::LoadBankFile(const std::string &name, BankFile *pBank)
std::ifstream is(fileName);
json_reader reader(is);
reader.read(pBank);
-
}
void Storage::SaveBankFile(const std::string &name, const BankFile &bankFile)
@@ -459,8 +452,9 @@ const PedalBoard &Storage::GetCurrentPreset()
bool Storage::LoadPreset(int64_t instanceId)
{
if (!currentBank.hasItem(instanceId))
- return false;
- if (instanceId != currentBank.selectedPreset()) {
+ return false;
+ if (instanceId != currentBank.selectedPreset())
+ {
currentBank.selectedPreset(instanceId);
SaveCurrentBank();
}
@@ -489,13 +483,13 @@ void Storage::SetPresetIndex(const PresetIndex &presets)
std::map *> entries;
for (int i = 0; i < this->currentBank.presets().size(); ++i)
{
- auto & preset = this->currentBank.presets()[i];
+ auto &preset = this->currentBank.presets()[i];
entries[preset->instanceId()] = &(this->currentBank.presets()[i]);
}
- std::vector*> newPresets;
+ std::vector *> newPresets;
for (size_t i = 0; i < presets.presets().size(); ++i)
{
- std::unique_ptr* p = entries[presets.presets()[i].instanceId()];
+ std::unique_ptr *p = entries[presets.presets()[i].instanceId()];
if (p == nullptr)
{
throw PiPedalStateException("Presets do not match the currently loaded bank.");
@@ -544,7 +538,7 @@ PedalBoard Storage::GetPreset(int64_t instanceId) const
int64_t Storage::DeletePreset(int64_t presetId)
{
- int64_t newSelection = currentBank.deletePreset(presetId);
+ int64_t newSelection = currentBank.deletePreset(presetId);
SaveCurrentBank();
return newSelection;
}
@@ -724,7 +718,7 @@ int64_t Storage::SaveBankAs(int64_t bankId, const std::string &newName)
std::filesystem::path newPath = this->GetBankFileName(newName);
try
{
- std::filesystem::copy(oldPath,newPath);
+ std::filesystem::copy(oldPath, newPath);
}
catch (std::exception &e)
{
@@ -732,36 +726,40 @@ int64_t Storage::SaveBankAs(int64_t bankId, const std::string &newName)
s << "Unable to save the bank. (" << e.what() << ")";
throw PiPedalException(s.str());
}
- int64_t newId = this->bankIndex.addBank(bankId,newName);
+ int64_t newId = this->bankIndex.addBank(bankId, newName);
SaveBankIndex();
return newId;
}
void Storage::MoveBank(int from, int to)
{
- this->bankIndex.move(from,to);
+ this->bankIndex.move(from, to);
this->SaveBankIndex();
}
int64_t Storage::DeleteBank(int64_t bankId)
{
- auto & entries = this->bankIndex.entries();
+ auto &entries = this->bankIndex.entries();
for (size_t i = 0; i < entries.size(); ++i)
{
- auto & entry = entries[i];
+ auto &entry = entries[i];
- if (entry.instanceId() == bankId) {
+ if (entry.instanceId() == bankId)
+ {
std::filesystem::path fileName = this->GetBankFileName(entry.name());
- entries.erase(entries.begin()+i);
+ entries.erase(entries.begin() + i);
int64_t newSelection;
if (i < entries.size())
{
newSelection = entries[i].instanceId();
- } else if (entries.size() > 0)
+ }
+ else if (entries.size() > 0)
+ {
+ newSelection = entries[entries.size() - 1].instanceId();
+ }
+ else
{
- newSelection = entries[entries.size()-1].instanceId();
- } else {
// zero entries?
// Create a default empty bank.
BankIndexEntry newEntry;
@@ -773,7 +771,7 @@ int64_t Storage::DeleteBank(int64_t bankId)
std::string name = "Default Bank";
SaveBankFile(name, defaultBank);
- int64_t selectedBank = bankIndex.addBank(-1,name);
+ int64_t selectedBank = bankIndex.addBank(-1, name);
newSelection = selectedBank;
}
if (this->bankIndex.selectedBank() == bankId)
@@ -793,7 +791,7 @@ int64_t Storage::GetCurrentPresetId() const
return this->currentBank.selectedPreset();
}
-int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
+int64_t Storage::UploadPreset(const BankFile &bankFile, int64_t uploadAfter)
{
int64_t lastPreset = this->currentBank.selectedPreset();
if (uploadAfter != -1)
@@ -801,7 +799,8 @@ int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
lastPreset = uploadAfter;
}
- if (bankFile.presets().size() == 0) {
+ if (bankFile.presets().size() == 0)
+ {
throw PiPedalException("Invalid preset.");
}
for (size_t i = 0; i < bankFile.presets().size(); ++i)
@@ -817,12 +816,12 @@ int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
preset.name(s.str());
}
- lastPreset = this->currentBank.addPreset(preset,lastPreset);
+ lastPreset = this->currentBank.addPreset(preset, lastPreset);
}
this->SaveCurrentBank();
return lastPreset;
}
-int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
+int64_t Storage::UploadBank(BankFile &bankFile, int64_t uploadAfter)
{
int64_t lastBank = this->bankIndex.selectedBank();
if (uploadAfter != -1)
@@ -830,7 +829,8 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
lastBank = uploadAfter;
}
- if (bankFile.presets().size() == 0) {
+ if (bankFile.presets().size() == 0)
+ {
throw PiPedalException("Invalid bank.");
}
bankFile.updateNextIndex();
@@ -845,24 +845,25 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
}
std::filesystem::path path = this->GetBankFileName(bankFile.name());
std::ofstream f(path);
- if (!f.is_open()) {
+ if (!f.is_open())
+ {
throw PiPedalException("Can't write to bank file.");
}
json_writer writer(f);
writer.write(bankFile);
- lastBank = this->bankIndex.addBank(lastBank,bankFile.name());
+ lastBank = this->bankIndex.addBank(lastBank, bankFile.name());
this->SaveBankIndex();
return lastBank;
}
-
-void Storage::SetGovernorSettings(const std::string & governor)
+void Storage::SetGovernorSettings(const std::string &governor)
{
userSettings.governor_ = governor;
SaveUserSettings();
}
-void Storage::SaveUserSettings() {
+void Storage::SaveUserSettings()
+{
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
{
std::ofstream f(path);
@@ -889,8 +890,7 @@ std::string Storage::GetGovernorSettings() const
return this->userSettings.governor_;
}
-
-void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSettings)
+void Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings)
{
WifiConfigSettings copyToSave = wifiConfigSettings;
copyToSave.rebootRequired_ = false;
@@ -915,19 +915,21 @@ void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSetting
if (copyToStore.enable_)
{
copyToStore.hasPassword_ = copyToStore.password_.length() != 0 || this->wifiConfigSettings.hasPassword_;
- } else {
+ }
+ else
+ {
copyToStore.hasPassword_ = false;
}
copyToStore.password_ = "";
copyToStore.rebootRequired_ = false;
- if (copyToStore.enable_ && !copyToStore.hasPassword_)
+ if (copyToStore.enable_ && !copyToStore.hasPassword_)
{
copyToStore.hasPassword_ = this->wifiConfigSettings.hasPassword_;
}
this->wifiConfigSettings = copyToStore;
}
-void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings & wifiDirectConfigSettings)
+void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings)
{
WifiDirectConfigSettings copyToSave = wifiDirectConfigSettings;
copyToSave.rebootRequired_ = false;
@@ -945,7 +947,8 @@ void Storage::LoadUserSettings()
{
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
- try {
+ try
+ {
if (std::filesystem::is_regular_file(path))
{
std::ifstream f(path);
@@ -956,17 +959,18 @@ void Storage::LoadUserSettings()
json_reader reader(f);
reader.read(&userSettings);
}
- } catch (const std::exception&)
+ }
+ catch (const std::exception &)
{
}
this->wifiConfigSettings.valid_ = true;
-
}
void Storage::LoadWifiConfigSettings()
{
std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME;
- try {
+ try
+ {
if (std::filesystem::is_regular_file(path))
{
std::ifstream f(path);
@@ -979,9 +983,9 @@ void Storage::LoadWifiConfigSettings()
reader.read(&wifiConfigSettings);
this->wifiConfigSettings = wifiConfigSettings;
}
- } catch (const std::exception&)
+ }
+ catch (const std::exception &)
{
-
}
this->wifiConfigSettings.valid_ = true;
}
@@ -996,8 +1000,6 @@ void Storage::LoadWifiDirectConfigSettings()
this->wifiDirectConfigSettings = settings;
}
-
-
WifiConfigSettings Storage::GetWifiConfigSettings()
{
return this->wifiConfigSettings;
@@ -1007,43 +1009,47 @@ WifiDirectConfigSettings Storage::GetWifiDirectConfigSettings()
return this->wifiDirectConfigSettings;
}
-
void Storage::SaveCurrentPreset(const CurrentPreset ¤tPreset)
{
- try {
+ try
+ {
std::filesystem::path path = GetCurrentPresetPath();
std::ofstream f(path);
json_writer writer(f);
writer.write(currentPreset);
- } catch (std::exception&)
+ }
+ catch (std::exception &)
{
// called from destructor. Must be nothrow().
}
-
}
-bool Storage::RestoreCurrentPreset(CurrentPreset*pResult)
+bool Storage::RestoreCurrentPreset(CurrentPreset *pResult)
{
std::filesystem::path path = GetCurrentPresetPath();
- if (std::filesystem::exists(path)) {
- try {
+ if (std::filesystem::exists(path))
+ {
+ try
+ {
std::ifstream f(path);
json_reader reader(f);
reader.read(pResult);
std::filesystem::remove(path); // one-shot only, restore the state from the last *orderly* shutdown.
- } catch (const std::exception&)
+ }
+ catch (const std::exception &)
{
return false;
}
return true;
- } else {
+ }
+ else
+ {
return false;
}
-
}
bool Storage::HasPluginPresets(const std::string &pluginUri) const
{
- for (const auto &entry: this->pluginPresetIndex.entries_)
+ for (const auto &entry : this->pluginPresetIndex.entries_)
{
if (entry.pluginUri_ == pluginUri)
{
@@ -1055,7 +1061,7 @@ bool Storage::HasPluginPresets(const std::string &pluginUri) const
std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri) const
{
- for (const auto &entry: this->pluginPresetIndex.entries_)
+ for (const auto &entry : this->pluginPresetIndex.entries_)
{
if (entry.pluginUri_ == pluginUri)
{
@@ -1063,9 +1069,8 @@ std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri)
}
}
throw PiPedalArgumentException("Plugin preset file not found.");
-
}
-void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets&presets)
+void Storage::SavePluginPresets(const std::string &pluginUri, const PluginPresets &presets)
{
std::string name;
std::filesystem::path path;
@@ -1075,10 +1080,12 @@ void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets
name = SS(pluginPresetIndex.nextInstanceId_ << ".json");
path = GetPluginPresetsDirectory() / name;
presetAdded = true;
- } else {
+ }
+ else
+ {
path = GetPluginPresetPath(pluginUri);
}
- auto tempPath = path.string()+".$$$";
+ auto tempPath = path.string() + ".$$$";
{
std::ofstream os;
os.open(tempPath, std::ios_base::trunc);
@@ -1093,22 +1100,20 @@ void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets
{
std::filesystem::remove(path);
}
- std::filesystem::rename(tempPath,path);
+ std::filesystem::rename(tempPath, path);
if (presetAdded)
{
pluginPresetIndex.entries_.push_back(
PluginPresetIndexEntry(
- pluginUri,name
- )
- );
+ pluginUri, name));
pluginPresetIndex.nextInstanceId_++;
this->pluginPresetIndexChanged = true;
SavePluginPresetIndex();
}
}
-PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const
+PluginPresets Storage::GetPluginPresets(const std::string &pluginUri) const
{
PluginPresets result;
if (!HasPluginPresets(pluginUri))
@@ -1127,36 +1132,33 @@ PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const
reader.read(&result);
return result;
}
-PluginUiPresets Storage::GetPluginUiPresets(const std::string&pluginUri) const
+PluginUiPresets Storage::GetPluginUiPresets(const std::string &pluginUri) const
{
PluginPresets presets = GetPluginPresets(pluginUri);
PluginUiPresets result;
result.pluginUri_ = presets.pluginUri_;
for (size_t i = 0; i < presets.presets_.size(); ++i)
{
- const auto& preset = presets.presets_[i];
+ const auto &preset = presets.presets_[i];
result.presets_.push_back(
- PluginUiPreset
- {
+ PluginUiPreset{
preset.label_,
- preset.instanceId_
- }
- );
+ preset.instanceId_});
}
return result;
}
-std::vector Storage::GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId)
+std::vector Storage::GetPluginPresetValues(const std::string &pluginUri, uint64_t instanceId)
{
auto presets = GetPluginPresets(pluginUri);
- for (const auto & preset: presets.presets_)
+ for (const auto &preset : presets.presets_)
{
if (preset.instanceId_ == instanceId)
{
std::vector result;
- for (const auto &valuePair: preset.controlValues_)
+ for (const auto &valuePair : preset.controlValues_)
{
- result.push_back(ControlValue(valuePair.first.c_str(),valuePair.second));
+ result.push_back(ControlValue(valuePair.first.c_str(), valuePair.second));
}
return result;
}
@@ -1165,16 +1167,16 @@ std::vector Storage::GetPluginPresetValues(const std::string&plugi
}
uint64_t Storage::SavePluginPreset(
- const std::string&pluginUri,
- const std::string&name,
- const std::map & values)
+ const std::string &pluginUri,
+ const std::string &name,
+ const std::map &values)
{
auto presets = GetPluginPresets(pluginUri);
uint64_t result = -1;
bool existing = false;
for (size_t i = 0; i < presets.presets_.size(); ++i)
{
- auto & preset = presets.presets_[i];
+ auto &preset = presets.presets_[i];
if (preset.label_ == name)
{
preset.controlValues_ = values;
@@ -1190,10 +1192,9 @@ uint64_t Storage::SavePluginPreset(
PluginPreset(
result,
name,
- values
- ));
+ values));
}
- this->SavePluginPresets(pluginUri,presets);
+ this->SavePluginPresets(pluginUri, presets);
return result;
}
@@ -1201,26 +1202,24 @@ void Storage::UpdatePluginPresets(const PluginUiPresets &pluginPresets)
{
// handles deletions, renaming, and reordering only.
// If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead.
-
+
PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_);
PluginPresets newPresets;
newPresets.pluginUri_ = pluginPresets.pluginUri_;
newPresets.nextInstanceId_ = presets.nextInstanceId_;
- for (const auto&preset: pluginPresets.presets_)
+ for (const auto &preset : pluginPresets.presets_)
{
PluginPreset newPreset = presets.GetPreset(preset.instanceId_);
newPreset.label_ = preset.label_;
newPresets.presets_.push_back(std::move(newPreset));
}
- SavePluginPresets(newPresets.pluginUri_,newPresets);
-
-
+ SavePluginPresets(newPresets.pluginUri_, newPresets);
}
static std::string stripCopySuffix(const std::string &s)
{
- int pos = s.length()-1;
+ int pos = s.length() - 1;
if (pos >= 0 && s[pos] == ')')
{
--pos;
@@ -1228,7 +1227,7 @@ static std::string stripCopySuffix(const std::string &s)
{
--pos;
}
- if (pos >= 0 && s[pos] == '(')
+ if (pos >= 0 && s[pos] == '(')
{
--pos;
}
@@ -1236,11 +1235,11 @@ static std::string stripCopySuffix(const std::string &s)
{
--pos;
}
- return s.substr(0,pos+1);
+ return s.substr(0, pos + 1);
}
return s;
}
-uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId)
+uint64_t Storage::CopyPluginPreset(const std::string &pluginUri, uint64_t presetId)
{
PluginPresets presets = this->GetPluginPresets(pluginUri);
size_t pos = presets.Find(presetId);
@@ -1266,17 +1265,14 @@ uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId
}
}
t.label_ = name;
- presets.presets_.insert(presets.presets_.begin()+pos+1,t);
- SavePluginPresets(presets.pluginUri_,presets);
+ presets.presets_.insert(presets.presets_.begin() + pos + 1, t);
+ SavePluginPresets(presets.pluginUri_, presets);
return t.instanceId_;
-
-
-
}
-std::map Storage::GetFavorites() const
+std::map Storage::GetFavorites() const
{
- std::map result;
+ std::map result;
std::filesystem::path fileName = this->dataRoot / "favorites.json";
if (!std::filesystem::exists(fileName))
@@ -1291,10 +1287,9 @@ std::map Storage::GetFavorites() const
reader.read(&result);
}
return result;
-
-
}
-void Storage::SetFavorites(const std::map&favorites) {
+void Storage::SetFavorites(const std::map &favorites)
+{
std::filesystem::path fileName = this->dataRoot / "favorites.json";
std::ofstream f;
f.open(fileName);
@@ -1305,15 +1300,45 @@ void Storage::SetFavorites(const std::map&favorites) {
}
}
-
+pipedal::JackServerSettings Storage::GetJackServerSettings()
+{
+ JackServerSettings result;
+#if JACK_HOST
+ result.Initialize();
+#else
+ std::filesystem::path fileName = this->dataRoot / "AudioConfig.json";
+ std::ifstream f;
+ f.open(fileName);
+ if (f.is_open())
+ {
+ json_reader reader(f);
+ reader.read(&result);
+ }
+#endif
+ return result;
+}
+void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfiguration)
+{
+#if JACK_HOST
+#error IMPLEMENT ME
+#else
+ std::filesystem::path fileName = this->dataRoot / "AudioConfig.json";
+ std::ofstream f;
+ f.open(fileName);
+ if (f.is_open())
+ {
+ json_writer writer(f);
+ writer.write(jackConfiguration);
+ }
+#endif
+}
JSON_MAP_BEGIN(UserSettings)
- JSON_MAP_REFERENCE(UserSettings,governor)
- JSON_MAP_REFERENCE(UserSettings,showStatusMonitor)
+JSON_MAP_REFERENCE(UserSettings, governor)
+JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
JSON_MAP_END()
JSON_MAP_BEGIN(CurrentPreset)
- JSON_MAP_REFERENCE(CurrentPreset,modified)
- JSON_MAP_REFERENCE(CurrentPreset,preset)
+JSON_MAP_REFERENCE(CurrentPreset, modified)
+JSON_MAP_REFERENCE(CurrentPreset, preset)
JSON_MAP_END()
-
diff --git a/src/Storage.hpp b/src/Storage.hpp
index 24032c7..af30a65 100644
--- a/src/Storage.hpp
+++ b/src/Storage.hpp
@@ -25,6 +25,7 @@
#include "PluginPreset.hpp"
#include "Banks.hpp"
#include "JackConfiguration.hpp"
+#include "JackServerSettings.hpp"
#include "WifiConfigSettings.hpp"
#include "WifiDirectConfigSettings.hpp"
#include