1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
#include "songparser.hh"
#include "util.hh"
#include "libxml++-impl.hh"
#include <boost/algorithm/string.hpp>
#include <stdexcept>
/// @file
/// Functions used for parsing the SingStar XML song format
using namespace SongParserUtil;
/// 'Magick' to check if this file looks like correct format
bool SongParser::xmlCheck(std::string const& data) const {
static const std::string header = "<?";
return std::equal(header.begin(), header.end(), data.begin());
}
/*
// LibXML2 logging facility
extern "C" void xmlLogger(void* logger, char const* msg, ...) { if (logger) *(std::ostream*)logger << msg; }
void enableXMLLogger(std::ostream& os = std::cerr) { xmlSetGenericErrorFunc(&os, xmlLogger); }
void disableXMLLogger() { xmlSetGenericErrorFunc(NULL, xmlLogger); }
*/
struct SSDom: public xmlpp::DomParser {
xmlpp::Node::PrefixNsMap nsmap;
SSDom(std::stringstream const& _ss) {
load(_ss.str());
}
void load(std::string const& buf) {
set_substitute_entities();
/*
struct DisableLogger {
DisableLogger() { disableXMLLogger(); }
~DisableLogger() { enableXMLLogger(); }
} disabler;
*/
parse_memory(buf);
nsmap["ss"] = get_document()->get_root_node()->get_namespace_uri();
}
bool find(xmlpp::Element const& elem, std::string xpath, xmlpp::const_NodeSet& n) {
if (nsmap["ss"].empty()) boost::erase_all(xpath, "ss:");
n = elem.find(xpath, nsmap);
return !n.empty();
}
bool find(std::string const& xpath, xmlpp::const_NodeSet& n) {
return find(*get_document()->get_root_node(), xpath, n);
}
bool getValue(std::string const& xpath, std::string& result) {
xmlpp::const_NodeSet n;
if (!find(xpath, n)) return false;
result = xmlpp::get_first_child_text(dynamic_cast<const xmlpp::Element&>(*n[0]))->get_content();
return true;
}
};
namespace {
/// Parse str (from XML comment node) for header/value and store cleaned up value in result.
bool parseComment(std::string const& str, std::string const& header, std::string& result) {
if (!boost::starts_with(str, header)) return false;
result = boost::replace_all_copy(
boost::trim_left_copy(str.substr(header.size())),
"&", "&");
return true;
}
}
/// Parse header data for Songs screen
void SongParser::xmlParseHeader() {
Song& s = m_song;
// Parse notes.xml
SSDom dom(m_ss);
// Extract artist and title from XML comments
{
xmlpp::const_NodeSet comments;
// Comments inside or before root element
dom.find("/ss:MELODY/comment()", comments) || dom.find("/ss:MELODY/../comment()", comments);
for (auto const& node: comments) {
std::string str = dynamic_cast<xmlpp::CommentNode const&>(*node).get_content();
boost::trim(str);
parseComment(str, "Artist:", s.artist) || parseComment(str, "Title:", s.title);
}
if (s.title.empty() || s.artist.empty()) throw std::runtime_error("Required header fields missing");
}
// Read TRACK elements (singer names), if available
std::string singers; // Only used for "Together" track
xmlpp::const_NodeSet tracks;
dom.find("/ss:MELODY/ss:TRACK", tracks);
for (auto const& elem: tracks) {
auto const& trackNode = dynamic_cast<xmlpp::Element const&>(*elem);
std::string name = trackNode.get_attribute("Name")->get_value(); // "Player1" or "Player2"
auto attr = trackNode.get_attribute("Artist");
std::string artist = attr ? std::string(attr->get_value()) : name; // Singer name
if (attr) singers += (singers.empty() ? "" : " & ") + artist;
m_song.insertVocalTrack(name, VocalTrack(artist));
}
// If no tracks are specified, we can assume that it isn't duet
if (tracks.empty()) m_song.insertVocalTrack("Player1", VocalTrack(s.artist));
else if (tracks.size() > 1) m_song.insertVocalTrack("Together", VocalTrack(singers.empty() ? _("Together") : singers));
}
void addNoteToTrack(VocalTrack& vocal, const Note& note) {
if (note.type == Note::SLEEP) {
// Skip extra sleep notes
if (vocal.notes.empty() || vocal.notes.back().type == Note::SLEEP) return;
} else {
vocal.noteMin = std::min(vocal.noteMin, note.note);
vocal.noteMax = std::max(vocal.noteMax, note.note);
}
vocal.notes.push_back(note);
}
/// Parse notes
void SongParser::xmlParse() {
// Parse notes.xml
SSDom dom(m_ss);
Song& s = m_song;
// Extract tempo
{
xmlpp::const_NodeSet n;
if (!dom.find("/ss:MELODY", n)) throw std::runtime_error("Unable to find BPM info");
auto const& e = dynamic_cast<xmlpp::Element const&>(*n[0]);
std::string res = e.get_attribute("Resolution")->get_value();
SongParserUtil::assign(m_bpm, e.get_attribute("Tempo")->get_value().c_str());
if (res == "Semiquaver") {}
else if (res == "Demisemiquaver") m_bpm *= 2.0;
else throw std::runtime_error("Unknown tempo resolution: " + res);
}
addBPM(0, m_bpm);
// Parse each track...
xmlpp::const_NodeSet tracks;
// First try version 1 (MELODY/SENTENCE), fallback to version 2/4 (MELODY/TRACK/SENTENCE).
dom.find("/ss:MELODY[ss:SENTENCE]", tracks) || dom.find("/ss:MELODY/ss:TRACK[ss:SENTENCE]", tracks);
if (tracks.empty()) throw std::runtime_error("No valid tracks or sentences found");
// Process each vocalTrack separately; use the same XML (version 1) track twice if needed
unsigned players = clamp<unsigned>(s.vocalTracks.size(), 1, 2); // Don't count "Together" track
auto vocalIt = s.vocalTracks.begin();
for (unsigned player = 0; player < players; ++player, ++vocalIt) {
if (vocalIt == s.vocalTracks.end()) throw std::logic_error("SongParser-xml vocalIt past the end");
VocalTrack& vocal = vocalIt->second;
auto const& trackElem = dynamic_cast<xmlpp::Element const&>(*tracks[player % tracks.size()]);
std::string sentenceSinger = "Solo 1"; // The default value
xmlpp::const_NodeSet sentences;
dom.find(trackElem, "ss:SENTENCE", sentences);
unsigned ts = 0;
for (auto it = sentences.begin(); it != sentences.end(); ++it ) {
auto const& sentenceNode = dynamic_cast<xmlpp::Element const&>(**it);
// Check if SENTENCE has new attributes
{
auto attr = sentenceNode.get_attribute("Part");
if (attr) m_song.songsections.push_back(Song::SongSection(attr->get_value(), tsTime(ts)));
attr = sentenceNode.get_attribute("Singer");
if (attr) sentenceSinger = attr->get_value();
}
// Begin each sentence with sleep (extras will be purged in addNoteToTrack)
{
Note sleep;
sleep.type = Note::SLEEP;
sleep.begin = sleep.end = tsTime(ts);
addNoteToTrack(vocal, sleep);
}
// Notes of a sentence
xmlpp::const_NodeSet notes;
dom.find(sentenceNode, "ss:NOTE", notes);
for (auto const& elem: notes) {
auto const& noteNode = dynamic_cast<xmlpp::Element const&>(*elem);
Note n = xmlParseNote(noteNode, ts);
if (n.note == 0) continue;
// Skip sentences that do not belong to current player
if (player != 0 && sentenceSinger == "Solo 1") continue;
if (players > 1 && player != 1 && sentenceSinger == "Solo 2") continue;
// Actually add the note to current player's track...
addNoteToTrack(vocal, n);
} // notes
} // sentences
} // players (vocal tracks)
}
Note SongParser::xmlParseNote(xmlpp::Element const& noteNode, unsigned& ts) {
Note n;
std::string lyric = noteNode.get_attribute("Lyric")->get_value();
// Pretty print hyphenation
if (lyric.size() > 0 && lyric[lyric.size() - 1] == '-') {
if (lyric.size() > 1 && lyric[lyric.size() - 2] == ' ') lyric.erase(lyric.size() - 2);
else lyric[lyric.size() - 1] = '~';
} else {
lyric += ' ';
}
unsigned note, duration;
SongParserUtil::assign(note, noteNode.get_attribute("MidiNote")->get_value().c_str());
SongParserUtil::assign(duration, noteNode.get_attribute("Duration")->get_value().c_str());
if (noteNode.get_attribute("FreeStyle")) n.type = Note::FREESTYLE;
else if (noteNode.get_attribute("Bonus")) n.type = Note::GOLDEN;
else n.type = Note::NORMAL;
n.begin = tsTime(ts);
ts += duration;
n.end = tsTime(ts);
n.syllable = lyric;
n.note = note;
n.notePrev = note;
return n;
}
|