yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
instrument_editor_view.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cmath>
6
7#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11#include "implot.h"
12
13namespace yaze {
14namespace editor {
15namespace music {
16
17using namespace yaze::zelda3::music;
18
19static void HelpMarker(const char* desc) {
20 ImGui::TextDisabled("(?)");
21 if (ImGui::IsItemHovered()) {
22 ImGui::BeginTooltip();
23 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
24 ImGui::TextUnformatted(desc);
25 ImGui::PopTextWrapPos();
26 ImGui::EndTooltip();
27 }
28}
29
31 // Layout: List on left (25%), Properties on right (75%)
32 float list_width = ImGui::GetContentRegionAvail().x * 0.25f;
33 if (list_width < 150.0f)
34 list_width = 150.0f;
35
36 ImGui::BeginChild("InstrumentList", ImVec2(list_width, 0), true);
38 ImGui::EndChild();
39
40 ImGui::SameLine();
41
42 ImGui::BeginChild("InstrumentProps", ImVec2(0, 0), true);
45 static_cast<int>(bank.GetInstrumentCount())) {
47 } else {
48 ImGui::TextDisabled(tr("Select an instrument to edit"));
49 }
50 ImGui::EndChild();
51}
52
54 if (ImGui::Button(tr("Add Instrument"))) {
55 bank.CreateNewInstrument("New Instrument");
56 if (on_edit_)
57 on_edit_();
58 }
59
60 ImGui::Separator();
61
62 for (size_t i = 0; i < bank.GetInstrumentCount(); ++i) {
63 const auto* inst = bank.GetInstrument(i);
64 std::string label = absl::StrFormat("%02X: %s", i, inst->name);
65 if (ImGui::Selectable(label.c_str(),
66 selected_instrument_index_ == static_cast<int>(i))) {
67 selected_instrument_index_ = static_cast<int>(i);
68 }
69 }
70}
71
73 MusicBank& bank) {
74 bool changed = false;
75
76 // Name
77 char name_buf[64];
78 strncpy(name_buf, instrument.name.c_str(), sizeof(name_buf));
79 if (ImGui::InputText(tr("Name"), name_buf, sizeof(name_buf))) {
80 instrument.name = name_buf;
81 changed = true;
82 }
83
84 ImGui::SameLine();
85 if (on_preview_) {
86 if (ImGui::Button(ICON_MD_PLAY_ARROW " Preview")) {
88 }
89 if (ImGui::IsItemHovered()) {
90 ImGui::SetTooltip(
91 tr("Play a C4 note with this instrument (requires ROM loaded)"));
92 }
93 } else {
94 ImGui::BeginDisabled();
95 ImGui::Button(ICON_MD_PLAY_ARROW " Preview");
96 ImGui::EndDisabled();
97 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
98 ImGui::SetTooltip(tr("Preview not available - load a ROM first"));
99 }
100 }
101
102 // Sample Selection
103 if (ImGui::BeginCombo(
104 tr("Sample"),
105 absl::StrFormat("%02X", instrument.sample_index).c_str())) {
106 for (size_t i = 0; i < bank.GetSampleCount(); ++i) {
107 bool is_selected = (instrument.sample_index == i);
108 const auto* sample = bank.GetSample(i);
109 std::string label = absl::StrFormat(
110 "%02X: %s", i, sample ? sample->name.c_str() : "Unknown");
111 if (ImGui::Selectable(label.c_str(), is_selected)) {
112 instrument.sample_index = static_cast<uint8_t>(i);
113 changed = true;
114 }
115 if (is_selected)
116 ImGui::SetItemDefaultFocus();
117 }
118 ImGui::EndCombo();
119 }
120 ImGui::SameLine();
121 HelpMarker("The BRR sample used by this instrument.");
122
123 // Pitch Multiplier (Tuning)
124 int pitch = instrument.pitch_mult;
125 if (ImGui::InputInt(tr("Pitch Multiplier"), &pitch, 1, 16,
126 ImGuiInputTextFlags_CharsHexadecimal)) {
127 instrument.pitch_mult = static_cast<uint16_t>(std::clamp(pitch, 0, 0xFFFF));
128 changed = true;
129 }
130 ImGui::SameLine();
131 HelpMarker(
132 "Base pitch adjustment. $1000 = 1.0x (Standard C). Lower values lower "
133 "the pitch.");
134
135 ImGui::Separator();
136 ImGui::Text(tr("Envelope (ADSR)"));
137 ImGui::SameLine();
138 HelpMarker("Attack, Decay, Sustain, Release envelope controls.");
139
140 // ADSR Controls
141 // Attack: 0-15
142 int attack = instrument.attack;
143 if (ImGui::SliderInt(tr("Attack Rate"), &attack, 0, 15)) {
144 instrument.attack = static_cast<uint8_t>(attack);
145 changed = true;
146 }
147 if (ImGui::IsItemHovered())
148 ImGui::SetTooltip(
149 tr("How fast the volume reaches peak. 15 = Fastest (Instant), 0 = "
150 "Slowest."));
151
152 // Decay: 0-7
153 int decay = instrument.decay;
154 if (ImGui::SliderInt(tr("Decay Rate"), &decay, 0, 7)) {
155 instrument.decay = static_cast<uint8_t>(decay);
156 changed = true;
157 }
158 if (ImGui::IsItemHovered())
159 ImGui::SetTooltip(
160 tr("How fast volume drops from peak to Sustain Level. 7 = Fastest, 0 = "
161 "Slowest."));
162
163 // Sustain Level: 0-7
164 int sustain_level = instrument.sustain_level;
165 if (ImGui::SliderInt(tr("Sustain Level"), &sustain_level, 0, 7)) {
166 instrument.sustain_level = static_cast<uint8_t>(sustain_level);
167 changed = true;
168 }
169 if (ImGui::IsItemHovered())
170 ImGui::SetTooltip(
171 tr("The volume level (1/8ths) to sustain at. 7 = Max Volume, 0 = "
172 "Silence."));
173
174 // Sustain Rate: 0-31
175 int sustain_rate = instrument.sustain_rate;
176 if (ImGui::SliderInt(tr("Sustain Rate"), &sustain_rate, 0, 31)) {
177 instrument.sustain_rate = static_cast<uint8_t>(sustain_rate);
178 changed = true;
179 }
180 if (ImGui::IsItemHovered())
181 ImGui::SetTooltip(tr(
182 "How fast volume decays WHILE holding the key (after reaching Sustain "
183 "Level). 0 = Infinite sustain, 31 = Fast fade out."));
184
185 // Gain (if not using ADSR, but usually ADSR is preferred for instruments)
186 // TODO: Add Gain Mode toggle
187
188 if (changed && on_edit_) {
189 on_edit_();
190 }
191
192 ImGui::Separator();
193 DrawAdsrGraph(instrument);
194}
195
197 // Visualize ADSR
198 // Attack: Linear increase to max
199 // Decay: Exponential decrease to Sustain Level
200 // Sustain: Exponential decrease at Sustain Rate
201
202 // Ensure ImPlot context exists before plotting
204
205 // Helper to convert SNES rates to time/slope
206 // (Simplified for visualization)
207
208 plot_x_.clear();
209 plot_y_.clear();
210
211 float t = 0.0f;
212
213 // Attack Phase
214 // Rate N: (Rate * 2 + 1) ms to full volume? Roughly.
215 // Simplification: t += 1.0 / (attack + 1)
216 float attack_time = 1.0f / (instrument.attack + 1.0f);
217 if (instrument.attack == 15)
218 attack_time = 0.0f; // Instant
219
220 plot_x_.push_back(0.0f);
221 plot_y_.push_back(0.0f);
222
223 // Attack is linear in SNES DSP (add 1/64 per tick)
224 plot_x_.push_back(attack_time);
225 plot_y_.push_back(1.0f); // Max volume
226 t = attack_time;
227
228 // Decay Phase
229 // Exponential decay to Sustain Level
230 // Sustain Level is instrument.sustain_level/8.0f + 1
231 float s_level = (instrument.sustain_level + 1) / 8.0f;
232
233 // Simulate exponential decay
234 // k = decay rate factor
235 for (int i = 0; i < 20; ++i) {
236 t += 0.02f;
237 // Fake exponential: vol = s_level + (1 - s_level) * exp(-k * dt)
238 // Or simple lerp for now
239 float alpha = (float)i / 20.0f;
240 float curve = alpha * alpha; // Quadratic approximation for exponential
241 float vol = 1.0f - (1.0f - s_level) * curve;
242
243 plot_x_.push_back(t);
244 plot_y_.push_back(vol);
245 }
246
247 // Sustain Phase (Decrease)
248 // Decreases at sustain_rate until key off
249 float sustain_time = 1.0f; // Show 1 second of sustain
250 float sustain_drop_per_sec = instrument.sustain_rate / 31.0f;
251
252 plot_x_.push_back(t + sustain_time);
253 plot_y_.push_back(std::max(0.0f, s_level - sustain_drop_per_sec));
254
255 if (ImPlot::BeginPlot("ADSR Envelope", ImVec2(-1, 200))) {
256 ImPlot::SetupAxes("Time", "Volume");
257 ImPlot::SetupAxesLimits(0, 2.0, 0, 1.1);
258 ImPlot::PlotLine("Volume", plot_x_.data(), plot_y_.data(),
259 static_cast<int>(plot_x_.size()));
260
261 // Mark phases
262 ImPlot::TagX(attack_time, ImVec4(1, 1, 0, 0.5), "Decay Start");
263
264 ImPlot::EndPlot();
265 }
266}
267
268} // namespace music
269} // namespace editor
270} // namespace yaze
void DrawAdsrGraph(const MusicInstrument &instrument)
void Draw(MusicBank &bank)
Draw the instrument editor.
void DrawProperties(MusicInstrument &instrument, MusicBank &bank)
Manages the collection of songs, instruments, and samples from a ROM.
Definition music_bank.h:27
MusicInstrument * GetInstrument(int index)
Get an instrument by index.
int CreateNewInstrument(const std::string &name)
Create a new instrument.
size_t GetSampleCount() const
Get the number of samples.
Definition music_bank.h:198
size_t GetInstrumentCount() const
Get the number of instruments.
Definition music_bank.h:176
MusicSample * GetSample(int index)
Get a sample by index.
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
Contains classes and functions for handling music data in Zelda 3.
An instrument definition with ADSR envelope.
Definition song_data.h:358