diff --git a/CMakeLists.txt b/CMakeLists.txt
index dd38c7d..29d9a7c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -119,6 +119,7 @@ if(DEFINED ANDROID_NDK)
set(USE_SPEEX OFF CACHE BOOL "" FORCE)
set(USE_G719 OFF CACHE BOOL "" FORCE)
set(USE_VORBIS OFF CACHE BOOL "" FORCE)
+ set(BUILD_FMT ON CACHE BOOL "" FORCE)
endif()
if (BUILD_SDL)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
diff --git a/add-mime-types.sh b/add-mime-types.sh
new file mode 100755
index 0000000..f0a0960
--- /dev/null
+++ b/add-mime-types.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+OLDDIR="$(pwd)"
+cd "$(dirname "$0")"
+xdg-mime install assets/zsm-mime.xml
+cd "$OLDDIR"
diff --git a/assets/com.complecwaft.Looper.desktop b/assets/com.complecwaft.Looper.desktop
index 9328c4f..b0a40d2 100755
--- a/assets/com.complecwaft.Looper.desktop
+++ b/assets/com.complecwaft.Looper.desktop
@@ -7,7 +7,7 @@ GenericName=Looping audio player
Exec=looper -n %f
Icon=looper
StartupWMClass=looper;com.complecwaft.Looper;com.complecwaft.Looper.GTK
-MimeType=audio/x-wav;audio/ogg;audio/x-vorbis+ogg;audio/x-opus+ogg;audio/mpeg;audio/flac;audio/xm;audio/x-mod;
+MimeType=audio/x-wav;audio/ogg;audio/x-vorbis+ogg;audio/x-opus+ogg;audio/mpeg;audio/flac;audio/xm;audio/x-mod;audio/x-zsound;
Categories=Audio;AudioVideo;
Terminal=false
SingleMainWindow=true
diff --git a/assets/zsm-mime.xml b/assets/zsm-mime.xml
new file mode 100644
index 0000000..d7374c7
--- /dev/null
+++ b/assets/zsm-mime.xml
@@ -0,0 +1,11 @@
+
+
+
+ ZSound file for the Commander X16
+
+
+
+
+
+
+
diff --git a/backends/playback/zsm/properties.inc b/backends/playback/zsm/properties.inc
new file mode 100644
index 0000000..379722b
--- /dev/null
+++ b/backends/playback/zsm/properties.inc
@@ -0,0 +1,10 @@
+#define BOOL_PROPERTY(name, default_value) _PROPERTY(name, bool, default_value)
+#define DOUBLE_PROPERTY(name, default_value) _PROPERTY(name, double, default_value)
+BOOL_PROPERTY(pcm_enable, true)
+BOOL_PROPERTY(psg_enable, true)
+BOOL_PROPERTY(fm_enable, true)
+DOUBLE_PROPERTY(pcm_volume, 1.0)
+DOUBLE_PROPERTY(psg_volume, 1.0)
+DOUBLE_PROPERTY(fm_volume, 1.0)
+#undef BOOL_PROPERTY
+#undef DOUBLE_PROPERTY
diff --git a/backends/playback/zsm/zsm_backend.cpp b/backends/playback/zsm/zsm_backend.cpp
index 91cb1d4..c9b497d 100644
--- a/backends/playback/zsm/zsm_backend.cpp
+++ b/backends/playback/zsm/zsm_backend.cpp
@@ -17,17 +17,26 @@ extern "C" {
#define HZ (AUDIO_SAMPLERATE)
#define BUFFERS 32
#define _PROPERTY(name, type, default_value) \
-bool ZsmBackend::name() { \
+type ZsmBackend::name() { \
std::optional value_maybe = get(#name); \
if (value_maybe.has_value()) { \
- return resolve_##type(value_maybe.value()); \
+ return resolve_value(value_maybe.value()); \
} \
return default_value; \
}
-#define BOOL_PROPERTY(name, default_value) _PROPERTY(name, bool, default_value)
-BOOL_PROPERTY(pcm_enable, true)
-BOOL_PROPERTY(psg_enable, true)
-BOOL_PROPERTY(fm_enable, true)
+
+#include "properties.inc"
+#undef _PROPERTY
+std::vector ZsmBackend::get_property_list() {
+ std::vector properties;
+ properties.push_back(make_property(PropertyType::Boolean, "Enable PCM channel", "pcm_enable"));
+ properties.push_back(make_property(PropertyType::Boolean, "Enable PSG channels", "psg_enable"));
+ properties.push_back(make_property(PropertyType::Boolean, "Enable FM channels", "fm_enable"));
+ properties.push_back(make_property(PropertyType::Double, "Volume of PCM channel", "pcm_volume", make_hint(0.0, 1.0)));
+ properties.push_back(make_property(PropertyType::Double, "Volume of PSG channels", "psg_volume", make_hint(0.0, 1.0)));
+ properties.push_back(make_property(PropertyType::Double, "Volume of FM channels", "fm_volume", make_hint(0.0, 1.0)));
+ return properties;
+}
void ZsmBackend::load(const char *filename) {
memset(&spec, 0, sizeof(spec));
spec.format = AUDIO_S16SYS;
@@ -129,6 +138,22 @@ void ZsmBackend::load(const char *filename) {
loop_start = this->loop_pos;
fm_stream = SDL_NewAudioStream(AUDIO_S16SYS, 2, YM_FREQ, AUDIO_S16SYS, 2, PSG_FREQ);
DEBUG.writefln("fm_stream: %ld -> %ld", YM_FREQ, PSG_FREQ);
+#define _PROPERTY(name, type, default_value) \
+ { \
+ std::string type_str = #type; \
+ google::protobuf::Any value; \
+ if (type_str == "bool") { \
+ BooleanProperty value_b; \
+ value_b.set_value(default_value); \
+ value.PackFrom(value_b); \
+ } else if (type_str == "double") { \
+ DoubleProperty value_d; \
+ value_d.set_value(default_value); \
+ value.PackFrom(value_d); \
+ } \
+ property_defaults[#name] = value; \
+ }
+#include "properties.inc"
}
extern SDL_AudioSpec obtained;
void ZsmBackend::switch_stream(int idx) {
@@ -399,3 +424,61 @@ double ZsmBackend::get_position() {
int ZsmBackend::get_stream_idx() {
return 0;
}
+
+void ZsmBackend::audio_step(size_t samples) {
+ if (samples == 0) return;
+ while (remain != 0 && pcm_fifo_avail() < samples) {
+ if (pcm_read_rate() == 0) break;
+ if ((--remain) == 0) {
+ if (islooped) {
+ cur = loop;
+ remain = loop_rem;
+ } else {
+ break;
+ }
+ }
+ size_t oldpos = file->get_pos();
+ uint8_t sample = audio_sample[cur++];
+ pcm_write_fifo(sample);
+ }
+ samples *= 2;
+ int16_t *psg_ptr = psg_buf.get_item_sized(samples);
+ int16_t *pcm_ptr = pcm_buf.get_item_sized(samples);
+ psg_render(psg_ptr, samples / 2);
+ pcm_render(pcm_ptr, samples / 2);
+ int16_t *out_ptr = out_buf.get_item_sized(samples);
+ // The exact amount of samples needed for the stream.
+ double ratio = ((double)YM_FREQ) / ((double)PSG_FREQ);
+ size_t needed_samples = ((size_t)std::floor(samples * ratio)) / 2;
+ int16_t *ym_ptr = ym_buf.get_item_sized(needed_samples * 2);
+ YM_stream_update(ym_ptr, needed_samples);
+ assert(SDL_AudioStreamPut(fm_stream, ym_ptr, needed_samples * 2 * sizeof(int16_t)) == 0);
+ while (SDL_AudioStreamAvailable(fm_stream) < ((samples + 2) * sizeof(int16_t))) {
+ YM_stream_update(ym_ptr, 1);
+ assert(SDL_AudioStreamPut(fm_stream, ym_ptr, 2 * sizeof(int16_t)) == 0);
+ }
+ int16_t *ym_resample_ptr = ym_resample_buf.get_item_sized(samples);
+ ssize_t ym_resample_len = SDL_AudioStreamGet(fm_stream, ym_resample_ptr, (samples + 2) * sizeof(int16_t));
+ assert(ym_resample_len >= 0);
+ ym_resample_len /= sizeof(int16_t);
+ for (size_t i = 0; i < samples / 2; i++) {
+ size_t j = i * 2;
+ int16_t psg[2] = {(int16_t)(psg_ptr[j] >> 1), (int16_t)(psg_ptr[j + 1] >> 1)};
+ int16_t pcm[2] = {(int16_t)(pcm_ptr[j] >> 2), (int16_t)(pcm_ptr[j + 1] >> 2)};
+ if (!pcm_enable()) memset(pcm, 0, sizeof(pcm));
+ if (!psg_enable()) memset(psg, 0, sizeof(psg));
+ pcm[0] *= pcm_volume();
+ pcm[1] *= pcm_volume();
+ psg[0] *= psg_volume();
+ psg[1] *= psg_volume();
+ int16_t vera[2] = {(int16_t)(psg[0] + pcm[0]), (int16_t)(psg[1] + pcm[1])};
+ int16_t fm[2] = {ym_resample_ptr[j], ym_resample_ptr[j + 1]};
+ if (!fm_enable()) memset(fm, 0, sizeof(fm));
+ fm[0] *= fm_volume();
+ fm[1] *= fm_volume();
+ int16_t mix[2] = {(int16_t)(vera[0] + (fm[0] >> 1)), (int16_t)(vera[1] + (fm[1] >> 1))};
+ out_ptr[j++] = mix[0];
+ out_ptr[j++] = mix[1];
+ }
+ audio_buf.push(out_ptr, samples);
+}
diff --git a/backends/playback/zsm/zsm_backend.hpp b/backends/playback/zsm/zsm_backend.hpp
index 2b5cf54..90ecfa7 100644
--- a/backends/playback/zsm/zsm_backend.hpp
+++ b/backends/playback/zsm/zsm_backend.hpp
@@ -96,60 +96,10 @@ class ZsmBackend : public PlaybackBackend {
bool pcm_enable();
bool psg_enable();
bool fm_enable();
- int16_t combine_audio(int16_t a, int16_t b) {
- return (int16_t)((((int32_t)a) + ((int32_t)b)) >> 1);
- }
- void audio_step(size_t samples) {
- if (samples == 0) return;
- while (remain != 0 && pcm_fifo_avail() < samples) {
- if (pcm_read_rate() == 0) break;
- if ((--remain) == 0) {
- if (islooped) {
- cur = loop;
- remain = loop_rem;
- } else {
- break;
- }
- }
- size_t oldpos = file->get_pos();
- uint8_t sample = audio_sample[cur++];
- pcm_write_fifo(sample);
- }
- samples *= 2;
- int16_t *psg_ptr = psg_buf.get_item_sized(samples);
- int16_t *pcm_ptr = pcm_buf.get_item_sized(samples);
- psg_render(psg_ptr, samples / 2);
- pcm_render(pcm_ptr, samples / 2);
- int16_t *out_ptr = out_buf.get_item_sized(samples);
- // The exact amount of samples needed for the stream.
- double ratio = ((double)YM_FREQ) / ((double)PSG_FREQ);
- size_t needed_samples = ((size_t)std::floor(samples * ratio)) / 2;
- int16_t *ym_ptr = ym_buf.get_item_sized(needed_samples * 2);
- YM_stream_update(ym_ptr, needed_samples);
- assert(SDL_AudioStreamPut(fm_stream, ym_ptr, needed_samples * 2 * sizeof(int16_t)) == 0);
- while (SDL_AudioStreamAvailable(fm_stream) < ((samples + 2) * sizeof(int16_t))) {
- YM_stream_update(ym_ptr, 1);
- assert(SDL_AudioStreamPut(fm_stream, ym_ptr, 2 * sizeof(int16_t)) == 0);
- }
- int16_t *ym_resample_ptr = ym_resample_buf.get_item_sized(samples);
- ssize_t ym_resample_len = SDL_AudioStreamGet(fm_stream, ym_resample_ptr, (samples + 2) * sizeof(int16_t));
- assert(ym_resample_len >= 0);
- ym_resample_len /= sizeof(int16_t);
- for (size_t i = 0; i < samples / 2; i++) {
- size_t j = i * 2;
- int16_t psg[2] = {psg_ptr[j] >> 1, psg_ptr[j + 1] >> 1};
- int16_t pcm[2] = {pcm_ptr[j] >> 1, pcm_ptr[j + 1] >> 1};
- if (!pcm_enable()) memset(pcm, 0, sizeof(pcm));
- if (!psg_enable()) memset(psg, 0, sizeof(psg));
- int16_t vera[2] = {psg[0] + pcm[0], psg[1] + pcm[1]};
- int16_t fm[2] = {ym_resample_ptr[j], ym_resample_ptr[j + 1]};
- if (!fm_enable()) memset(fm, 0, sizeof(fm));
- int16_t mix[2] = {vera[0] + (fm[0] >> 1), vera[1] + (fm[1] >> 1)};
- out_ptr[j++] = mix[0];
- out_ptr[j++] = mix[1];
- }
- audio_buf.push(out_ptr, samples);
- }
+ double pcm_volume();
+ double psg_volume();
+ double fm_volume();
+ void audio_step(size_t samples);
inline void *reserve(size_t len) {
return (void*)audio_buf.reserve(len);
}
@@ -183,6 +133,7 @@ class ZsmBackend : public PlaybackBackend {
inline std::string get_name() override {
return "ZSM player";
}
+ std::vector get_property_list() override;
void seek(double position) override;
void load(const char *filename) override;
void switch_stream(int idx) override;
diff --git a/backends/ui/imgui/main.cpp b/backends/ui/imgui/main.cpp
index 2ef410c..08d430d 100644
--- a/backends/ui/imgui/main.cpp
+++ b/backends/ui/imgui/main.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include "imgui/imgui.h"
#include "ui_backend.hpp"
#include "thirdparty/CLI11.hpp"
#include
@@ -141,6 +142,9 @@ void MainLoop::FileLoaded() {
SetWindowTitle("Looper");
}
streams = playback->get_streams();
+ properties = playback->get_property_list();
+ boolean_properties.clear();
+ double_properties.clear();
}
void MainLoop::GuiFunction() {
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
@@ -195,10 +199,10 @@ void MainLoop::GuiFunction() {
show_demo_window = !show_demo_window;
set_option("ui.imgui.demo_window", show_demo_window);
}
- ImGui::EndMenu();
if (ImGui::MenuItem(_TR_CTX("Main menu | Debug", "Edit properties"), nullptr, property_editor)) {
property_editor = !property_editor;
}
+ ImGui::EndMenu();
}
if (ImGui::BeginMenu(_TRI_CTX(ICON_FK_INFO_CIRCLE, "Main menu", "Help"))) {
if (ImGui::MenuItem(_TRI_CTX(ICON_FK_INFO, "Main menu | Help", "About"), nullptr, about_window)) {
@@ -304,17 +308,88 @@ void MainLoop::GuiFunction() {
ImGui::SetNextWindowDockID(dockid);
ImGui::Begin("Property Editor", &property_editor);
{
- static std::string property_name;
- static bool property_value;
- ImGui::InputText("Property Name", &property_name);
- ImGui::Checkbox("Enabled", &property_value);
- if (ImGui::Button("Set")) {
- BooleanProperty property;
- property.set_value(property_value);
- google::protobuf::Any value;
- value.PackFrom(property);
- playback->set_property(property_name, value);
- }
+ for (auto property : properties) {
+ ImGui::PushID(property.path().c_str());
+ bool valid = false;
+ switch (property.type()) {
+ case PropertyType::Double: {
+ std::optional min;
+ std::optional max;
+ double value = 0.0;
+ if (double_properties.contains(property.path())) {
+ value = double_properties[property.path()];
+ } else {
+ auto value_to_resolve = playback->get_property(property.path());
+ if (value_to_resolve.has_value()) {
+ value = resolve_value(value_to_resolve.value());
+ }
+ double_properties[property.path()] = value;
+ }
+ if (property.has_hint() && property.hint().has_range()) {
+ auto range = property.hint().range();
+ if (range.has_min() && range.has_max()) {
+ float flt = (float)value;
+ if (ImGui::SliderFloat(property.path().c_str(), &flt, (float)range.min(), (float)range.max())) {
+ double_properties[property.path()] = flt;
+ }
+ valid = true;
+ } else {
+ if (range.has_min()) min = range.min();
+ if (range.has_max()) max = range.max();
+ }
+ }
+ if (!valid) {
+ ImGui::InputDouble(property.path().c_str(), &value);
+ if (min.has_value() && value < min) {
+ value = min.value();
+ }
+ if (max.has_value() && value > max) {
+ value = max.value();
+ }
+ double_properties[property.path()] = value;
+ valid = true;
+ }
+ } break;
+ case PropertyType::Boolean: {
+ bool value = false;
+ if (boolean_properties.contains(property.path())) {
+ value = boolean_properties[property.path()];
+ } else {
+ auto value_to_resolve = playback->get_property(property.path());
+ if (value_to_resolve.has_value()) {
+ value = resolve_value(value_to_resolve.value());
+ }
+ boolean_properties[property.path()] = value;
+ }
+ if (ImGui::Checkbox(property.path().c_str(), &value)) {
+ boolean_properties[property.path()] = value;
+ }
+ valid = true;
+ } break;
+ }
+ if (valid) {
+ ImGui::SameLine();
+ if (ImGui::Button("Set")) {
+ switch (property.type()) {
+ case PropertyType::Double: {
+ DoubleProperty property_d;
+ property_d.set_value(double_properties[property.path()]);
+ google::protobuf::Any value;
+ value.PackFrom(property_d);
+ playback->set_property(property.path(), value);
+ } break;
+ case PropertyType::Boolean: {
+ BooleanProperty property_b;
+ property_b.set_value(boolean_properties[property.path()]);
+ google::protobuf::Any value;
+ value.PackFrom(property_b);
+ playback->set_property(property.path(), value);
+ } break;
+ }
+ }
+ }
+ ImGui::PopID();
+ }
}
ImGui::End();
}
diff --git a/backends/ui/imgui/main.h b/backends/ui/imgui/main.h
index d368792..5f68451 100644
--- a/backends/ui/imgui/main.h
+++ b/backends/ui/imgui/main.h
@@ -12,7 +12,7 @@
#include
#include
#include
-
+#include