Skip to content

Commit

Permalink
Custom Music QoL Update (#722)
Browse files Browse the repository at this point in the history
- Change cmeta to be text files
- Sequences can be put in folders:
    - If a node has a non-node folder, it looks through it and all it's children for sequences.
    - If there are sequences, whether directly or in folders, in the Custom Music folder, they're considered too.
      The cmeta is read and the sequence is added to the node of the category, or if music shuffle is mixed, to the root node.
- Change Underground node to Ganon's Castle
  • Loading branch information
Kewlan authored Mar 17, 2024
1 parent 3d5291f commit 61741bf
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 49 deletions.
116 changes: 116 additions & 0 deletions source/custom_music/cmeta_interpreter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <algorithm>
#include <cctype>

#include "cmeta_interpreter.hpp"

static const std::string CMETAKEY_BANKS = "banks";
static const std::string CMETAKEY_CHFLAGS = "chFlags";
static const std::string CMETAKEY_VOLUME = "volume";
static const std::string CMETAKEY_CATEGORY = "category";

CMetaInterpreter::CMetaInterpreter(std::string path) : banks{ 7, 7, 7, 7 }, channelFlags(-1), volume(0x7F) {
// Set banks to Orchestra by default
// Enable all channel flags by default
// 100% volume (assumed, as it's unsigned) by default

std::fstream f(path);
std::string f_out;
while (std::getline(f, f_out)) {
std::stringstream ss(f_out);
std::string key;
std::string value;
std::getline(ss, key, '=');
std::getline(ss, value, '=');

// Removes leading and trailing spaces, tabs, and newline characters
const auto sanitizeInput = [](std::string& str) {
std::replace(str.begin(), str.end(), '\t', ' ');
str.erase(0, str.find_first_not_of(' '));
str.erase(str.find_last_not_of(' ') + 1);
while (str.back() == '\n' || str.back() == '\r') {
str.pop_back();
}
};

sanitizeInput(key);
sanitizeInput(value);

const auto getIntFromValue = [](std::string value) {
if (value.length() > 2) {
// Hexadecimal
if (value[0] == '0' && value[1] == 'x') {
return std::stoi(value, nullptr, 16);
}
// Binary
else if (value[0] == '0' && value[1] == 'b') {
value.erase(0, 2);
return std::stoi(value, nullptr, 2);
}
}
// Percentage, decimal (Should only be used for volume)
if (value.back() == '%') {
value.pop_back();
return std::min(0xFF, (int)(0xFF * (std::stoi(value) / 200.0f)));
}
// Decimal
return std::stoi(value);
};

if (key == CMETAKEY_BANKS) {
// Effectively treat all non-alphanumerical symbols as separators
for (size_t i = 0; i < value.length(); i++) {
if (!isalnum(value.at(i))) {
value.at(i) = ' ';
}
}
std::stringstream bs(value);
u8 i = 0;
std::string bs_out;
while (std::getline(bs, bs_out, ' ')) {
// Two spaces in a row gives an empty token; skip them
if (bs_out.empty()) {
continue;
}
// At most four banks can be set
if (i >= 4) {
break;
}
banks.at(i) = getIntFromValue(bs_out);
i++;
}
} else if (key == CMETAKEY_CHFLAGS) {
channelFlags = getIntFromValue(value);
} else if (key == CMETAKEY_VOLUME) {
volume = getIntFromValue(value);
} else if (key == CMETAKEY_CATEGORY) {
category = value;
if (category.front() == '!') {
category.erase(0, category.find_first_not_of('!'));
}
}
}

f.close();
}

CMetaInterpreter::~CMetaInterpreter() = default;

const std::array<u32, 4>& CMetaInterpreter::GetBanks() {
return banks;
}

u16 CMetaInterpreter::GetChannelFlags() {
return channelFlags;
}

u8 CMetaInterpreter::GetVolume() {
return volume;
}

const std::string& CMetaInterpreter::GetCategory() {
return category;
}
22 changes: 22 additions & 0 deletions source/custom_music/cmeta_interpreter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <3ds.h>
#include <string>
#include <array>

class CMetaInterpreter {
public:
CMetaInterpreter(std::string path);
~CMetaInterpreter();

const std::array<u32, 4>& GetBanks();
u16 GetChannelFlags();
u8 GetVolume();
const std::string& GetCategory();

private:
std::array<u32, 4> banks;
u16 channelFlags;
u8 volume;
std::string category;
};
95 changes: 54 additions & 41 deletions source/custom_music/sequence_data.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#include <filesystem>
#include <functional>

#include "sequence_data.hpp"
#include "ctr_binary_data.hpp"
#include "random.hpp"

namespace fs = std::filesystem;

static const std::string bcseqExtension = ".bcseq";
static const std::string cmetaExtension = ".cmeta";

// Sequence Data

SequenceData::SequenceData()
Expand All @@ -19,23 +17,36 @@ SequenceData::SequenceData(std::vector<u8>* rawBytesPtr_, std::array<u32, 4> ban
: dataType(DataType_Raw), rawBytesPtr(rawBytesPtr_), banks(banks_), chFlags(chFlags_), volume(volume_) {
}

SequenceData::SequenceData(std::string filePath_, std::array<u32, 4> banks_, u16 chFlags_, u8 volume_)
: dataType(DataType_Path), filePath(filePath_), banks(banks_), chFlags(chFlags_), volume(volume_) {
SequenceData::SequenceData(std::string bcseqPath_, CMetaInterpreter& ci)
: dataType(DataType_Path), bcseqPath(bcseqPath_), banks(ci.GetBanks()), chFlags(ci.GetChannelFlags()),
volume(ci.GetVolume()) {
valuesSet = true;
}

SequenceData::SequenceData(std::string bcseqPath_, std::string cmetaPath_)
: dataType(DataType_Path), bcseqPath(bcseqPath_), cmetaPath(cmetaPath_) {
}

SequenceData::~SequenceData() = default;

std::vector<u8> SequenceData::GetData(FS_Archive archive_) {
std::vector<u8>& SequenceData::GetData(FS_Archive archive_) {
// Pointer to original file
if (dataType == DataType_Raw && rawBytesPtr != nullptr) {
return *rawBytesPtr;
}
// External sequence, or empty data
else {
if (dataType == DataType_Path && rawBytes.empty()) {
BinaryDataReader br(archive_, filePath);
BinaryDataReader br(archive_, bcseqPath);
rawBytes = br.ReadAll();
br.Close();
if (!valuesSet) {
CMetaInterpreter ci(cmetaPath);
banks = ci.GetBanks();
chFlags = ci.GetChannelFlags();
volume = ci.GetVolume();
valuesSet = true;
}
}
return rawBytes;
}
Expand Down Expand Up @@ -66,7 +77,8 @@ MusicCategoryNode::~MusicCategoryNode() = default;

std::string MusicCategoryNode::GetFullPath() {
if (fullPath.empty()) {
std::string finalPath = Name + '/';
// The exclamation point implies that the folder is a node
std::string finalPath = "!" + Name + '/';
if (parent != nullptr) {
finalPath.insert(0, parent->GetFullPath());
} else {
Expand Down Expand Up @@ -113,43 +125,30 @@ void MusicCategoryNode::AddExternalSeqDatas(FS_Archive sdmcArchive) {
// Make sure directory exists to avoid issues
FSUSER_CreateDirectory(sdmcArchive, fsMakePath(PATH_ASCII, GetFullPath().c_str()), FS_ATTRIBUTE_DIRECTORY);

for (const auto& bcseq : fs::directory_iterator(GetFullPath())) {
if (bcseq.is_regular_file() && bcseq.path().extension().string() == bcseqExtension) {
std::array<u32, 4> banks = { 7, 7, 7, 7 }; // Set banks to Orchestra by default
u16 chFlags = -1; // Enable all channel flags by default
u8 volume = 127; // 100% (assumed, as it's unsigned) by default

// Check for cmeta file
auto fileName = bcseq.path().stem().string();
for (const auto& cmeta : fs::directory_iterator(GetFullPath())) {
if (cmeta.is_regular_file() && cmeta.path().stem().string() == fileName &&
cmeta.path().extension().string() == cmetaExtension) {

BinaryDataReader br(sdmcArchive, cmeta.path().string());
// Bank
for (size_t bnkIdx = 0; bnkIdx < 4; bnkIdx++) {
if (br.GetFileSize() >= bnkIdx + 1) {
banks[bnkIdx] = br.ReadByte();
} else {
break;
}
}
// Channel Flags
if (br.GetFileSize() >= 6) {
chFlags = br.ReadU16();
}
// Volume
if (br.GetFileSize() >= 7) {
volume = br.ReadByte();
std::function<void(std::string)> addCategorizedSeqDatas = [&](std::string folderPath) {
namespace fs = std::filesystem;
for (const auto& bcseq : fs::directory_iterator(folderPath)) {
if (bcseq.is_directory()) {
// Exclamation mark indicates a tree node
if (bcseq.path().stem().string().front() == '!') {
continue;
} else {
addCategorizedSeqDatas(bcseq.path().string());
}
} else if (bcseq.is_regular_file() && bcseq.path().extension().string() == bcseqExtension) {
for (const auto& cmeta : fs::directory_iterator(folderPath)) {
if (cmeta.is_regular_file() && cmeta.path().stem().string() == bcseq.path().stem().string() &&
cmeta.path().extension().string() == cmetaExtension) {

AddNewSeqData(SequenceData(bcseq.path().string(), cmeta.path().string()));
break;
}
br.Close();
break;
}
}

seqDatas.push_back(SequenceData(bcseq.path().string(), banks, chFlags, volume));
}
}
};
addCategorizedSeqDatas(GetFullPath());

for (auto child : children) {
child->AddExternalSeqDatas(sdmcArchive);
}
Expand Down Expand Up @@ -216,6 +215,20 @@ std::vector<MusicCategoryLeaf*> MusicCategoryNode::GetAllLeaves() {
return leaves;
}

MusicCategoryNode* MusicCategoryNode::GetNodeByName(const std::string name) {
if (this->Name == name) {
return this;
}
MusicCategoryNode* out;
for (auto child : children) {
out = child->GetNodeByName(name);
if (out != nullptr) {
return out;
}
}
return nullptr;
}

void MusicCategoryNode::ForDynamicCast() {
}

Expand Down
20 changes: 17 additions & 3 deletions source/custom_music/sequence_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,37 @@
#include <string>
#include <array>

#include "cmeta_interpreter.hpp"

static const std::string CustomMusicRootPath = "/OoT3DR/Custom Music/";
static const std::string bcseqExtension = ".bcseq";
static const std::string cmetaExtension = ".cmeta";

class SequenceData {
public:
SequenceData();
SequenceData(std::vector<u8>* rawBytesPtr_, std::array<u32, 4> banks_, u16 chFlags_, u8 volume_);
SequenceData(std::string filePath_, std::array<u32, 4> banks_, u16 chFlags_, u8 volume_);
SequenceData(std::string bcseqPath_, CMetaInterpreter& ci);
SequenceData(std::string bcseqPath_, std::string cmetaPath_);
~SequenceData();

/// Returns the raw bytes of the sequence.
/// - RAW Type: Dereferences and returns a copy.
/// If this Sequence Data is empty, returns an empty byte vector.
/// - PATH Type: Returns the raw bytes of the external sequence.
/// Reads the file only the first time this is called.
std::vector<u8> GetData(FS_Archive archive_);
std::vector<u8>& GetData(FS_Archive archive_);

/// Returns the bank index.
std::array<u32, 4> GetBanks();
/// Returns the bits of the set flags.
u16 GetChFlags();
/// Returns the volume.
u8 GetVolume();

/// Keeps track of if the meta data has been set
bool valuesSet = false;

private:
enum DataType {
DataType_Raw,
Expand All @@ -39,7 +48,9 @@ class SequenceData {
std::vector<u8>* rawBytesPtr;

/// PATH Type: The full path to the external sequence
std::string filePath;
std::string bcseqPath;
/// PATH Type: The full path to the cmeta file
std::string cmetaPath;
/// PATH Type: The raw bytes of the external sequence
std::vector<u8> rawBytes;

Expand Down Expand Up @@ -81,6 +92,9 @@ class MusicCategoryNode {

/// Returns all leaves in this node's children.
std::vector<MusicCategoryLeaf*> GetAllLeaves();
/// Returns the first (and hopefully only) node in this family tree with the same name.
/// If no match was found, returns nullptr.
MusicCategoryNode* GetNodeByName(const std::string name);

const std::string Name;

Expand Down
3 changes: 2 additions & 1 deletion source/custom_music/sound_archive.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#include <cstring>

#include "sound_archive.hpp"
#include "ctr_binary_data.hpp"
#include "reference_structures.hpp"
#include "file_reader.hpp"
#include "common_structures.hpp"
#include <cstring>

SoundArchive::SoundArchive(FS_Archive archive_, const std::string& filePath_) {
BinaryDataReader br(archive_, filePath_);
Expand Down
Loading

0 comments on commit 61741bf

Please sign in to comment.