Starting to implement memon 1.0.0 saving

This commit is contained in:
Stepland 2022-03-31 03:50:15 +02:00
parent b967bc0499
commit 6fe06dfd80
26 changed files with 19288 additions and 17678 deletions

File diff suppressed because it is too large Load Diff

33
src/better_beats.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "better_beats.hpp"
#include <boost/multiprecision/gmp.hpp>
#include "json.hpp"
bool is_expressible_as_240th(const Fraction& beat) {
return (
(
(240 * boost::multiprecision::numerator(beat))
% boost::multiprecision::denominator(beat)
) == 0
);
};
nlohmann::ordered_json beat_to_best_form(const Fraction& beat) {
if (is_expressible_as_240th(beat)) {
return nlohmann::ordered_json(
(240 * boost::multiprecision::numerator(beat))
/ boost::multiprecision::denominator(beat)
);
} else {
return beat_to_fraction_tuple(beat);
};
};
nlohmann::ordered_json beat_to_fraction_tuple(const Fraction& beat) {
const auto integer_part = static_cast<nlohmann::ordered_json::number_unsigned_t>(beat);
const auto remainder = beat % 1;
return {
integer_part,
static_cast<nlohmann::ordered_json::number_unsigned_t>(boost::multiprecision::numerator(remainder)),
static_cast<nlohmann::ordered_json::number_unsigned_t>(boost::multiprecision::denominator(remainder)),
};
};

9
src/better_beats.hpp Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <json.hpp>
#include "special_numeric_types.hpp"
bool is_expressible_as_240th(const Fraction& beat);
nlohmann::ordered_json beat_to_best_form(const Fraction& beat);
nlohmann::ordered_json beat_to_fraction_tuple(const Fraction& beat);

16
src/better_chart.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "better_chart.hpp"
#include <json.hpp>
namespace better {
nlohmann::ordered_json Chart::dump_for_memon_1_0_0() const {
nlohmann::ordered_json json_chart;
if (level) {
json_chart["level"] = level->format("f");
}
if (timing) {
json_chart["timing"] = timing->dump_for_memon_1_0_0();
}
json_chart["notes"] = notes.dump_for_memon_1_0_0();
}
}

23
src/better_chart.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <memory>
#include <optional>
#include <set>
#include <json.hpp>
#include "better_hakus.hpp"
#include "better_notes.hpp"
#include "better_timing.hpp"
#include "special_numeric_types.hpp"
namespace better {
struct Chart {
std::optional<Decimal> level;
std::optional<Timing> timing;
std::optional<Hakus> hakus;
Notes notes;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
};
}

11
src/better_hakus.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "better_hakus.hpp"
#include "better_beats.hpp"
nlohmann::ordered_json dump_hakus(const Hakus& hakus) {
auto j = nlohmann::ordered_json::array();
for (const auto& haku : hakus) {
j.push_back(beat_to_best_form(haku));
}
return j;
}

11
src/better_hakus.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include <set>
#include <json.hpp>
#include "special_numeric_types.hpp"
using Hakus = std::set<Fraction>;
nlohmann::ordered_json dump_hakus(const Hakus& hakus);

22
src/better_metadata.hpp Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include <string>
#include "special_numeric_types.hpp"
namespace better {
struct PreviewLoop {
Decimal start = 0;
Decimal duration = 0;
};
struct Metadata {
std::string title = "";
std::string artist = "";
std::string audio = "";
std::string jacket = "";
PreviewLoop preview_loop;
std::string preview_file = "";
bool use_preview_file = false;
};
}

View File

@ -1,6 +1,9 @@
#include "better_note.hpp"
#include <variant>
#include "better_beats.hpp"
namespace better {
Position::Position(unsigned int index) : x(index % 4), y (index / 4) {
if (index > 15) {
@ -47,6 +50,13 @@ namespace better {
return position;
};
nlohmann::ordered_json TapNote::dump_for_memon_1_0_0() const {
return {
{"n", position.index()},
{"t", beat_to_best_form(time)}
};
};
LongNote::LongNote(Fraction time, Position position, Fraction duration, Position tail_tip)
:
time(time),
@ -123,8 +133,24 @@ namespace better {
return 90;
}
}
}
};
nlohmann::ordered_json LongNote::dump_for_memon_1_0_0() const {
return {
{"n", position.index()},
{"t", beat_to_best_form(time)},
{"l", beat_to_best_form(duration)},
{"p", tail_as_6_notation()}
};
};
int LongNote::tail_as_6_notation() const {
if (tail_tip.get_y() == position.get_y()) {
return tail_tip.get_x() - static_cast<int>(tail_tip.get_x() > position.get_x());
} else {
return 3 + tail_tip.get_y() - int(tail_tip.get_y() > position.get_y());
}
}
auto _time_bounds = VariantVisitor {
[](const TapNote& t) -> std::pair<Fraction, Fraction> { return {t.get_time(), t.get_time()}; },
@ -146,5 +172,9 @@ namespace better {
Fraction Note::get_end() const {
return this->get_time_bounds().second;
}
nlohmann::ordered_json Note::dump_for_memon_1_0_0() const {
return std::visit([](const auto& n){return n.dump_for_memon_1_0_0();}, this->note);
}
}

View File

@ -6,6 +6,8 @@
#include <utility>
#include <variant>
#include <json.hpp>
#include "special_numeric_types.hpp"
#include "variant_visitor.hpp"
@ -46,6 +48,7 @@ namespace better {
bool operator==(const TapNote&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
private:
Fraction time;
Position position;
@ -64,6 +67,9 @@ namespace better {
unsigned int get_tail_angle() const;
bool operator==(const LongNote&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
int tail_as_6_notation() const;
private:
Fraction time;
Position position;
@ -84,6 +90,8 @@ namespace better {
auto visit(T& visitor) const {return std::visit(visitor, this->note);};
bool operator==(const Note&) const = default;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
private:
std::variant<TapNote, LongNote> note;
};

View File

@ -1,5 +1,9 @@
#include "better_notes.hpp"
#include <SFML/System/Time.hpp>
#include <algorithm>
#include "json.hpp"
namespace better {
std::pair<Notes::iterator, bool> Notes::insert(const Note& note) {
auto conflicting_note = end();
@ -56,4 +60,50 @@ namespace better {
auto it = find(note);
interval_tree::erase(it);
};
bool Notes::is_colliding(const better::Note& note, const better::Timing& timing) {
const auto [start_beat, end_beat] = note.get_time_bounds();
/*
Two notes collide if they are within ~one second of each other :
Approach and burst animations of original jubeat markers last 16 frames
at (supposedly) 30 fps, which means a note needs (a bit more than) half
a second of leeway both before *and* after itself to properly display
its marker animation, consequently, two consecutive notes on the same
button cannot be closer than ~one second from each other.
I don't really know why I shrink the collision zone down here ?
Shouldn't it be 32/30 seconds ? (~1066ms instead of 1000ms ?)
Reverse-engineering of the jubeat plus iOS app suggest the "official"
note collision zone size is 1030 ms, so actually I wasn't that far off
with 1000 ms !
TODO: Make the collision zone customizable
*/
const auto collision_start = timing.beats_at(timing.time_at(start_beat) - sf::seconds(1));
const auto collision_end = timing.beats_at(timing.time_at(end_beat) + sf::seconds(1));
bool found_collision = false;
in(
{collision_start, collision_end},
[&](Notes::const_iterator it){
if (it->second.get_position() == note.get_position()) {
if (it->second != note) {
found_collision = true;
}
}
}
);
return found_collision;
};
nlohmann::ordered_json Notes::dump_for_memon_1_0_0() const {
auto json_notes = nlohmann::ordered_json::array();
for (const auto& [_, note] : *this) {
json_notes.push_back(note.dump_for_memon_1_0_0());
}
return json_notes;
}
}

View File

@ -5,9 +5,12 @@
#include <type_traits>
#include <utility>
#include "interval_tree.hpp"
#include <interval_tree.hpp>
#include <json.hpp>
#include "better_note.hpp"
#include "better_timing.hpp"
#include "json.hpp"
#include "special_numeric_types.hpp"
namespace better {
@ -21,5 +24,14 @@ namespace better {
const_iterator find(const Note& note) const;
bool contains(const Note& note) const;
void erase(const Note& note);
/*
Returns true if the given note (assumed to already be in the container)
is colliding with ANOTHER note. This means notes exactly equal to the
one passed as an argument are NOT taken into account.
*/
bool is_colliding(const better::Note& note, const better::Timing& timing);
nlohmann::ordered_json dump_for_memon_1_0_0() const;
};
}

View File

@ -2,78 +2,67 @@
#include <SFML/System/Time.hpp>
#include "better_hakus.hpp"
#include "json.hpp"
#include "src/better_hakus.hpp"
#include "std_optional_extras.hpp"
#include "variant_visitor.hpp"
namespace better {
std::optional<sf::Time> Chart::time_of_last_event() const {
if (notes.empty()) {
return {};
} else {
return timing.time_at(notes.crbegin()->second.get_end());
}
};
bool Chart::is_colliding(const better::Note& note) {
const auto [start_beat, end_beat] = note.get_time_bounds();
/*
Two notes collide if they are within ~one second of each other :
Approach and burst animations of original jubeat markers last 16 frames
at (supposedly) 30 fps, which means a note needs (a bit more than) half
a second of leeway both before *and* after itself, consequently, two
consecutive notes on the same button cannot be closer than ~one second
from each other.
I don't really know why I shrink the collision zone down here ?
Shouldn't it be 32/30 seconds ? (1.0666... seconds instead of 1 ?)
TODO: Make the collision zone customizable
*/
const auto collision_start = timing.beats_at(timing.time_at(start_beat) - sf::seconds(1));
const auto collision_end = timing.beats_at(timing.time_at(end_beat) + sf::seconds(1));
bool found_collision = false;
notes.in(
{collision_start, collision_end},
[&](Notes::const_iterator it){
if (it->second.get_position() == note.get_position()) {
if (it->second != note) {
found_collision = true;
}
}
}
);
return found_collision;
}
PreviewLoop::PreviewLoop(Decimal start, Decimal duration) :
start(start),
duration(duration)
{
if (start < 0) {
std::stringstream ss;
ss << "Attempted to create a PreviewLoop with negative start ";
ss << "position : " << start;
throw std::invalid_argument(ss.str());
}
if (duration < 0) {
std::stringstream ss;
ss << "Attempted to create a PreviewLoop with negative ";
ss << "duration : " << duration;
throw std::invalid_argument(ss.str());
}
};
Decimal PreviewLoop::get_start() const {
return start;
};
Decimal PreviewLoop::get_duration() const {
return duration;
};
std::string stringify_level(std::optional<Decimal> level) {
return stringify_or(level, "(no level defined)");
};
nlohmann::ordered_json Song::dump_as_memon_1_0_0() const {
nlohmann::ordered_json memon;
memon["version"] = "1.0.0";
auto json_metadata = dump_metadata_1_0_0();
if (not json_metadata.empty()) {
memon["metadata"] = json_metadata;
}
auto json_timing = nlohmann::ordered_json::object();
if (timing) {
json_timing.update(timing->dump_for_memon_1_0_0());
}
if (hakus) {
json_timing["hakus"] = dump_hakus(*hakus);
}
if (not json_timing.empty()) {
memon["timing"] = json_timing;
}
auto json_charts = nlohmann::ordered_json::object();
for (const auto& [name, chart] : charts) {
json_charts[name] = chart.dump_for_memon_1_0_0();
}
return memon;
}
nlohmann::ordered_json Song::dump_metadata_1_0_0() const {
nlohmann::ordered_json json_metadata;
if (not metadata.title.empty()) {
json_metadata["title"] = metadata.title;
}
if (not metadata.artist.empty()) {
json_metadata["artist"] = metadata.artist;
}
if (not metadata.audio.empty()) {
json_metadata["audio"] = metadata.audio;
}
if (not metadata.jacket.empty()) {
json_metadata["jacket"] = metadata.jacket;
}
if (metadata.use_preview_file) {
if (not metadata.preview_file.empty()) {
json_metadata["preview"] = metadata.preview_file;
}
} else {
if (metadata.preview_loop.duration != 0) {
json_metadata["preview"] = {
{"start", metadata.preview_loop.start.format("f")},
{"duration", metadata.preview_loop.duration.format("f")}
};
}
}
return json_metadata;
};
}

View File

@ -8,51 +8,19 @@
#include <string>
#include <tuple>
#include <json.hpp>
#include <SFML/System/Time.hpp>
#include "better_chart.hpp"
#include "better_hakus.hpp"
#include "better_metadata.hpp"
#include "better_notes.hpp"
#include "better_timing.hpp"
#include "special_numeric_types.hpp"
namespace better {
struct Chart {
std::optional<Decimal> level;
Timing timing;
std::optional<std::set<Fraction>> hakus;
Notes notes;
/*
Returns true if the given note (assumed to already be part of the
chart) is colliding with ANOTHER note in the chart. This means notes
exactly equal to the one passed as an argument are NOT taken into
account.
*/
bool is_colliding(const better::Note& note);
std::optional<sf::Time> time_of_last_event() const;
};
std::string stringify_level(std::optional<Decimal> level);
class PreviewLoop {
public:
PreviewLoop(Decimal start, Decimal duration);
Decimal get_start() const;
Decimal get_duration() const;
private:
Decimal start;
Decimal duration;
};
struct Metadata {
std::optional<std::string> title;
std::optional<std::string> artist;
std::optional<std::filesystem::path> audio;
std::optional<std::filesystem::path> jacket;
std::optional<std::variant<PreviewLoop, std::filesystem::path>> preview;
};
const auto difficulty_name_comp_key = [](const std::string& s) {
if (s == "BSC") {
return std::make_tuple(1, std::string{});
@ -76,6 +44,10 @@ namespace better {
decltype(order_by_difficulty_name)
> charts{order_by_difficulty_name};
Metadata metadata;
Timing timing;
std::optional<Timing> timing;
std::optional<Hakus> hakus;
nlohmann::ordered_json dump_as_memon_1_0_0() const;
nlohmann::ordered_json dump_metadata_1_0_0() const;
};
}

View File

@ -1,7 +1,12 @@
#include "better_timing.hpp"
#include <json.hpp>
#include "better_beats.hpp"
#include "src/better_beats.hpp"
namespace better {
BPMAtBeat::BPMAtBeat(Fraction beats, Fraction bpm) : beats(beats), bpm(bpm) {
BPMAtBeat::BPMAtBeat(Fraction beats, Decimal bpm) : beats(beats), bpm(bpm) {
if (bpm <= 0) {
std::stringstream ss;
ss << "Attempted to create a BPMAtBeat with negative BPM : ";
@ -10,14 +15,23 @@ namespace better {
}
};
BPMEvent::BPMEvent(Fraction beats, Fraction seconds, Fraction bpm) :
BPMEvent::BPMEvent(Fraction beats, Fraction seconds, Decimal bpm) :
BPMAtBeat(beats, bpm),
seconds(seconds)
{};
Fraction BPMAtBeat::get_beats() const {
return beats;
}
Decimal BPMAtBeat::get_bpm() const {
return bpm;
}
Fraction BPMEvent::get_seconds() const {
return seconds;
};
/*
Create a Time Map from a list of BPM changes with times given in
@ -75,7 +89,7 @@ namespace better {
auto beats_since_last_event =
current->get_beats() - previous->get_beats();
auto seconds_since_last_event =
(60 * beats_since_last_event) / previous->get_bpm();
(60 * beats_since_last_event) / convert_to_fraction(previous->get_bpm());
current_second += seconds_since_last_event;
bpm_changes.emplace_back(
current->get_beats(),
@ -125,7 +139,7 @@ namespace better {
auto seconds_since_previous_event = (
Fraction{60}
* beats_since_previous_event
/ bpm_change->get_bpm()
/ convert_to_fraction(bpm_change->get_bpm())
);
return bpm_change->get_seconds() + seconds_since_previous_event;
};
@ -156,10 +170,27 @@ namespace better {
}
auto seconds_since_previous_event = fractional_seconds - bpm_change->get_seconds();
auto beats_since_previous_event = (
bpm_change->get_bpm()
convert_to_fraction(bpm_change->get_bpm())
* seconds_since_previous_event
/ Fraction{60}
);
return bpm_change->get_beats() + beats_since_previous_event;
};
nlohmann::ordered_json Timing::dump_for_memon_1_0_0() const {
nlohmann::ordered_json j;
j["offset"] = convert_to_decimal(fractional_seconds_at(0), 5).format("f");
auto bpms = nlohmann::ordered_json::array();
for (const auto& bpm_change : events_by_beats) {
nlohmann::ordered_json bpm_event;
bpm_event["beat"] = beat_to_best_form(bpm_change.get_beats());
bpm_event["bpm"] = bpm_change.get_bpm().format("f");
bpms.push_back({
{"beat", beat_to_best_form(bpm_change.get_beats())},
{"bpm", bpm_change.get_bpm().format("f")}
});
}
j["bpms"] = bpms;
return j;
}
}

View File

@ -9,6 +9,7 @@
#include <SFML/System/Time.hpp>
#include "json.hpp"
#include "special_numeric_types.hpp"
namespace better {
@ -19,17 +20,17 @@ namespace better {
class BPMAtBeat {
public:
BPMAtBeat(Fraction beats, Fraction bpm);
BPMAtBeat(Fraction beats, Decimal bpm);
Fraction get_beats() const;
Fraction get_bpm() const;
Decimal get_bpm() const;
private:
Fraction beats;
Fraction bpm;
Decimal bpm;
};
class BPMEvent : public BPMAtBeat {
public:
BPMEvent(Fraction beats, Fraction seconds, Fraction bpm);
BPMEvent(Fraction beats, Fraction seconds, Decimal bpm);
Fraction get_seconds() const;
private:
Fraction seconds;
@ -53,6 +54,8 @@ namespace better {
sf::Time time_between(Fraction beat_a, Fraction beat_b) const;
Fraction beats_at(sf::Time time) const;
nlohmann::ordered_json dump_for_memon_1_0_0() const;
private:
std::set<BPMEvent, decltype(order_by_beats)> events_by_beats{order_by_beats};

View File

@ -19,6 +19,7 @@
#include "imgui_extras.hpp"
#include "metadata_in_gui.hpp"
#include "special_numeric_types.hpp"
#include "src/better_song.hpp"
#include "src/chart.hpp"
#include "std_optional_extras.hpp"
#include "variant_visitor.hpp"
@ -32,7 +33,6 @@ EditorState::EditorState(
playfield(assets_),
linear_view(assets_),
song(song_),
metadata_in_gui(song_.metadata),
applicable_timing(song.timing),
assets(assets_)
{
@ -241,12 +241,12 @@ void EditorState::display_properties() {
}
ImGui::EndChild();
ImGui::InputText("Title", &metadata_in_gui.title);
ImGui::InputText("Artist", &metadata_in_gui.artist);
ImGui::InputText("Title", &song.metadata.title);
ImGui::InputText("Artist", &song.metadata.artist);
if (feis::InputTextColored(
"Audio",
&metadata_in_gui.audio,
&song.metadata.audio,
music_state.has_value(),
"Invalid Audio Path"
)) {
@ -254,7 +254,7 @@ void EditorState::display_properties() {
}
if (feis::InputTextColored(
"Jacket",
&metadata_in_gui.jacket,
&song.metadata.jacket,
jacket.has_value(),
"Invalid Jacket Path"
)) {
@ -264,45 +264,45 @@ void EditorState::display_properties() {
ImGui::Separator();
ImGui::Text("Preview");
ImGui::Checkbox("Use separate preview file", &metadata_in_gui.use_preview_file);
if (metadata_in_gui.use_preview_file) {
ImGui::Checkbox("Use separate preview file", &song.metadata.use_preview_file);
if (song.metadata.use_preview_file) {
if (feis::InputTextColored(
"File",
&metadata_in_gui.preview_file,
&song.metadata.preview_file,
preview_audio.has_value(),
"Invalid Path"
)) {
reload_preview_audio();
}
} else {
if (feis::InputDecimal("Start", &metadata_in_gui.preview_loop.start)) {
metadata_in_gui.preview_loop.start = std::max(
if (feis::InputDecimal("Start", &song.metadata.preview_loop.start)) {
song.metadata.preview_loop.start = std::max(
Decimal{0},
metadata_in_gui.preview_loop.start
song.metadata.preview_loop.start
);
if (music_state.has_value()) {
metadata_in_gui.preview_loop.start = std::min(
song.metadata.preview_loop.start = std::min(
Decimal{music_state->music.getDuration().asMicroseconds()} / 1000000,
metadata_in_gui.preview_loop.start
song.metadata.preview_loop.start
);
}
}
if (feis::InputDecimal("Duration", &metadata_in_gui.preview_loop.duration)) {
metadata_in_gui.preview_loop.duration = std::max(
if (feis::InputDecimal("Duration", &song.metadata.preview_loop.duration)) {
song.metadata.preview_loop.duration = std::max(
Decimal{0},
metadata_in_gui.preview_loop.duration
song.metadata.preview_loop.duration
);
if (music_state.has_value()) {
metadata_in_gui.preview_loop.start = std::min(
song.metadata.preview_loop.start = std::min(
(
Decimal{
music_state->music
.getDuration()
.asMicroseconds()
} / 1000000
- metadata_in_gui.preview_loop.start
- song.metadata.preview_loop.start
),
metadata_in_gui.preview_loop.start
song.metadata.preview_loop.start
);
}
}
@ -321,11 +321,11 @@ void EditorState::display_status() {
ImGui::Begin("Status", &showStatus, ImGuiWindowFlags_AlwaysAutoResize);
{
if (not music_state) {
if (not metadata_in_gui.audio.empty()) {
if (not song.metadata.audio.empty()) {
ImGui::TextColored(
ImVec4(1, 0.42, 0.41, 1),
"Invalid music path : %s",
metadata_in_gui.audio.c_str());
song.metadata.audio.c_str());
} else {
ImGui::TextColored(
ImVec4(1, 0.42, 0.41, 1),
@ -334,11 +334,11 @@ void EditorState::display_status() {
}
if (not jacket) {
if (not metadata_in_gui.jacket.empty()) {
if (not song.metadata.jacket.empty()) {
ImGui::TextColored(
ImVec4(1, 0.42, 0.41, 1),
"Invalid jacket path : %s",
metadata_in_gui.jacket.c_str());
song.metadata.jacket.c_str());
} else {
ImGui::TextColored(
ImVec4(1, 0.42, 0.41, 1),
@ -636,13 +636,13 @@ void EditorState::reload_editable_range() {
* of the song Resets the album cover state if anything fails
*/
void EditorState::reload_jacket() {
if (not song_path.has_value() or not song.metadata.jacket.has_value()) {
if (not song_path.has_value() or song.metadata.jacket.empty()) {
jacket.reset();
return;
}
jacket.emplace();
auto jacket_path = song_path->parent_path() / metadata_in_gui.jacket;
auto jacket_path = song_path->parent_path() / song.metadata.jacket;
if (
not std::filesystem::exists(jacket_path)
@ -658,12 +658,12 @@ void EditorState::reload_jacket() {
* Updates playbackPosition and preview_end as well
*/
void EditorState::reload_music() {
if (not song_path.has_value()) {
if (not song_path.has_value() or song.metadata.audio.empty()) {
music_state.reset();
return;
}
const auto absolute_music_path = song_path->parent_path() / metadata_in_gui.audio;
const auto absolute_music_path = song_path->parent_path() / song.metadata.audio;
try {
music_state.emplace(absolute_music_path);
} catch (const std::exception& e) {
@ -680,35 +680,35 @@ void EditorState::reload_music() {
};
void EditorState::reload_preview_audio() {
if (not song_path.has_value()) {
if (not song_path.has_value() or song.metadata.preview_file.empty()) {
preview_audio.reset();
return;
}
const auto path = song_path->parent_path() / metadata_in_gui.preview_file;
const auto path = song_path->parent_path() / song.metadata.preview_file;
try {
preview_audio.emplace(path);
preview_audio.emplace(path.string());
} catch (const std::exception& e) {
preview_audio.reset();
}
};
void reload_applicable_timing() {
// TODO: implement
void EditorState::reload_applicable_timing() {
if (chart_state) {
applicable_timing = chart_state->chart.timing;
} else {
applicable_timing = song.timing;
}
}
void EditorState::open_chart(better::Chart& chart) {
chart_state.emplace(chart, assets);
void EditorState::open_chart(better::Chart& chart, const std::string& name) {
chart_state.emplace(chart, name, assets);
reload_applicable_timing();
reload_editable_range();
};
void ESHelper::save(EditorState& ed) {
try {
ed.song.autoSaveAsMemon();
} catch (const std::exception& e) {
tinyfd_messageBox("Error", e.what(), "ok", "error", 1);
}
void EditorState::save(const std::filesystem::path& file) {
const auto memon = song.dump_as_memon_1_0_0();
}
void ESHelper::open(std::optional<EditorState>& ed, std::filesystem::path assets, std::filesystem::path settings) {

View File

@ -108,11 +108,12 @@ public:
void undo(NotificationsQueue& nq);
void redo(NotificationsQueue& nq);
void save(const std::filesystem::path& file);
private:
better::Song song;
MetadataInGui metadata_in_gui;
/*
sf::Time bounds (in the audio file "coordinates") which are accessible
(and maybe editable) from the editor, can extend before and after
@ -127,15 +128,12 @@ private:
better::Timing& applicable_timing;
void reload_applicable_timing();
void open_chart(better::Chart& chart);
void open_chart(better::Chart& chart, const std::string& name);
std::filesystem::path assets;
};
namespace ESHelper {
void save(EditorState& ed);
void open(std::optional<EditorState>& ed, std::filesystem::path assets, std::filesystem::path settings);
void openFromFile(
std::optional<EditorState>& ed,

View File

@ -0,0 +1,7 @@
#include "json_decimal_parser.hpp"
bool json_decimal_parser::number_float(number_float_t /*unused*/, const string_t& val) {
string_t copy = val;
string(copy);
return true;
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <json.hpp>
/*
This class allows parsing json files with nlohmann::json while losslessly
recovering decimal number literals by storing their original string
representation when parsing intead of their float or double conversion
Usage :
nlohmann::json j;
json_decimal_parser sax{j};
nlohmann::json::sax_parse(..., &sax);
*/
using sax_parser = nlohmann::detail::json_sax_dom_parser<nlohmann::json>;
class json_decimal_parser : public sax_parser {
public:
/*
Inherit the constructor because life is too short to write constructors for
derived classes
*/
using sax_parser::json_sax_dom_parser;
// override float parsing, divert it to parsing the original string
// as a json string literal
bool number_float(number_float_t /*unused*/, const string_t& val);
};
template<class InputType>
nlohmann::json parse_decimal_json(InputType&& input) {
nlohmann::json j;
json_decimal_parser sax{j};
nlohmann::json::sax_parse(std::forward<InputType>(input), &sax);
return j;
}

View File

@ -1,4 +1,6 @@
sources += files(
'better_chart.cpp',
'better_metadata.cpp',
'better_note.cpp',
'better_notes.cpp',
'better_song.cpp',
@ -9,6 +11,7 @@ sources += files(
'fumen.cpp',
'history_actions.cpp',
'imgui_extras.cpp',
'json_decimal_parser.cpp',
'ln_marker.cpp',
'main.cpp',
'marker.cpp',
@ -21,7 +24,6 @@ sources += files(
'precise_music.cpp',
'preferences.cpp',
'sound_effect.cpp',
'time_interval.cpp',
'toolbox.cpp',
)

View File

@ -2,12 +2,13 @@
#include <filesystem>
#include <variant>
#include "src/better_song.hpp"
MetadataInGui::MetadataInGui(const better::Metadata& metadata) :
title(metadata.title.value_or("")),
artist(metadata.artist.value_or("")),
audio(metadata.audio.value_or(std::filesystem::path{}).string()),
jacket(metadata.jacket.value_or(std::filesystem::path{}).string()),
jacket(metadata.jacket.value_or(std::filesystem::path{}).string())
{
if (metadata.preview.has_value()) {
if (std::holds_alternative<better::PreviewLoop>(*metadata.preview)) {
@ -19,4 +20,31 @@ MetadataInGui::MetadataInGui(const better::Metadata& metadata) :
use_preview_file = true;
}
}
}
MetadataInGui::operator better::Metadata() const {
auto m = better::Metadata{};
if (not title.empty()) {
m.title = title;
}
if (not artist.empty()) {
m.artist = artist;
}
if (not audio.empty()) {
m.audio = audio;
}
if (not jacket.empty()) {
m.jacket = jacket;
}
if (use_preview_file) {
if (not preview_file.empty()) {
m.preview = std::filesystem::path(preview_file);
}
} else if (preview_loop.start != preview_loop.duration) {
m.preview = better::PreviewLoop{
preview_loop.start,
preview_loop.duration
};
}
return m;
}

View File

@ -1,5 +1,7 @@
#include "special_numeric_types.hpp"
#include <boost/math/special_functions/math_fwd.hpp>
#include <boost/math/special_functions/pow.hpp>
#include <boost/multiprecision/gmp.hpp>
#include <boost/multiprecision/number.hpp>
@ -36,4 +38,16 @@ Decimal convert_to_decimal(const Fraction& f, unsigned int precision) {
Decimal{static_cast<long long>(boost::multiprecision::numerator(floored))}
/ Decimal{static_cast<long long>(boost::multiprecision::denominator(floored))}
);
}
Fraction convert_to_fraction(const Decimal& d) {
const auto reduced = d.reduce();
const auto sign = reduced.sign();
const auto exponent = reduced.exponent();
const auto coefficient = reduced.coeff().u64();
if (exponent >= 0) {
return Fraction{sign > 0 ? 1 : -1} * Fraction{coefficient} * fast_pow(Fraction{10}, exponent);
} else {
return Fraction{sign > 0 ? 1 : -1} * Fraction{coefficient} / fast_pow(Fraction{10}, exponent);
}
}

View File

@ -22,6 +22,7 @@ Fraction operator%(Fraction a, const Fraction& b);
Fraction floor_fraction(const Fraction& f);
Fraction round_fraction(const Fraction& f);
Decimal convert_to_decimal(const Fraction& f, unsigned int precision);
Fraction convert_to_fraction(const Decimal& d);
// Rounds a given beat to the nearest given division (defaults to nearest 1/240th)
const auto round_beats = [](Fraction beats, unsigned int denominator = 240) {
@ -34,4 +35,20 @@ const auto floor_beats = [](Fraction beats, unsigned int denominator = 240) {
beats *= denominator;
const auto nearest = floor_fraction(beats);
return nearest / Fraction{denominator};
};
};
// Stolen from : https://github.com/progrock-libraries/kickstart/blob/d62c22efc92006dd76d455cf8f9d4f2a045e9126/source/library/kickstart/main_library/core/ns%E2%96%B8language/operations/intpow.hpp#L36
// Essentially this is Horner's rule adapted to calculating a power, so that the
// number of floating point multiplications is at worst O(log₂n).
template<class Number>
Number fast_pow( const Number base, const unsigned int exponent ) {
Number result = 1;
Number weight = base;
for (unsigned int n = exponent; n != 0; weight *= weight) {
if(n % 2 != 0) {
result *= weight;
}
n /= 2;
}
return result;
}

30
tests/json_decimal.cpp Normal file
View File

@ -0,0 +1,30 @@
#include <iostream>
#include <map>
#include <cstdint>
#include <string>
#include <vector>
#include <json.hpp>
#include <libmpdec++/decimal.hh>
class json_decimal_parser : public nlohmann::detail::json_sax_dom_parser<nlohmann::json> {
public:
using nlohmann::detail::json_sax_dom_parser<nlohmann::json>::json_sax_dom_parser;
bool number_float(number_float_t /*unused*/, const string_t& val) {
string_t copy = val;
string(copy);
return true;
}
};
int main() {
nlohmann::json j{"blablabla"};
// json_decimal_parser sax{j};
// std::string json;
// std::getline(std::cin, json);
// nlohmann::json::sax_parse(json, &sax);
std::cout << "from const char * {} : " << nlohmann::json{"blablabla"} << std::endl;
std::cout << "from string {} : " << nlohmann::json{std::string{"blibloblu"}} << std::endl;
std::cout << "from const char * () : " << nlohmann::json("blablabla") << std::endl;
std::cout << "from string {} : " << nlohmann::json(std::string("blibloblu")) << std::endl;
}

View File

@ -15,6 +15,13 @@ executable(
include_directories: inc,
)
executable(
'json_decimal',
'json_decimal.cpp',
dependencies: deps,
include_directories: inc,
)
executable(
'scrollwheel',
'scrollwheel.cpp',