Files
op-pedal/src/main.cpp
T
shawn 01584f50da Initialize MixerEngine on startup (BB-6)
Add MixerEngine creation and wiring after model.Load() in main.cpp.
The mixer engine now gets created with current audio settings (sample rate,
buffer size from JackServerSettings), activated, and attached to the model.
This enables WebSocket mixer commands (mixerGetState, mixerAddChannel,
mixerAddBus, etc.) to work through /pipedal endpoint.

Missing initialization was the root cause of 'Invalid format' errors when
sending mixer commands via WebSocket.
2026-06-20 16:15:12 -04:00

514 lines
15 KiB
C++

// Copyright (c) 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 "util.hpp"
#include "Locale.hpp"
#include "AudioConfig.hpp"
#include "WebServer.hpp"
#include <iostream>
#include "Lv2Log.hpp"
#include "ServiceConfiguration.hpp"
#include "AvahiService.hpp"
#include "WebServerConfig.hpp"
#include <execinfo.h>
#include "PiPedalSocket.hpp"
#include "MixerEngine.hpp"
#include "PluginHost.hpp"
#include <boost/system/error_code.hpp>
#include <filesystem>
#include "PiPedalConfiguration.hpp"
#include "AdminClient.hpp"
#include "CommandLineParser.hpp"
#include "Lv2SystemdLogger.hpp"
#include <sys/stat.h>
#include <boost/asio.hpp>
#include "HtmlHelper.hpp"
#include <thread>
#include <atomic>
#include "AlsaDriver.hpp"
#include <signal.h>
#include <semaphore.h>
#include "SchedulerPriority.hpp"
#include "AudioFiles.hpp"
#include <systemd/sd-daemon.h>
#include "AlsaSequencer.hpp"
using namespace pipedal;
#ifdef __ARM_ARCH_ISA_A64
#define AARCH64
#endif
class application_category : public boost::system::error_category
{
public:
const char *name() const noexcept { return "PiPedal"; }
std::string message(int ev) const { return "error message"; }
};
static std::atomic<bool> g_SigBreak = false;
static std::atomic<bool> g_restart = false;
void sig_handler(int signo)
{
// we're using sig_wait. No need to do anything.
}
void throwSystemError(int error)
{
}
static bool isJackServiceRunning()
{
// look for the jack shmem .
std::filesystem::path path = "/dev/shm/jack_default_0";
return std::filesystem::exists(path);
}
#if 0 && ENABLE_BACKTRACE
// void segvHandler(int sig)
// {
// void *array[10];
// // Get void*'s for all entries on the stack
// size_t size;
// size = backtrace(array, 10);
// // Print out all the frames to stderr
// const char *message = "Error: SEGV signal received.\n";
// auto _ = write(STDERR_FILENO, message, strlen(message));
// backtrace_symbols_fd(array + 2, size - 2, STDERR_FILENO);
// _exit(EXIT_FAILURE);
// }
// static void EnableBacktrace()
// {
// signal(SIGSEGV, segvHandler);
// }
#endif
#define ENABLE_SEGV_DEBUG 0
#define ENABLE_SIGXCPU_DEBUG 1
#if ENABLE_SEGV_DEBUG
void debug_segvHandler(int sig)
{
// Print out all the frames to stderr
const char *message = "Error: SEGV signal received.\n";
auto _ = write(STDERR_FILENO, message, strlen(message));
flush(STD_ERR_FILENO);
_exit(EXIT_FAILURE);
}
static void EnableDebugSevHandler()
{
signal(SIGSEGV, debug_segvHandler);
signal(SIGILL, debug_segvHandler);
signal(SIGKILL, debug_segvHandler);
}
#endif
#if ENABLE_SIGXCPU_DEBUG
void debug_XSigXcpuHandler(int sig)
{
}
static void EnableSigXcpuHandler()
{
signal(SIGXCPU, debug_XSigXcpuHandler);
}
#endif
static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result)
{
if (strLogLevel == "debug")
{
*result = LogLevel::Debug;
return true;
}
if (strLogLevel == "info")
{
*result = LogLevel::Info;
return true;
}
if (strLogLevel == "warning")
{
*result = LogLevel::Warning;
return true;
}
if (strLogLevel == "error")
{
*result = LogLevel::Error;
return true;
}
*result = LogLevel::Info;
return false;
}
int main(int argc, char *argv[])
{
#ifndef WIN32
umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction.
#endif
#if ENABLE_BACKTRACE
EnableBacktrace();
#endif
// Check command line arguments.
bool help = false;
bool error = false;
bool systemd = false;
bool testExtraDevice = false;
std::string logLevel;
std::string portOption;
std::string driverOption = "alsa";
CommandLineParser parser;
parser.AddOption("-h", &help);
parser.AddOption("--help", &help);
parser.AddOption("-systemd", &systemd);
parser.AddOption("-log-level", &logLevel);
parser.AddOption("-port", &portOption);
parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect)
parser.AddOption("--driver", &driverOption);
try
{
parser.Parse(argc, (const char **)argv);
if (parser.Arguments().size() > 2)
{
throw PiPedalException("Too many arguments.");
}
if (parser.Arguments().size() == 0)
{
throw PiPedalException("<doc_root> not provided.");
}
if (help || parser.Arguments().size() == 0)
{
std::cout << "pipedald - Pipedal web socket server.\n"
"Copyright (c) 2022-2024 Robin Davies.\n"
"\n";
}
}
catch (const std::exception &e)
{
std::cout << "Error: " << e.what() << "\n\n";
error = true;
help = true;
}
if (help)
{
std::cout << "Usage: pipedald <doc_root> [<web_root>] [options...]\n\n"
<< "Options:\n"
<< " -systemd: Log to systemd journals instead of to the console.\n"
<< " -port: Port to listen on e.g. 80, or 0.0.0.0:80\n"
<< " -log-level: (debug|info|warning|error)\n"
<< " --driver: Audio driver (alsa|pipewire, default: alsa)\n"
<< "Example:\n"
<< " pipedald /etc/pipedal/config /etc/pipedal/react -port 80 \n"
"\n"
"Description:\n\n"
" Configuration is read from <doc_root>/config.json\n"
"\n"
" If <web_root> is not provided, pipedal will serve from <doc_root>\n"
"\n"
" While debugging, bind the port to 0.0.0.0:8080, and connect to the default React\n"
" server that's provided when you run 'npm run start' in 'react/src'. By default, the\n"
" React debug server will connect to the socket server on 0.0.0.0:8080.\n\n";
return error ? EXIT_FAILURE : EXIT_SUCCESS;
}
if (systemd)
{
Lv2Log::set_logger(MakeLv2SystemdLogger());
}
SetThreadName("main");
std::filesystem::path doc_root = parser.Arguments()[0];
std::filesystem::path web_root = doc_root;
if (parser.Arguments().size() >= 2)
{
web_root = parser.Arguments()[1];
}
PiPedalConfiguration configuration;
try
{
configuration.Load(doc_root, web_root);
}
catch (const std::exception &e)
{
std::stringstream s;
s << "Unable to read configuration from '" << (doc_root / "config.json") << "'. (" << e.what() << ")";
Lv2Log::error(s.str());
return EXIT_SUCCESS; // indicate to systemd that we don't want a restart.
}
Lv2Log::log_level(configuration.GetLogLevel());
if (logLevel.length() != 0)
{
LogLevel lv2LogLevel;
if (TryGetLogLevel(logLevel, &lv2LogLevel))
{
Lv2Log::log_level(lv2LogLevel);
}
else
{
Lv2Log::error(SS("Invalid log level: " << logLevel));
return EXIT_SUCCESS; // indicate to systemd that we don't want a restart.
}
}
if (portOption.length() != 0)
{
configuration.SetSocketServerEndpoint(portOption);
}
// clean up orphaned temporary files.
const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp";
if (!webTempDirectory.empty())
{
try
{
std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging.
std::filesystem::create_directories(webTempDirectory);
}
catch (const std::exception e)
{
Lv2Log::warning(e.what());
}
}
// configure AudiDirectoryInfo to use the correct directories.
AudioDirectoryInfo::SetResourceDirectory(configuration.GetWebRoot());
AudioDirectoryInfo::SetTemporaryDirectory(webTempDirectory / "audiofiles");
uint16_t port;
std::shared_ptr<WebServer> server;
try
{
// (web server threads inherit the main thread priority)
SetThreadPriority(SchedulerPriority::WebServerThread);
auto const address = boost::asio::ip::make_address(configuration.GetSocketServerAddress());
port = static_cast<uint16_t>(configuration.GetSocketServerPort());
auto const threads = std::max<int>(1, configuration.GetThreads());
server = WebServer::create(
address, port, web_root.c_str(), threads, configuration.GetMaxUploadSize());
Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads);
server->SetLogHttpRequests(configuration.LogHttpRequests());
}
catch (const std::exception &e)
{
std::stringstream s;
s << "Fatal error: " << e.what() << std::endl;
Lv2Log::error(s.str());
return EXIT_SUCCESS; // indiate to systemd that we don't want a restart.
}
try
{
{
{
auto locale = Locale::GetInstance();
Lv2Log::info(SS("Locale: " << locale->CurrentLocale()));
try
{
auto collator = locale->GetCollator();
}
catch (std::exception &e)
{
Lv2Log::error(e.what());
return EXIT_SUCCESS; // tell systemd not to auto-restart.
}
}
// only accept signals on the main thread.
// block now so that threads spawned by model inherit the block mask.
int sig;
sigset_t sigSet;
int s;
sigemptyset(&sigSet);
sigaddset(&sigSet, SIGINT);
sigaddset(&sigSet, SIGTERM);
sigaddset(&sigSet, SIGUSR1);
// block these signasl for all threads.
s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL);
if (s != 0)
{
throw std::logic_error("pthread_sigmask failed.");
}
// Clear any pending signals before waiting
struct timespec timeout = {0, 0};
while (sigtimedwait(&sigSet, NULL, &timeout) > 0)
{
// Consume any pending signals
}
#if ENABLE_SEGV_DEBUG
EnableDebugSevHandler();
#endif
#if ENABLE_SIGXCPU_DEBUG
EnableSigXcpuHandler();
#endif
PiPedalModel model;
model.SetNetworkChangedListener(
[&server]() mutable
{
if (server)
{
server->DisplayIpAddresses();
}
});
model.SetRestartListener(
[]()
{
g_restart = true;
raise(SIGTERM); // throws an exception under gdb, but correctly restarts the service when running live.
});
model.Init(configuration);
model.SetDriverType(driverOption);
// Get heavy IO out of the way before letting dependent (Jack/ALSA) services run.
model.LoadLv2PluginInfo();
if (systemd)
{
// Tell systemd we're done.
sd_notifyf(0, "READY=1\n"
"MAINPID=%lu",
(unsigned long)getpid());
}
model.WaitForAudioDeviceToComeOnline();
#if JACK_HOST
if (systemd)
{
if (!isJackServiceRunning())
{
Lv2Log::info("Waiting for Jack service.");
// wait up to 15 seconds for the jack service to come online.
for (int i = 0; i < 15; ++i)
{
// use the time to prepopulate ALSA device cache before jack
// opens the device and we can't read properties.
model.GetAlsaDevices();
sleep(1);
if (isJackServiceRunning())
{
break;
}
}
}
if (isJackServiceRunning())
{
Lv2Log::info("Found Jack service.");
sleep(3); // jack needs a little time to get up to speed.
}
else
{
Lv2Log::info("Jack service not started.");
}
model.GetAlsaDevices();
}
#endif
model.Load();
// Initialize Band-in-a-Box mixer engine
{
auto mixerEngine = std::make_shared<MixerEngine>();
// Configure with current audio settings
auto jackSettings = model.GetJackServerSettings();
mixerEngine->setSampleRate((uint32_t)jackSettings.GetSampleRate());
mixerEngine->setMaxBufferSize((size_t)jackSettings.GetBufferSize());
mixerEngine->Activate();
model.SetMixerEngine(mixerEngine);
Lv2Log::info("MixerEngine initialized (BB-6)");
}
auto pipedalSocketFactory = MakePiPedalSocketFactory(model);
server->AddSocketFactory(pipedalSocketFactory);
ConfigureWebServer(*server, model, port, configuration.GetMaxUploadSize());
{
server->RunInBackground(-1);
model.StartHotspotMonitoring();
{
sigwait(&sigSet, &sig);
if (systemd)
{
sd_notify(0, "STOPPING=1");
}
}
}
Lv2Log::info("Closing audio session.");
server->StopListening(); // prevents premature reconnect attempts while we're shutting down clients in an orderly manner.
model.Close();
Lv2Log::info("Stopping web server.");
server->ShutDown(5000);
server->Join();
}
Lv2Log::info("Shutdown complete.");
FreeAlsaGlobals();
if (systemd)
{
sd_notify(0, "STOPPING=1");
}
if (g_restart)
return EXIT_FAILURE; // indicate to systemd that we want a restart.
}
catch (const std::exception &e)
{
Lv2Log::error(e.what());
return EXIT_FAILURE;
}
return EXIT_SUCCESS; // only exit in response to a signal.
}