mirror of
https://gitlab.com/square-game-liberation-front/F.E.I.S.git
synced 2024-11-12 02:00:53 +01:00
Starting to implement memon 1.0.0 saving
This commit is contained in:
parent
b967bc0499
commit
6fe06dfd80
36277
include/json.hpp
36277
include/json.hpp
File diff suppressed because it is too large
Load Diff
33
src/better_beats.cpp
Normal file
33
src/better_beats.cpp
Normal 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
9
src/better_beats.hpp
Normal 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
16
src/better_chart.cpp
Normal 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
23
src/better_chart.hpp
Normal 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
11
src/better_hakus.cpp
Normal 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
11
src/better_hakus.hpp
Normal 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
22
src/better_metadata.hpp
Normal 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;
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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};
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
7
src/json_decimal_parser.cpp
Normal file
7
src/json_decimal_parser.cpp
Normal 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;
|
||||
}
|
38
src/json_decimal_parser.hpp
Normal file
38
src/json_decimal_parser.hpp
Normal 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;
|
||||
}
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
30
tests/json_decimal.cpp
Normal 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;
|
||||
}
|
@ -15,6 +15,13 @@ executable(
|
||||
include_directories: inc,
|
||||
)
|
||||
|
||||
executable(
|
||||
'json_decimal',
|
||||
'json_decimal.cpp',
|
||||
dependencies: deps,
|
||||
include_directories: inc,
|
||||
)
|
||||
|
||||
executable(
|
||||
'scrollwheel',
|
||||
'scrollwheel.cpp',
|
||||
|
Loading…
Reference in New Issue
Block a user