Toob File Player checkpoint.
This commit is contained in:
Vendored
+6
-2
@@ -16,7 +16,9 @@
|
|||||||
"cStandard": "c17",
|
"cStandard": "c17",
|
||||||
"cppStandard": "c++20",
|
"cppStandard": "c++20",
|
||||||
"intelliSenseMode": "linux-gcc-arm64",
|
"intelliSenseMode": "linux-gcc-arm64",
|
||||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
"compileCommands": [
|
||||||
|
"${workspaceFolder}/build/compile_commands.json"
|
||||||
|
],
|
||||||
"configurationProvider": "ms-vscode.cmake-tools"
|
"configurationProvider": "ms-vscode.cmake-tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,7 +35,9 @@
|
|||||||
"cStandard": "c17",
|
"cStandard": "c17",
|
||||||
"cppStandard": "c++20",
|
"cppStandard": "c++20",
|
||||||
"intelliSenseMode": "linux-gcc-x64",
|
"intelliSenseMode": "linux-gcc-x64",
|
||||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
"compileCommands": [
|
||||||
|
"${workspaceFolder}/build/compile_commands.json"
|
||||||
|
],
|
||||||
"configurationProvider": "ms-vscode.cmake-tools",
|
"configurationProvider": "ms-vscode.cmake-tools",
|
||||||
"mergeConfigurations": true
|
"mergeConfigurations": true
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-1
@@ -22,7 +22,6 @@
|
|||||||
// Resolved by CMake Tools:
|
// Resolved by CMake Tools:
|
||||||
"program": "${command:cmake.launchTargetPath}",
|
"program": "${command:cmake.launchTargetPath}",
|
||||||
"args": [
|
"args": [
|
||||||
"--no-sudo"
|
|
||||||
],
|
],
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -102,7 +102,8 @@
|
|||||||
"format": "cpp",
|
"format": "cpp",
|
||||||
"locale": "cpp",
|
"locale": "cpp",
|
||||||
"stdfloat": "cpp",
|
"stdfloat": "cpp",
|
||||||
"text_encoding": "cpp"
|
"text_encoding": "cpp",
|
||||||
|
"forward_list": "cpp"
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Alsa",
|
"Alsa",
|
||||||
|
|||||||
@@ -1809,6 +1809,9 @@ namespace pipedal
|
|||||||
{
|
{
|
||||||
inputSysexBuffer.resize(1024);
|
inputSysexBuffer.resize(1024);
|
||||||
}
|
}
|
||||||
|
~AlsaMidiDeviceImpl() {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
void Open(const AlsaMidiDeviceInfo &device)
|
void Open(const AlsaMidiDeviceInfo &device)
|
||||||
{
|
{
|
||||||
runningStatus = 0;
|
runningStatus = 0;
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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 "AudioFileMetadata.hpp"
|
||||||
|
#include <json.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <json_variant.hpp>
|
||||||
|
#include "LRUCache.hpp"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
namespace chrono = std::chrono;
|
||||||
|
|
||||||
|
|
||||||
|
// Safely encode a filename for use in a shell command
|
||||||
|
static std::string shell_escape_filename(const std::string& filename) {
|
||||||
|
std::string escaped;
|
||||||
|
escaped.reserve(filename.size() + 2); // Reserve space for quotes and content
|
||||||
|
|
||||||
|
// Wrap the filename in single quotes
|
||||||
|
escaped += '\'';
|
||||||
|
|
||||||
|
for (char c : filename) {
|
||||||
|
// Escape single quotes by closing the quote, adding escaped quote, and reopening
|
||||||
|
if (c == '\'') {
|
||||||
|
escaped += "'\\''";
|
||||||
|
} else {
|
||||||
|
escaped += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escaped += '\'';
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string readPipeToString(FILE* file) {
|
||||||
|
std::string content;
|
||||||
|
char buffer[4096];
|
||||||
|
size_t bytesRead;
|
||||||
|
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
|
||||||
|
content.append(buffer, bytesRead);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string GetJsonMetadata(const std::filesystem::path &path)
|
||||||
|
{
|
||||||
|
/* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json
|
||||||
|
~/filename.flac */
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json "
|
||||||
|
<< shell_escape_filename(path.string())
|
||||||
|
<< " 2>/dev/null";
|
||||||
|
|
||||||
|
FILE *output = popen(ss.str().c_str(),"r");
|
||||||
|
if(output == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Failed to process file.");
|
||||||
|
}
|
||||||
|
std::string result;
|
||||||
|
try {
|
||||||
|
result = readPipeToString(output);
|
||||||
|
} catch (const std::exception&e)
|
||||||
|
{
|
||||||
|
pclose(output);
|
||||||
|
throw std::runtime_error(e.what());
|
||||||
|
}
|
||||||
|
int retcode = pclose(output);
|
||||||
|
if (retcode != 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float MetadataFloat(const json_variant&vt, float defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (vt.is_string())
|
||||||
|
{
|
||||||
|
std::string s = vt.as_string();
|
||||||
|
std::stringstream ss(s);
|
||||||
|
float result = defaultValue;
|
||||||
|
ss >> result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string MetadataString(const json_variant&o,const std::vector<std::string>& names) {
|
||||||
|
for (const auto&name: names)
|
||||||
|
{
|
||||||
|
auto result = (*o.as_object())[name];
|
||||||
|
if (result.is_string())
|
||||||
|
{
|
||||||
|
return result.as_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const std::string json = GetJsonMetadata(file);
|
||||||
|
|
||||||
|
json_variant vt;
|
||||||
|
|
||||||
|
std::stringstream ss(json);
|
||||||
|
json_reader reader(ss);
|
||||||
|
|
||||||
|
reader.read(&vt);
|
||||||
|
|
||||||
|
auto top = vt.as_object();
|
||||||
|
auto format = top->at("format").as_object();
|
||||||
|
|
||||||
|
this->duration_ = MetadataFloat(format->at("duration"),0);
|
||||||
|
|
||||||
|
auto tags = (*format)["tags"];
|
||||||
|
if (tags.is_object())
|
||||||
|
{
|
||||||
|
this->album_ = MetadataString(tags,{"ALBUM","album"});
|
||||||
|
this->artist_ = MetadataString(tags,{"ARTIST","artist"});
|
||||||
|
this->albumArtist_ = MetadataString(tags,{"ALBUM ARTIST","album_artist","album artist"});
|
||||||
|
this->title_ = MetadataString(tags,{"TITLE","title"});
|
||||||
|
this->date_ = MetadataString(tags,{"DATE","date"});
|
||||||
|
this->year_ = MetadataString(tags,{"YEAR","year"});
|
||||||
|
this->track_ = MetadataString(tags,{"track","TRACK",});
|
||||||
|
this->disc_ = MetadataString(tags,{"disc","DISC"});
|
||||||
|
|
||||||
|
if (title_ == "") {
|
||||||
|
this->title_ = file.stem();
|
||||||
|
}
|
||||||
|
this->totalTracks_ = MetadataString(tags,{"TOTALTRACKS"});
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
std::string title = file.stem();
|
||||||
|
this->title_ = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class AudioCacheKey {
|
||||||
|
public:
|
||||||
|
AudioCacheKey(const std::filesystem::path &path) {
|
||||||
|
this->path = path.string();
|
||||||
|
this->lastWrite = std::filesystem::last_write_time(path);
|
||||||
|
|
||||||
|
}
|
||||||
|
// Equality operator
|
||||||
|
bool operator==(const AudioCacheKey& other) const {
|
||||||
|
return path == other.path && lastWrite == other.lastWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string path;
|
||||||
|
fs::file_time_type lastWrite;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
template <>
|
||||||
|
struct hash<AudioCacheKey> {
|
||||||
|
|
||||||
|
|
||||||
|
size_t operator()(const AudioCacheKey& key) const {
|
||||||
|
size_t path_hash = hash<std::string>{}(key.path);
|
||||||
|
size_t time_hash = hash<uintmax_t>{}(
|
||||||
|
duration_cast<std::chrono::nanoseconds>(key.lastWrite.time_since_epoch()).count()
|
||||||
|
);
|
||||||
|
// Combine hashes (boost::hash_combine approach)
|
||||||
|
return path_hash ^ (time_hash + 0x9e3779b9 + (path_hash << 6) + (path_hash >> 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static LRUCache<AudioCacheKey,std::string> metadataCache {500};
|
||||||
|
static std::mutex metadataCacheMutex;
|
||||||
|
|
||||||
|
std::string pipedal::GetAudioFileMetadataString(const std::filesystem::path &path)
|
||||||
|
{
|
||||||
|
std::lock_guard lock { metadataCacheMutex};
|
||||||
|
AudioCacheKey key { path};
|
||||||
|
std::string result;
|
||||||
|
if (metadataCache.get(key,result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
auto metadata = AudioFileMetadata(path);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
json_writer writer(ss);
|
||||||
|
writer.write(metadata);
|
||||||
|
result = ss.str();
|
||||||
|
|
||||||
|
metadataCache.put(key,result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(AudioFileMetadata)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, duration)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, title)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, track)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, album)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, disc)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, artist)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, albumArtist)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, totalTracks)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, date)
|
||||||
|
JSON_MAP_REFERENCE(AudioFileMetadata, year)
|
||||||
|
|
||||||
|
JSON_MAP_END()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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 <string>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "json.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
|
{
|
||||||
|
|
||||||
|
#define GETTER_SETTER_REF(name) \
|
||||||
|
const decltype(name##_) &name() const { return name##_; } \
|
||||||
|
void name(const decltype(name##_) &value) { name##_ = value; }
|
||||||
|
|
||||||
|
#define GETTER_SETTER_VEC(name) \
|
||||||
|
decltype(name##_) &name() { return name##_; } \
|
||||||
|
const decltype(name##_) &name() const { return name##_; }
|
||||||
|
|
||||||
|
// void name(decltype(const name##_) &value) { name##_ = value; }
|
||||||
|
// void name(decltype(const name##_) &&value) { name##_ = std::move(value); }
|
||||||
|
|
||||||
|
#define GETTER_SETTER(name) \
|
||||||
|
decltype(name##_) name() const { return name##_; } \
|
||||||
|
void name(decltype(name##_) value) { name##_ = value; }
|
||||||
|
|
||||||
|
class AudioFileMetadata
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
float duration_ = 0;
|
||||||
|
std::string title_;
|
||||||
|
std::string track_;
|
||||||
|
std::string album_;
|
||||||
|
std::string disc_;
|
||||||
|
std::string artist_;
|
||||||
|
std::string albumArtist_;
|
||||||
|
std::string totalTracks_;
|
||||||
|
std::string date_;
|
||||||
|
std::string year_;
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
AudioFileMetadata(const std::filesystem::path &file);
|
||||||
|
|
||||||
|
GETTER_SETTER(duration);
|
||||||
|
GETTER_SETTER_REF(title);
|
||||||
|
GETTER_SETTER_REF(track);
|
||||||
|
GETTER_SETTER_REF(album);
|
||||||
|
GETTER_SETTER_REF(disc);
|
||||||
|
GETTER_SETTER_REF(artist);
|
||||||
|
GETTER_SETTER_REF(albumArtist);
|
||||||
|
|
||||||
|
public:
|
||||||
|
DECLARE_JSON_MAP(AudioFileMetadata);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string GetAudioFileMetadataString(const std::filesystem::path &path);
|
||||||
|
|
||||||
|
#undef GETTER_SETTER
|
||||||
|
#undef GETTER_SETTER_VEC
|
||||||
|
#undef GETTER_SETTER_REF
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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 "AudioFiles.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
|
||||||
|
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class DirectoryList {
|
||||||
|
public:
|
||||||
|
DirectoryList(const fs::path&path);
|
||||||
|
|
||||||
|
const std::vector<std::filesystem::path> &files() {
|
||||||
|
return files_;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> files_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioFileInfoImpl: public AudioFileInfo {
|
||||||
|
public:
|
||||||
|
virtual ~AudioFileInfoImpl() { }
|
||||||
|
protected:
|
||||||
|
virtual std::string GetNextAudioFile(const std::string&path) override;
|
||||||
|
virtual std::string GetPreviousAudioFile(const std::string&path) override;
|
||||||
|
};
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
DirectoryList::DirectoryList(const fs::path&path)
|
||||||
|
{
|
||||||
|
for (const auto&dirEntry: fs::directory_iterator(path))
|
||||||
|
{
|
||||||
|
if (dirEntry.is_regular_file())
|
||||||
|
{
|
||||||
|
fs::path t = dirEntry.path();
|
||||||
|
if (!t.filename().string().starts_with('.'))
|
||||||
|
{
|
||||||
|
files_.push_back(std::move(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AudioFileInfoImpl::GetNextAudioFile(const std::string&path_) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs::path path {path_};
|
||||||
|
if (fs::exists(path) && fs::is_regular_file(path))
|
||||||
|
{
|
||||||
|
DirectoryList directoryList(path.parent_path());
|
||||||
|
size_t index = INVALID_INDEX;
|
||||||
|
auto &files = directoryList.files();
|
||||||
|
for (size_t i= 0; i < files.size(); ++i)
|
||||||
|
{
|
||||||
|
if (files[i] == path)
|
||||||
|
{
|
||||||
|
size_t next = i;
|
||||||
|
if (next == files.size()-1)
|
||||||
|
{
|
||||||
|
next = 0;
|
||||||
|
} else {
|
||||||
|
++next;
|
||||||
|
}
|
||||||
|
return files[next];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (const std::exception&e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
|
||||||
|
}
|
||||||
|
std::string AudioFileInfoImpl::GetPreviousAudioFile(const std::string&path_)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs::path path {path_};
|
||||||
|
if (fs::exists(path) && fs::is_regular_file(path))
|
||||||
|
{
|
||||||
|
DirectoryList directoryList(path.parent_path());
|
||||||
|
size_t index = INVALID_INDEX;
|
||||||
|
auto &files = directoryList.files();
|
||||||
|
for (size_t i= 0; i < files.size(); ++i)
|
||||||
|
{
|
||||||
|
if (files[i] == path)
|
||||||
|
{
|
||||||
|
size_t previous = i;
|
||||||
|
if (previous == 0)
|
||||||
|
{
|
||||||
|
previous = files.size()-1;
|
||||||
|
} else {
|
||||||
|
--previous;
|
||||||
|
}
|
||||||
|
return files[previous];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (const std::exception&e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static AudioFileInfoImpl instance;
|
||||||
|
|
||||||
|
AudioFileInfo&AudioFileInfo::GetInstance()
|
||||||
|
{
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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 <string>
|
||||||
|
|
||||||
|
namespace pipedal {
|
||||||
|
class AudioFileInfo {
|
||||||
|
protected:
|
||||||
|
AudioFileInfo() { }
|
||||||
|
virtual ~AudioFileInfo() {}
|
||||||
|
public:
|
||||||
|
static AudioFileInfo&GetInstance();
|
||||||
|
|
||||||
|
virtual std::string GetNextAudioFile(const std::string&path) = 0;
|
||||||
|
virtual std::string GetPreviousAudioFile(const std::string&path) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-1
@@ -1173,7 +1173,7 @@ private:
|
|||||||
|
|
||||||
if (buffersValid)
|
if (buffersValid)
|
||||||
{
|
{
|
||||||
pedalboard->ProcessParameterRequests(pParameterRequests);
|
pedalboard->ProcessParameterRequests(pParameterRequests,nframes);
|
||||||
|
|
||||||
processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter);
|
processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter);
|
||||||
if (processed)
|
if (processed)
|
||||||
|
|||||||
+10
-4
@@ -69,6 +69,7 @@ namespace pipedal
|
|||||||
|
|
||||||
const char *errorMessage = nullptr;
|
const char *errorMessage = nullptr;
|
||||||
std::string jsonResponse;
|
std::string jsonResponse;
|
||||||
|
int64_t sampleTimeout = 0;
|
||||||
|
|
||||||
RealtimePatchPropertyRequest *pNext = nullptr;
|
RealtimePatchPropertyRequest *pNext = nullptr;
|
||||||
|
|
||||||
@@ -102,13 +103,16 @@ namespace pipedal
|
|||||||
int64_t instanceId_,
|
int64_t instanceId_,
|
||||||
LV2_URID uridUri_,
|
LV2_URID uridUri_,
|
||||||
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
||||||
std::function<void(const std::string &error)> onError_)
|
std::function<void(const std::string &error)> onError_,
|
||||||
|
size_t sampleTimeout)
|
||||||
: onPatchRequestComplete(onPatchRequestcomplete_),
|
: onPatchRequestComplete(onPatchRequestcomplete_),
|
||||||
clientId(clientId_),
|
clientId(clientId_),
|
||||||
instanceId(instanceId_),
|
instanceId(instanceId_),
|
||||||
uridUri(uridUri_),
|
uridUri(uridUri_),
|
||||||
onSuccess(onSuccess_),
|
onSuccess(onSuccess_),
|
||||||
onError(onError_)
|
onError(onError_),
|
||||||
|
sampleTimeout((int64_t)sampleTimeout)
|
||||||
|
|
||||||
{
|
{
|
||||||
requestType = RequestType::PatchGet;
|
requestType = RequestType::PatchGet;
|
||||||
}
|
}
|
||||||
@@ -119,13 +123,15 @@ namespace pipedal
|
|||||||
LV2_URID uridUri_,
|
LV2_URID uridUri_,
|
||||||
LV2_Atom *atomValue,
|
LV2_Atom *atomValue,
|
||||||
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
||||||
std::function<void(const std::string &error)> onError_)
|
std::function<void(const std::string &error)> onError_,
|
||||||
|
size_t sampleTimeout)
|
||||||
: onPatchRequestComplete(onPatchRequestcomplete_),
|
: onPatchRequestComplete(onPatchRequestcomplete_),
|
||||||
clientId(clientId_),
|
clientId(clientId_),
|
||||||
instanceId(instanceId_),
|
instanceId(instanceId_),
|
||||||
uridUri(uridUri_),
|
uridUri(uridUri_),
|
||||||
onSuccess(onSuccess_),
|
onSuccess(onSuccess_),
|
||||||
onError(onError_)
|
onError(onError_),
|
||||||
|
sampleTimeout(sampleTimeout)
|
||||||
{
|
{
|
||||||
requestType = RequestType::PatchSet;
|
requestType = RequestType::PatchSet;
|
||||||
size_t size = atomValue->size + sizeof(LV2_Atom);
|
size_t size = atomValue->size + sizeof(LV2_Atom);
|
||||||
|
|||||||
+12
-3
@@ -12,6 +12,7 @@ set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10
|
|||||||
set(CXX_STANDARD 20)
|
set(CXX_STANDARD 20)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
# FOR FTXUI
|
# FOR FTXUI
|
||||||
@@ -45,6 +46,9 @@ include(FindPkgConfig)
|
|||||||
|
|
||||||
find_package(sdbus-c++ REQUIRED)
|
find_package(sdbus-c++ REQUIRED)
|
||||||
|
|
||||||
|
find_package(SQLiteCpp REQUIRED)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
find_package(ICU REQUIRED COMPONENTS uc i18n )
|
find_package(ICU REQUIRED COMPONENTS uc i18n )
|
||||||
message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}")
|
message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}")
|
||||||
@@ -192,6 +196,9 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
set (PIPEDAL_SOURCES
|
||||||
|
AudioFiles.cpp AudioFiles.hpp
|
||||||
|
AudioFileMetadata.cpp AudioFileMetadata.hpp
|
||||||
|
LRUCache.hpp
|
||||||
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
|
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
|
||||||
SchedulerPriority.hpp SchedulerPriority.cpp
|
SchedulerPriority.hpp SchedulerPriority.cpp
|
||||||
ModFileTypes.cpp ModFileTypes.hpp
|
ModFileTypes.cpp ModFileTypes.hpp
|
||||||
@@ -304,6 +311,7 @@ set (PIPEDAL_INCLUDES
|
|||||||
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
|
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
|
||||||
${VST3_INCLUDES}
|
${VST3_INCLUDES}
|
||||||
${WEBSOCKETPP_INCLUDE_DIRS}
|
${WEBSOCKETPP_INCLUDE_DIRS}
|
||||||
|
${SQLite3_INCLUDE_DIRS}
|
||||||
.
|
.
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -314,7 +322,7 @@ set(PIPEDAL_LIBS libpipedald zip
|
|||||||
${VST3_LIBRARIES}
|
${VST3_LIBRARIES}
|
||||||
${LILV_0_LIBRARIES}
|
${LILV_0_LIBRARIES}
|
||||||
# ${JACK_LIBRARIES} - pending delete for JACK support.
|
# ${JACK_LIBRARIES} - pending delete for JACK support.
|
||||||
|
${SQLite3_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -383,8 +391,9 @@ add_executable(AuxInTest
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_executable(pipedaltest
|
add_executable(pipedaltest
|
||||||
testMain.cpp
|
testMain.cpp
|
||||||
|
|
||||||
|
LRUCacheTest.cpp
|
||||||
ModFileTypesTest.cpp
|
ModFileTypesTest.cpp
|
||||||
jsonTest.cpp
|
jsonTest.cpp
|
||||||
UpdaterTest.cpp
|
UpdaterTest.cpp
|
||||||
@@ -822,7 +831,7 @@ add_custom_command(OUTPUT ${REACT_NOTICES_FILE}
|
|||||||
--output ${REACT_NOTICES_FILE}
|
--output ${REACT_NOTICES_FILE}
|
||||||
--projectCopyright ${DEBIAN_COPYRIGHT_FILE}
|
--projectCopyright ${DEBIAN_COPYRIGHT_FILE}
|
||||||
liblilv-0-0 ${BOOST_COPYRIGHT_DIR} lv2-dev libsdbus-c++-dev librsvg2-2 libpango-1.0-0 libx11-6 libxrandr2
|
liblilv-0-0 ${BOOST_COPYRIGHT_DIR} lv2-dev libsdbus-c++-dev librsvg2-2 libpango-1.0-0 libx11-6 libxrandr2
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$<TARGET_FILE:processcopyrights>"
|
DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$<TARGET_FILE:processcopyrights>"
|
||||||
COMMENT "Updating copyright notices."
|
COMMENT "Updating copyright notices."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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 <unordered_map>
|
||||||
|
#include <list>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
template <typename KEY, typename VALUE>
|
||||||
|
class LRUCache {
|
||||||
|
public:
|
||||||
|
struct CacheNode {
|
||||||
|
KEY key;
|
||||||
|
VALUE value;
|
||||||
|
CacheNode(KEY k, VALUE v) : key(k), value(v) {}
|
||||||
|
};
|
||||||
|
private:
|
||||||
|
size_t capacity;
|
||||||
|
std::list<CacheNode> cache_list; // Doubly-linked list for LRU order
|
||||||
|
std::unordered_map<KEY, typename std::list<CacheNode>::iterator> cache_map; // Map key to list iterator
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LRUCache(size_t cap) : capacity(cap) {
|
||||||
|
if (cap == 0) {
|
||||||
|
throw std::invalid_argument("Cache capacity must be greater than 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get value by key, return false if not found
|
||||||
|
bool get(const KEY& key, VALUE& value) {
|
||||||
|
auto it = cache_map.find(key);
|
||||||
|
if (it == cache_map.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Move to front (most recently used)
|
||||||
|
cache_list.splice(cache_list.begin(), cache_list, it->second);
|
||||||
|
value = it->second->value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put key-value pair into cache
|
||||||
|
void put(const KEY& key, const VALUE& value) {
|
||||||
|
auto it = cache_map.find(key);
|
||||||
|
if (it != cache_map.end()) {
|
||||||
|
// Update existing key
|
||||||
|
cache_list.splice(cache_list.begin(), cache_list, it->second);
|
||||||
|
it->second->value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new key-value pair
|
||||||
|
cache_list.emplace_front(key, value);
|
||||||
|
cache_map[key] = cache_list.begin();
|
||||||
|
|
||||||
|
// Evict least recently used if over capacity
|
||||||
|
if (cache_map.size() > capacity) {
|
||||||
|
auto lru = cache_list.back();
|
||||||
|
cache_map.erase(lru.key);
|
||||||
|
cache_list.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if key exists
|
||||||
|
bool contains(const KEY& key) const {
|
||||||
|
return cache_map.find(key) != cache_map.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current size
|
||||||
|
size_t size() const {
|
||||||
|
return cache_map.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get capacity
|
||||||
|
size_t get_capacity() const {
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
const std::list<CacheNode> &cache() const {
|
||||||
|
return cache_list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright (c) 2025 Robin 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 "LRUCache.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
TEST_CASE( "LRUCache test", "[lrucache]" ) {
|
||||||
|
|
||||||
|
LRUCache<int,int> lruCache(5);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
lruCache.put(i,i);
|
||||||
|
}
|
||||||
|
REQUIRE(lruCache.cache().size() == 5);
|
||||||
|
|
||||||
|
REQUIRE(lruCache.cache().begin()->value == 9);
|
||||||
|
|
||||||
|
int value;
|
||||||
|
REQUIRE(lruCache.get(5,value) == true);
|
||||||
|
REQUIRE(value == 5);
|
||||||
|
REQUIRE(lruCache.cache().begin()->value == 5);
|
||||||
|
|
||||||
|
cout << '[';
|
||||||
|
for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||||
|
{
|
||||||
|
cout << it->value << ',';
|
||||||
|
}
|
||||||
|
cout << ']' << endl;
|
||||||
|
for (auto it = ++lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||||
|
{
|
||||||
|
REQUIRE(it->value != 5);
|
||||||
|
}
|
||||||
|
lruCache.put(1,1);
|
||||||
|
cout << '[';
|
||||||
|
for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||||
|
{
|
||||||
|
cout << it->value << ',';
|
||||||
|
}
|
||||||
|
cout << ']' << endl;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1116,3 +1116,4 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
|
|||||||
{
|
{
|
||||||
mainThreadPathProperties[propertyUri] = jsonAtom;
|
mainThreadPathProperties[propertyUri] = jsonAtom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ namespace pipedal
|
|||||||
virtual void OnLogDebug(const char*message);
|
virtual void OnLogDebug(const char*message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
std::unordered_map<std::string,int> controlIndex;
|
std::unordered_map<std::string,int> controlIndex;
|
||||||
|
|
||||||
FileBrowserFilesFeature fileBrowserFilesFeature;
|
FileBrowserFilesFeature fileBrowserFilesFeature;
|
||||||
|
|||||||
@@ -562,11 +562,13 @@ void Lv2Pedalboard::GatherPathPatchProperties(IPatchWriterCallback *cbPatchWrite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests)
|
void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime)
|
||||||
{
|
{
|
||||||
while (pParameterRequests != nullptr)
|
while (pParameterRequests != nullptr)
|
||||||
{
|
{
|
||||||
|
pParameterRequests->sampleTimeout -= samplesThisTime;
|
||||||
IEffect *pEffect = this->GetEffect(pParameterRequests->instanceId);
|
IEffect *pEffect = this->GetEffect(pParameterRequests->instanceId);
|
||||||
|
|
||||||
if (pEffect == nullptr)
|
if (pEffect == nullptr)
|
||||||
{
|
{
|
||||||
pParameterRequests->errorMessage = "No such effect.";
|
pParameterRequests->errorMessage = "No such effect.";
|
||||||
@@ -574,8 +576,12 @@ void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pPara
|
|||||||
else if (pEffect->IsVst3())
|
else if (pEffect->IsVst3())
|
||||||
{
|
{
|
||||||
pParameterRequests->errorMessage = "Not supported for VST3 plugins";
|
pParameterRequests->errorMessage = "Not supported for VST3 plugins";
|
||||||
|
} else if (pParameterRequests->sampleTimeout < 0)
|
||||||
|
{
|
||||||
|
pParameterRequests->sampleTimeout = 0;
|
||||||
|
pParameterRequests->errorMessage = "Timed out.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (pEffect->IsLv2Effect())
|
if (pEffect->IsLv2Effect())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ namespace pipedal
|
|||||||
|
|
||||||
void ResetAtomBuffers();
|
void ResetAtomBuffers();
|
||||||
|
|
||||||
void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests);
|
void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime);
|
||||||
void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests);
|
void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests);
|
||||||
void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter);
|
void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter);
|
||||||
|
|
||||||
|
|||||||
+35
-18
@@ -1615,19 +1615,30 @@ void PiPedalModel::SendSetPatchProperty(
|
|||||||
{
|
{
|
||||||
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
if (!audioHost)
|
||||||
|
{
|
||||||
|
onError("Audio not running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// save the property to the preset (currently used to reconstruct snapshots only)
|
// save the property to the preset (currently used to reconstruct snapshots only)
|
||||||
PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId);
|
PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId);
|
||||||
if (pedalboardItem)
|
if (pedalboardItem && value.is_string())
|
||||||
{
|
{
|
||||||
json_variant abstractPath = pluginHost.AbstractPath(value);
|
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
|
||||||
std::ostringstream ss;
|
auto pipedalUi = pluginInfo->piPedalUI();
|
||||||
json_writer writer(ss);
|
auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
|
||||||
writer.write(abstractPath);
|
if (fileProperty && value.is_string()) {
|
||||||
std::string atomString = ss.str();
|
|
||||||
pedalboardItem->pathProperties_[propertyUri] = atomString;
|
json_variant abstractPath = pluginHost.AbstractPath(value);
|
||||||
|
std::ostringstream ss;
|
||||||
|
json_writer writer(ss);
|
||||||
|
writer.write(abstractPath);
|
||||||
|
std::string atomString = ss.str();
|
||||||
|
pedalboardItem->pathProperties_[propertyUri] = atomString;
|
||||||
|
}
|
||||||
|
this->SetPresetChanged(clientId, true);
|
||||||
}
|
}
|
||||||
this->SetPresetChanged(clientId, true);
|
|
||||||
LV2_Atom *atomValue = atomConverter.ToAtom(value);
|
LV2_Atom *atomValue = atomConverter.ToAtom(value);
|
||||||
|
|
||||||
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
|
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
|
||||||
@@ -1657,7 +1668,7 @@ void PiPedalModel::SendSetPatchProperty(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (pParameter->onSuccess)
|
if (onSuccess)
|
||||||
{
|
{
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}
|
}
|
||||||
@@ -1668,10 +1679,12 @@ void PiPedalModel::SendSetPatchProperty(
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
|
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
|
||||||
|
size_t sampleTimeout = 0.5*audioHost->GetSampleRate();
|
||||||
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
|
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
|
||||||
onRequestComplete,
|
onRequestComplete,
|
||||||
clientId, instanceId, urid, atomValue, nullptr, onError);
|
clientId, instanceId, urid, atomValue, nullptr, onError,
|
||||||
|
sampleTimeout
|
||||||
|
);
|
||||||
|
|
||||||
outstandingParameterRequests.push_back(request);
|
outstandingParameterRequests.push_back(request);
|
||||||
if (this->audioHost)
|
if (this->audioHost)
|
||||||
@@ -1684,7 +1697,7 @@ void PiPedalModel::SendGetPatchProperty(
|
|||||||
int64_t clientId,
|
int64_t clientId,
|
||||||
int64_t instanceId,
|
int64_t instanceId,
|
||||||
const std::string uri,
|
const std::string uri,
|
||||||
std::function<void(const std::string &jsonResjult)> onSuccess,
|
std::function<void(const std::string &jsonResult)> onSuccess,
|
||||||
std::function<void(const std::string &error)> onError)
|
std::function<void(const std::string &error)> onError)
|
||||||
{
|
{
|
||||||
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
|
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
|
||||||
@@ -1732,16 +1745,20 @@ void PiPedalModel::SendGetPatchProperty(
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str());
|
LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str());
|
||||||
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
|
|
||||||
onRequestComplete,
|
|
||||||
clientId, instanceId, urid, onSuccess, onError);
|
|
||||||
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
outstandingParameterRequests.push_back(request);
|
|
||||||
if (this->audioHost)
|
if (!this->audioHost)
|
||||||
{
|
{
|
||||||
this->audioHost->sendRealtimeParameterRequest(request);
|
onError("Audio stopped.");
|
||||||
}
|
}
|
||||||
|
size_t sampleTimeout = 0.3*audioHost->GetSampleRate();
|
||||||
|
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
|
||||||
|
onRequestComplete,
|
||||||
|
clientId, instanceId, urid, onSuccess, onError,sampleTimeout);
|
||||||
|
|
||||||
|
outstandingParameterRequests.push_back(request);
|
||||||
|
this->audioHost->sendRealtimeParameterRequest(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
BankIndex PiPedalModel::GetBankIndex() const
|
BankIndex PiPedalModel::GetBankIndex() const
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
#include "ModFileTypes.hpp"
|
#include "ModFileTypes.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
#define PIPEDAL_HOST_FEATURE "http://github.com/rerdavies/pipedal#host" // Plugin can only be hosted by PiPedal
|
||||||
|
|
||||||
#define PIPEDAL_PATCH "http://github.com/rerdavies/pipedal/patch"
|
#define PIPEDAL_PATCH "http://github.com/rerdavies/pipedal/patch"
|
||||||
#define PIPEDAL_PATCH_PREFIX PIPEDAL_PATCH "#"
|
#define PIPEDAL_PATCH_PREFIX PIPEDAL_PATCH "#"
|
||||||
#define PIPEDAL_PATCH__readable (PIPEDAL_PATCH_PREFIX "readable")
|
#define PIPEDAL_PATCH__readable (PIPEDAL_PATCH_PREFIX "readable")
|
||||||
|
|||||||
+2
-1
@@ -615,7 +615,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
// example:
|
// example:
|
||||||
|
|
||||||
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
||||||
// a lv2:Parameter;
|
// a lv2:Parameter;
|
||||||
// rdfs:label "Model";
|
// rdfs:label "Model";
|
||||||
// rdfs:range atom:Path.
|
// rdfs:range atom:Path.
|
||||||
// ...
|
// ...
|
||||||
@@ -901,6 +901,7 @@ std::vector<std::string> supportedFeatures = {
|
|||||||
LV2_STATE__mapPath,
|
LV2_STATE__mapPath,
|
||||||
LV2_STATE__freePath,
|
LV2_STATE__freePath,
|
||||||
LV2_CORE__inPlaceBroken,
|
LV2_CORE__inPlaceBroken,
|
||||||
|
PIPEDAL_HOST_FEATURE,
|
||||||
|
|
||||||
// UI features that we can ignore, since we won't load their ui.
|
// UI features that we can ignore, since we won't load their ui.
|
||||||
"http://lv2plug.in/ns/extensions/ui#makeResident",
|
"http://lv2plug.in/ns/extensions/ui#makeResident",
|
||||||
|
|||||||
+100
-22
@@ -35,6 +35,9 @@
|
|||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "HotspotManager.hpp"
|
#include "HotspotManager.hpp"
|
||||||
#include "MimeTypes.hpp"
|
#include "MimeTypes.hpp"
|
||||||
|
#include "AudioFileMetadata.hpp"
|
||||||
|
#include "AudioFiles.hpp"
|
||||||
|
|
||||||
|
|
||||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||||
#define PRESET_EXTENSION ".piPreset"
|
#define PRESET_EXTENSION ".piPreset"
|
||||||
@@ -52,20 +55,20 @@ using namespace pipedal;
|
|||||||
using namespace boost::system;
|
using namespace boost::system;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static bool HasDotDot(const std::filesystem::path &path)
|
||||||
|
{
|
||||||
static bool HasDotDot(const std::filesystem::path &path) {
|
|
||||||
for (auto &part : path)
|
for (auto &part : path)
|
||||||
{
|
{
|
||||||
if (part == "..") {
|
if (part == "..")
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (part == ".")
|
if (part == ".")
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserUploadResponse
|
class UserUploadResponse
|
||||||
@@ -80,12 +83,12 @@ JSON_MAP_REFERENCE(UserUploadResponse, errorMessage)
|
|||||||
JSON_MAP_REFERENCE(UserUploadResponse, path)
|
JSON_MAP_REFERENCE(UserUploadResponse, path)
|
||||||
JSON_MAP_END()
|
JSON_MAP_END()
|
||||||
|
|
||||||
static std::string GetMimeType(const std::filesystem::path&path) {
|
static std::string GetMimeType(const std::filesystem::path &path)
|
||||||
|
{
|
||||||
std::string extension = path.extension();
|
std::string extension = path.extension();
|
||||||
const MimeTypes&mimeTypes = MimeTypes::instance();
|
const MimeTypes &mimeTypes = MimeTypes::instance();
|
||||||
auto result = mimeTypes.MimeTypeFromExtension(extension);
|
auto result = mimeTypes.MimeTypeFromExtension(extension);
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
static bool IsZipFile(const std::filesystem::path &path)
|
static bool IsZipFile(const std::filesystem::path &path)
|
||||||
{
|
{
|
||||||
@@ -179,6 +182,17 @@ public:
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (segment == "AudioMetadata")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if (segment == "NextAudioFile")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if (segment == "PreviousAudioFile")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,15 +260,17 @@ public:
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::string segment = request_uri.segment(1);
|
std::string segment = request_uri.segment(1);
|
||||||
if (segment == "downloadMediaFile") {
|
if (segment == "downloadMediaFile")
|
||||||
|
{
|
||||||
fs::path path = request_uri.query("path");
|
fs::path path = request_uri.query("path");
|
||||||
|
|
||||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||||
{
|
{
|
||||||
throw PiPedalException("File not found.");
|
throw PiPedalException("File not found.");
|
||||||
}
|
}
|
||||||
auto mimeType = GetMimeType(path);
|
auto mimeType = GetMimeType(path);
|
||||||
if (mimeType.empty()) {
|
if (mimeType.empty())
|
||||||
|
{
|
||||||
throw PiPedalException("Can't download files of this type.");
|
throw PiPedalException("Can't download files of this type.");
|
||||||
}
|
}
|
||||||
res.set(HttpField::content_type, mimeType);
|
res.set(HttpField::content_type, mimeType);
|
||||||
@@ -343,18 +359,20 @@ public:
|
|||||||
{
|
{
|
||||||
std::string segment = request_uri.segment(1);
|
std::string segment = request_uri.segment(1);
|
||||||
|
|
||||||
if (segment == "downloadMediaFile") {
|
if (segment == "downloadMediaFile")
|
||||||
|
{
|
||||||
fs::path path = request_uri.query("path");
|
fs::path path = request_uri.query("path");
|
||||||
|
|
||||||
bool t = this->model->IsInUploadsDirectory(path);
|
bool t = this->model->IsInUploadsDirectory(path);
|
||||||
std::cout << (t? "true": "false") << std::endl;
|
std::cout << (t ? "true" : "false") << std::endl;
|
||||||
(void)t;
|
(void)t;
|
||||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||||
{
|
{
|
||||||
throw PiPedalException("File not found.");
|
throw PiPedalException("File not found.");
|
||||||
}
|
}
|
||||||
auto mimeType = GetMimeType(path);
|
auto mimeType = GetMimeType(path);
|
||||||
if (mimeType.empty()) {
|
if (mimeType.empty())
|
||||||
|
{
|
||||||
throw PiPedalException("Can't download files of this type.");
|
throw PiPedalException("Can't download files of this type.");
|
||||||
}
|
}
|
||||||
res.set(HttpField::content_type, mimeType);
|
res.set(HttpField::content_type, mimeType);
|
||||||
@@ -363,9 +381,10 @@ public:
|
|||||||
res.set(HttpField::content_disposition, disposition);
|
res.set(HttpField::content_disposition, disposition);
|
||||||
size_t contentLength = std::filesystem::file_size(path);
|
size_t contentLength = std::filesystem::file_size(path);
|
||||||
res.setContentLength(contentLength);
|
res.setContentLength(contentLength);
|
||||||
res.setBodyFile(path,false);
|
res.setBodyFile(path, false);
|
||||||
return;
|
return;
|
||||||
} else if (segment == "downloadPluginPresets")
|
}
|
||||||
|
else if (segment == "downloadPluginPresets")
|
||||||
{
|
{
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string content;
|
std::string content;
|
||||||
@@ -416,6 +435,65 @@ public:
|
|||||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||||
res.setBodyFile(tmpFile);
|
res.setBodyFile(tmpFile);
|
||||||
}
|
}
|
||||||
|
else if (segment == "AudioMetadata")
|
||||||
|
{
|
||||||
|
res.set(HttpField::content_type, "application/json");
|
||||||
|
// res.set(HttpField::cache_control, "no-cache");
|
||||||
|
fs::path path = request_uri.query("path");
|
||||||
|
|
||||||
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||||
|
{
|
||||||
|
throw PiPedalException("File not found.");
|
||||||
|
}
|
||||||
|
std::string metadata = GetAudioFileMetadataString(path);
|
||||||
|
res.setBody(metadata);
|
||||||
|
}
|
||||||
|
else if (segment == "NextAudioFile")
|
||||||
|
{
|
||||||
|
res.set(HttpField::content_type, "application/json");
|
||||||
|
// res.set(HttpField::cache_control, "no-cache");
|
||||||
|
fs::path path = request_uri.query("path");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||||
|
{
|
||||||
|
throw PiPedalException("File not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string nextFile = AudioFileInfo::GetInstance().GetNextAudioFile(path);
|
||||||
|
std::stringstream ss;
|
||||||
|
json_writer writer(ss);
|
||||||
|
writer.write(nextFile);
|
||||||
|
|
||||||
|
res.setBody(ss.str());
|
||||||
|
} catch (const std::exception&e)
|
||||||
|
{
|
||||||
|
res.setBody("''");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (segment == "PreviousAudioFile")
|
||||||
|
{
|
||||||
|
res.set(HttpField::content_type, "application/json");
|
||||||
|
// res.set(HttpField::cache_control, "no-cache");
|
||||||
|
fs::path path = request_uri.query("path");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||||
|
{
|
||||||
|
throw PiPedalException("File not found.");
|
||||||
|
}
|
||||||
|
std::string previousFile = AudioFileInfo::GetInstance().GetPreviousAudioFile(path);
|
||||||
|
std::stringstream ss;
|
||||||
|
json_writer writer(ss);
|
||||||
|
writer.write(previousFile);
|
||||||
|
|
||||||
|
res.setBody(ss.str());
|
||||||
|
} catch (const std::exception&e)
|
||||||
|
{
|
||||||
|
res.setBody("\"\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw PiPedalException("Not found");
|
throw PiPedalException("Not found");
|
||||||
@@ -628,7 +706,7 @@ public:
|
|||||||
|
|
||||||
int64_t instanceId;
|
int64_t instanceId;
|
||||||
{
|
{
|
||||||
std::istringstream ss { instanceIdString};
|
std::istringstream ss{instanceIdString};
|
||||||
ss >> instanceId;
|
ss >> instanceId;
|
||||||
if (!ss)
|
if (!ss)
|
||||||
{
|
{
|
||||||
@@ -669,7 +747,7 @@ public:
|
|||||||
if (extensionChecker.IsValidExtension(extension))
|
if (extensionChecker.IsValidExtension(extension))
|
||||||
{
|
{
|
||||||
auto si = zipFile->GetFileInputStream(inputFile);
|
auto si = zipFile->GetFileInputStream(inputFile);
|
||||||
std::string path = this->model->UploadUserFile(directory, instanceId,patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
|
std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,7 +765,7 @@ public:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
outputFileName = this->model->UploadUserFile(directory, instanceId,patchProperty, filename, req.get_body_input_stream(), req.content_length());
|
outputFileName = this->model->UploadUserFile(directory, instanceId, patchProperty, filename, req.get_body_input_stream(), req.content_length());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFileName.is_relative())
|
if (outputFileName.is_relative())
|
||||||
@@ -783,7 +861,7 @@ public:
|
|||||||
<< ", \"socket_server_address\": \"" << webSocketAddress
|
<< ", \"socket_server_address\": \"" << webSocketAddress
|
||||||
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
|
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
|
||||||
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
|
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
|
||||||
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true": "false")
|
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false")
|
||||||
<< " }";
|
<< " }";
|
||||||
|
|
||||||
return s.str();
|
return s.str();
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
Sort order.
|
||||||
|
Do we have a better plugin category than "Utility"
|
||||||
|
.index files.
|
||||||
|
- pipewire aux in?
|
||||||
|
|
||||||
|
libsqlite3-dev install
|
||||||
|
libsqlitecpp-dev install
|
||||||
|
|
||||||
|
Check that OnNotifyPatchProperty still worsk for
|
||||||
|
- FilePropertyControl
|
||||||
|
- PowerStage2.
|
||||||
|
- ToobSpectrumAnaylyzer.
|
||||||
|
|
||||||
pcm.pipedal_aux_in {
|
pcm.pipedal_aux_in {
|
||||||
type file
|
type file
|
||||||
file "/tmp/aux_input_fifo"
|
file "/tmp/aux_input_fifo"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const androidHosted = !!(window.AndroidHost);
|
const androidHosted = !!(window.AndroidHost);
|
||||||
|
|
||||||
var colorScheme = localStorage.getItem("colorScheme");
|
var colorScheme = localStorage.getItem("colorScheme");
|
||||||
|
|||||||
Generated
+57
@@ -14,6 +14,7 @@
|
|||||||
"@mui/icons-material": "^6.4.4",
|
"@mui/icons-material": "^6.4.4",
|
||||||
"@mui/material": "^6.4.4",
|
"@mui/material": "^6.4.4",
|
||||||
"@mui/styles": "^6.4.4",
|
"@mui/styles": "^6.4.4",
|
||||||
|
"@react-hook/window-size": "^3.1.1",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -1517,6 +1518,62 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-hook/debounce": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-hook/latest": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-hook/event": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-hook/latest": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-hook/throttle": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-hook/throttle/-/throttle-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LJ5eg+yMV8lXtqK3lR+OtOZ2WH/EfWvuiEEu0M3bhR7dZRfTyEJKxH1oK9uyBxiXPtWXiQggWbZirMCXam51tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-hook/latest": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-hook/window-size": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-hook/window-size/-/window-size-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-yWnVS5LKnOUIrEsI44oz3bIIUYqflamPL27n+k/PC//PsX/YeWBky09oPeAoc9As6jSH16Wgo8plI+ECZaHk3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-hook/debounce": "^3.0.0",
|
||||||
|
"@react-hook/event": "^1.2.1",
|
||||||
|
"@react-hook/throttle": "^2.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/pluginutils": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@mui/icons-material": "^6.4.4",
|
"@mui/icons-material": "^6.4.4",
|
||||||
"@mui/material": "^6.4.4",
|
"@mui/material": "^6.4.4",
|
||||||
"@mui/styles": "^6.4.4",
|
"@mui/styles": "^6.4.4",
|
||||||
|
"@react-hook/window-size": "^3.1.1",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
<body style="overscroll-behavior: none; overflow: hidden">
|
<body style="overscroll-behavior: none; overflow: hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" style="height: 100%;position: absolute;left: 0px;right: 0px;top:0px;bottom: 0px"></div>
|
<div id="root" style="height: 100%;position: absolute;left: px;right: 0px;top:0px;bottom: 0px"></div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|||||||
@@ -14,6 +14,29 @@
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
background: "red";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="number"].scrollMod::-webkit-outer-spin-button,
|
||||||
|
input[type="number"].scrollMod::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
color: #FFF;
|
||||||
|
background: #0001 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVQYlWNgwAT/sYhhKPiPT+F/LJgEsHv37v+EMGkmkuImoh2NoQAANlcun/q4OoYAAAAASUVORK5CYII=) no-repeat center center;
|
||||||
|
filter: invert(100%);
|
||||||
|
width: 1em;
|
||||||
|
padding: 2px;
|
||||||
|
opacity: .6; /* shows Spin Buttons per default (Chrome >= 39) */
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
input[type="number"].scrollMod::-webkit-inner-spin-button:hover,
|
||||||
|
input[type="number"].scrollMod::-webkit-inner-spin-button:active {
|
||||||
|
background: #0003 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVQYlWNgwAT/sYhhKPiPT+F/LJgEsHv37v+EMGkmkuImoh2NoQAANlcun/q4OoYAAAAASUVORK5CYII=) no-repeat center center;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export default class AudioFileMetadata {
|
||||||
|
//AudioFileMetadata&operator=(const AudioFileMetadata&) = default;
|
||||||
|
deserialize(o: any) {
|
||||||
|
this.title = o.title;
|
||||||
|
this.duration = o.duration;
|
||||||
|
this.track = o.track;
|
||||||
|
this.album = o.album;
|
||||||
|
this.disc = o.disc;
|
||||||
|
this.artist = o.artist;
|
||||||
|
this.albumArtist = o.albumArtist;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
duration: number = 0;
|
||||||
|
title: string = "";
|
||||||
|
track: string = "";
|
||||||
|
album: string = "";
|
||||||
|
disc: string = "";
|
||||||
|
artist: string = "";
|
||||||
|
albumArtist: string = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ButtonTooltipProps {
|
||||||
|
title: string,
|
||||||
|
children: ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ButtonTooltip(props: ButtonTooltipProps)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Tooltip placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||||
|
title={
|
||||||
|
(
|
||||||
|
<Typography variant="body2">
|
||||||
|
{props.title}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,13 +30,15 @@ import { GxTunerViewFactory } from './GxTunerView';
|
|||||||
import ToobPowerstage2ViewFactory from './ToobPowerStage2View';
|
import ToobPowerstage2ViewFactory from './ToobPowerStage2View';
|
||||||
import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView';
|
import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView';
|
||||||
import ToobMLViewFactory from './ToobMLView';
|
import ToobMLViewFactory from './ToobMLView';
|
||||||
|
import ToobPlayerFactory from './ToobPlayerView';
|
||||||
|
|
||||||
|
|
||||||
let pluginFactories: IControlViewFactory[] = [
|
let pluginFactories: IControlViewFactory[] = [
|
||||||
new GxTunerViewFactory(),
|
new GxTunerViewFactory(),
|
||||||
new ToobPowerstage2ViewFactory(),
|
new ToobPowerstage2ViewFactory(),
|
||||||
new ToobSpectrumAnalyzerViewFactory(),
|
new ToobSpectrumAnalyzerViewFactory(),
|
||||||
new ToobMLViewFactory()
|
new ToobMLViewFactory(),
|
||||||
|
new ToobPlayerFactory()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -481,9 +481,6 @@ export default withStyles(
|
|||||||
|
|
||||||
}
|
}
|
||||||
renderBreadcrumbs() {
|
renderBreadcrumbs() {
|
||||||
if (this.state.navDirectory === "") {
|
|
||||||
return (<Divider />);
|
|
||||||
}
|
|
||||||
let breadcrumbs: React.ReactElement[] = [(
|
let breadcrumbs: React.ReactElement[] = [(
|
||||||
<Button variant="text"
|
<Button variant="text"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ const GxTunerView =
|
|||||||
}
|
}
|
||||||
throw new Error("GxTuner: Control '" + key + "' not found.");
|
throw new Error("GxTuner: Control '" + key + "' not found.");
|
||||||
}
|
}
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
let refFreqIndex = this.getControlIndex("REFFREQ");
|
let refFreqIndex = this.getControlIndex("REFFREQ");
|
||||||
let thresholdIndex = this.getControlIndex("THRESHOLD");
|
let thresholdIndex = this.getControlIndex("THRESHOLD");
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const styles = ({ palette }: Theme) => { return {
|
|||||||
flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||||
}),
|
}),
|
||||||
controlContent: css({
|
controlContent: css({
|
||||||
flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240
|
flex: "1 1 auto", width: "100%", overflowY: "hidden", minHeight: 300
|
||||||
}),
|
}),
|
||||||
controlContentSmall: css({
|
controlContentSmall: css({
|
||||||
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
||||||
@@ -609,7 +609,7 @@ export const MainPage =
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
<div id="mainPageControls" className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||||
{
|
{
|
||||||
missing ? (
|
missing ? (
|
||||||
<div style={{ marginLeft: 40, marginTop: 20 }}>
|
<div style={{ marginLeft: 40, marginTop: 20 }}>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import AlsaDeviceInfo from './AlsaDeviceInfo';
|
|||||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||||
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
|
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
|
||||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||||
|
import AudioFileMetadata from './AudioFileMetadaa';
|
||||||
|
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
@@ -56,14 +57,11 @@ export enum State {
|
|||||||
HotspotChanging,
|
HotspotChanging,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getErrorMessage(error: any)
|
function getErrorMessage(error: any) {
|
||||||
{
|
if (error instanceof Error) {
|
||||||
if (error instanceof Error)
|
|
||||||
{
|
|
||||||
return (error as Error).message;
|
return (error as Error).message;
|
||||||
}
|
}
|
||||||
if (!error)
|
if (!error) {
|
||||||
{
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return error.toString();
|
return error.toString();
|
||||||
@@ -479,14 +477,12 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
private expectDisconnectTimer?: number = undefined;
|
private expectDisconnectTimer?: number = undefined;
|
||||||
|
|
||||||
cancelExpectDisconnectTimer()
|
cancelExpectDisconnectTimer() {
|
||||||
{
|
|
||||||
if (this.expectDisconnectTimer) {
|
if (this.expectDisconnectTimer) {
|
||||||
clearTimeout(this.expectDisconnectTimer);
|
clearTimeout(this.expectDisconnectTimer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startExpectDisconnectTimer()
|
startExpectDisconnectTimer() {
|
||||||
{
|
|
||||||
this.cancelExpectDisconnectTimer();
|
this.cancelExpectDisconnectTimer();
|
||||||
// poll for access to a running pipedal server
|
// poll for access to a running pipedal server
|
||||||
this.expectDisconnectTimer = setTimeout(
|
this.expectDisconnectTimer = setTimeout(
|
||||||
@@ -500,8 +496,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
expectDisconnect(reason: ReconnectReason) {
|
expectDisconnect(reason: ReconnectReason) {
|
||||||
this.cancelExpectDisconnectTimer();
|
this.cancelExpectDisconnectTimer();
|
||||||
this.reconnectReason = reason;
|
this.reconnectReason = reason;
|
||||||
if (this.reconnectReason !== ReconnectReason.Disconnected)
|
if (this.reconnectReason !== ReconnectReason.Disconnected) {
|
||||||
{
|
|
||||||
this.startExpectDisconnectTimer();
|
this.startExpectDisconnectTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,17 +614,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection);
|
let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection);
|
||||||
this.jackSettings.set(channelSelection);
|
this.jackSettings.set(channelSelection);
|
||||||
} else if (message === "onSnapshotModified") {
|
} else if (message === "onSnapshotModified") {
|
||||||
let {snapshotIndex,modified} = (body as {snapshotIndex: number, modified: boolean});
|
let { snapshotIndex, modified } = (body as { snapshotIndex: number, modified: boolean });
|
||||||
let snapshots = this.pedalboard.get().snapshots;
|
let snapshots = this.pedalboard.get().snapshots;
|
||||||
if (snapshotIndex >= 0 && snapshotIndex < snapshots.length)
|
if (snapshotIndex >= 0 && snapshotIndex < snapshots.length) {
|
||||||
{
|
|
||||||
let snapshot = snapshots[snapshotIndex]
|
let snapshot = snapshots[snapshotIndex]
|
||||||
if (snapshot)
|
if (snapshot) {
|
||||||
{
|
if (snapshot.isModified !== modified) {
|
||||||
if (snapshot.isModified !== modified)
|
|
||||||
{
|
|
||||||
snapshot.isModified = modified;
|
snapshot.isModified = modified;
|
||||||
this.onSnapshotModified.fire({snapshotIndex: snapshotIndex,modified: modified});
|
this.onSnapshotModified.fire({ snapshotIndex: snapshotIndex, modified: modified });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,8 +683,8 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
} else if (message === "onNotifyPathPatchPropertyChanged") {
|
} else if (message === "onNotifyPathPatchPropertyChanged") {
|
||||||
let instanceId = body.instanceId as number;
|
let instanceId = body.instanceId as number;
|
||||||
let propertyUri = body.propertyUri as string;
|
let propertyUri = body.propertyUri as string;
|
||||||
let atomJson = body.atomJson as any;
|
let atomJsonString = body.atomJson as string;
|
||||||
this.handleNotifyPathPatchPropertyChanged(instanceId, propertyUri, atomJson);
|
this.handleNotifyPathPatchPropertyChanged(instanceId, propertyUri, atomJsonString);
|
||||||
if (header.replyTo) {
|
if (header.replyTo) {
|
||||||
this.webSocket?.reply(header.replyTo, "onNotifyPathPatchPropertyChanged", true);
|
this.webSocket?.reply(header.replyTo, "onNotifyPathPatchPropertyChanged", true);
|
||||||
}
|
}
|
||||||
@@ -752,7 +744,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
else if (message === "onHasWifiChanged") {
|
else if (message === "onHasWifiChanged") {
|
||||||
let hasWifi = body as boolean;
|
let hasWifi = body as boolean;
|
||||||
this.hasWifiDevice.set(hasWifi);
|
this.hasWifiDevice.set(hasWifi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -923,6 +915,8 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async onSocketReconnected(): Promise<void> {
|
async onSocketReconnected(): Promise<void> {
|
||||||
this.cancelOnNetworkChanging();
|
this.cancelOnNetworkChanging();
|
||||||
this.cancelAndroidReconnectTimer();
|
this.cancelAndroidReconnectTimer();
|
||||||
@@ -932,13 +926,12 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||||
|
|
||||||
// reload state, but not configuration.
|
// reload state, but not configuration.
|
||||||
this.clientId = await this.getWebSocket().request<number>("hello");
|
this.clientId = await this.getWebSocket().request<number>("hello");
|
||||||
|
|
||||||
let newServerVersion = this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
let newServerVersion = this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
||||||
if (newServerVersion.serverVersion !== this.serverVersion.serverVersion)
|
if (newServerVersion.serverVersion !== this.serverVersion.serverVersion) {
|
||||||
{
|
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -955,6 +948,48 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNextAudioFile(filePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
let url =
|
||||||
|
this.varServerUrl
|
||||||
|
+ "NextAudioFile?path=" + encodeURIComponent(filePath);
|
||||||
|
|
||||||
|
let response = await fetch(url);
|
||||||
|
let json = await response.json();
|
||||||
|
return json as string;
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getPreviousAudioFile(filePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
let url =
|
||||||
|
this.varServerUrl
|
||||||
|
+ "PreviousAudioFile?path=" + encodeURIComponent(filePath);
|
||||||
|
|
||||||
|
let response = await fetch(url);
|
||||||
|
let json = await response.json();
|
||||||
|
return json as string;
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAudioFileMetadata(filePath: string): Promise<AudioFileMetadata> {
|
||||||
|
try {
|
||||||
|
let url =
|
||||||
|
this.varServerUrl
|
||||||
|
+ "AudioMetadata?path=" + encodeURIComponent(filePath);
|
||||||
|
|
||||||
|
let response = await fetch(url);
|
||||||
|
let json = await response.json();
|
||||||
|
return new AudioFileMetadata().deserialize(json);
|
||||||
|
} catch (e) {
|
||||||
|
return new AudioFileMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
maxFileUploadSize: number = 512 * 1024 * 1024;
|
maxFileUploadSize: number = 512 * 1024 * 1024;
|
||||||
maxPresetUploadSize: number = 1024 * 1024;
|
maxPresetUploadSize: number = 1024 * 1024;
|
||||||
debug: boolean = false;
|
debug: boolean = false;
|
||||||
@@ -964,7 +999,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
async requestConfig(): Promise<boolean> {
|
async requestConfig(): Promise<boolean> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const myRequest = new Request(this.varRequest('config.json'));
|
const myRequest = new Request(this.varRequest('config.json'));
|
||||||
let response: Response = await fetch(myRequest);
|
let response: Response = await fetch(myRequest);
|
||||||
let data = await response.json();
|
let data = await response.json();
|
||||||
|
|
||||||
@@ -984,12 +1019,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
if (!socket_server_port) socket_server_port = 8080;
|
if (!socket_server_port) socket_server_port = 8080;
|
||||||
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
||||||
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
|
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
|
||||||
|
|
||||||
this.socketServerUrl = socket_server;
|
this.socketServerUrl = socket_server;
|
||||||
this.varServerUrl = var_server_url;
|
this.varServerUrl = var_server_url;
|
||||||
this.maxFileUploadSize = parseInt(max_upload_size);
|
this.maxFileUploadSize = parseInt(max_upload_size);
|
||||||
} catch (error: any)
|
} catch (error: any) {
|
||||||
{
|
|
||||||
this.setError("Can't connect to server. " + getErrorMessage(error));
|
this.setError("Can't connect to server. " + getErrorMessage(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1005,10 +1039,10 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.webSocket.connect();
|
await this.webSocket.connect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setError("Failed to connect to server. " + getErrorMessage(error) );
|
this.setError("Failed to connect to server. " + getErrorMessage(error));
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1017,23 +1051,21 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
this.clientId = (await this.getWebSocket().request<number>("hello")) as number;
|
this.clientId = (await this.getWebSocket().request<number>("hello")) as number;
|
||||||
|
|
||||||
this.preloadImages( (await this.getWebSocket().request<string>("imageList")));
|
this.preloadImages((await this.getWebSocket().request<string>("imageList")));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setError("Failed to establish connection. " + getErrorMessage(error));
|
this.setError("Failed to establish connection. " + getErrorMessage(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return await this.loadServerState();
|
return await this.loadServerState();
|
||||||
}
|
}
|
||||||
async loadServerState() : Promise<boolean>
|
async loadServerState(): Promise<boolean> {
|
||||||
{
|
try {
|
||||||
try
|
|
||||||
{
|
|
||||||
this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
||||||
|
|
||||||
this.updateStatus.set(new UpdateStatus().deserialize(await this.getUpdateStatus()));
|
this.updateStatus.set(new UpdateStatus().deserialize(await this.getUpdateStatus()));
|
||||||
|
|
||||||
this.hasWifiDevice.set(await this.getWebSocket().request<boolean>("getHasWifi"));
|
this.hasWifiDevice.set(await this.getWebSocket().request<boolean>("getHasWifi"));
|
||||||
|
|
||||||
this.ui_plugins.set(
|
this.ui_plugins.set(
|
||||||
UiPlugin.deserialize_array(await this.getWebSocket().request<any>("plugins"))
|
UiPlugin.deserialize_array(await this.getWebSocket().request<any>("plugins"))
|
||||||
);
|
);
|
||||||
@@ -1059,7 +1091,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.wifiDirectConfigSettings.set(
|
this.wifiDirectConfigSettings.set(
|
||||||
new WifiDirectConfigSettings().deserialize(
|
new WifiDirectConfigSettings().deserialize(
|
||||||
await this.getWebSocket().request<any>("getWifiDirectConfigSettings")
|
await this.getWebSocket().request<any>("getWifiDirectConfigSettings")
|
||||||
));
|
));
|
||||||
this.governorSettings.set(new GovernorSettings().deserialize(
|
this.governorSettings.set(new GovernorSettings().deserialize(
|
||||||
await this.getWebSocket().request<any>("getGovernorSettings")
|
await this.getWebSocket().request<any>("getGovernorSettings")
|
||||||
));
|
));
|
||||||
@@ -1077,7 +1109,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.jackSettings.set(new JackChannelSelection().deserialize(
|
this.jackSettings.set(new JackChannelSelection().deserialize(
|
||||||
await this.getWebSocket().request<any>("getJackSettings")
|
await this.getWebSocket().request<any>("getJackSettings")
|
||||||
));
|
));
|
||||||
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
|
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
|
||||||
|
|
||||||
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
|
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
|
||||||
|
|
||||||
@@ -1089,7 +1121,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.setState(State.Ready);
|
this.setState(State.Ready);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch(error) {
|
catch (error) {
|
||||||
this.setError("Failed to fetch server state.\n\n" + getErrorMessage(error));
|
this.setError("Failed to fetch server state.\n\n" + getErrorMessage(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1473,7 +1505,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sendPedalboardControlTrigger(instanceId: number, key: string, value: number) : void {
|
sendPedalboardControlTrigger(instanceId: number, key: string, value: number): void {
|
||||||
// no state change, no saving the value, just send it to the realtime thread/
|
// no state change, no saving the value, just send it to the realtime thread/
|
||||||
this._setServerControl("previewControl", instanceId, key, value);
|
this._setServerControl("previewControl", instanceId, key, value);
|
||||||
}
|
}
|
||||||
@@ -2334,19 +2366,15 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleNotifyPathPatchPropertyChanged(
|
private handleNotifyPathPatchPropertyChanged(
|
||||||
instanceId: number, propertyUri: string, jsonObject: any
|
instanceId: number, propertyUri: string, jsonObjectString: string
|
||||||
) {
|
) {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
let pedalboardItem = pedalboard.getItem(instanceId);
|
let pedalboardItem = pedalboard.getItem(instanceId);
|
||||||
if (pedalboardItem) {
|
if (pedalboardItem) {
|
||||||
pedalboardItem.pathProperties[propertyUri] = jsonObject;
|
pedalboardItem.pathProperties[propertyUri] = jsonObjectString;
|
||||||
}
|
|
||||||
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
|
|
||||||
let listener = this.monitorPatchPropertyListeners[i];
|
|
||||||
if (listener.instanceId === instanceId) {
|
|
||||||
listener.callback(instanceId, propertyUri, jsonObject);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// No NOT notify monitorPatchProperty listeners, because they extpect objects not strings,
|
||||||
|
// AND they will a NotifyPatchPropertychanged message anyway.
|
||||||
|
|
||||||
}
|
}
|
||||||
private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) {
|
private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) {
|
||||||
@@ -2451,16 +2479,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
let response = json as {errorMessage: string, path: string};
|
let response = json as { errorMessage: string, path: string };
|
||||||
if (response.errorMessage !== "")
|
if (response.errorMessage !== "") {
|
||||||
{
|
|
||||||
throw new Error(response.errorMessage);
|
throw new Error(response.errorMessage);
|
||||||
}
|
}
|
||||||
resolve(response.path);
|
resolve(response.path);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error instanceof Error)
|
if (error instanceof Error) {
|
||||||
{
|
|
||||||
reject("Upload failed. " + (error as Error).message);
|
reject("Upload failed. " + (error as Error).message);
|
||||||
} else {
|
} else {
|
||||||
reject("Upload failed. " + error);
|
reject("Upload failed. " + error);
|
||||||
@@ -2605,7 +2631,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty,selectedPath: string): Promise<FilePropertyDirectoryTree> {
|
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty, selectedPath: string): Promise<FilePropertyDirectoryTree> {
|
||||||
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
@@ -2614,7 +2640,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
ws.request<FilePropertyDirectoryTree>(
|
ws.request<FilePropertyDirectoryTree>(
|
||||||
"getFilePropertyDirectoryTree",
|
"getFilePropertyDirectoryTree",
|
||||||
{fileProperty: uiFileProperty, selectedPath: selectedPath}
|
{ fileProperty: uiFileProperty, selectedPath: selectedPath }
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
|||||||
@@ -65,14 +65,14 @@ function preventNextClickAfterDrag() {
|
|||||||
// on ANY element under the mouse. Prevent this click event
|
// on ANY element under the mouse. Prevent this click event
|
||||||
// (and any other click event) from happening for 100ms
|
// (and any other click event) from happening for 100ms
|
||||||
// after the drag stops.
|
// after the drag stops.
|
||||||
let clickHandler = (e: MouseEvent) => {
|
let clickHandler = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
document.addEventListener('click',clickHandler,true);
|
document.addEventListener('click', clickHandler, true);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
document.removeEventListener('click',clickHandler,true);
|
document.removeEventListener('click', clickHandler, true);
|
||||||
},100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function androidEmoji(text: string) {
|
function androidEmoji(text: string) {
|
||||||
@@ -119,7 +119,7 @@ export const pluginControlStyles = (theme: Theme) => createStyles({
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
background: theme.mainBackground,
|
background: "transparent",
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
// zIndex: -1,
|
// zIndex: -1,
|
||||||
}),
|
}),
|
||||||
@@ -214,19 +214,19 @@ const PluginControl =
|
|||||||
inputChanged: boolean = false;
|
inputChanged: boolean = false;
|
||||||
|
|
||||||
onInputLostFocus(event: any): void {
|
onInputLostFocus(event: any): void {
|
||||||
this.setState({editFocused: false});
|
this.setState({ editFocused: false });
|
||||||
if (this.inputChanged) // validation requried?
|
if (this.inputChanged) // validation requried?
|
||||||
{
|
{
|
||||||
this.inputChanged = false;
|
this.inputChanged = false;
|
||||||
this.validateInput(event, true);
|
this.validateInput(event, true);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//this.displayValueRef.current!.style.display = "block";
|
//this.displayValueRef.current!.style.display = "block";
|
||||||
}
|
}
|
||||||
onInputFocus(event: SyntheticEvent): void {
|
onInputFocus(event: SyntheticEvent): void {
|
||||||
//this.displayValueRef.current!.style.display = "none";
|
//this.displayValueRef.current!.style.display = "none";
|
||||||
this.setState({editFocused: true});
|
this.setState({ editFocused: true });
|
||||||
if (Utility.hasIMEKeyboard()) {
|
if (Utility.hasIMEKeyboard()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -356,11 +356,9 @@ const PluginControl =
|
|||||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (document.activeElement)
|
if (document.activeElement) {
|
||||||
{
|
|
||||||
let e = document.activeElement as any;
|
let e = document.activeElement as any;
|
||||||
if (e.blur)
|
if (e.blur) {
|
||||||
{
|
|
||||||
e.blur();
|
e.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,8 +490,7 @@ const PluginControl =
|
|||||||
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
||||||
|
|
||||||
if (this.isCapturedPointer(e)) {
|
if (this.isCapturedPointer(e)) {
|
||||||
if (this.pointersDown !== 0)
|
if (this.pointersDown !== 0) {
|
||||||
{
|
|
||||||
--this.pointersDown;
|
--this.pointersDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,8 +512,7 @@ const PluginControl =
|
|||||||
preventNextClickAfterDrag();
|
preventNextClickAfterDrag();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (this.pointersDown !== 0)
|
if (this.pointersDown !== 0) {
|
||||||
{
|
|
||||||
--this.pointersDown;
|
--this.pointersDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +616,7 @@ const PluginControl =
|
|||||||
UpdateGraphicEqPath(imgElement, range);
|
UpdateGraphicEqPath(imgElement, range);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
let transform = this.rangeToRotationTransform(range);
|
let transform = this.rangeToRotationTransform(range);
|
||||||
if (this.mouseDown && !commitValue) {
|
if (this.mouseDown && !commitValue) {
|
||||||
transform += " scale(1.5, 1.5)";
|
transform += " scale(1.5, 1.5)";
|
||||||
@@ -1068,21 +1064,30 @@ const PluginControl =
|
|||||||
defaultValue={control.formatShortValue(value)}
|
defaultValue={control.formatShortValue(value)}
|
||||||
error={this.state.error}
|
error={this.state.error}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
|
className: "scrollMod",
|
||||||
min: this.props.uiControl?.min_value,
|
min: this.props.uiControl?.min_value,
|
||||||
max: this.props.uiControl?.max_value,
|
max: this.props.uiControl?.max_value,
|
||||||
'aria-label':
|
|
||||||
control.symbol + " value",
|
|
||||||
style: { textAlign: "center", fontSize: FONT_SIZE },
|
|
||||||
}}
|
}}
|
||||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
|
||||||
onBlur={this.onInputLostFocus}
|
|
||||||
onFocus={this.onInputFocus}
|
|
||||||
|
|
||||||
onKeyPress={this.onInputKeyPress} />
|
sx={{
|
||||||
<div className={classes.displayValue}
|
// Style the input element
|
||||||
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
'& input[type=number]': {
|
||||||
style={{display: this.state.editFocused? "none": "block"}}
|
width: 60,
|
||||||
>
|
opacity: this.state.editFocused ? 1 : 0,
|
||||||
|
textAlign: "center", fontSize: FONT_SIZE,
|
||||||
|
borderBottom: "0px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||||
|
onBlur={this.onInputLostFocus}
|
||||||
|
onFocus={this.onInputFocus}
|
||||||
|
|
||||||
|
onKeyPress={this.onInputKeyPress} />
|
||||||
|
<div className={classes.displayValue}
|
||||||
|
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
||||||
|
style={{ display: this.state.editFocused ? "none" : "block" }}
|
||||||
|
>
|
||||||
<Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop: 4, paddingBottom: 6 }}
|
<Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop: 4, paddingBottom: 6 }}
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import WithStyles, {withTheme} from './WithStyles';
|
import WithStyles, { withTheme } from './WithStyles';
|
||||||
import {createStyles} from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
@@ -97,10 +97,37 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
overflowY: "hidden"
|
overflowY: "hidden"
|
||||||
}),
|
}),
|
||||||
|
noScrollFrame: css({
|
||||||
|
display: "block",
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
|
||||||
|
left: 0, top: 0,
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
paddingTop: "0px",
|
||||||
|
paddingBottom: "0px",
|
||||||
|
overflowX: "hidden",
|
||||||
|
overflowY: "hidden"
|
||||||
|
|
||||||
|
}),
|
||||||
|
frameScrollNone: css({
|
||||||
|
display: "block",
|
||||||
|
position: "relative",
|
||||||
|
left: 0, top: 0,
|
||||||
|
width: "100%", height: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
paddingTop: "0px",
|
||||||
|
paddingBottom: "0px",
|
||||||
|
overflowX: "auto",
|
||||||
|
overflowY: "hidden"
|
||||||
|
}),
|
||||||
frameScrollLandscape: css({
|
frameScrollLandscape: css({
|
||||||
display: "block",
|
display: "block",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0, top: 0, right: 0, bottom:0,
|
left: 0, top: 0, right: 0, bottom: 0,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
flexWrap: "nowrap",
|
flexWrap: "nowrap",
|
||||||
paddingTop: "0px",
|
paddingTop: "0px",
|
||||||
@@ -111,7 +138,7 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
frameScrollPortrait: css({
|
frameScrollPortrait: css({
|
||||||
display: "block",
|
display: "block",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0, top: 0, right: 0, bottom:0,
|
left: 0, top: 0, right: 0, bottom: 0,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
flexWrap: "nowrap",
|
flexWrap: "nowrap",
|
||||||
paddingTop: "0px",
|
paddingTop: "0px",
|
||||||
@@ -150,12 +177,26 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
noScrollGrid: css({
|
||||||
|
position: "relative",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
paddingLeft: 30,
|
||||||
|
paddingRight: 45,
|
||||||
|
paddingTop: 0,
|
||||||
|
|
||||||
|
flex: "1 1 auto",
|
||||||
|
}),
|
||||||
|
|
||||||
normalGrid: css({
|
normalGrid: css({
|
||||||
position: "relative",
|
position: "relative",
|
||||||
paddingLeft: 30,
|
paddingLeft: 30,
|
||||||
paddingRight: 30,
|
paddingRight: 45,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
|
|
||||||
flex: "1 1 auto",
|
flex: "1 1 auto",
|
||||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||||
justifyContent: "flex-start", alignItems: "flex_start",
|
justifyContent: "flex-start", alignItems: "flex_start",
|
||||||
@@ -171,7 +212,8 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
// See the spacer div added after all controls in render() with provides the same effect.
|
// See the spacer div added after all controls in render() with provides the same effect.
|
||||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||||
justifyContent: "flex-start", alignItems: "flex-start",
|
justifyContent: "flex-start", alignItems: "flex-start",
|
||||||
|
position: "relative",
|
||||||
|
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
@@ -218,7 +260,7 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
elevation: 12,
|
elevation: 12,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row", flexWrap: "wrap",
|
flexDirection: "row", flexWrap: "wrap",
|
||||||
|
|
||||||
flex: "0 1 auto",
|
flex: "0 1 auto",
|
||||||
}),
|
}),
|
||||||
portGroupLandscape: css({
|
portGroupLandscape: css({
|
||||||
@@ -229,7 +271,7 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
|
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
border: "2pt #AAA solid",
|
border: "2pt #AAA solid",
|
||||||
@@ -238,7 +280,7 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
flexDirection: "row", flexWrap: "nowrap",
|
flexDirection: "row", flexWrap: "nowrap",
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
width: "fit-content",
|
width: "fit-content",
|
||||||
minWidth: "max-content"
|
minWidth: "max-content"
|
||||||
}),
|
}),
|
||||||
@@ -287,7 +329,8 @@ export type ControlNodes = (ReactNode | ControlGroup)[];
|
|||||||
|
|
||||||
|
|
||||||
export interface ControlViewCustomization {
|
export interface ControlViewCustomization {
|
||||||
ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
|
||||||
|
fullScreen(): boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,11 +450,13 @@ const PluginControlView =
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
private ixKey: number;
|
||||||
|
|
||||||
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
||||||
let symbol = uiControl.symbol;
|
let symbol = uiControl.symbol;
|
||||||
if (!uiControl.is_input) {
|
if (!uiControl.is_input) {
|
||||||
return (
|
return (
|
||||||
<PluginOutputControl instanceId={this.props.instanceId} uiControl={uiControl} />
|
<PluginOutputControl key={uiControl.symbol } instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -428,7 +473,7 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
return ((
|
return ((
|
||||||
|
|
||||||
<PluginControl instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
<PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
||||||
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
|
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
|
||||||
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
||||||
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
|
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
|
||||||
@@ -438,27 +483,25 @@ const PluginControlView =
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
push_control(controls:ControlNodes, pluginControl: UiControl,controlValues: ControlValue[])
|
push_control(controls: ControlNodes, pluginControl: UiControl, controlValues: ControlValue[]) {
|
||||||
{
|
|
||||||
// combine lamps with their previous control
|
// combine lamps with their previous control
|
||||||
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0)
|
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0) {
|
||||||
{
|
|
||||||
const classes = withStyles.getClasses(this.props);
|
const classes = withStyles.getClasses(this.props);
|
||||||
let newControl = this.makeStandardControl(pluginControl,controlValues);
|
let newControl = this.makeStandardControl(pluginControl, controlValues);
|
||||||
let previousControl = controls[controls.length-1];
|
let previousControl = controls[controls.length - 1];
|
||||||
if (!(previousControl instanceof ControlGroup)) {
|
if (!(previousControl instanceof ControlGroup)) {
|
||||||
let pair = (
|
let pair = (
|
||||||
<div className={classes.controlPair}>
|
<div key={"k"+this.ixKey++} className={classes.controlPair}>
|
||||||
{previousControl as ReactNode}
|
{previousControl as ReactNode}
|
||||||
{newControl}
|
{newControl}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
controls[controls.length-1] = pair;
|
controls[controls.length - 1] = pair;
|
||||||
// push a spacer control in order to make placing of extended controls predictable.
|
// push a spacer control in order to make placing of extended controls predictable.
|
||||||
// (e.g.. inserting at position 4 still places the extended control after four previous controls
|
// (e.g.. inserting at position 4 still places the extended control after four previous controls
|
||||||
controls.push((
|
controls.push((
|
||||||
|
|
||||||
<div className={classes.controlSpacer} />
|
<div key={"k"+this.ixKey++} className={classes.controlSpacer} />
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
controls.push(newControl);
|
controls.push(newControl);
|
||||||
@@ -492,7 +535,7 @@ const PluginControlView =
|
|||||||
pluginControl = plugin.controls[i];
|
pluginControl = plugin.controls[i];
|
||||||
|
|
||||||
if (!pluginControl.isHidden()) {
|
if (!pluginControl.isHidden()) {
|
||||||
this.push_control(groupControls,pluginControl,controlValues);
|
this.push_control(groupControls, pluginControl, controlValues);
|
||||||
indexes.push(pluginControl.index);
|
indexes.push(pluginControl.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,7 +545,7 @@ const PluginControlView =
|
|||||||
)
|
)
|
||||||
portGroupMap[pluginControl.port_group] = controlGroup;
|
portGroupMap[pluginControl.port_group] = controlGroup;
|
||||||
} else {
|
} else {
|
||||||
this.push_control(result,pluginControl,controlValues);
|
this.push_control(result, pluginControl, controlValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -647,8 +690,8 @@ const PluginControlView =
|
|||||||
result.push((
|
result.push((
|
||||||
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
|
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
|
||||||
<div className={classes.portGroupTitle}>
|
<div className={classes.portGroupTitle}>
|
||||||
<Tooltip title={controlGroup.name}
|
<Tooltip title={controlGroup.name}
|
||||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||||
>
|
>
|
||||||
<Typography noWrap variant="caption" >{controlGroup.name}</Typography>
|
<Typography noWrap variant="caption" >{controlGroup.name}</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -663,11 +706,21 @@ const PluginControlView =
|
|||||||
));
|
));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
result.push((
|
if (this.fullScreen())
|
||||||
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
|
{
|
||||||
{node as ReactNode}
|
result.push(
|
||||||
</div>
|
<div style={{position: "relative", width: "100%", height:"100%"}}
|
||||||
));
|
>
|
||||||
|
{node as ReactNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result.push((
|
||||||
|
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
|
||||||
|
{node as ReactNode}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -686,7 +739,7 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MidiChannelBindingControl key="channelBindingCtl" midiChannelBinding={pedalboardItem.midiChannelBinding}
|
<MidiChannelBindingControl key="channelBindingCtl" midiChannelBinding={pedalboardItem.midiChannelBinding}
|
||||||
onChange={(result)=> {
|
onChange={(result) => {
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -694,6 +747,12 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fullScreen() {
|
||||||
|
if (this.props.customization) {
|
||||||
|
return this.props.customization.fullScreen();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
this.controlKeyIndex = 0;
|
this.controlKeyIndex = 0;
|
||||||
@@ -734,20 +793,26 @@ const PluginControlView =
|
|||||||
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
||||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||||
let controlNodes: ControlNodes;
|
let controlNodes: ControlNodes;
|
||||||
|
let frameClass = classes.frame;
|
||||||
|
|
||||||
|
if (this.fullScreen()) {
|
||||||
|
gridClass = classes.noScrollGrid;
|
||||||
|
scrollClass = classes.frameScrollNone;
|
||||||
|
frameClass = classes.noScrollFrame;
|
||||||
|
}
|
||||||
|
|
||||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||||
|
|
||||||
if (this.props.customization) {
|
if (this.props.customization) {
|
||||||
// allow wrapper class to insert/remove/rebuild controls.
|
// allow wrapper class to insert/remove/rebuild controls.
|
||||||
controlNodes = this.props.customization.ModifyControls(controlNodes);
|
controlNodes = this.props.customization.modifyControls(controlNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodes = this.controlNodesToNodes(controlNodes);
|
let nodes = this.controlNodesToNodes(controlNodes);
|
||||||
|
|
||||||
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding)
|
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding) {
|
||||||
{
|
|
||||||
pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue();
|
pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue();
|
||||||
|
|
||||||
}
|
}
|
||||||
if (pedalboardItem.midiChannelBinding) {
|
if (pedalboardItem.midiChannelBinding) {
|
||||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||||
@@ -755,7 +820,7 @@ const PluginControlView =
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.frame}>
|
<div className={frameClass}>
|
||||||
<div className={classes.vuMeterL}>
|
<div className={classes.vuMeterL}>
|
||||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||||
</div>
|
</div>
|
||||||
@@ -768,9 +833,11 @@ const PluginControlView =
|
|||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
||||||
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
{!this.fullScreen() && (
|
||||||
|
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
(!this.state.landscapeGrid) && (
|
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
|
||||||
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -785,7 +852,7 @@ const PluginControlView =
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ showFileDialog: false });
|
this.setState({ showFileDialog: false });
|
||||||
}}
|
}}
|
||||||
onApply={(fileProperty,selectedFile) => {
|
onApply={(fileProperty, selectedFile) => {
|
||||||
this.model.setPatchProperty(
|
this.model.setPatchProperty(
|
||||||
this.props.instanceId,
|
this.props.instanceId,
|
||||||
fileProperty.patchProperty,
|
fileProperty.patchProperty,
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ const ToobCabSimView =
|
|||||||
this.state = {
|
this.state = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
controls.splice(0,0,
|
controls.splice(0,0,
|
||||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ const ToobInputStageView =
|
|||||||
this.state = {
|
this.state = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
return controls;
|
return controls;
|
||||||
// let group = controls[1] as ControlGroup;
|
// let group = controls[1] as ControlGroup;
|
||||||
|
|||||||
@@ -103,8 +103,11 @@ const ToobMLView =
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.removeGainEnabledSubscription();
|
this.removeGainEnabledSubscription();
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||||
// Find EQ group
|
// Find EQ group
|
||||||
// let group = controls.find((control) => typeof control !== 'string' && (control as ControlGroup).name === "EQ") as ControlGroup;
|
// let group = controls.find((control) => typeof control !== 'string' && (control as ControlGroup).name === "EQ") as ControlGroup;
|
||||||
// if (group) {
|
// if (group) {
|
||||||
|
|||||||
@@ -0,0 +1,733 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Pause from '@mui/icons-material/Pause';
|
||||||
|
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||||
|
import FastForward from '@mui/icons-material/FastForward';
|
||||||
|
import FastRewind from '@mui/icons-material/FastRewind';
|
||||||
|
import { PiPedalModelFactory } from './PiPedalModel';
|
||||||
|
import ButtonTooltip from './ButtonTooltip';
|
||||||
|
import { pathFileNameOnly } from './FilePropertyDialog'
|
||||||
|
import ButtonBase from '@mui/material/ButtonBase';
|
||||||
|
import FilePropertyDialog from './FilePropertyDialog';
|
||||||
|
import JsonAtom from './JsonAtom';
|
||||||
|
import { UiFileProperty } from './Lv2Plugin';
|
||||||
|
import { Divider } from '@mui/material';
|
||||||
|
import useWindowSize from './UseWindowSize';
|
||||||
|
|
||||||
|
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
|
||||||
|
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
|
||||||
|
class PluginState {
|
||||||
|
// Must match PluginState values in ToobPlayer.hpp
|
||||||
|
static Idle = 0.0;
|
||||||
|
static CuePlaying = 1;
|
||||||
|
static CuePlayPaused = 2.0;
|
||||||
|
static Pausing = 3;
|
||||||
|
static Paused = 4;
|
||||||
|
static Playing = 5;
|
||||||
|
static Error = 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WallPaper = styled('div')({
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: 'linear-gradient(rgb(255, 38, 142) 0%, rgb(255, 105, 79) 100%)',
|
||||||
|
transition: 'all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s',
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
width: '140%',
|
||||||
|
height: '140%',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-40%',
|
||||||
|
right: '-50%',
|
||||||
|
background:
|
||||||
|
'radial-gradient(at center center, rgb(62, 79, 249) 0%, rgba(62, 79, 249, 0) 64%)',
|
||||||
|
},
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
width: '140%',
|
||||||
|
height: '140%',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-50%',
|
||||||
|
left: '-30%',
|
||||||
|
background:
|
||||||
|
'radial-gradient(at center center, rgb(247, 237, 225) 0%, rgba(247, 237, 225, 0) 70%)',
|
||||||
|
transform: 'rotate(30deg)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function jsonToPath(json: string) {
|
||||||
|
try {
|
||||||
|
let o = JSON.parse(json);
|
||||||
|
return o.value;
|
||||||
|
}
|
||||||
|
catch (_) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetProps {
|
||||||
|
noBorders: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetBorders = styled('div')(({ theme }) => ({
|
||||||
|
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 16,
|
||||||
|
minWidth: 300,
|
||||||
|
maxWidth: 800,
|
||||||
|
width: "70%",
|
||||||
|
margin: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||||
|
boxShadow: "1px 3px 20px #0008",
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WidgetNoBorders = styled('div')(({ theme }) => ({
|
||||||
|
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
margin: 0,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
function Widget(props: WidgetProps) {
|
||||||
|
if (props.noBorders) {
|
||||||
|
return (<WidgetNoBorders>{props.children} </WidgetNoBorders>);
|
||||||
|
} else {
|
||||||
|
return (<WidgetBorders>{props.children} </WidgetBorders>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CoverImage = styled('div')({
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
objectFit: 'cover',
|
||||||
|
overflow: 'hidden',
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||||
|
'& > img': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const TinyText = styled(Typography)({
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
opacity: 0.9,
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export interface ToobPlayerControlProps {
|
||||||
|
instanceId: number;
|
||||||
|
extraControls: React.ReactElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToobPlayerControl(
|
||||||
|
props: ToobPlayerControlProps
|
||||||
|
) {
|
||||||
|
const defaultCoverArt = "/img/default_album.jpg";
|
||||||
|
const [duration, setDuration] = React.useState(0.0);
|
||||||
|
const [position, setPosition] = React.useState(0.0);
|
||||||
|
const [dragging, setDragging] = React.useState(false);
|
||||||
|
const [pluginState, setPluginState] = React.useState(0.0);
|
||||||
|
const [sliderValue, setSliderValue] = React.useState(0.0);
|
||||||
|
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
||||||
|
const [audioFile, setAudioFile] = React.useState("");
|
||||||
|
const [title, setTitle] = React.useState("");
|
||||||
|
const [album, setAlbum] = React.useState("");
|
||||||
|
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
||||||
|
|
||||||
|
const model = PiPedalModelFactory.getInstance();
|
||||||
|
|
||||||
|
const [size] = useWindowSize();
|
||||||
|
const width = size.width;
|
||||||
|
const height = size.height;
|
||||||
|
|
||||||
|
const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500;
|
||||||
|
|
||||||
|
const useHorizontalLayout = height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK && width > 573 && width > height;
|
||||||
|
const useVerticalScroll = width < 573;
|
||||||
|
const useQuadMixPanel = width < 720;
|
||||||
|
const noBorders = width < 420 || height < 720;
|
||||||
|
|
||||||
|
function SelectFile() {
|
||||||
|
setShowFileDialog(true);
|
||||||
|
}
|
||||||
|
function formatDuration(value_: number) {
|
||||||
|
let value = Math.ceil(value_);
|
||||||
|
const minute = Math.floor(value / 60);
|
||||||
|
const secondLeft = value - minute * 60;
|
||||||
|
return `${minute}:${secondLeft < 10 ? `0${secondLeft}` : secondLeft}`;
|
||||||
|
}
|
||||||
|
function onAudioFileChanged(path: string) {
|
||||||
|
setAudioFile(path);
|
||||||
|
model.getAudioFileMetadata(path)
|
||||||
|
.then((metadata) => {
|
||||||
|
if (metadata.track !== "") {
|
||||||
|
let track = metadata.track;
|
||||||
|
if (!track.endsWith('.')) {
|
||||||
|
track += '.';
|
||||||
|
}
|
||||||
|
setTitle(track + " " + metadata.title);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setTitle(metadata.title);
|
||||||
|
}
|
||||||
|
setAlbum(metadata.album);
|
||||||
|
})
|
||||||
|
.catch(()=>{
|
||||||
|
setTitle("#error");
|
||||||
|
setAlbum("");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
function ControlCluster() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mt: -1,
|
||||||
|
'& svg': {
|
||||||
|
color: '#000',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
color: '#fff',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ButtonTooltip title="Previous track">
|
||||||
|
<IconButton
|
||||||
|
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||||
|
onClick={() => {
|
||||||
|
if (position > 3) {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"stop",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onPreviousTrack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FastRewind fontSize="large" />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
<ButtonTooltip title="Play/Pause">
|
||||||
|
<IconButton
|
||||||
|
|
||||||
|
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||||
|
onClick={() => {
|
||||||
|
if (pluginState != PluginState.Idle) {
|
||||||
|
if (paused) {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"play",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"pause",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{paused ? (
|
||||||
|
<PlayArrow sx={{ fontSize: '3rem' }} />
|
||||||
|
) : (
|
||||||
|
<Pause sx={{ fontSize: '3rem' }} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
<ButtonTooltip title="Next track">
|
||||||
|
<IconButton
|
||||||
|
style={{ opacity: (pluginState == PluginState.Idle ? 0.2 : 0.7) }}
|
||||||
|
onClick={() => { onNextTrack(); }}
|
||||||
|
>
|
||||||
|
<FastForward fontSize="large" />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function FilePanel() {
|
||||||
|
return (
|
||||||
|
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
|
||||||
|
onClick={() => { SelectFile() }}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', padding: "2px" }}>
|
||||||
|
<CoverImage>
|
||||||
|
<img style={{ opacity: pluginState === PluginState.Idle ? 0.3 : 1.0 }}
|
||||||
|
src={
|
||||||
|
coverArt
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CoverImage>
|
||||||
|
<Box sx={{ ml: 1.5, minWidth: 0 }}>
|
||||||
|
{pluginState === PluginState.Idle ? (
|
||||||
|
<Typography variant="caption" noWrap>
|
||||||
|
Tap to select
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Typography variant="body1" noWrap>
|
||||||
|
{effectiveTitle}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" noWrap sx={{}}>
|
||||||
|
{album}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
function getUiFileProperty(uri: string): UiFileProperty {
|
||||||
|
let pedalboardItem = model.pedalboard.get().getItem(props.instanceId);
|
||||||
|
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
|
||||||
|
if (!uiPlugin) {
|
||||||
|
throw "uiPlugin not found.";
|
||||||
|
}
|
||||||
|
for (let property of uiPlugin.fileProperties) {
|
||||||
|
if (property.patchProperty === uri) {
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "FileProperty not found.";
|
||||||
|
}
|
||||||
|
function onNextTrack() {
|
||||||
|
model.getNextAudioFile(audioFile)
|
||||||
|
.then((file) => {
|
||||||
|
let json = JsonAtom.Path(file);
|
||||||
|
model.setPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
|
json);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
function onPreviousTrack() {
|
||||||
|
model.getPreviousAudioFile(audioFile)
|
||||||
|
.then((file) => {
|
||||||
|
let json = JsonAtom.Path(file);
|
||||||
|
model.setPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
|
json);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function OnSeek(value: number) {
|
||||||
|
setDragging(true);
|
||||||
|
model.setPatchProperty(props.instanceId, Player__seek, value)
|
||||||
|
.then(() => {
|
||||||
|
setPosition(value);
|
||||||
|
setDragging(false);
|
||||||
|
}).catch((e) => {
|
||||||
|
console.warn("Seek error. " + e.toString());
|
||||||
|
setPosition(value);
|
||||||
|
setDragging(false);
|
||||||
|
});
|
||||||
|
setSliderValue(value);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
||||||
|
(value) => {
|
||||||
|
setDuration(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let positionHandle = model.monitorPort(props.instanceId, "position", 1.0,
|
||||||
|
(value) => {
|
||||||
|
setPosition(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
|
||||||
|
(value) => {
|
||||||
|
setPluginState(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let filePropertyHandle = model.monitorPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
|
(instanceId: number, propertyUri: string, atomObject: any) => {
|
||||||
|
if (typeof (atomObject) === "object") {
|
||||||
|
let path = atomObject.value;
|
||||||
|
onAudioFileChanged(path);
|
||||||
|
} else if (typeof(atomObject) === "string")
|
||||||
|
{
|
||||||
|
onAudioFileChanged(atomObject as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
|
||||||
|
.then((o) => {
|
||||||
|
let path = o.value;
|
||||||
|
onAudioFileChanged(path);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
model.unmonitorPort(durationHandle);
|
||||||
|
model.unmonitorPort(positionHandle);
|
||||||
|
model.unmonitorPort(pluginStateHandle);
|
||||||
|
model.cancelMonitorPatchProperty(filePropertyHandle);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const effectivePosition =
|
||||||
|
(pluginState !== PluginState.Idle) ? Math.min(position, duration) : 0.0;
|
||||||
|
const effectiveTitle = title !== "" ? title : pathFileNameOnly(audioFile);
|
||||||
|
|
||||||
|
const paused = (pluginState === PluginState.Idle
|
||||||
|
|| pluginState === PluginState.CuePlayPaused
|
||||||
|
|| pluginState === PluginState.Paused);
|
||||||
|
|
||||||
|
function LinearMixPanel() {
|
||||||
|
return (
|
||||||
|
<Box style={{ width: "100%" }} >
|
||||||
|
<div style={{ display: "flex", flex: "0 0 auto", justifyContent: "center", alignItems: 'center', flexFlow: "row nowrap" }}>
|
||||||
|
{props.extraControls}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
function QuadMixPanel() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", width: "100%", flex: "0 0 auto", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||||
|
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
||||||
|
{props.extraControls[0]}
|
||||||
|
{props.extraControls[1]}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
||||||
|
{props.extraControls[2]}
|
||||||
|
{props.extraControls[3]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function SliderCluster() {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
|
||||||
|
<Slider
|
||||||
|
value={dragging ? sliderValue : effectivePosition}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
max={duration}
|
||||||
|
onChange={(_, val) => {
|
||||||
|
let v = val as number;
|
||||||
|
setPosition(v);
|
||||||
|
setSliderValue(v);
|
||||||
|
}}
|
||||||
|
onChangeCommitted={(e, value) => {
|
||||||
|
let v = value as number;
|
||||||
|
OnSeek(v);
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
setDragging(true);
|
||||||
|
}}
|
||||||
|
onPointerUp={(e) => {
|
||||||
|
// setDragging(false);
|
||||||
|
}}
|
||||||
|
disabled={duration == 0}
|
||||||
|
sx={(t) => ({
|
||||||
|
width: "100%",
|
||||||
|
color: 'rgba(0,0,0,0.87)',
|
||||||
|
height: 4,
|
||||||
|
'& .MuiSlider-thumb': {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
|
||||||
|
'&::before': {
|
||||||
|
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
||||||
|
},
|
||||||
|
'&:hover, &.Mui-focusVisible': {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'&.Mui-active': {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiSlider-rail': {
|
||||||
|
opacity: 0.28,
|
||||||
|
},
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
color: '#fff',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "0 0 auto",
|
||||||
|
width: "100%",
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: -4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
||||||
|
<TinyText>{formatDuration(duration)}</TinyText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function VerticalWidget() {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: "relative", width: "100%", height: "100%",
|
||||||
|
display: "flex", flexFlow: "column nowrap"
|
||||||
|
}} >
|
||||||
|
<div style={{ flex: "1 1 1px" }} />
|
||||||
|
|
||||||
|
<Widget noBorders={noBorders}>
|
||||||
|
{
|
||||||
|
FilePanel()
|
||||||
|
}
|
||||||
|
<Slider
|
||||||
|
value={dragging ? sliderValue : effectivePosition}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
max={duration}
|
||||||
|
onChange={(_, val) => {
|
||||||
|
let v = val as number;
|
||||||
|
setPosition(v);
|
||||||
|
setSliderValue(v);
|
||||||
|
}}
|
||||||
|
onChangeCommitted={(e, value) => {
|
||||||
|
let v = value as number;
|
||||||
|
OnSeek(v);
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
setDragging(true);
|
||||||
|
}}
|
||||||
|
onPointerUp={(e) => {
|
||||||
|
// setDragging(false);
|
||||||
|
}}
|
||||||
|
disabled={duration == 0}
|
||||||
|
sx={(t) => ({
|
||||||
|
width: "100%",
|
||||||
|
color: 'rgba(0,0,0,0.87)',
|
||||||
|
height: 4,
|
||||||
|
'& .MuiSlider-thumb': {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
|
||||||
|
'&::before': {
|
||||||
|
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
||||||
|
},
|
||||||
|
'&:hover, &.Mui-focusVisible': {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'&.Mui-active': {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiSlider-rail': {
|
||||||
|
opacity: 0.28,
|
||||||
|
},
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
color: '#fff',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mt: -2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
||||||
|
<TinyText>{formatDuration(duration)}</TinyText>
|
||||||
|
</Box>
|
||||||
|
{
|
||||||
|
ControlCluster()
|
||||||
|
}
|
||||||
|
<Divider style={{ marginTop: 8, marginBottom: 8 }} />
|
||||||
|
{useQuadMixPanel ? QuadMixPanel()
|
||||||
|
:
|
||||||
|
LinearMixPanel()
|
||||||
|
}
|
||||||
|
</Widget>
|
||||||
|
<div style={{ flex: "2 2 1px" }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HorizontalWidget() {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: "relative", width: "100%", height: "100%",
|
||||||
|
display: "flex", flexFlow: "column nowrap"
|
||||||
|
}} >
|
||||||
|
|
||||||
|
<Widget noBorders={true}>
|
||||||
|
<div style={{
|
||||||
|
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "stretch",
|
||||||
|
width: "100%"
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "stretch", flexFlow: "column nowrap", flex: "1 1 100%"
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<div style={{ display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
||||||
|
<div style={{ position: "relative", display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
||||||
|
{
|
||||||
|
FilePanel()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: "0 0 auto" }}>
|
||||||
|
{
|
||||||
|
SliderCluster()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
position: "relative", display: "flex", flexFlow: "row nowrap", flex: "0 0 auto"
|
||||||
|
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
ControlCluster()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: "0 0 auto" }}>
|
||||||
|
{LinearMixPanel()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</Widget>
|
||||||
|
<div style={{ flex: "2 2 1px" }} />
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="toobPlayerViewFrame" style={{
|
||||||
|
width: '100%', height: "100%", overflow: 'hidden', position: 'relative',
|
||||||
|
}}>
|
||||||
|
<WallPaper />
|
||||||
|
{
|
||||||
|
useHorizontalLayout ?
|
||||||
|
HorizontalWidget() : VerticalWidget()
|
||||||
|
}
|
||||||
|
{showFileDialog && (
|
||||||
|
<FilePropertyDialog open={showFileDialog}
|
||||||
|
fileProperty={getUiFileProperty(AUDIO_FILE_PROPERTY_URI)}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
selectedFile={audioFile}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowFileDialog(false);
|
||||||
|
}}
|
||||||
|
onApply={(fileProperty, selectedFile) => {
|
||||||
|
model.setPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
|
JsonAtom.Path(selectedFile)
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
model.showAlert("Unable to complete the operation. " + error);
|
||||||
|
});
|
||||||
|
|
||||||
|
}}
|
||||||
|
onOk={(fileProperty, selectedFile) => {
|
||||||
|
|
||||||
|
model.setPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
fileProperty.patchProperty,
|
||||||
|
JsonAtom.Path(selectedFile)
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
model.showAlert("Unable to complete the operation. " + error);
|
||||||
|
});
|
||||||
|
setShowFileDialog(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2022 Robin 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.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import WithStyles from './WithStyles';
|
||||||
|
import { createStyles } from './WithStyles';
|
||||||
|
|
||||||
|
import { withStyles } from "tss-react/mui";
|
||||||
|
|
||||||
|
import IControlViewFactory from './IControlViewFactory';
|
||||||
|
import { PiPedalModelFactory, PiPedalModel, MonitorPortHandle } from "./PiPedalModel";
|
||||||
|
import { PedalboardItem } from './Pedalboard';
|
||||||
|
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||||
|
// import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||||
|
import ToobPlayerControl from './ToobPlayerControl';
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => createStyles({
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ToobPlayerProps extends WithStyles<typeof styles> {
|
||||||
|
instanceId: number;
|
||||||
|
item: PedalboardItem;
|
||||||
|
|
||||||
|
}
|
||||||
|
interface ToobPlayerState {
|
||||||
|
playPosition: number;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToobPlayerView =
|
||||||
|
withStyles(
|
||||||
|
class extends React.Component<ToobPlayerProps, ToobPlayerState>
|
||||||
|
implements ControlViewCustomization {
|
||||||
|
model: PiPedalModel;
|
||||||
|
gainRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
|
customizationId: number = 1;
|
||||||
|
|
||||||
|
constructor(props: ToobPlayerProps) {
|
||||||
|
super(props);
|
||||||
|
this.model = PiPedalModelFactory.getInstance();
|
||||||
|
this.gainRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
playPosition: 0.0,
|
||||||
|
duration: 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribedId?: number = undefined;
|
||||||
|
monitorPlayPositionHandle?: MonitorPortHandle = undefined;
|
||||||
|
monitorDurationHandle?: MonitorPortHandle = undefined;
|
||||||
|
removePortSubscriptions() {
|
||||||
|
|
||||||
|
if (this.monitorPlayPositionHandle) {
|
||||||
|
this.model.unmonitorPort(this.monitorPlayPositionHandle);
|
||||||
|
this.monitorPlayPositionHandle = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPortSubscriptions(instanceId: number) {
|
||||||
|
this.removePortSubscriptions();
|
||||||
|
this.subscribedId = instanceId;
|
||||||
|
this.monitorPlayPositionHandle = this.model.monitorPort(instanceId, "position", 1 / 15.0,
|
||||||
|
(value: number) => {
|
||||||
|
this.setState({ playPosition: value });
|
||||||
|
});
|
||||||
|
this.monitorDurationHandle = this.model.monitorPort(instanceId, "duration", 1 / 15.0,
|
||||||
|
(value: number) => {
|
||||||
|
this.setState({ duration: value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// componentDidUpdate() {
|
||||||
|
// if (this.props.instanceId !== this.subscribedId) {
|
||||||
|
// this.removeGainEnabledSubscription();
|
||||||
|
// this.addGainEnabledSubscription(this.props.instanceId);
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
componentDidMount() {
|
||||||
|
this.addPortSubscriptions(this.props.instanceId);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.removePortSubscriptions();
|
||||||
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||||
|
let extraControls: React.ReactElement[] = [];
|
||||||
|
let mixPanel = controls[3] as ControlGroup;
|
||||||
|
let iKey = 0;
|
||||||
|
for (let mixControl of mixPanel.controls) {
|
||||||
|
extraControls.push(
|
||||||
|
(
|
||||||
|
<div key={"k"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
|
||||||
|
{mixControl}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let panel = (
|
||||||
|
<ToobPlayerControl key="MusicPlayer" instanceId={this.props.instanceId} extraControls={extraControls} />
|
||||||
|
);
|
||||||
|
|
||||||
|
let result: (React.ReactNode | ControlGroup)[] = [];
|
||||||
|
result.push(panel);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PluginControlView
|
||||||
|
instanceId={this.props.instanceId}
|
||||||
|
item={this.props.item}
|
||||||
|
customization={this}
|
||||||
|
customizationId={this.customizationId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
styles
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ToobPlayerViewFactory implements IControlViewFactory {
|
||||||
|
uri: string = "http://two-play.com/plugins/toob-player";
|
||||||
|
|
||||||
|
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
|
||||||
|
return (<ToobPlayerView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
export default ToobPlayerViewFactory;
|
||||||
@@ -126,8 +126,11 @@ const ToobPowerstage2View =
|
|||||||
this.isControlMounted = false;
|
this.isControlMounted = false;
|
||||||
this.maybeListenForAtomOutput();
|
this.maybeListenForAtomOutput();
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
let time = Date.now()*0.001;
|
let time = Date.now()*0.001;
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,11 @@ const ToobSpectrumAnalyzerView =
|
|||||||
this.state = {
|
this.state = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
controls.splice(0,0,
|
controls.splice(0,0,
|
||||||
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
|
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
|
||||||
|
|||||||
@@ -86,8 +86,11 @@ const ToobToneStackView =
|
|||||||
this.controlValueChangedHandle = undefined;
|
this.controlValueChangedHandle = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fullScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||||
{
|
{
|
||||||
if (this.state.isBaxandall)
|
if (this.state.isBaxandall)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Robin E. R. Davies
|
||||||
|
* All rights reserved.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
const getSize = () => {
|
||||||
|
return {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface WindowSize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
export default function useWindowSize() {
|
||||||
|
|
||||||
|
const [size, setSize] = useState<WindowSize>(getSize());
|
||||||
|
|
||||||
|
const handleResize = useCallback(() => {
|
||||||
|
let ticking = false;
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
setSize(getSize());
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [size];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user