diff --git a/Applications/Note/ArpDirVisualizer.h b/Applications/Note/ArpDirVisualizer.h new file mode 100644 index 00000000..8494bdf6 --- /dev/null +++ b/Applications/Note/ArpDirVisualizer.h @@ -0,0 +1,186 @@ +#pragma once +#include "MatrixOS.h" +#include "ui/UI.h" +#include "Arpeggiator.h" + +struct ArpDirVisual { + uint8_t step[8]; // 8 steps to visualize the pattern, 0=no show, 1-4=first cycle, 5-8=second cycle +}; + +const ArpDirVisual arpDirVisuals[16] = { + // 0 as no show, 1~4 means first repeat, 5~8 means second repeat + /*ARP_UP*/ {{1, 2, 3, 4, 5, 6, 7, 8}}, + /*ARP_DOWN*/ {{4, 3, 2, 1, 8, 7, 6, 5}}, + /*ARP_UP_DOWN*/ {{1, 2, 3, 4, 3, 2, 5, 0}}, + /*ARP_DOWN_UP*/ {{4, 3, 2, 1, 2, 3, 8, 7}}, + /*ARP_UP_N_DOWN*/ {{1, 2, 3, 4, 4, 3, 2, 5}}, + /*ARP_DOWN_N_UP*/ {{4, 3, 2, 1, 1, 2, 3, 8}}, + /*ARP_RANDOM*/ {{0, 0, 0, 0, 0, 0, 0, 0}}, + /*ARP_PLAY_ORDER*/ {{0, 0, 0, 0, 0, 0, 0, 0}}, + /*ARP_CONVERGE*/ {{1, 4, 2, 3, 5, 8, 6, 7}}, + /*ARP_DIVERGE*/ {{2, 3, 1, 4, 6, 7, 5, 8}}, + /*ARP_CON_DIVERGE*/ {{1, 4, 2, 3, 2, 3, 1, 4}}, + /*ARP_DIV_CONVERGE*/ {{2, 3, 1, 4, 1, 4, 2, 3}}, + /*ARP_PINKY_UP*/ {{1, 4, 2, 4, 3, 4, 5, 8}}, + /*ARP_PINKY_UP_DOWN*/ {{1, 4, 2, 4, 3, 4, 2, 4}}, + /*ARP_THUMB_UP*/ {{1, 2, 1, 3, 1, 4, 5, 6}}, + /*ARP_THUMB_UP_DOWN*/ {{1, 2, 1, 3, 1, 4, 1, 3}}, +}; + +class ArpDirVisualizer : public UIComponent { + public: + Color color; + ArpDirection* direction; + ArpDirection lastDirection; + uint64_t lastUpdateTime = 0; + uint8_t currentStep = 0; + + ArpDirVisualizer(ArpDirection* direction, Color color) { + this->direction = direction; + this->lastDirection = *direction; + this->color = color; + } + + virtual Color GetColor() { return color; } + virtual Dimension GetSize() { return Dimension(8, 4); } + + bool IsEnabled() { + if (enableFunc) { + enabled = (*enableFunc)(); + } + if(!enabled) + { + currentStep = 0; + lastUpdateTime = 0; + } + return enabled; + } + + virtual bool Render(Point origin) { + // For other modes, use the animated pattern from arpDirVisuals + uint64_t currentTime = MatrixOS::SYS::Millis(); + + // Reset animation if direction changed or not inited + if (*direction != lastDirection || lastUpdateTime == 0) { + currentStep = 0; + lastUpdateTime = currentTime; + lastDirection = *direction; + } + + if(*direction == ARP_RANDOM) + { + // R + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // n + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 3), Color(0xFFFFFF)); + + // d + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + } + else if(*direction == ARP_PLAY_ORDER) + { + // O + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // R + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 3), Color(0xFFFFFF)); + + // d + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + } + else + { + // Update animation step every 300ms + if (currentTime - lastUpdateTime > 300) { + currentStep = (currentStep + 1) % 9; + lastUpdateTime = currentTime; + } + + const ArpDirVisual& visual = arpDirVisuals[*direction]; + + for (uint8_t i = 0; i < 8; i++) { + uint8_t stepValue = visual.step[i]; + + if (stepValue == 0) { + // No show - skip + continue; + } + + uint8_t y = stepValue; // 1-based y coordinate + Point xy; + Color ledColor; + + if (y <= 4) { + // Normal range: render at Point(i, 4-y) + xy = origin + Point(i, 4 - y); + if (i < currentStep) { + ledColor = color; // Current step - bright + } else { + ledColor = color.Dim(); // Dim + } + } else { + // y > 4: render at Point(i, 8-y) with white color + xy = origin + Point(i, 8 - y); + if (i < currentStep) { + ledColor = Color(0xFFFFFF); // Current step - bright white + } else { + ledColor = Color(0xFFFFFF).Dim(); // Dim white + } + } + + MatrixOS::LED::SetColor(xy, ledColor); + } + } + return true; + } + + virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) { + // No key interaction for visualizer + if(keyInfo->State() == HOLD) + { + MatrixOS::UIUtility::TextScroll(arpDirectionNames[*direction], color); + } + return false; + } +}; \ No newline at end of file diff --git a/Applications/Note/Arpeggiator.cpp b/Applications/Note/Arpeggiator.cpp new file mode 100644 index 00000000..be9977dd --- /dev/null +++ b/Applications/Note/Arpeggiator.cpp @@ -0,0 +1,629 @@ +#include "Arpeggiator.h" +#include +#include + +Arpeggiator::Arpeggiator(ArpeggiatorConfig* cfg) : config(cfg) { + CalculateStepDurations(); +} + +void Arpeggiator::Tick(deque& input, deque& output) { + if (disableOnNextTick) { + disableOnNextTick = false; + enabled = false; + Reset(); + + for (const MidiPacket& packet : input) { + output.push_back(packet); + } + return; + } + + uint64_t currentTime = MatrixOS::SYS::Micros(); + + // Track if notePool was empty at the start of this tick + bool wasEmpty = notePool.empty(); + + // Process input packets - always track notes even when division is OFF + for (const MidiPacket& packet : input) { + if (packet.status == NoteOn || packet.status == NoteOff || packet.status == AfterTouch) { + if (packet.status == NoteOn && packet.Velocity() > 0) { + ProcessNoteOn(packet, output); + } else if (packet.status == AfterTouch) { + ProcessAfterTouch(packet, output); + } else { + ProcessNoteOff(packet, output); + } + } + + // Pass through all packets when division is OFF + if (division == DIV_OFF) { + output.push_back(packet); + } else if (packet.status != NoteOn && packet.status != NoteOff && packet.status != AfterTouch) { + output.push_back(packet); + } + } + + // Only do arpeggiator logic when division is not OFF + if (division != DIV_OFF) { + // Check for gate off timing - process from front of queue + while (!gateOffQueue.empty() && gateOffQueue.front().gateOffTime <= currentTime && + gateOffQueue.front().gateOffTime != UINT64_MAX) { + const GateOffEvent& event = gateOffQueue.front(); + output.push_back(MidiPacket::NoteOff(event.channel, event.note, 0)); + gateOffQueue.pop_front(); + } + + // Step arpeggiator if it's time (using swing timing) and not exceeded repeat limit + if (!notePool.empty() && (currentTime - lastStepTime) >= stepDuration[currentIndex % 2] && + (config->repeat == 0 || currentRepeat < config->repeat)) { + StepArpeggiator(output); + lastStepTime = currentTime; + } + + // If notePool was empty but now has notes, force start + if (wasEmpty && !notePool.empty()) { + currentIndex = 0; + currentRepeat = 0; // Reset repeat counter when starting fresh + lastSequenceIndex = 0; + if (config->repeat == 0 || currentRepeat < config->repeat) { + StepArpeggiator(output); + lastStepTime = currentTime; + } + } + + // If notePool becomes empty, turn off any sustained notes (for gate=0 mode) + if (!wasEmpty && notePool.empty()) { + for (const auto& event : gateOffQueue) { + output.push_back(MidiPacket::NoteOff(event.channel, event.note, 0)); // Turn off all sustained notes + } + gateOffQueue.clear(); // Clear all gate timers + } + } +} + +void Arpeggiator::ProcessNoteOn(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + uint8_t velocity = packet.Velocity(); + uint8_t channel = packet.Channel(); + uint64_t timestamp = MatrixOS::SYS::Micros(); + + // Add to note pool if not already present + bool found = false; + for (const ArpNote& arpNote : notePool) { + if (arpNote.note == note) { + found = true; + break; + } + } + + if (!found) { + ArpNote arpNote = {note, velocity, channel, (uint32_t)timestamp}; + notePool.push_back(arpNote); + UpdateSequence(); + } +} + +void Arpeggiator::ProcessNoteOff(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + + // Remove from note pool + notePool.erase(std::remove_if(notePool.begin(), notePool.end(), + [note](const ArpNote& arpNote) { return arpNote.note == note; }), + notePool.end()); + + // If this note is being sustained by the arpeggiator, turn it off immediately + // Find all instances of this note (there may be multiple with different gate times) + auto it = gateOffQueue.begin(); + while (it != gateOffQueue.end()) { + if (it->note == note) { + output.push_back(MidiPacket::NoteOff(it->channel, note, 0)); + it = gateOffQueue.erase(it); + } else { + ++it; + } + } + + UpdateSequence(); + + // Reset index if we've gone beyond the sequence + if (currentIndex >= arpSequence.size() && !arpSequence.empty()) { + currentIndex = 0; + } +} + +void Arpeggiator::ProcessAfterTouch(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + uint8_t velocity = packet.Velocity(); + + // Update velocity for the note in notePool + for (ArpNote& arpNote : notePool) { + if (arpNote.note == note) { + arpNote.velocity = velocity; + break; + } + } + + // Update velocity in arpSequence as well + for (ArpNote& seqNote : arpSequence) { + if (seqNote.note == note) { + seqNote.velocity = velocity; + } + } +} + +void Arpeggiator::UpdateSequence() { + arpSequence.clear(); + + if (notePool.empty()) { + return; + } + + // For RANDOM mode, just copy the pool - we'll pick randomly in StepArpeggiator + if (config->direction == ARP_RANDOM) { + // Build sequence with octave steps and offset for random selection + uint8_t actualSteps = config->step == 0 ? 1 : config->step; + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : notePool) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + return; + } + + // Sort notes based on direction + vector sortedNotes = notePool; + + switch (config->direction) { + case ARP_UP: + case ARP_UP_DOWN: + case ARP_UP_N_DOWN: + case ARP_CONVERGE: + case ARP_CON_DIVERGE: + case ARP_PINKY_UP: + case ARP_PINKY_UP_DOWN: + case ARP_THUMB_UP: + case ARP_THUMB_UP_DOWN: + std::sort(sortedNotes.begin(), sortedNotes.end(), + [](const ArpNote& a, const ArpNote& b) { return a.note < b.note; }); + break; + case ARP_DOWN: + case ARP_DOWN_UP: + case ARP_DOWN_N_UP: + case ARP_DIVERGE: + case ARP_DIV_CONVERGE: + std::sort(sortedNotes.begin(), sortedNotes.end(), + [](const ArpNote& a, const ArpNote& b) { return a.note > b.note; }); + break; + case ARP_PLAY_ORDER: + std::sort(sortedNotes.begin(), sortedNotes.end(), + [](const ArpNote& a, const ArpNote& b) { return a.timestamp < b.timestamp; }); + break; + } + + // Build base sequence based on direction + uint8_t actualSteps = config->step == 0 ? 1 : config->step; + + switch (config->direction) { + case ARP_UP: + case ARP_DOWN: + case ARP_PLAY_ORDER: + // Simple sequential patterns + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + break; + + case ARP_UP_DOWN: + // Up then down without repeating endpoints + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + if (arpSequence.size() > 1) { + for (int i = arpSequence.size() - 2; i > 0; i--) { + arpSequence.push_back(arpSequence[i]); + } + } + break; + + case ARP_DOWN_UP: + // Down then up without repeating endpoints + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + if (arpSequence.size() > 1) { + for (int i = arpSequence.size() - 2; i > 0; i--) { + arpSequence.push_back(arpSequence[i]); + } + } + break; + + case ARP_UP_N_DOWN: + // Up then down WITH repeating endpoints + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + if (arpSequence.size() > 0) { + for (int i = arpSequence.size() - 1; i >= 0; i--) { + arpSequence.push_back(arpSequence[i]); + } + } + break; + + case ARP_DOWN_N_UP: + // Down then up WITH repeating endpoints + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + if (arpSequence.size() > 0) { + for (int i = arpSequence.size() - 1; i >= 0; i--) { + arpSequence.push_back(arpSequence[i]); + } + } + break; + + case ARP_CONVERGE: + // Play from outside to inside + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + vector stepNotes; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + stepNotes.push_back(stepNote); + } + } + // Add notes alternating from ends toward middle + int left = 0, right = stepNotes.size() - 1; + while (left <= right) { + arpSequence.push_back(stepNotes[left++]); + if (left <= right) { + arpSequence.push_back(stepNotes[right--]); + } + } + } + break; + + case ARP_DIVERGE: + // Play from inside to outside + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + vector stepNotes; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + stepNotes.push_back(stepNote); + } + } + // Add notes from middle toward ends + int mid = stepNotes.size() / 2; + for (int i = 0; i <= mid && i < stepNotes.size(); i++) { + if (mid - i >= 0) { + arpSequence.push_back(stepNotes[mid - i]); + } + if (mid + i + 1 < stepNotes.size() && i > 0) { + arpSequence.push_back(stepNotes[mid + i]); + } + } + } + break; + + case ARP_CON_DIVERGE: + // Converge then diverge + { + vector convergeSeq; + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + vector stepNotes; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + stepNotes.push_back(stepNote); + } + } + int left = 0, right = stepNotes.size() - 1; + while (left <= right) { + convergeSeq.push_back(stepNotes[left++]); + if (left <= right) { + convergeSeq.push_back(stepNotes[right--]); + } + } + } + // Add converge sequence + arpSequence = convergeSeq; + // Add diverge sequence (reverse of converge) + for (int i = convergeSeq.size() - 2; i > 0; i--) { + arpSequence.push_back(convergeSeq[i]); + } + } + break; + + case ARP_DIV_CONVERGE: + // Diverge then converge + { + vector divergeSeq; + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + vector stepNotes; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + stepNotes.push_back(stepNote); + } + } + int mid = stepNotes.size() / 2; + for (int i = 0; i <= mid && i < stepNotes.size(); i++) { + if (mid - i >= 0) { + divergeSeq.push_back(stepNotes[mid - i]); + } + if (mid + i + 1 < stepNotes.size() && i > 0) { + divergeSeq.push_back(stepNotes[mid + i]); + } + } + } + // Add diverge sequence + arpSequence = divergeSeq; + // Add converge sequence (reverse of diverge) + for (int i = divergeSeq.size() - 2; i > 0; i--) { + arpSequence.push_back(divergeSeq[i]); + } + } + break; + + case ARP_PINKY_UP: + // Play lowest note, then rest ascending + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + bool first = true; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + if (first) { + arpSequence.push_back(stepNote); + first = false; + } + } + } + for (size_t i = 1; i < sortedNotes.size(); i++) { + ArpNote stepNote = sortedNotes[i]; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + break; + + case ARP_PINKY_UP_DOWN: + // Play lowest, then up, then down + { + vector baseSeq; + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + bool first = true; + for (const ArpNote& note : sortedNotes) { + ArpNote stepNote = note; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + if (first) { + baseSeq.push_back(stepNote); + first = false; + } + } + } + for (size_t i = 1; i < sortedNotes.size(); i++) { + ArpNote stepNote = sortedNotes[i]; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + baseSeq.push_back(stepNote); + } + } + } + arpSequence = baseSeq; + // Add reverse without endpoints + for (int i = baseSeq.size() - 2; i > 0; i--) { + arpSequence.push_back(baseSeq[i]); + } + } + break; + + case ARP_THUMB_UP: + // Play highest note, then rest ascending from lowest + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + // Add highest note first + if (!sortedNotes.empty()) { + ArpNote highNote = sortedNotes.back(); + highNote.note += stepNum * config->stepOffset; + if (highNote.note < 128 && highNote.note >= 0) { + arpSequence.push_back(highNote); + } + } + // Add rest ascending except the highest + for (size_t i = 0; i < sortedNotes.size() - 1; i++) { + ArpNote stepNote = sortedNotes[i]; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + arpSequence.push_back(stepNote); + } + } + } + break; + + case ARP_THUMB_UP_DOWN: + // Play highest, then up from lowest, then down + { + vector baseSeq; + for (uint8_t stepNum = 0; stepNum < actualSteps; stepNum++) { + // Add highest note first + if (!sortedNotes.empty()) { + ArpNote highNote = sortedNotes.back(); + highNote.note += stepNum * config->stepOffset; + if (highNote.note < 128 && highNote.note >= 0) { + baseSeq.push_back(highNote); + } + } + // Add rest ascending except the highest + for (size_t i = 0; i < sortedNotes.size() - 1; i++) { + ArpNote stepNote = sortedNotes[i]; + stepNote.note += stepNum * config->stepOffset; + if (stepNote.note < 128 && stepNote.note >= 0) { + baseSeq.push_back(stepNote); + } + } + } + arpSequence = baseSeq; + // Add reverse without endpoints + for (int i = baseSeq.size() - 2; i > 0; i--) { + arpSequence.push_back(baseSeq[i]); + } + } + break; + } +} + +void Arpeggiator::StepArpeggiator(deque& output) { + if (arpSequence.empty()) { + return; + } + + // Check if we've completed a sequence and need to handle repeats + if (config->repeat != 0 && currentIndex == 0 && lastSequenceIndex != 0) { + // We've wrapped around to the beginning of the sequence + currentRepeat++; + + // If we've reached the repeat limit, turn off sustained notes but don't play more + if (currentRepeat >= config->repeat) { + // Turn off all active notes by processing all pending gate-off events + for (const auto& gateEvent : gateOffQueue) { + output.push_back(MidiPacket::NoteOff(gateEvent.channel, gateEvent.note, 0)); + } + gateOffQueue.clear(); + return; + } + } + lastSequenceIndex = currentIndex; + + // For random mode, pick a random note from the sequence + if (config->direction == ARP_RANDOM) { + currentIndex = rand() % arpSequence.size(); + } + + // Play current note + const ArpNote& currentNote = arpSequence[currentIndex]; + output.push_back(MidiPacket::NoteOn(currentNote.channel, currentNote.note, currentNote.velocity)); + + + // Calculate gate off time based on gate percentage and current step duration + if (config->gateTime == 0) { + // Gate time 0 = always on until arp stops + // Use UINT64_MAX to indicate infinite gate time + gateOffQueue.push_back({UINT64_MAX, currentNote.note, currentNote.channel}); + } else { + // Calculate gate duration as percentage of step duration + uint32_t currentStepDuration = stepDuration[currentIndex % 2]; + uint32_t gateDuration = (currentStepDuration * config->gateTime) / 100; + uint64_t gateOffTime = MatrixOS::SYS::Micros() + gateDuration; + + // Add this note to the gate off queue + gateOffQueue.push_back({gateOffTime, currentNote.note, currentNote.channel}); + } + + // Advance to next note + currentIndex = (currentIndex + 1) % arpSequence.size(); +} + +void Arpeggiator::CalculateStepDurations() { + if (division == DIV_OFF || division == 0) { + stepDuration[0] = stepDuration[1] = 1000000; // 1 second fallback in microseconds + return; + } + + // Calculate base duration in microseconds based on BPM and division + uint32_t quarterNoteUs = (60 * 1000000) / config->bpm; + uint32_t baseDuration = quarterNoteUs * 4 / division; + + // Apply swing based on 20-80 range with 50 as center (no swing) + // Convert swing amount (20-80) to ratio (-0.3 to +0.3) + float swingRatio = (config->swing - 50) / 100.0f; + + stepDuration[0] = (uint32_t)(baseDuration * (1.0f + swingRatio)); // On-beat + stepDuration[1] = (uint32_t)(baseDuration * (1.0f - swingRatio)); // Off-beat +} + +void Arpeggiator::Reset() { + notePool.clear(); + arpSequence.clear(); + currentIndex = 0; + lastStepTime = 0; + gateOffQueue.clear(); // Clear all gate timers + disableOnNextTick = false; + currentRepeat = 0; // Reset repeat counter + lastSequenceIndex = 0; +} + +void Arpeggiator::SetEnabled(bool state) { + if (!state && enabled) { + disableOnNextTick = true; + } else { + enabled = state; + if (state) { + disableOnNextTick = false; + } + } +} + +void Arpeggiator::UpdateConfig(ArpeggiatorConfig* cfg) { + if(cfg != nullptr) + { + config = cfg; + } + CalculateStepDurations(); // Recalculate timings when config changes +} + +void Arpeggiator::SetDivision(ArpDivision div) { + ArpDivision oldDivision = division; + division = div; + CalculateStepDurations(); + + // If turning on arpeggiator with notes already held, start immediately + if (oldDivision == DIV_OFF && div != DIV_OFF && !notePool.empty()) { + currentIndex = 0; + currentRepeat = 0; // Reset repeat counter when restarting + lastSequenceIndex = 0; + lastStepTime = MatrixOS::SYS::Micros(); + // Note: We can't call StepArpeggiator here since we don't have output queue + // The force start will happen on the next Tick() + } +} \ No newline at end of file diff --git a/Applications/Note/Arpeggiator.h b/Applications/Note/Arpeggiator.h new file mode 100644 index 00000000..72d65272 --- /dev/null +++ b/Applications/Note/Arpeggiator.h @@ -0,0 +1,137 @@ +#pragma once + +#include "MatrixOS.h" +#include "MidiEffect.h" +#include +#include +#include + +using std::vector; +using std::deque; +using std::map; + +enum ArpDirection { + ARP_UP, + ARP_DOWN, + ARP_UP_DOWN, + ARP_DOWN_UP, + ARP_UP_N_DOWN, + ARP_DOWN_N_UP, + ARP_RANDOM, + ARP_PLAY_ORDER, + ARP_CONVERGE, + ARP_DIVERGE, + ARP_CON_DIVERGE, + ARP_DIV_CONVERGE, + ARP_PINKY_UP, + ARP_PINKY_UP_DOWN, + ARP_THUMB_UP, + ARP_THUMB_UP_DOWN, +}; + +enum ArpDivision { + DIV_OFF = 0, + DIV_WHOLE = 1, + DIV_HALF = 2, + DIV_THIRD = 3, + DIV_QUARTER = 4, + DIV_SIXTH = 6, + DIV_EIGHTH = 8, + DIV_TWELFTH = 12, + DIV_SIXTEENTH = 16, + DIV_TWENTYFOURTH = 24, + DIV_THIRTYSECOND = 32, + DIV_SIXTYFOURTH = 64 +}; + +inline const char* arpDirectionNames[16] = { + "Up", + "Down", + "Up Down", + "Down Up", + "Up & Down", + "Down & Up", + "Random", + "Play Order", + "Converge", + "Diverge", + "Con & Diverge", + "Div & Converge", + "Pinky Up", + "Pinky Up Down", + "Thumb Up", + "Thumb Up Down" +}; + +enum ArpClockSource { + CLOCK_INTERNAL, + CLOCK_INTERNAL_CLOCKOUT, // Send clock to external devices + CLOCK_EXTERNAL +}; + +struct ArpeggiatorConfig { + // Timing + uint32_t bpm = 120; // BPM (20-299) + ArpClockSource clockSource = CLOCK_INTERNAL; + uint8_t swing = 50; // Swing amount (20-80, 50=no swing) + + // Playback + uint8_t gateTime = 50; // Gate time (0% to 200%, 0=always on) + ArpDirection direction = ARP_UP; // Direction of arpeggiator + uint8_t step = 1; // Octave step (1 to 8) + int8_t stepOffset = 12; // Step offset (-48 to 48 semitones) + uint8_t repeat = 0; // Repeat the the sequence # times before stopping (0 to 100) (0 will be inf) +}; + +struct ArpNote { + uint8_t note; + uint8_t velocity; + uint8_t channel; + uint32_t timestamp; // When the note was pressed +}; + +class Arpeggiator : public MidiEffect { +private: + ArpeggiatorConfig* config; // Configuration pointer + vector notePool; // All active notes + vector arpSequence; // Current arp sequence + uint8_t currentIndex = 0; // Current position in sequence + + uint64_t lastStepTime = 0; // Last step time in microseconds + uint32_t stepDuration[2]; // [0] = on-beat, [1] = off-beat (for swing) + struct GateOffEvent { + uint64_t gateOffTime; + uint8_t note; + uint8_t channel; + }; + deque gateOffQueue; // Chronologically ordered queue of gate-off events + + bool disableOnNextTick = false; + + // Repeat tracking + uint8_t currentRepeat = 0; // Current repeat count + uint8_t lastSequenceIndex = 0; // Track last index to detect sequence completion + + // Helper functions + void ProcessNoteOn(const MidiPacket& packet, deque& output); + void ProcessNoteOff(const MidiPacket& packet, deque& output); + void ProcessAfterTouch(const MidiPacket& packet, deque& output); + void UpdateSequence(); + void StepArpeggiator(deque& output); + void CalculateStepDurations(); + +public: + ArpDivision division = DIV_OFF; // Note division (internal control) + + Arpeggiator(ArpeggiatorConfig* cfg); + + void Tick(deque& input, deque& output) override; + void Reset() override; + void SetEnabled(bool state) override; + + // Configuration access + void UpdateConfig(ArpeggiatorConfig* cfg = nullptr); + + // Division control + void SetDivision(ArpDivision div); +}; \ No newline at end of file diff --git a/Applications/Note/ChordEffect.cpp b/Applications/Note/ChordEffect.cpp new file mode 100644 index 00000000..af0cf36f --- /dev/null +++ b/Applications/Note/ChordEffect.cpp @@ -0,0 +1,282 @@ +#include "ChordEffect.h" +#include + +void ChordEffect::Tick(deque& input, deque& output) { + if (disableOnNextTick) { + disableOnNextTick = false; + enabled = false; + + // Release all chords + ReleaseAllChords(output); + + // Pass through remaining input + for (const MidiPacket& packet : input) { + output.push_back(packet); + } + return; + } + + // If chord combo changed, update all active chords + if (chordChanged) { + UpdateChords(output); + chordChanged = false; + } + + // Process each packet + for (const MidiPacket& packet : input) { + if (packet.status == NoteOn || packet.status == NoteOff) { + if (packet.status == NoteOn && packet.Velocity() > 0) { + ProcessNoteOn(packet, output); + } else { + ProcessNoteOff(packet, output); + } + } else if (packet.status == AfterTouch) { + ProcessAfterTouch(packet, output); + } else { + output.push_back(packet); + } + }; +} + +void ChordEffect::ProcessNoteOn(const MidiPacket& packet, deque& output) { + uint8_t root = packet.Note(); + uint8_t velocity = packet.Velocity(); + uint8_t channel = packet.Channel(); + + // Initialize NoteData for this root note + NoteData& data = noteMap[root]; + data.velocity = velocity; + data.chordNotes.clear(); + + // Add to note order if not already present + auto it = std::find(noteOrder.begin(), noteOrder.end(), root); + if (it == noteOrder.end()) { + noteOrder.push_back(root); + } + + // Build chord notes + vector newChordNotes = BuildChordFromNote(root); + + // Handle chord note conflicts and send MIDI + for (uint8_t chordNote : newChordNotes) { + // Check if another root note owns this chord note + if (noteOwner.find(chordNote) != noteOwner.end()) { + uint8_t previousOwner = noteOwner[chordNote]; + // Remove from previous owner's vector + if (noteMap.find(previousOwner) != noteMap.end()) { + auto& prevData = noteMap[previousOwner]; + prevData.chordNotes.erase(std::remove(prevData.chordNotes.begin(), prevData.chordNotes.end(), chordNote), prevData.chordNotes.end()); + } + } + + // Add to current root's chord notes + data.chordNotes.push_back(chordNote); + // Update reverse lookup + noteOwner[chordNote] = root; + + // Send note on + output.push_back(MidiPacket::NoteOn(channel, chordNote, velocity)); + } +} + +void ChordEffect::ProcessNoteOff(const MidiPacket& packet, deque& output) { + uint8_t root = packet.Note(); + uint8_t channel = packet.Channel(); + + // Check if this root note exists in noteMap + if (noteMap.find(root) == noteMap.end()) return; + + // Send note off for all chord notes in the noteMap under this root + for (uint8_t chordNote : noteMap[root].chordNotes) { + output.push_back(MidiPacket::NoteOff(channel, chordNote, 0)); + // Remove from reverse lookup + noteOwner.erase(chordNote); + } + + // Clear the root's entry from noteMap + noteMap.erase(root); + + // Remove from note order + noteOrder.erase(std::remove(noteOrder.begin(), noteOrder.end(), root), noteOrder.end()); +} + +void ChordEffect::ProcessAfterTouch(const MidiPacket& packet, deque& output) { + uint8_t root = packet.Note(); + uint8_t velocity = packet.Velocity(); + uint8_t channel = packet.Channel(); + + // Check if this root note exists in noteMap + if (noteMap.find(root) == noteMap.end()) return; + + // Update velocity based on velocity (aftertouch modulates velocity) + noteMap[root].velocity = velocity; + + // Send aftertouch for all chord notes in the noteMap under this root + for (uint8_t chordNote : noteMap[root].chordNotes) { + output.push_back(MidiPacket::AfterTouch(channel, chordNote, velocity)); + } +} + +void ChordEffect::Reset() { + noteMap.clear(); + noteOwner.clear(); + noteOrder.clear(); + chordChanged = true; + disableOnNextTick = false; +} + +void ChordEffect::SetEnabled(bool state) { + if (!state && enabled) { + // Schedule graceful disable on next tick + disableOnNextTick = true; + } else { + enabled = state; + if (state) { + disableOnNextTick = false; + } + } +} + +void ChordEffect::SetChordCombo(ChordCombo combo) { + chordCombo = combo; // This now populates chordIntervals + chordChanged = true; +} + +void ChordEffect::SetInversion(int8_t inversion) +{ + this->inversion = inversion; + chordChanged = true; +} + +void ChordEffect::CalculateChord() { + // Clear and rebuild intervals directly + chordIntervals.clear(); + chordIntervals.push_back(0); // Root note + + // Base triads + if (chordCombo.dim) { + chordIntervals.push_back(3); // b3 + chordIntervals.push_back(6); // b5 + } + + if (chordCombo.min) { + chordIntervals.push_back(3); // b3 + chordIntervals.push_back(7); // 5 + } + + if (chordCombo.maj) { + chordIntervals.push_back(4); // 3 + chordIntervals.push_back(7); // 5 + } + + if (chordCombo.sus) { + chordIntervals.push_back(5); // 4 + chordIntervals.push_back(7); // 5 + } + + // Extensions + if (chordCombo.ext6) { + chordIntervals.push_back(9); // 6 + } + if (chordCombo.extMin7) { + chordIntervals.push_back(10); // b7 + } + if (chordCombo.extMaj7) { + chordIntervals.push_back(11); // 7 + } + if (chordCombo.ext9) { + chordIntervals.push_back(14); // 9 + } +} + +void ChordEffect::ReleaseAllChords(deque& output) +{ + // Go through all keys in noteOwner and send note off + for (auto& pair : noteOwner) { + uint8_t note = pair.first; + output.push_back(MidiPacket::NoteOff(0, note, 0)); + } + + // Clear all maps and vectors + noteMap.clear(); + noteOwner.clear(); + noteOrder.clear(); +} + +void ChordEffect::UpdateChords(deque& output) +{ + CalculateChord(); + + // First, note off all current chord notes (including root notes) + for (auto& pair : noteMap) { + uint8_t root = pair.first; + NoteData& data = pair.second; + for (uint8_t chordNote : data.chordNotes) { + output.push_back(MidiPacket::NoteOff(0, chordNote, 0)); + } + } + + // Clear noteOwner for chord notes + noteOwner.clear(); + + // Recalculate and send new chords for all active root notes in FIFO order + for (uint8_t root : noteOrder) { + if (noteMap.find(root) == noteMap.end()) continue; // Safety check + + NoteData& data = noteMap[root]; + // Use the velocity stored in NoteData + uint8_t velocity = data.velocity; + + // Build new chord notes + vector newChordNotes = BuildChordFromNote(root); + + // Handle chord note conflicts and send MIDI + for (uint8_t chordNote : newChordNotes) { + // Check if another root already owns this note + if (noteOwner.find(chordNote) != noteOwner.end()) { + // Find the previous owner and remove from their vector + uint8_t prevOwner = noteOwner[chordNote]; + if (noteMap.find(prevOwner) != noteMap.end()) { + auto& prevData = noteMap[prevOwner]; + prevData.chordNotes.erase(std::remove(prevData.chordNotes.begin(), prevData.chordNotes.end(), chordNote), prevData.chordNotes.end()); + } + } + + noteOwner[chordNote] = root; + output.push_back(MidiPacket::NoteOn(0, chordNote, velocity)); // Use saved velocity + } + + // Update the noteMap with new chord notes + data.chordNotes = newChordNotes; + } +} + +vector ChordEffect::BuildChordFromNote(uint8_t root) +{ + vector chordNotes; + + // Use pre-calculated intervals from CalculateChord + const vector& intervals = chordIntervals; + + // Apply inversion logic + for (uint8_t i = 0; i < intervals.size(); i++) { + uint8_t interval = intervals[i]; + uint8_t chordNote = root + interval; + + // Apply inversion: notes below the inversion point get moved up by octaves + if (i < (inversion % intervals.size())) { + uint8_t octaveShift = (inversion / intervals.size() + 1) * 12; + chordNote += octaveShift; + } else if (inversion >= intervals.size()) { + // For higher inversions, add base octave shifts + uint8_t octaveShift = (inversion / intervals.size()) * 12; + chordNote += octaveShift; + } + + if (chordNote < 128) { + chordNotes.push_back(chordNote); + } + } + + return chordNotes; +} \ No newline at end of file diff --git a/Applications/Note/ChordEffect.h b/Applications/Note/ChordEffect.h new file mode 100644 index 00000000..35e77e1f --- /dev/null +++ b/Applications/Note/ChordEffect.h @@ -0,0 +1,50 @@ +#pragma once + +#include "MatrixOS.h" +#include "MidiEffect.h" + +struct NoteData { + uint8_t velocity; + vector chordNotes; +}; + +struct ChordCombo { + bool dim:1; + bool min:1; + bool maj:1; + bool sus:1; + + bool ext6:1; + bool extMin7:1; + bool extMaj7:1; + bool ext9:1; +}; + +class ChordEffect : public MidiEffect { +private: + unordered_map noteMap; // Maps root note -> its velocity and generated chord notes + unordered_map noteOwner; // Reverse lookup: maps chord note -> root note that owns it + vector noteOrder; // Tracks insertion order for FIFO processing + vector chordIntervals; // Pre-calculated chord intervals + bool chordChanged = true; + bool disableOnNextTick = false; + + // Helper functions + void ProcessNoteOn(const MidiPacket& packet, deque& output); + void ProcessNoteOff(const MidiPacket& packet, deque& output); + void ProcessAfterTouch(const MidiPacket& packet, deque& output); + vector BuildChordFromNote(uint8_t root); + void CalculateChord(); + +public: + int8_t inversion = 0; + ChordCombo chordCombo = {0}; + + void Tick(deque& input, deque& output) override; + void Reset() override; + void SetEnabled(bool state) override; + void SetChordCombo(ChordCombo combo); + void ReleaseAllChords(deque& output); + void UpdateChords(deque& output); + void SetInversion(int8_t inversion); +}; \ No newline at end of file diff --git a/Applications/Note/InfDisplay.h b/Applications/Note/InfDisplay.h new file mode 100644 index 00000000..f4787285 --- /dev/null +++ b/Applications/Note/InfDisplay.h @@ -0,0 +1,57 @@ +#pragma once +#include "MatrixOS.h" +#include "ui/UI.h" + + +class InfDisplay : public UIComponent { + public: + Color color; + + InfDisplay(Color color) { + this->color = color; + } + + virtual Dimension GetSize() { return Dimension(8, 4); } + + virtual bool Render(Point origin) { + Dimension size = GetSize(); + for(int8_t y = 0; y < size.y; y++) + { + for(int8_t x = 0; x < size.x; x++) + { + MatrixOS::LED::SetColor(origin + Point(x, y), Color(0)); + } + } + + // I + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 1), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + + // N + MatrixOS::LED::SetColor(origin + Point(2, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // F + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + + + return true; + } + + virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) { + return false;// passthrough keypress + } +}; \ No newline at end of file diff --git a/Applications/Note/MidiEffect.h b/Applications/Note/MidiEffect.h new file mode 100644 index 00000000..375133a2 --- /dev/null +++ b/Applications/Note/MidiEffect.h @@ -0,0 +1,24 @@ +#pragma once + +#include "MatrixOS.h" + +// Base class for MIDI effects - processes arrays of packets +class MidiEffect { +protected: + bool enabled = true; + +public: + virtual ~MidiEffect() = default; + + // Process input array and populate output array + // Input: packets to process (can be empty for generators like arpeggiator/LFO) + // Output: processed packets (effect should append to this) + virtual void Tick(deque& input, deque& output) = 0; + + // Called when effect is reset - cleanup state + virtual void Reset() {} + + // Enable/disable control + virtual void SetEnabled(bool state) { enabled = state; } + bool IsEnabled() const { return enabled; } +}; \ No newline at end of file diff --git a/Applications/Note/MidiPipeline.cpp b/Applications/Note/MidiPipeline.cpp new file mode 100644 index 00000000..c0ec35b2 --- /dev/null +++ b/Applications/Note/MidiPipeline.cpp @@ -0,0 +1,133 @@ +#include "MidiPipeline.h" + +void MidiPipeline::Send(const MidiPacket& packet) { + inputQueue.push_back(packet); +} + +bool MidiPipeline::Get(MidiPacket& packet) { + if (outputQueue.empty()) { + return false; + } + packet = outputQueue.front(); + outputQueue.pop_front(); + return true; +} + +void MidiPipeline::Tick() { + if (effects.empty()) { + // No effects, direct passthrough + outputQueue.insert(outputQueue.end(), + std::make_move_iterator(inputQueue.begin()), + std::make_move_iterator(inputQueue.end())); + inputQueue.clear(); + return; + } + + std::deque effect_input; + std::deque effect_output; + + // Start with inputQueue as effect_output + effect_output.swap(inputQueue); + + for (MidiEffect* effect : effects) { + if (effect->IsEnabled()) { + effect_input.swap(effect_output); // take previous output as new input + effect_output.clear(); // prepare output buffer + effect->Tick(effect_input, effect_output); + } + } + + // Update note states and move final output to main queue + for (const MidiPacket& packet : effect_output) { + // Track note on/off states + if (packet.status == NoteOn && packet.Velocity() > 0) { + SetNoteState(packet.Note(), true); + } + else if (packet.status == NoteOff || (packet.status == NoteOn && packet.Velocity() == 0)) { + SetNoteState(packet.Note(), false); + } + } + + outputQueue.insert(outputQueue.end(), + std::make_move_iterator(effect_output.begin()), + std::make_move_iterator(effect_output.end())); + effect_output.clear(); +} + +int32_t MidiPipeline::AddEffect(const string& key, MidiEffect* effect, const string& addAfter) { + if (addAfter == "head") { + // Add to front + effectKeys.insert(effectKeys.begin(), key); + effects.insert(effects.begin(), effect); + return 0; + } else if (addAfter == "tail" || addAfter.empty()) { + // Add to back (default) + effectKeys.push_back(key); + effects.push_back(effect); + return static_cast(effectKeys.size() - 1); + } else { + // Add after specific key + for (size_t i = 0; i < effectKeys.size(); ++i) { + if (effectKeys[i] == addAfter) { + effectKeys.insert(effectKeys.begin() + i + 1, key); + effects.insert(effects.begin() + i + 1, effect); + return static_cast(i + 1); + } + } + // If key not found, return -1 + return -1; + } +} + +void MidiPipeline::RemoveEffect(const string& key) { + for (size_t i = 0; i < effectKeys.size(); ++i) { + if (effectKeys[i] == key) { + effectKeys.erase(effectKeys.begin() + i); + effects.erase(effects.begin() + i); + break; + } + } +} + +void MidiPipeline::Reset() { + for (MidiEffect* effect : effects) { + if (effect) { + effect->Reset(); + } + } +} + +void MidiPipeline::Clear() { + effectKeys.clear(); + effects.clear(); + inputQueue.clear(); + outputQueue.clear(); + memset(noteStates, 0, sizeof(noteStates)); +} + +size_t MidiPipeline::GetEffectCount() const { + return effects.size(); +} + +void MidiPipeline::SetNoteState(uint8_t note, bool on) { + if (note >= 128) return; + uint8_t byteIndex = note / 8; + uint8_t bitIndex = note % 8; + if (byteIndex < 16) { + if (on) { + noteStates[byteIndex] |= (1 << bitIndex); + } else { + noteStates[byteIndex] &= ~(1 << bitIndex); + } + } +} + +bool MidiPipeline::IsNoteActive(uint8_t note) const { + if (note >= 128) return false; + uint8_t byteIndex = note / 8; + uint8_t bitIndex = note % 8; + if (byteIndex < 16) { + return (noteStates[byteIndex] & (1 << bitIndex)) != 0; + } + return false; +} \ No newline at end of file diff --git a/Applications/Note/MidiPipeline.h b/Applications/Note/MidiPipeline.h new file mode 100644 index 00000000..e1c28b82 --- /dev/null +++ b/Applications/Note/MidiPipeline.h @@ -0,0 +1,50 @@ +#pragma once + +#include "MatrixOS.h" +#include "MidiEffect.h" + +// Pipeline manager class - minimal and efficient +class MidiPipeline { +private: + vector effectKeys; // Effect keys in order + vector effects; // Effects in same order + deque inputQueue; // Input queue + deque outputQueue; // Output queue + uint8_t noteStates[16] = {0}; // Bitmap tracking active notes (each bit represents a note) + + // Private note state management + void SetNoteState(uint8_t note, bool on); + +public: + // Send packet to input queue + void Send(const MidiPacket& packet); + + // Get packet from output queue + bool Get(MidiPacket& packet); + + // Process all input queue packets and call Tick on all effects + void Tick(); + + // Add effect to chain with a key + // addAfter: key to add after, or "head" for front, "tail" or "" for back (default) + // Returns index where effect was inserted, or -1 if addAfter key is invalid + int32_t AddEffect(const string& key, MidiEffect* effect, const string& addAfter = ""); + + // Remove effect from chain by key + void RemoveEffect(const string& key); + + // Get effect by key + MidiEffect* GetEffect(const string& key) const; + + // Reset pipeline - calls Reset() on all effects + void Reset(); + + // Clear all effects + void Clear(); + + // Get effect count + size_t GetEffectCount() const; + + // Public note state queries + bool IsNoteActive(uint8_t note) const; +}; \ No newline at end of file diff --git a/Applications/Note/Note.cpp b/Applications/Note/Note.cpp index 611e6859..f70281c4 100644 --- a/Applications/Note/Note.cpp +++ b/Applications/Note/Note.cpp @@ -2,6 +2,10 @@ #include "OctaveShifter.h" #include "ScaleVisualizer.h" #include "UnderglowLight.h" +#include "NoteControlBar.h" +#include "ArpDirVisualizer.h" +#include "TimedDisplay.h" +#include "InfDisplay.h" void Note::Setup(const vector& args) { // Set up / Load configs -------------------------------------------------------------------------- @@ -12,8 +16,17 @@ void Note::Setup(const vector& args) { // Load From NVS if (nvsVersion == (uint32_t)NOTE_APP_VERSION) - { - MatrixOS::NVS::GetVariable(NOTE_CONFIGS_HASH, notePadConfigs, sizeof(notePadConfigs)); + { + size_t storedSize = MatrixOS::NVS::GetSize(NOTE_CONFIGS_HASH); + + // Check if stored size matches current structure size + if (storedSize == sizeof(notePadConfigs)) { + MatrixOS::NVS::GetVariable(NOTE_CONFIGS_HASH, notePadConfigs, sizeof(notePadConfigs)); + } else { + // Size mismatch - structure has changed, use defaults and save them + MLOGD("Note", "Config size mismatch: stored=%d, expected=%d. Using defaults.", storedSize, sizeof(notePadConfigs)); + MatrixOS::NVS::SetVariable(NOTE_CONFIGS_HASH, notePadConfigs, sizeof(notePadConfigs)); + } } else { @@ -60,9 +73,6 @@ void Note::Setup(const vector& args) { } actionMenu.AddUIComponent(forceSensitiveToggle, Point(7, 5)); - OctaveShifter octaveShifter(8, notePadConfigs, &activeConfig.value); - actionMenu.AddUIComponent(octaveShifter, Point(0, 0)); - // Split View UIButton splitViewToggle; splitViewToggle.SetName("Split View"); @@ -84,16 +94,18 @@ void Note::Setup(const vector& args) { case HORIZ_SPLIT: MatrixOS::UIUtility::TextScroll("Horizontal Split", Color(0xFF00FF)); break; } }); - actionMenu.AddUIComponent(splitViewToggle, Point(1, 0)); + actionMenu.AddUIComponent(splitViewToggle, Point(0, 0)); UIButton notepad1SelectBtn; notepad1SelectBtn.SetName("Note Pad 1"); + notepad1SelectBtn.SetSize(Dimension(2, 1)); notepad1SelectBtn.SetColorFunc([&]() -> Color { return notePadConfigs[0].color.DimIfNot(activeConfig.Get() == 0); }); notepad1SelectBtn.OnPress([&]() -> void { activeConfig = 0; }); - actionMenu.AddUIComponent(notepad1SelectBtn, Point(3, 0)); + actionMenu.AddUIComponent(notepad1SelectBtn, Point(2, 0)); UIButton notepad2SelectBtn; notepad2SelectBtn.SetName("Note Pad 2"); + notepad2SelectBtn.SetSize(Dimension(2, 1)); notepad2SelectBtn.SetColorFunc([&]() -> Color { return notePadConfigs[1].color.DimIfNot(activeConfig.Get() == 1); }); notepad2SelectBtn.OnPress([&]() -> void { activeConfig = 1; }); actionMenu.AddUIComponent(notepad2SelectBtn, Point(4, 0)); @@ -104,6 +116,65 @@ void Note::Setup(const vector& args) { notepadColorBtn.OnPress([&]() -> void { ColorSelector(); }); actionMenu.AddUIComponent(notepadColorBtn, Point(7, 0)); + // Octave Control + int32_t octaveAbs; + actionMenu.SetLoopFunc([&]() -> void { + octaveAbs = (int32_t)abs(notePadConfigs[activeConfig].octave); + }); + + UI4pxNumber octaveDisplay; + octaveDisplay.SetColor(notePadConfigs[activeConfig].color); + octaveDisplay.SetDigits(2); + octaveDisplay.SetValuePointer(&octaveAbs); + octaveDisplay.SetAlternativeColor(notePadConfigs[activeConfig].rootColor); + actionMenu.AddUIComponent(octaveDisplay, Point(0, 2)); + + UIButton octaveNegSign; + octaveNegSign.SetColor(notePadConfigs[activeConfig].rootColor); + octaveNegSign.SetSize(Dimension(2, 1)); + octaveNegSign.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].octave < 0; }); + actionMenu.AddUIComponent(octaveNegSign, Point(2, 4)); + + UIButton octavePlusBtn; + octavePlusBtn.SetName("Octave +1"); + octavePlusBtn.SetSize(Dimension(2, 1)); + octavePlusBtn.SetColorFunc([&]() -> Color { return Color(0x80FF00).DimIfNot(notePadConfigs[activeConfig].octave < 12); }); + octavePlusBtn.OnPress([&]() -> void { + if(notePadConfigs[activeConfig].octave < 12) + { + notePadConfigs[activeConfig].octave++; + } + }); + actionMenu.AddUIComponent(octavePlusBtn, Point(4, 7)); + + UIButton octaveMinusBtn; + octaveMinusBtn.SetName("Octave -1"); + octaveMinusBtn.SetSize(Dimension(2, 1)); + octaveMinusBtn.SetColorFunc([&]() -> Color { return Color(0xFF0060).DimIfNot(notePadConfigs[activeConfig].octave > -2); }); + octaveMinusBtn.OnPress([&]() -> void { + if(notePadConfigs[activeConfig].octave > -2) + { + notePadConfigs[activeConfig].octave--; + } + }); + actionMenu.AddUIComponent(octaveMinusBtn, Point(2, 7)); + + // Control Bar + UIToggle controlBarToggle; + controlBarToggle.SetName("Control Bar"); + controlBarToggle.SetColor(Color(0xFF8000)); + controlBarToggle.SetValuePointer(&controlBar); + controlBarToggle.OnPress([&]() -> void {controlBar.Save();}); + actionMenu.AddUIComponent(controlBarToggle, Point(0, 7)); + + UIButton arpConfigBtn; + arpConfigBtn.SetName("Arpeggiator Config"); + arpConfigBtn.SetSize(Dimension(1, 4)); + arpConfigBtn.SetColor(Color(0x80FF00)); + arpConfigBtn.OnPress([&]() -> void {ArpConfigMenu();}); + arpConfigBtn.SetEnableFunc([&]() -> bool {return controlBar;}); + actionMenu.AddUIComponent(arpConfigBtn, Point(0, 2)); + // Other Controls UIButton systemSettingBtn; systemSettingBtn.SetName("System Setting"); @@ -125,6 +196,7 @@ void Note::Setup(const vector& args) { } return false; }); + actionMenu.AllowExit(false); actionMenu.SetSetupFunc([&]() -> void {PlayView();}); actionMenu.Start(); @@ -141,11 +213,11 @@ void Note::PlayView() { switch (splitView) { case SINGLE_VIEW: - padSize = Dimension(8, 8); + padSize = Dimension(8, 8 - (controlBar ? 1 : 0)); underglowSize = Dimension(10, 10); break; case VERT_SPLIT: - padSize = Dimension(4, 8); + padSize = Dimension(4, 8 - (controlBar ? 1 : 0)); underglowSize = Dimension(5, 10); break; case HORIZ_SPLIT: @@ -155,14 +227,23 @@ void Note::PlayView() { } - NotePad notePad1(padSize, ¬ePadConfigs[activeConfig.Get() == 1]); + // Create NotePadRuntime structures + NotePadRuntime NotePadRuntime1; + NotePadRuntime1.config = ¬ePadConfigs[activeConfig.Get() == 1]; + + NotePadRuntime NotePadRuntime2; + NotePadRuntime2.config = ¬ePadConfigs[activeConfig.Get() == 0]; + + NotePad notePad1(padSize, &NotePadRuntime1); playView.AddUIComponent(notePad1, Point(0, 0)); + activeNotePads[0] = ¬ePad1; UnderglowLight underglow1(underglowSize, notePadConfigs[activeConfig.Get() == 1].color); playView.AddUIComponent(underglow1, Point(-1, -1)); - NotePad notePad2(padSize, ¬ePadConfigs[activeConfig.Get() == 0]); + NotePad notePad2(padSize, &NotePadRuntime2); UnderglowLight underglow2(underglowSize, notePadConfigs[activeConfig.Get() == 0].color); + activeNotePads[1] = ¬ePad2; if (splitView == VERT_SPLIT) { playView.AddUIComponent(notePad2, Point(4, 0)); @@ -173,8 +254,23 @@ void Note::PlayView() { playView.AddUIComponent(notePad2, Point(0, 4)); playView.AddUIComponent(underglow2, Point(-1, 4)); } + + NoteControlBar noteControlBar(this, ¬ePad1, ¬ePad2, &underglow1, &underglow2); + + if(controlBar) + { + playView.AddUIComponent(noteControlBar, Point(0, 8 - CTL_BAR_Y)); + } + + playView.SetLoopFunc([&]() -> void { + notePad1.Tick(); + notePad2.Tick(); + }); playView.Start(); + + activeNotePads[0] = nullptr; + activeNotePads[1] = nullptr; } void Note::ScaleSelector() { @@ -193,6 +289,11 @@ void Note::ScaleSelector() { scaleSelectorBar.SetIndividualNameFunc([&](uint16_t index) -> string { return scale_names[index]; }); scaleSelector.AddUIComponent(scaleSelectorBar, Point(0, 4)); + scaleSelector.SetLoopFunc([&]() -> void { + if(activeNotePads[0] != nullptr) {activeNotePads[0]->Tick();} + if(activeNotePads[1] != nullptr) {activeNotePads[1]->Tick();} + }); + scaleSelector.Start(); } @@ -200,7 +301,11 @@ void Note::ColorSelector() { UI colorSelector("Color Selector", notePadConfigs[activeConfig].color, false); uint8_t page = 0; // 0 = Preset, 1 = Customize - NotePad notePad(Dimension(8, 4), ¬ePadConfigs[activeConfig]); + // Create NotePadRuntime structure for color selector + NotePadRuntime colorSelectorData; + colorSelectorData.config = ¬ePadConfigs[activeConfig]; + + NotePad notePad(Dimension(8, 4), &colorSelectorData); colorSelector.AddUIComponent(notePad, Point(0, 0)); UIButton presetsBtn; @@ -384,21 +489,22 @@ void Note::ColorSelector() { } void Note::LayoutSelector() { - UI layoutSelector("Layout Selector", Color(0xFFFF00), false); + const Color color = Color(0xFFFF00); + UI layoutSelector("Layout Selector", color, false); int32_t x_offset = notePadConfigs[activeConfig].x_offset; int32_t y_offset = notePadConfigs[activeConfig].y_offset; UIButton octaveModeBtn; octaveModeBtn.SetName("Octave Mode"); - octaveModeBtn.SetColorFunc([&]() -> Color { return Color(0xFFFF00).DimIfNot(notePadConfigs[activeConfig].mode == OCTAVE_LAYOUT); }); + octaveModeBtn.SetColorFunc([&]() -> Color { Color c = color; return c.DimIfNot(notePadConfigs[activeConfig].mode == OCTAVE_LAYOUT); }); octaveModeBtn.OnPress([&]() -> void { notePadConfigs[activeConfig].mode = OCTAVE_LAYOUT; }); layoutSelector.AddUIComponent(octaveModeBtn, Point(2, 0)); UIButton offsetModeBtn; offsetModeBtn.SetName("Offset Mode"); - offsetModeBtn.SetColorFunc([&]() -> Color { return Color(0xFFFF00).DimIfNot(notePadConfigs[activeConfig].mode == OFFSET_LAYOUT); }); - offsetModeBtn.OnPress([&]() -> void { + offsetModeBtn.SetColorFunc([&]() -> Color { Color c = color; return c.DimIfNot(notePadConfigs[activeConfig].mode == OFFSET_LAYOUT); }); + offsetModeBtn.OnPress([&]() -> void { if(notePadConfigs[activeConfig].mode != OFFSET_LAYOUT) { notePadConfigs[activeConfig].mode = OFFSET_LAYOUT; @@ -410,51 +516,195 @@ void Note::LayoutSelector() { UIButton chromaticModeBtn; chromaticModeBtn.SetName("Chromatic Mode"); - chromaticModeBtn.SetColorFunc([&]() -> Color { return Color(0xFFFF00).DimIfNot(notePadConfigs[activeConfig].mode == CHROMATIC_LAYOUT); }); + chromaticModeBtn.SetColorFunc([&]() -> Color { Color c = color; return c.DimIfNot(notePadConfigs[activeConfig].mode == CHROMATIC_LAYOUT); }); chromaticModeBtn.OnPress([&]() -> void { notePadConfigs[activeConfig].mode = CHROMATIC_LAYOUT; }); layoutSelector.AddUIComponent(chromaticModeBtn, Point(4, 0)); UIButton pianoModeBtn; - pianoModeBtn.SetName("Piano Mode"); - pianoModeBtn.SetColorFunc([&]() -> Color { return Color(0xFFFF00).DimIfNot(notePadConfigs[activeConfig].mode == PIANO_LAYOUT); }); + pianoModeBtn.SetName("Piano Keyboard"); + pianoModeBtn.SetColorFunc([&]() -> Color { Color c = color; return c.DimIfNot(notePadConfigs[activeConfig].mode == PIANO_LAYOUT); }); pianoModeBtn.OnPress([&]() -> void { notePadConfigs[activeConfig].mode = PIANO_LAYOUT; }); layoutSelector.AddUIComponent(pianoModeBtn, Point(5, 0)); + // Octave mode + TimedDisplay octTextDisplay(UINT32_MAX); + octTextDisplay.SetDimension(Dimension(8, 4)); + octTextDisplay.SetRenderFunc([&](Point origin) -> void { + // O + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // C + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // T + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + }); + octTextDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OCTAVE_LAYOUT; }); + layoutSelector.AddUIComponent(octTextDisplay, Point(0, 2)); + + // Offset Mode + const Color xColor = Color(0x00FFFF); + const Color yColor = Color(0xFF00FF); + + UI4pxNumber yOffsetDisplay; + yOffsetDisplay.SetColor(yColor); + yOffsetDisplay.SetDigits(1); + yOffsetDisplay.SetValuePointer((int32_t*)&y_offset); + yOffsetDisplay.SetAlternativeColor(yColor); + yOffsetDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); + layoutSelector.AddUIComponent(yOffsetDisplay, Point(2, 2)); + + UI4pxNumber xOffsetDisplay; + xOffsetDisplay.SetColor(xColor); + xOffsetDisplay.SetDigits(1); + xOffsetDisplay.SetValuePointer((int32_t*)&x_offset); + xOffsetDisplay.SetAlternativeColor(xColor); + xOffsetDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); + layoutSelector.AddUIComponent(xOffsetDisplay, Point(5, 2)); + + TimedDisplay ofsTextDisplay(500); + ofsTextDisplay.SetDimension(Dimension(6, 4)); + ofsTextDisplay.SetRenderFunc([&](Point origin) -> void { + // Y + MatrixOS::LED::SetColor(origin + Point(0, 0), yColor); + MatrixOS::LED::SetColor(origin + Point(0, 1), yColor); + MatrixOS::LED::SetColor(origin + Point(1, 2), yColor); + MatrixOS::LED::SetColor(origin + Point(1, 3), yColor); + MatrixOS::LED::SetColor(origin + Point(2, 0), yColor); + MatrixOS::LED::SetColor(origin + Point(2, 1), yColor); + + // X + MatrixOS::LED::SetColor(origin + Point(3, 0), xColor); + MatrixOS::LED::SetColor(origin + Point(3, 3), xColor); + MatrixOS::LED::SetColor(origin + Point(4, 1), xColor); + MatrixOS::LED::SetColor(origin + Point(4, 2), xColor); + MatrixOS::LED::SetColor(origin + Point(5, 0), xColor); + MatrixOS::LED::SetColor(origin + Point(5, 3), xColor); + }); + ofsTextDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); + layoutSelector.AddUIComponent(ofsTextDisplay, Point(2, 2)); + UISelector yOffsetInput; yOffsetInput.SetDimension(Dimension(1, 8)); yOffsetInput.SetName("Y Offset"); - yOffsetInput.SetColor(Color(0xFF00FF)); + yOffsetInput.SetColor(yColor); yOffsetInput.SetValuePointer((uint16_t*)&y_offset); yOffsetInput.SetLitMode(UISelectorLitMode::LIT_LESS_EQUAL_THAN); yOffsetInput.SetDirection(UISelectorDirection::UP_THEN_RIGHT); yOffsetInput.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); + yOffsetInput.OnChange([&](uint16_t val) -> void { + notePadConfigs[activeConfig].y_offset = val; + ofsTextDisplay.Disable(); + }); layoutSelector.AddUIComponent(yOffsetInput, Point(0, 0)); - UI4pxNumber yOffsetDisplay; - yOffsetDisplay.SetColor(Color(0xFF00FF)); - yOffsetDisplay.SetDigits(1); - yOffsetDisplay.SetValuePointer((int32_t*)&y_offset); - yOffsetDisplay.SetAlternativeColor(Color(0xFF00FF)); - yOffsetDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); - layoutSelector.AddUIComponent(yOffsetDisplay, Point(2, 2)); - UISelector xOffsetInput; xOffsetInput.SetDimension(Dimension(8, 1)); xOffsetInput.SetName("X Offset"); - xOffsetInput.SetColor(Color(0x00FFFF)); + xOffsetInput.SetColor(xColor); xOffsetInput.SetValuePointer((uint16_t*)&x_offset); xOffsetInput.SetLitMode(UISelectorLitMode::LIT_LESS_EQUAL_THAN); xOffsetInput.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); + xOffsetInput.OnChange([&](uint16_t val) -> void { + notePadConfigs[activeConfig].x_offset = val; + ofsTextDisplay.Disable(); + }); layoutSelector.AddUIComponent(xOffsetInput, Point(0, 7)); - UI4pxNumber xOffsetDisplay; - xOffsetDisplay.SetColor(Color(0x00FFFF)); - xOffsetDisplay.SetDigits(1); - xOffsetDisplay.SetValuePointer((int32_t*)&x_offset); - xOffsetDisplay.SetAlternativeColor(Color(0x00FFFF)); - xOffsetDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == OFFSET_LAYOUT; }); - layoutSelector.AddUIComponent(xOffsetDisplay, Point(5, 2)); + // Chromatic + TimedDisplay chmTextDisplay(UINT32_MAX); + chmTextDisplay.SetDimension(Dimension(8, 4)); + chmTextDisplay.SetRenderFunc([&](Point origin) -> void { + // C + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + + // H + MatrixOS::LED::SetColor(origin + Point(2, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // M + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + chmTextDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == CHROMATIC_LAYOUT; }); + layoutSelector.AddUIComponent(chmTextDisplay, Point(0, 2)); + + // Piano + TimedDisplay pioTextDisplay(UINT32_MAX); + pioTextDisplay.SetDimension(Dimension(8, 4)); + pioTextDisplay.SetRenderFunc([&](Point origin) -> void { + // P + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + + // I + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + + // O + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + pioTextDisplay.SetEnableFunc([&]() -> bool { return notePadConfigs[activeConfig].mode == PIANO_LAYOUT; }); + layoutSelector.AddUIComponent(pioTextDisplay, Point(0, 2)); // Show Off Scale Notes Toggle (only for Octave and Offset modes) UIButton inKeyNoteOnlyToggle; @@ -465,20 +715,15 @@ void Note::LayoutSelector() { layoutSelector.AddUIComponent(inKeyNoteOnlyToggle, Point(0, 7)); layoutSelector.Start(); - - if(notePadConfigs[activeConfig].mode == OFFSET_LAYOUT) - { - notePadConfigs[activeConfig].x_offset = x_offset; - notePadConfigs[activeConfig].y_offset = y_offset; - } } void Note::ChannelSelector() { - UI channelSelector("Channel Selector", Color(0x60FF00), false); + Color color = Color(0x60FF00); + UI channelSelector("Channel Selector", color, false); int32_t offsettedChannel = notePadConfigs[activeConfig].channel + 1; UI4pxNumber numDisplay; - numDisplay.SetColor(Color(0x60FF00)); + numDisplay.SetColor(color); numDisplay.SetDigits(2); numDisplay.SetValuePointer(&offsettedChannel); numDisplay.SetAlternativeColor(Color(0xFFFFFF)); @@ -488,12 +733,577 @@ void Note::ChannelSelector() { UISelector channelInput; channelInput.SetDimension(Dimension(8, 2)); channelInput.SetName("Channel"); - channelInput.SetColor(Color(0x60FF00)); + channelInput.SetColor(color); channelInput.SetCount(16); channelInput.SetValuePointer((uint16_t*)¬ePadConfigs[activeConfig].channel); channelInput.OnChange([&](uint16_t val) -> void { offsettedChannel = val + 1; }); channelSelector.AddUIComponent(channelInput, Point(0, 6)); + channelSelector.SetPostRenderFunc([&]() -> void { + // C + MatrixOS::LED::SetColor(Point(0, 0), color); + MatrixOS::LED::SetColor(Point(0, 1), color); + MatrixOS::LED::SetColor(Point(0, 2), color); + MatrixOS::LED::SetColor(Point(0, 3), color); + MatrixOS::LED::SetColor(Point(1, 0), color); + MatrixOS::LED::SetColor(Point(1, 3), color); + + if(notePadConfigs[activeConfig].channel < 9) + { + //h + MatrixOS::LED::SetColor(Point(2, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(2, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(2, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(2, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(Point(4, 3), Color(0xFFFFFF)); + } + }); + channelSelector.Start(); -} \ No newline at end of file +} + +void Note::ArpConfigMenu() { + UI arpConfigMenu("Arpeggiator Config", Color(0x80FF00)); + uint64_t menuOpenTime = MatrixOS::SYS::Millis(); + + enum ArpConfigType:uint16_t + { + ARP_BPM, + ARP_SWING, + ARP_GATE, + ARP_DIRECTION, + ARP_STEP, + ARP_STEP_OFFSET, + ARP_REPEAT, + }; + + ArpConfigType page = ARP_BPM; + + Color arpConfigColor[7] + { + Color(0xFF0000), // Red - BPM + Color(0xFF8000), // Orange - Swing + Color(0xFFFF00), // Yellow - Gate + Color(0x00FF00), // Green - Direction + Color(0x00FFFF), // Cyan - Step + Color(0x4000FF), // Blue - Step Offset + Color(0xFF00FF), // Magenta - Repeat + }; + + string arpConfigName[7] + { + "BPM", + "Swing", + "Gate", + "Direction", + "Step", + "Step Offset", + "Repeat", + }; + + // Shared arrays for all number modifiers + int32_t coarseModifier[8] = {-25, -10, -5, -1, 1, 5, 10, 25}; + int32_t fineModifier[8] = {-10, -5, -2, -1, 1, 2, 5, 10}; + uint8_t modifierGradient[8] = {255, 127, 64, 32, 32, 64, 127, 255}; + + UISelector arpConfigSelector; + arpConfigSelector.SetCount(7); + arpConfigSelector.SetDimension(Dimension(7, 1)); + arpConfigSelector.SetIndividualColorFunc([&](uint16_t index) -> Color { return arpConfigColor[index]; }); + arpConfigSelector.SetIndividualNameFunc([&](uint16_t index) -> string { return arpConfigName[index]; }); + arpConfigSelector.SetValuePointer((uint16_t*)&page); + arpConfigSelector.OnChange([&](uint16_t val) -> void { + if(page != (ArpConfigType)val) { + page = (ArpConfigType)val; + menuOpenTime = MatrixOS::SYS::Millis(); + } + }); + arpConfigMenu.AddUIComponent(arpConfigSelector, Point(0, 0)); + + // BPM selector + TimedDisplay bpmTextDisplay(500); + bpmTextDisplay.SetDimension(Dimension(8, 4)); + bpmTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_BPM]; + + // B + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 1), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // P + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + + // M + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + bpmTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_BPM; }); + arpConfigMenu.AddUIComponent(bpmTextDisplay, Point(0, 2)); + + int32_t bpmValue = notePadConfigs[activeConfig].arpConfig.bpm; + + UI4pxNumber bpmDisplay; + bpmDisplay.SetColor(arpConfigColor[ARP_BPM]); + bpmDisplay.SetDigits(3); + bpmDisplay.SetValuePointer(&bpmValue); + bpmDisplay.SetAlternativeColor(Color(0xFFFFFF)); + bpmDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_BPM) && !bpmTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(bpmDisplay, Point(-1, 2)); + + UINumberModifier bpmNumberModifier; + bpmNumberModifier.SetColor(arpConfigColor[ARP_BPM]); + bpmNumberModifier.SetLength(8); + bpmNumberModifier.SetValuePointer(&bpmValue); + bpmNumberModifier.SetModifiers(coarseModifier); + bpmNumberModifier.SetControlGradient(modifierGradient); + bpmNumberModifier.SetLowerLimit(20); + bpmNumberModifier.SetUpperLimit(299); + bpmNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_BPM; }); + bpmNumberModifier.OnChange([&](int32_t val) -> void { + bpmValue = val; + notePadConfigs[activeConfig].arpConfig.bpm = (uint32_t)val; + bpmTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + arpConfigMenu.AddUIComponent(bpmNumberModifier, Point(0, 7)); + + // Swing selector + TimedDisplay swingTextDisplay(500); + swingTextDisplay.SetDimension(Dimension(8, 4)); + swingTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_SWING]; + + // S + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + + // W + MatrixOS::LED::SetColor(origin + Point(2, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(2, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // G + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + swingTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_SWING; }); + arpConfigMenu.AddUIComponent(swingTextDisplay, Point(0, 2)); + + int32_t swingValue = (int32_t)notePadConfigs[activeConfig].arpConfig.swing; + + UI4pxNumber swingDisplay; + swingDisplay.SetColor(arpConfigColor[ARP_SWING]); + swingDisplay.SetDigits(3); + swingDisplay.SetValuePointer(&swingValue); + swingDisplay.SetAlternativeColor(Color(0xFFFFFF)); + swingDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_SWING) && !swingTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(swingDisplay, Point(-1, 2)); + + UINumberModifier swingNumberModifier; + swingNumberModifier.SetColor(arpConfigColor[ARP_SWING]); + swingNumberModifier.SetLength(8); + swingNumberModifier.SetValuePointer(&swingValue); + swingNumberModifier.SetModifiers(fineModifier); + swingNumberModifier.SetControlGradient(modifierGradient); + swingNumberModifier.SetLowerLimit(20); + swingNumberModifier.SetUpperLimit(80); + swingNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_SWING; }); + swingNumberModifier.OnChange([&](int32_t val) -> void { + swingTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + notePadConfigs[activeConfig].arpConfig.swing = val; + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + arpConfigMenu.AddUIComponent(swingNumberModifier, Point(0, 7)); + + // Gate selector + TimedDisplay gateTextDisplay(500); + gateTextDisplay.SetDimension(Dimension(8, 4)); + gateTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_GATE]; + + // G + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // A + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 3), Color(0xFFFFFF)); + + // T + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(8, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + gateTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_GATE; }); + arpConfigMenu.AddUIComponent(gateTextDisplay, Point(0, 2)); + + int32_t gateValue = notePadConfigs[activeConfig].arpConfig.gateTime; + + UI4pxNumber gateDisplay; + gateDisplay.SetColor(arpConfigColor[ARP_GATE]); + gateDisplay.SetDigits(3); + gateDisplay.SetValuePointer(&gateValue); + gateDisplay.SetAlternativeColor(Color(0xFFFFFF)); + gateDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_GATE) && !gateTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(gateDisplay, Point(-1, 2)); + + InfDisplay gateInfDisplay(arpConfigColor[ARP_GATE]); + gateInfDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_GATE) && notePadConfigs[activeConfig].arpConfig.gateTime == 0 && !gateTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(gateInfDisplay, Point(0, 2)); + + UINumberModifier gateNumberModifier; + gateNumberModifier.SetColor(arpConfigColor[ARP_GATE]); + gateNumberModifier.SetLength(8); + gateNumberModifier.SetValuePointer(&gateValue); + gateNumberModifier.SetModifiers(fineModifier); + gateNumberModifier.SetControlGradient(modifierGradient); + gateNumberModifier.SetLowerLimit(0); + gateNumberModifier.SetUpperLimit(200); + gateNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_GATE; }); + gateNumberModifier.OnChange([&](int32_t val) -> void { + gateValue = val; + notePadConfigs[activeConfig].arpConfig.gateTime = (uint8_t)val; + gateTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + arpConfigMenu.AddUIComponent(gateNumberModifier, Point(0, 7)); + + // Direction Selector + TimedDisplay directionTextDisplay(500); + directionTextDisplay.SetDimension(Dimension(8, 4)); + directionTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_DIRECTION]; + + // D + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 1), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + MatrixOS::LED::SetColor(origin + Point(3, 1), color); + MatrixOS::LED::SetColor(origin + Point(3, 2), color); + + // I + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // R + MatrixOS::LED::SetColor(origin + Point(5, 0), color); + MatrixOS::LED::SetColor(origin + Point(5, 1), color); + MatrixOS::LED::SetColor(origin + Point(5, 2), color); + MatrixOS::LED::SetColor(origin + Point(5, 3), color); + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + directionTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_DIRECTION; }); + arpConfigMenu.AddUIComponent(directionTextDisplay, Point(0, 2)); + + ArpDirVisualizer arpDirVisualizer(¬ePadConfigs[activeConfig].arpConfig.direction, arpConfigColor[ARP_DIRECTION]); + arpDirVisualizer.SetEnableFunc([&]() -> bool { return page == ARP_DIRECTION && !directionTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(arpDirVisualizer, Point(0, 2)); + + UISelector directionSelector; + directionSelector.SetDimension(Dimension(8, 4)); + directionSelector.SetColor(arpConfigColor[ARP_DIRECTION]); + directionSelector.SetValueFunc([&]() -> uint16_t { return (uint16_t)notePadConfigs[activeConfig].arpConfig.direction; }); + directionSelector.OnChange([&](uint16_t value) -> void { + notePadConfigs[activeConfig].arpConfig.direction = (ArpDirection)value; + directionTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + directionSelector.SetCount(16); + directionSelector.SetIndividualNameFunc([&](uint16_t index) -> string { return arpDirectionNames[index]; }); + directionSelector.SetEnableFunc([&]() -> bool { return page == ARP_DIRECTION; }); + arpConfigMenu.AddUIComponent(directionSelector, Point(0, 6)); + + // Step selector + TimedDisplay stepTextDisplay(500); + stepTextDisplay.SetDimension(Dimension(8, 4)); + stepTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_STEP]; + + // S + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // T + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + + // P + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + }); + stepTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_STEP; }); + arpConfigMenu.AddUIComponent(stepTextDisplay, Point(0, 2)); + + int32_t stepValue = notePadConfigs[activeConfig].arpConfig.step; + + UI4pxNumber stepDisplay; + stepDisplay.SetColor(arpConfigColor[ARP_STEP]); + stepDisplay.SetDigits(3); + stepDisplay.SetValuePointer(&stepValue); + stepDisplay.SetAlternativeColor(Color(0xFFFFFF)); + stepDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_STEP) && !stepTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(stepDisplay, Point(-1, 2)); + + // Custom modifier for step (1-8 range) + + UINumberModifier stepNumberModifier; + stepNumberModifier.SetColor(arpConfigColor[ARP_STEP]); + stepNumberModifier.SetLength(8); + stepNumberModifier.SetValuePointer(&stepValue); + stepNumberModifier.SetModifiers(fineModifier); + stepNumberModifier.SetControlGradient(modifierGradient); + stepNumberModifier.SetLowerLimit(0); + stepNumberModifier.SetUpperLimit(16); + stepNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_STEP; }); + stepNumberModifier.OnChange([&](int32_t val) -> void { + stepValue = val; + notePadConfigs[activeConfig].arpConfig.step = (uint8_t)val; + stepTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + arpConfigMenu.AddUIComponent(stepNumberModifier, Point(0, 7)); + + // Step Offset selector (with minus sign support) + TimedDisplay offsetTextDisplay(500); + offsetTextDisplay.SetDimension(Dimension(8, 4)); + offsetTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_STEP_OFFSET]; + + // O + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 3), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // F + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 0), Color(0xFFFFFF)); + + // S + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(7, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 3), color); + }); + offsetTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_STEP_OFFSET; }); + arpConfigMenu.AddUIComponent(offsetTextDisplay, Point(0, 2)); + + int32_t stepOffsetValue = notePadConfigs[activeConfig].arpConfig.stepOffset; + int32_t stepOffsetDisplayValue = abs(stepOffsetValue); + + UIButton stepOffsetNegSign; + stepOffsetNegSign.SetColor(arpConfigColor[ARP_STEP_OFFSET]); + stepOffsetNegSign.SetSize(Dimension(2, 1)); + stepOffsetNegSign.SetEnableFunc([&]() -> bool { return (page == ARP_STEP_OFFSET) && !offsetTextDisplay.IsEnabled() && stepOffsetValue < 0; }); + arpConfigMenu.AddUIComponent(stepOffsetNegSign, Point(0, 4)); + + UI4pxNumber stepOffsetDisplay; + stepOffsetDisplay.SetColor(arpConfigColor[ARP_STEP_OFFSET]); + stepOffsetDisplay.SetDigits(2); + stepOffsetDisplay.SetValuePointer(&stepOffsetDisplayValue); + stepOffsetDisplay.SetAlternativeColor(Color(0xFFFFFF)); + stepOffsetDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_STEP_OFFSET) && !offsetTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(stepOffsetDisplay, Point(2, 2)); + + // Custom modifier for step offset (-24 to 24 semitones) + UINumberModifier stepOffsetNumberModifier; + stepOffsetNumberModifier.SetColor(arpConfigColor[ARP_STEP_OFFSET]); + stepOffsetNumberModifier.SetLength(8); + stepOffsetNumberModifier.SetValuePointer(&stepOffsetValue); + stepOffsetNumberModifier.SetModifiers(fineModifier); + stepOffsetNumberModifier.SetControlGradient(modifierGradient); + stepOffsetNumberModifier.SetLowerLimit(-48); + stepOffsetNumberModifier.SetUpperLimit(48); + stepOffsetNumberModifier.OnChange([&](int32_t value) -> void { + stepOffsetValue = value; + notePadConfigs[activeConfig].arpConfig.stepOffset = (int8_t)value; + stepOffsetDisplayValue = abs(value); + offsetTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + stepOffsetNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_STEP_OFFSET; }); + arpConfigMenu.AddUIComponent(stepOffsetNumberModifier, Point(0, 7)); + + // Repeat selector + TimedDisplay repeatTextDisplay(500); + repeatTextDisplay.SetDimension(Dimension(8, 4)); + repeatTextDisplay.SetRenderFunc([&](Point origin) -> void { + Color color = arpConfigColor[ARP_REPEAT]; + + // R + MatrixOS::LED::SetColor(origin + Point(0, 0), color); + MatrixOS::LED::SetColor(origin + Point(0, 1), color); + MatrixOS::LED::SetColor(origin + Point(0, 2), color); + MatrixOS::LED::SetColor(origin + Point(0, 3), color); + MatrixOS::LED::SetColor(origin + Point(1, 0), color); + MatrixOS::LED::SetColor(origin + Point(1, 2), color); + MatrixOS::LED::SetColor(origin + Point(2, 0), color); + MatrixOS::LED::SetColor(origin + Point(2, 1), color); + MatrixOS::LED::SetColor(origin + Point(2, 3), color); + + // E + MatrixOS::LED::SetColor(origin + Point(3, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 2), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(3, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 1), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(4, 3), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 0), Color(0xFFFFFF)); + MatrixOS::LED::SetColor(origin + Point(5, 3), Color(0xFFFFFF)); + + // P + MatrixOS::LED::SetColor(origin + Point(6, 0), color); + MatrixOS::LED::SetColor(origin + Point(6, 1), color); + MatrixOS::LED::SetColor(origin + Point(6, 2), color); + MatrixOS::LED::SetColor(origin + Point(6, 3), color); + MatrixOS::LED::SetColor(origin + Point(7, 0), color); + MatrixOS::LED::SetColor(origin + Point(7, 1), color); + }); + repeatTextDisplay.SetEnableFunc([&]() -> bool { return page == ARP_REPEAT; }); + arpConfigMenu.AddUIComponent(repeatTextDisplay, Point(0, 2)); + + int32_t repeatValue = notePadConfigs[activeConfig].arpConfig.repeat; + + UI4pxNumber repeatDisplay; + repeatDisplay.SetColor(arpConfigColor[ARP_REPEAT]); + repeatDisplay.SetDigits(3); + repeatDisplay.SetValuePointer(&repeatValue); + repeatDisplay.SetAlternativeColor(Color(0xFFFFFF)); + repeatDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_REPEAT) && !repeatTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(repeatDisplay, Point(-1, 2)); + + InfDisplay repeatInfDisplay(arpConfigColor[ARP_REPEAT]); + repeatInfDisplay.SetEnableFunc([&]() -> bool { return (page == ARP_REPEAT) && notePadConfigs[activeConfig].arpConfig.repeat == 0 && !repeatTextDisplay.IsEnabled(); }); + arpConfigMenu.AddUIComponent(repeatInfDisplay, Point(0, 2)); + + UINumberModifier repeatNumberModifier; + repeatNumberModifier.SetColor(arpConfigColor[ARP_REPEAT]); + repeatNumberModifier.SetLength(8); + repeatNumberModifier.SetValuePointer(&repeatValue); + repeatNumberModifier.SetModifiers(fineModifier); + repeatNumberModifier.SetControlGradient(modifierGradient); + repeatNumberModifier.SetLowerLimit(0); + repeatNumberModifier.SetUpperLimit(100); + repeatNumberModifier.OnChange([&](int32_t value) -> void { + repeatValue = value; + notePadConfigs[activeConfig].arpConfig.repeat = (uint8_t)value; + repeatTextDisplay.Disable(); + if(activeNotePads[0] != nullptr) { + activeNotePads[0]->rt->arpeggiator.UpdateConfig(); + } + }); + repeatNumberModifier.SetEnableFunc([&]() -> bool { return page == ARP_REPEAT; }); + arpConfigMenu.AddUIComponent(repeatNumberModifier, Point(0, 7)); + + arpConfigMenu.SetLoopFunc([&]() -> void { + if(activeNotePads[0] != nullptr) {activeNotePads[0]->Tick();} + if(activeNotePads[1] != nullptr) {activeNotePads[1]->Tick();} + }); + + arpConfigMenu.Start(); +} + diff --git a/Applications/Note/Note.h b/Applications/Note/Note.h index 953fdfc3..97603aa9 100644 --- a/Applications/Note/Note.h +++ b/Applications/Note/Note.h @@ -22,9 +22,10 @@ class Note : public Application { enum ESpiltView : uint8_t { SINGLE_VIEW, VERT_SPLIT, HORIZ_SPLIT}; // Saved Variables - CreateSavedVar("Note", nvsVersion, uint32_t, NOTE_APP_VERSION); // In case NoteLayoutConfig got changed + CreateSavedVar("Note", nvsVersion, uint32_t, NOTE_APP_VERSION); // In case NotePadConfig got changed CreateSavedVar("Note", activeConfig, uint8_t, 0); CreateSavedVar("Note", splitView, ESpiltView, SINGLE_VIEW); + CreateSavedVar("Note", controlBar, bool, false); void Setup(const vector& args) override; @@ -39,8 +40,10 @@ class Note : public Application { void LayoutSelector(); void ChannelSelector(); void ColorSelector(); + void ArpConfigMenu(); - NoteLayoutConfig notePadConfigs[2]; + NotePadConfig notePadConfigs[2]; + NotePad *activeNotePads[2] = {nullptr, nullptr}; Color colorPresets[6][2] = { diff --git a/Applications/Note/NoteControlBar.cpp b/Applications/Note/NoteControlBar.cpp new file mode 100644 index 00000000..6427aad0 --- /dev/null +++ b/Applications/Note/NoteControlBar.cpp @@ -0,0 +1,516 @@ +#include "NoteControlBar.h" +#include "ChordEffect.h" + +NoteControlBar::NoteControlBar(Note* notePtr, NotePad* notepad1, NotePad* notepad2, UnderglowLight* underglow1, UnderglowLight* underglow2) { + this->note = notePtr; + this->notePad[0] = notepad1; + this->notePad[1] = notepad2; + this->underglow[0] = underglow1; + this->underglow[1] = underglow2; + this->shift[0] = 0; + this->shift[1] = 0; + this->shift_event[0] = false; + this->shift_event[1] = false; +} + +void NoteControlBar::SwapActiveConfig() { + NotePadRuntime* padData1 = notePad[0]->rt; + NotePadRuntime* padData2 = notePad[1]->rt; + + if(notePad[0]) { + notePad[0]->SetPadRuntime(padData2); + } + if(notePad[1]) { + notePad[1]->SetPadRuntime(padData1); + } + if(underglow[0]) { + underglow[0]->SetColor(padData1->config->color); + } + if(underglow[1]) { + underglow[1]->SetColor(padData1->config->color); + } + note->activeConfig = note->activeConfig.Get() == 0 ? 1 : 0; +} + +bool NoteControlBar::ShiftActive() { + return shift[0] != 0 || shift[1] != 0; +} + +void NoteControlBar::ShiftEventOccured() { + if(shift[0] != 0) { + shift_event[0] = true; + } + else if(shift[1] != 0) { + shift_event[1] = true; + } +} + +void NoteControlBar::ShiftClear() { + shift[0] = 0; + shift[1] = 0; +} + + +bool NoteControlBar::KeyEvent(Point xy, KeyInfo* keyInfo) { + static uint32_t pitch_down = 0; + static uint32_t pitch_up = 0; + + if(xy.y < CTL_BAR_Y - 1) + { + switch(mode) { + case OFF_MODE: + return false; + case CHORD_MODE: + return ChordControlKeyEvent(xy, keyInfo); + case ARP_MODE: + return ArpControlKeyEvent(xy, keyInfo); + case KEY_MODE: + return KeyControlKeyEvent(xy, keyInfo); + } + } + + // Control Bar + // Pitch Down + else if(xy == Point(0, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(ShiftActive()) { + MatrixOS::MIDI::Send(MidiPacket::Stop()); + } + else { + pitch_down = MatrixOS::SYS::Millis(); + } + } + else if(keyInfo->State() == AFTERTOUCH) + { + if(pitch_down != 0 && pitch_down > pitch_up) + { + int32_t pitch_val = 8192 - (((uint16_t)keyInfo->Force() * 8192) >> 16); + if(pitch_val < 0) {pitch_val = 0;} + MLOGD("Note", "Pitch Bend: %d", pitch_val); + MatrixOS::MIDI::Send(MidiPacket::PitchBend(notePad[0]->rt->config->channel, pitch_val)); + } + } + else if(keyInfo->State() == RELEASED) { + if(pitch_down != 0 && pitch_down > pitch_up) + { + MatrixOS::MIDI::Send(MidiPacket::PitchBend(notePad[0]->rt->config->channel, 8192)); + } + pitch_down = 0; + } + return true; + } + + else if(xy == Point(1, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(ShiftActive()) { + MatrixOS::MIDI::Send(MidiPacket::Start()); + } + else { + pitch_up = MatrixOS::SYS::Millis(); + } + } + else if(keyInfo->State() == AFTERTOUCH) + { + if(pitch_up != 0 && pitch_up >= pitch_down) + { + int32_t pitch_val = 8192 + (((uint16_t)keyInfo->Force() * 8191) >> 16); + if(pitch_val > 16383) {pitch_val = 16383;} + MatrixOS::MIDI::Send(MidiPacket::PitchBend(notePad[0]->rt->config->channel, pitch_val)); + } + } + else if(keyInfo->State() == RELEASED) { + if(pitch_up != 0 && pitch_up >= pitch_down) + { + MatrixOS::MIDI::Send(MidiPacket::PitchBend(notePad[0]->rt->config->channel, 8192)); + } + pitch_up = 0; + } + return true; + } + + else if(xy == Point(2, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(ShiftActive()) { + notePad[0]->rt->noteLatch.SetToggleMode(!notePad[0]->rt->noteLatch.IsToggleMode()); + } + else + { + notePad[0]->rt->noteLatch.SetEnabled(!notePad[0]->rt->noteLatch.IsEnabled()); + } + } + return true; + } + + // Chord Mode + else if(xy == Point(3, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(mode == CHORD_MODE) { + mode = OFF_MODE; + notePad[0]->rt->chordEffect.SetEnabled(false); + } else { + mode = CHORD_MODE; + notePad[0]->rt->chordEffect.SetEnabled(true); + } + } + return true; + } + + // Arp Mode + else if(xy == Point(4, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(ShiftActive()) { + ShiftClear(); + note->ArpConfigMenu(); + } else if(mode == ARP_MODE) { + mode = OFF_MODE; + } else { + mode = ARP_MODE; + } + } + return true; + } + + // Key Mode + else if(xy == Point(5, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(ShiftActive()) { + ShiftClear(); + note->ScaleSelector(); + notePad[0]->GenerateKeymap(); + } else if(mode == KEY_MODE) { + mode = OFF_MODE; + } else { + mode = KEY_MODE; + } + } + return true; + } + + // Octave Down + else if(xy == Point(6, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(!ShiftActive()) { + shift[0] = MatrixOS::SYS::Millis(); + } + else { + SwapActiveConfig(); + ShiftEventOccured(); + } + } + else if(keyInfo->State() == RELEASED) { + if((MatrixOS::SYS::Millis() - shift[0] < hold_threshold) && shift_event[0] == false) { + int8_t new_octave = notePad[0]->rt->config->octave - 1; + notePad[0]->rt->config->octave = new_octave < -2 ? -2 : new_octave; + notePad[0]->GenerateKeymap(); + } + shift[0] = 0; + shift_event[0] = false; + } + return true; + } + + // Octave Up + else if(xy == Point(7, CTL_BAR_Y - 1)) { + if(keyInfo->State() == PRESSED) { + if(!ShiftActive()) { + shift[1] = MatrixOS::SYS::Millis(); + } + else { + SwapActiveConfig(); + ShiftEventOccured(); + } + } + else if(keyInfo->State() == RELEASED) { + if((MatrixOS::SYS::Millis() - shift[1] < hold_threshold) && shift_event[1] == false) { + int8_t new_octave = notePad[0]->rt->config->octave + 1; + notePad[0]->rt->config->octave = new_octave > 12 ? 12 : new_octave; + notePad[0]->GenerateKeymap(); + } + shift[1] = 0; + shift_event[1] = false; + } + return true; + } + return false; +} + +bool NoteControlBar::ChordControlKeyEvent(Point xy, KeyInfo* keyInfo) { + if(xy.y < CTL_BAR_Y - 3 || xy.y > CTL_BAR_Y - 2) { + return false; + } + + + // Section 1: Top left 4 buttons (CTL_BAR_Y - 3, x0-x3) - Basic chord types + if (xy.y == CTL_BAR_Y - 3 && xy.x <= 3) { + ChordCombo& combo = notePad[0]->rt->chordEffect.chordCombo; + + if (keyInfo->State() == PRESSED) { + // Clear all basic chord types first (mutually exclusive) + combo.dim = false; + combo.min = false; + combo.maj = false; + combo.sus = false; + + // Set only the selected one + switch(xy.x) { + case 0: combo.dim = true; break; + case 1: combo.min = true; break; + case 2: combo.maj = true; break; + case 3: combo.sus = true; break; + } + notePad[0]->rt->chordEffect.SetChordCombo(combo); + } else if (keyInfo->State() == RELEASED && !ShiftActive()) { + switch(xy.x) { + case 0: combo.dim = false; break; + case 1: combo.min = false; break; + case 2: combo.maj = false; break; + case 3: combo.sus = false; break; + } + notePad[0]->rt->chordEffect.SetChordCombo(combo); + } + + return true; + } + + // Section 2: Bottom left 4 buttons (CTL_BAR_Y - 2, x0-x3) - Extensions + if (xy.y == CTL_BAR_Y - 2 && xy.x <= 3) { + ChordCombo& combo = notePad[0]->rt->chordEffect.chordCombo; + + if (keyInfo->State() == PRESSED) { + switch(xy.x) { + case 0: combo.ext6 = !combo.ext6; break; + case 1: combo.extMin7 = !combo.extMin7; break; + case 2: combo.extMaj7 = !combo.extMaj7; break; + case 3: combo.ext9 = !combo.ext9; break; + } + notePad[0]->rt->chordEffect.SetChordCombo(combo); + } else if (keyInfo->State() == RELEASED && !ShiftActive()) { + switch(xy.x) { + case 0: combo.ext6 = false; break; + case 1: combo.extMin7 = false; break; + case 2: combo.extMaj7 = false; break; + case 3: combo.ext9 = false; break; + } + notePad[0]->rt->chordEffect.SetChordCombo(combo); + } + return true; + } + + // Section 3: Right 8 buttons (x4-x7 on both rows) - Inversion controls (0-7) + if (xy.x >= 4 && xy.x <= 7 && keyInfo->State() == PRESSED) { + uint8_t inversion; + if (xy.y == CTL_BAR_Y - 3) { + // Top row: inversion 0-3 + inversion = xy.x - 4; + } else if (xy.y == CTL_BAR_Y - 2) { + // Bottom row: inversion 4-7 + inversion = xy.x; + } else { + return false; + } + + notePad[0]->rt->chordEffect.SetInversion(inversion); + return true; + } + + return false; +} +bool NoteControlBar::ArpControlKeyEvent(Point xy, KeyInfo* keyInfo) { + if(xy.y != CTL_BAR_Y - 2 || keyInfo->State() != PRESSED) { + return false; + } + + // Map slider positions to ArpDivision values + ArpDivision divisions[8] = { + DIV_OFF, // 0 + // DIV_WHOLE, // 1 + // DIV_HALF, // 2 + // DIV_THIRD, // 3 + DIV_QUARTER, // 4 + DIV_SIXTH, // 6 + DIV_EIGHTH, // 8 + DIV_TWELFTH, // 12 + DIV_SIXTEENTH, // 16 + DIV_TWENTYFOURTH, // 24 + DIV_THIRTYSECOND, // 32 + // DIV_SIXTYFOURTH, // 64 + }; + + if(xy.x >= 0 && xy.x < 8) { + notePad[0]->rt->arpeggiator.SetDivision(divisions[xy.x]); + + return true; + } + + return false; +} + +bool NoteControlBar::KeyControlKeyEvent(Point xy, KeyInfo* keyInfo) { + if(xy.y < CTL_BAR_Y - 3) + { + return false; + } + + xy = xy - Point(0, CTL_BAR_Y - 3); + + if (xy.x == 7 || xy == Point(0, 0) || xy == Point(3, 0)) + { + return true; + } + notePad[0]->rt->config->rootKey = xy.x * 2 + xy.y - 1 - (xy.x > 2); + notePad[0]->GenerateKeymap(); + return true; +} + +const uint8_t OctaveGradient[8] = {0, 16, 42, 68, 124, 182, 255}; + +Color NoteControlBar::GetOctavePlusColor() { + int8_t octave = notePad[0]->rt->config->octave; + uint8_t brightness; + + if (octave >= 4) { + brightness = 255; + } else { + // Use gradient for octaves below 4 - dimmer as it goes down + uint8_t index = (4 - octave) > 6 ? 6 : (4 - octave); + brightness = OctaveGradient[6 - index]; + } + + return notePad[0]->rt->config->color.Scale(brightness); +} + +Color NoteControlBar::GetOctaveMinusColor() { + int8_t octave = notePad[0]->rt->config->octave; + uint8_t brightness; + + if (octave <= 4) { + brightness = 255; + } else { + // Use gradient for octaves above 4 - dimmer as it goes up + uint8_t index = (octave - 4) > 6 ? 6 : (octave - 4); + brightness = OctaveGradient[6 - index]; + } + + return notePad[0]->rt->config->color.Scale(brightness); +} + + +bool NoteControlBar::Render(Point origin) { + MatrixOS::LED::SetColor(origin + Point(0, CTL_BAR_Y - 1), MatrixOS::KeyPad::GetKey(origin + Point(0, CTL_BAR_Y - 1))->Active() ? Color(0xFFFFFF) : Color(0xFF0000)); + MatrixOS::LED::SetColor(origin + Point(1, CTL_BAR_Y - 1), MatrixOS::KeyPad::GetKey(origin + Point(1, CTL_BAR_Y - 1))->Active() ? Color(0xFFFFFF) : Color(0x00FF00)); + Color latchColor; + if (notePad[0]->rt->noteLatch.IsToggleMode()) { + latchColor = Color(0xFF00FF); + } else if(notePad[0]->rt->noteLatch.IsEnabled()) { + latchColor = Color(0xFFFFFF); // White when enabled + } else { + latchColor = Color(0xFFFF00); + } + MatrixOS::LED::SetColor(origin + Point(2, CTL_BAR_Y - 1), latchColor); + MatrixOS::LED::SetColor(origin + Point(3, CTL_BAR_Y - 1), mode == CHORD_MODE ? Color(0xFFFFFF) : Color(0x00FFFF)); + MatrixOS::LED::SetColor(origin + Point(4, CTL_BAR_Y - 1), mode == ARP_MODE ? Color(0xFFFFFF) : Color(0x80FF00)); + MatrixOS::LED::SetColor(origin + Point(5, CTL_BAR_Y - 1), mode == KEY_MODE ? Color(0xFFFFFF) : Color(0xFF0060)); + MatrixOS::LED::SetColor(origin + Point(6, CTL_BAR_Y - 1), MatrixOS::KeyPad::GetKey(origin + Point(6, CTL_BAR_Y - 1))->Active() ? Color(0xFFFFFF) : GetOctaveMinusColor()); + MatrixOS::LED::SetColor(origin + Point(7, CTL_BAR_Y - 1), MatrixOS::KeyPad::GetKey(origin + Point(7, CTL_BAR_Y - 1))->Active() ? Color(0xFFFFFF) : GetOctavePlusColor()); + + switch(mode) { + case OFF_MODE: + break; + case CHORD_MODE: + RenderChordControl(origin); + break; + case ARP_MODE: + RenderArpControl(origin); + break; + case KEY_MODE: + RenderKeyControl(origin); + break; + + } + return true; +} + +void NoteControlBar::RenderChordControl(Point origin) { + // Get current chord combo from the effect + ChordCombo& combo = notePad[0]->rt->chordEffect.chordCombo; + + // Colors for different chord types + Color baseColor = Color(0xFF00FF); + Color extColor = Color(0x00FFFF); + Color inversionColor = Color(0xFFFF00); + + // Row 0 + // x0: Dim, x1: min, x2: maj, x3: sus + MatrixOS::LED::SetColor(origin + Point(0, CTL_BAR_Y - 3), combo.dim ? Color(0xFFFFFF) : baseColor); + MatrixOS::LED::SetColor(origin + Point(1, CTL_BAR_Y - 3), combo.min ? Color(0xFFFFFF) : baseColor); + MatrixOS::LED::SetColor(origin + Point(2, CTL_BAR_Y - 3), combo.maj ? Color(0xFFFFFF) : baseColor); + MatrixOS::LED::SetColor(origin + Point(3, CTL_BAR_Y - 3), combo.sus ? Color(0xFFFFFF) : baseColor); + + // Row 2 + // x0: ext6, x1: extMin7, x2: extMaj7, x3: ext9 + MatrixOS::LED::SetColor(origin + Point(0, CTL_BAR_Y - 2), combo.ext6 ? Color(0xFFFFFF) : extColor); + MatrixOS::LED::SetColor(origin + Point(1, CTL_BAR_Y - 2), combo.extMin7 ? Color(0xFFFFFF) : extColor); + MatrixOS::LED::SetColor(origin + Point(2, CTL_BAR_Y - 2), combo.extMaj7 ? Color(0xFFFFFF) : extColor); + MatrixOS::LED::SetColor(origin + Point(3, CTL_BAR_Y - 2), combo.ext9 ? Color(0xFFFFFF) : extColor); + + // Right side + // Inversion + int8_t currentInversion = notePad[0]->rt->chordEffect.inversion; + MatrixOS::LED::SetColor(origin + Point(4, CTL_BAR_Y - 3), inversionColor.DimIfNot(currentInversion >= 0)); + MatrixOS::LED::SetColor(origin + Point(5, CTL_BAR_Y - 3), inversionColor.DimIfNot(currentInversion >= 1)); + MatrixOS::LED::SetColor(origin + Point(6, CTL_BAR_Y - 3), inversionColor.DimIfNot(currentInversion >= 2)); + MatrixOS::LED::SetColor(origin + Point(7, CTL_BAR_Y - 3), inversionColor.DimIfNot(currentInversion >= 3)); + MatrixOS::LED::SetColor(origin + Point(4, CTL_BAR_Y - 2), inversionColor.DimIfNot(currentInversion >= 4)); + MatrixOS::LED::SetColor(origin + Point(5, CTL_BAR_Y - 2), inversionColor.DimIfNot(currentInversion >= 5)); + MatrixOS::LED::SetColor(origin + Point(6, CTL_BAR_Y - 2), inversionColor.DimIfNot(currentInversion >= 6)); + MatrixOS::LED::SetColor(origin + Point(7, CTL_BAR_Y - 2), inversionColor.DimIfNot(currentInversion >= 7)); +} + +void NoteControlBar::RenderArpControl(Point origin) { + // Map ArpDivision enum to slider positions (0-7) + ArpDivision divisions[8] = { + DIV_OFF, // 0 + // DIV_WHOLE, // 1 + // DIV_HALF, // 2 + // DIV_THIRD, // 3 + DIV_QUARTER, // 4 + DIV_SIXTH, // 6 + DIV_EIGHTH, // 8 + DIV_TWELFTH, // 12 + DIV_SIXTEENTH, // 16 + DIV_TWENTYFOURTH, // 24 + DIV_THIRTYSECOND, // 32 + // DIV_SIXTYFOURTH, // 64 + }; + + // Render slider + for (uint8_t x = 0; x < 8; x++) { + Color color; + if (notePad[0]->rt->arpeggiator.division >= divisions[x]) { + color = Color(0x80FF00); + } else { + color = Color(0x80FF00).Dim(); + } + MatrixOS::LED::SetColor(origin + Point(x, CTL_BAR_Y - 2), color); + } +} + +void NoteControlBar::RenderKeyControl(Point origin) { + uint16_t c_aligned_scale_map = + ((notePad[0]->rt->config->scale << notePad[0]->rt->config->rootKey) + ((notePad[0]->rt->config->scale & 0xFFF) >> (12 - notePad[0]->rt->config->rootKey % 12))) & 0xFFF; // Root key should always < 12, + // might add an assert later + for (uint8_t note = 0; note < 12; note++) + { + Point xy = origin + Point(CTL_BAR_Y - 3) + ((note < 5) ? Point((note + 1) / 2, (note + 1) % 2) : Point((note + 2) / 2, note % 2)); + if (note == notePad[0]->rt->config->rootKey) + { MatrixOS::LED::SetColor(xy, notePad[0]->rt->config->rootColor); } + else if (bitRead(c_aligned_scale_map, note)) + { MatrixOS::LED::SetColor(xy, notePad[0]->rt->config->color); } + else + { MatrixOS::LED::SetColor(xy, notePad[0]->rt->config->color.DimIfNot()); } + } + MatrixOS::LED::SetColor(origin + Point(0, CTL_BAR_Y - 3), Color(0)); + MatrixOS::LED::SetColor(origin + Point(3, CTL_BAR_Y - 3), Color(0)); + MatrixOS::LED::SetColor(origin + Point(7, CTL_BAR_Y - 3), Color(0)); + MatrixOS::LED::SetColor(origin + Point(7, CTL_BAR_Y - 2), Color(0)); +} diff --git a/Applications/Note/NoteControlBar.h b/Applications/Note/NoteControlBar.h new file mode 100644 index 00000000..a2f03a03 --- /dev/null +++ b/Applications/Note/NoteControlBar.h @@ -0,0 +1,48 @@ +#pragma once + +#include "MatrixOS.h" +#include "ui/UI.h" +#include "Note.h" +#include "NotePad.h" +#include "UnderglowLight.h" + +#define CTL_BAR_Y 4 + +enum NoteControlBarMode : uint8_t { + OFF_MODE, + CHORD_MODE, + ARP_MODE, + KEY_MODE, +}; + +class NoteControlBar : public UIComponent { + private: + Note* note; + NotePad* notePad[2]; + UnderglowLight* underglow[2]; + uint32_t shift[2]; + bool shift_event[2]; + static const uint32_t hold_threshold = 500; // Define hold threshold + NoteControlBarMode mode = OFF_MODE; + + void SwapActiveConfig(); + bool ShiftActive(); + void ShiftEventOccured(); + void ShiftClear(); + Color GetOctavePlusColor(); + Color GetOctaveMinusColor(); + + bool ChordControlKeyEvent(Point xy, KeyInfo* keyInfo); + bool ArpControlKeyEvent(Point xy, KeyInfo* keyInfo); + bool KeyControlKeyEvent(Point xy, KeyInfo* keyInfo); + + void RenderChordControl(Point origin); + void RenderArpControl(Point origin); + void RenderKeyControl(Point origin); + public: + NoteControlBar(Note* notePtr, NotePad* notepad1, NotePad* notepad2, UnderglowLight* underglow1, UnderglowLight* underglow2); + + virtual Dimension GetSize() override { return Dimension(8, CTL_BAR_Y); } + virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) override; + virtual bool Render(Point origin) override; +}; \ No newline at end of file diff --git a/Applications/Note/NoteLatch.cpp b/Applications/Note/NoteLatch.cpp new file mode 100644 index 00000000..55879069 --- /dev/null +++ b/Applications/Note/NoteLatch.cpp @@ -0,0 +1,145 @@ +#include "NoteLatch.h" +#include + +void NoteLatch::Tick(deque& input, deque& output) { + // Check if we need to disable and release latched notes + if (disableOnNextTick) { + if (!latchedNotes.empty()) { + ReleaseAllLatchedNotes(output); + } + + enabled = false; + disableOnNextTick = false; + toggleMode = false; + + // Passthrough everything in input to output + for (const MidiPacket& packet : input) { + output.push_back(packet); + } + + return; + } + + for (const MidiPacket& packet : input) { + if (packet.status == NoteOn || packet.status == NoteOff) { + toggleMode ? ProcessNoteMessageToggleMode(packet, output) : ProcessNoteMessage(packet, output); + } else if (packet.status == AfterTouch) { + toggleMode ? ProcessAfterTouchToggleMode(packet, output) : ProcessAfterTouch(packet, output); + } + else + { + output.push_back(packet); + } + } +} + +void NoteLatch::Reset() { + // Clean up by clearing state + latchedNotes.clear(); + stillHoldingNotes.clear(); +} + +void NoteLatch::SetEnabled(bool state) { + if (state) { + enabled = true; + } else if (enabled) { + disableOnNextTick = true; + } +} + +void NoteLatch::SetToggleMode(bool enable) { + if (enable) { + enabled = true; + toggleMode = true; + } else if (enabled) { + toggleMode = false; + } +} + +void NoteLatch::ProcessNoteMessage(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + + if (packet.status == NoteOn && packet.Velocity() > 0) { + // Note On + if (stillHoldingNotes.empty() && !latchedNotes.empty()) { + // There are active latched notes - release them all + ReleaseAllLatchedNotes(output); + } + + // No active latched notes - latch this note + latchedNotes.push_back(note); + stillHoldingNotes.push_back(note); + + // Send the note on + output.push_back(packet); + } + else if (packet.status == NoteOff || (packet.status == NoteOn && packet.Velocity() == 0)) { + // Note Off + auto holdingIt = std::find(stillHoldingNotes.begin(), stillHoldingNotes.end(), note); + + if (holdingIt != stillHoldingNotes.end()) { + stillHoldingNotes.erase(holdingIt); + } + } +} + +void NoteLatch::ProcessAfterTouch(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + + // Check if this aftertouch is from the first still holding note + if (!stillHoldingNotes.empty() && stillHoldingNotes[0] == note) { + // Mirror this aftertouch to all latched notes + for (uint8_t latchedNote : latchedNotes) { + MidiPacket mirroredAfterTouch = MidiPacket::AfterTouch( + packet.Channel(), // Use same channel as input + latchedNote, + packet.data[2] // pressure value + ); + output.push_back(mirroredAfterTouch); + } + } +} + +void NoteLatch::ProcessNoteMessageToggleMode(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + + if (packet.status == NoteOn && packet.Velocity() > 0) { + // Note On - check if note is already latched + auto latchedIt = std::find(latchedNotes.begin(), latchedNotes.end(), note); + + if (latchedIt != latchedNotes.end()) { + // Note is already latched - send note off and remove from latched list + MidiPacket noteOff = MidiPacket::NoteOff(packet.Channel(), note, 0); + output.push_back(noteOff); + latchedNotes.erase(latchedIt); + } else { + // Note is not latched - add to latched list and send note on + latchedNotes.push_back(note); + output.push_back(packet); + } + } + else if (packet.status == NoteOff || (packet.status == NoteOn && packet.Velocity() == 0)) { + // Note Off - ignore for toggle mode (notes are toggled by note on only) + } +} + +void NoteLatch::ProcessAfterTouchToggleMode(const MidiPacket& packet, deque& output) { + uint8_t note = packet.Note(); + + // Only pass through aftertouch if note is in latched list + auto latchedIt = std::find(latchedNotes.begin(), latchedNotes.end(), note); + if (latchedIt != latchedNotes.end()) { + output.push_back(packet); + } +} + +void NoteLatch::ReleaseAllLatchedNotes(deque& output) { + // Send note off for all latched notes + for (uint8_t note : latchedNotes) { + MidiPacket noteOff = MidiPacket::NoteOff(0, note, 0); // Channel 0 for simplicity + output.push_back(noteOff); + } + + latchedNotes.clear(); + stillHoldingNotes.clear(); +} \ No newline at end of file diff --git a/Applications/Note/NoteLatch.h b/Applications/Note/NoteLatch.h new file mode 100644 index 00000000..856a509b --- /dev/null +++ b/Applications/Note/NoteLatch.h @@ -0,0 +1,28 @@ +#pragma once + +#include "MatrixOS.h" +#include "MidiEffect.h" + +// Note Latch Effect - Latches MIDI note-ons until all are turned off +// When all latched notes are off, the next note-on will release all latched notes +// AfterTouch from the first still-holding note is mirrored to all latched notes +class NoteLatch : public MidiEffect { +private: + std::vector latchedNotes; // Notes that are latched (sustaining) + std::vector stillHoldingNotes; // Notes that are physically still being held + bool disableOnNextTick = false; + bool toggleMode = false; +public: + void Tick(deque& input, deque& output) override; + void Reset() override; + void SetEnabled(bool state) override; + void SetToggleMode(bool enable); + bool IsToggleMode() const { return toggleMode; } + +private: + void ProcessNoteMessage(const MidiPacket& packet, deque& output); + void ProcessAfterTouch(const MidiPacket& packet, deque& output); + void ProcessNoteMessageToggleMode(const MidiPacket& packet, deque& output); + void ProcessAfterTouchToggleMode(const MidiPacket& packet, deque& output); + void ReleaseAllLatchedNotes(deque& output); +}; \ No newline at end of file diff --git a/Applications/Note/NotePad.cpp b/Applications/Note/NotePad.cpp new file mode 100644 index 00000000..122a9c95 --- /dev/null +++ b/Applications/Note/NotePad.cpp @@ -0,0 +1,482 @@ +#include "NotePad.h" +#include + +const Color polyNoteColor[12] = { + Color(0x00FFD9), + Color(0xFF0097), + Color(0xFFFB00), + Color(0x5D00FF), + Color(0xFF4B00), + Color(0x009BFF), + Color(0xFF003E), + Color(0xAEFF00), + Color(0xED00FF), + Color(0xFFAE00), + Color(0x1000FF), + Color(0xFF1D00) +}; + +const Color rainbowNoteColor[12] = { + Color(0xFF0000), // Red + Color(0xFF4000), + Color(0xFFFF00), // Yellow + Color(0x80FF00), + Color(0x00FF00), // Green + Color(0x00FF80), + Color(0x00FFFF), // Cyan + Color(0x0080FF), + Color(0x0000FF), // Blue + Color(0x8000FF), + Color(0xFF00FF), // Magenta + Color(0xFF0080) +}; + +NotePad::NotePad(Dimension dimension, NotePadRuntime* data) { + this->dimension = dimension; + this->rt = data; + + // Assign arpConfig pointer to point to the config's arpConfig + rt->arpConfig = &rt->config->arpConfig; + rt->arpeggiator = Arpeggiator(rt->arpConfig); + + rt->midiPipeline.AddEffect("NoteLatch", &rt->noteLatch); + rt->noteLatch.SetEnabled(false); + rt->midiPipeline.AddEffect("ChordEffect", &rt->chordEffect); + rt->midiPipeline.AddEffect("Arpeggiator", &rt->arpeggiator); + activeKeys.clear(); + GenerateKeymap(); +} + +NotePad::~NotePad() { + MatrixOS::MIDI::Send(MidiPacket::ControlChange(rt->config->channel, 123, 0)); // All notes off + activeKeys.clear(); +} + +void NotePad::Tick() +{ + rt->midiPipeline.Tick(); + MidiPacket midiPacket; + while (rt->midiPipeline.Get(midiPacket)) { + MatrixOS::MIDI::Send(midiPacket, MIDI_PORT_ALL); + } +} + +Color NotePad::GetColor() { + return rt->config->rootColor; +} + +Dimension NotePad::GetSize() { + return dimension; +} + +NoteType NotePad::InScale(int16_t note) { + note %= 12; + note = abs(note); + + if (note == rt->config->rootKey) + return ROOT_NOTE; // It is a root key + return bitRead(c_aligned_scale_map, note) ? SCALE_NOTE : OFF_SCALE_NOTE; +} + +int16_t NotePad::NoteFromRoot(int16_t note) { + return (note + rt->config->rootKey) % 12; +} + +int16_t NotePad::GetNextInScaleNote(int16_t note) { + for (int8_t i = 0; i < 12; i++) { + note++; + if (InScale(note) == SCALE_NOTE || InScale(note) == ROOT_NOTE) { + return note; + } + } + return UINT8_MAX; +} + +uint8_t NotePad::GetActiveNoteCount(uint8_t note) { + if (note >= 128) return 0; + bool upper = (note % 2) == 1; + uint8_t index = note / 2; + if (upper) { + return (rt->activeNotes[index] >> 4) & 0x0F; // Upper nibble + } else { + return rt->activeNotes[index] & 0x0F; // Lower nibble + } +} + +void NotePad::SetActiveNoteCount(uint8_t note, uint8_t count) { + if (note >= 128 || count > 15) return; + bool upper = (note % 2) == 1; + uint8_t index = note / 2; + if (upper) { + rt->activeNotes[index] = (rt->activeNotes[index] & 0x0F) | ((count & 0x0F) << 4); // Set upper nibble + } else { + rt->activeNotes[index] = (rt->activeNotes[index] & 0xF0) | (count & 0x0F); // Set lower nibble + } +} + +bool NotePad::IsNoteActive(uint8_t note) { + if (note >= 128) return false; + + return (GetActiveNoteCount(note) > 0); +} + +void NotePad::IncrementActiveNote(uint8_t note) { + if (note >= 128) return; + uint8_t count = GetActiveNoteCount(note); + if (count < 15) { + SetActiveNoteCount(note, count + 1); + } +} + +void NotePad::DecrementActiveNote(uint8_t note) { + if (note >= 128) return; + uint8_t count = GetActiveNoteCount(note); + if (count > 0) { + SetActiveNoteCount(note, count - 1); + } +} + +void NotePad::AddActiveKey(Point position, Fract16 velocity) { + activeKeys.emplace_back(position, velocity); +} + +void NotePad::RemoveActiveKey(Point position) { + activeKeys.erase( + std::remove_if(activeKeys.begin(), activeKeys.end(), + [position](const ActiveKey& key) { return key.position == position; }), + activeKeys.end() + ); +} + +void NotePad::UpdateActiveKeyVelocity(Point position, Fract16 velocity) { + for (auto& key : activeKeys) { + if (key.position == position) { + key.velocity = velocity; + break; + } + } +} + +void NotePad::GenerateOctaveKeymap() { + noteMap.reserve(dimension.Area()); + int16_t root = 12 * rt->config->octave + rt->config->rootKey; + int16_t nextNote = root; + uint8_t rootCount = 0; + for (int8_t y = 0; y < dimension.y; y++) { + int8_t ui_y = dimension.y - y - 1; + + if (rootCount >= 2) { + root += 12; + rootCount = 0; + nextNote = root; + } + + for (int8_t x = 0; x < dimension.x; x++) { + uint8_t id = ui_y * dimension.x + x; + if (nextNote > 127) { // If next note is out of range, fill with 255 + noteMap[id] = 255; + } + else if(!rt->config->inKeyNoteOnly) { // If enforce scale is false, just add the next note + noteMap[id] = nextNote; // Add to map + nextNote++; + } + else { // If enforce scale is true, find the next note that is in scale + while (true) { // Find next key that we should put in + uint8_t inScale = InScale(nextNote); + if (inScale == ROOT_NOTE) { rootCount++; } + if (inScale == SCALE_NOTE || inScale == ROOT_NOTE) { + noteMap[id] = nextNote < 0 ? 255 : (uint8_t)nextNote; // Add to map + nextNote++; + break; // Break from inf loop + } + else if (inScale == OFF_SCALE_NOTE) { + nextNote++; + continue; // Check next note + } + } + } + } + } +} + +void NotePad::GenerateOffsetKeymap() { + noteMap.reserve(dimension.Area()); + int16_t root = 12 * rt->config->octave + rt->config->rootKey; + if (!rt->config->inKeyNoteOnly) { + for (int8_t y = 0; y < dimension.y; y++) { + int8_t ui_y = dimension.y - y - 1; + for (int8_t x = 0; x < dimension.x; x++) { + int16_t note = root + rt->config->x_offset * x + rt->config->y_offset * y; + if (note > 127 || note < 0) { + noteMap[ui_y * dimension.x + x] = 255; + } else { + noteMap[ui_y * dimension.x + x] = note; + } + } + } + } + else { + for (uint8_t y = 0; y < dimension.y; y++) { + int8_t ui_y = dimension.y - y - 1; + int16_t note = root; + + for (uint8_t x = 0; x < dimension.x; x++) { + if (note > 127 || note < 0) { + noteMap[ui_y * dimension.x + x] = 255; + } else { + noteMap[ui_y * dimension.x + x] = note; + } + for (uint8_t i = 0; i < rt->config->x_offset; i++) { + int16_t nextNote = GetNextInScaleNote(note); + if (nextNote == INT16_MAX) { + note = INT16_MAX; // Mark as invalid + break; + } + note = nextNote; + } + } + + for (uint8_t i = 0; i < rt->config->y_offset; i++) { + int16_t nextRoot = GetNextInScaleNote(root); + if (nextRoot == INT16_MAX) { + root = INT16_MAX; // Mark as invalid + break; + } + root = nextRoot; + } + } + } +} + +void NotePad::GenerateChromaticKeymap() { + noteMap.reserve(dimension.Area()); + int16_t note = (12 * rt->config->octave) + rt->config->rootKey; + for(uint8_t i = 0; i < dimension.Area(); i++) { + uint8_t x = i % dimension.x; + uint8_t y = i / dimension.x; + int8_t ui_y = dimension.y - y - 1; + if (note > 127 || note < 0) { + noteMap[ui_y * dimension.x + x] = 255; + } else { + noteMap[ui_y * dimension.x + x] = note; + } + if(!rt->config->inKeyNoteOnly) { + note++; + } + else { + int16_t nextNote = GetNextInScaleNote(note); + if (nextNote == INT16_MAX) { + note = INT16_MAX; // Mark as invalid, subsequent notes will be 255 + } else { + note = nextNote; + } + } + } +} + +void NotePad::GeneratePianoKeymap() { + noteMap.reserve(dimension.Area()); + const int8_t blackKeys[7] = {-1, 1, 3, -1, 6, 8, 10}; + const int8_t whiteKeys[7] = {0, 2, 4, 5, 7, 9, 11}; + + for (int8_t y = 0; y < dimension.y; y++) { + int8_t ui_y = dimension.y - y - 1; + int16_t octave = rt->config->octave + (y / 2); + + if(y % 2 == 0) { // Bottom row + for (int8_t x = 0; x < dimension.x; x++) { + uint8_t id = ui_y * dimension.x + x; + int16_t note = (octave + (x / 7)) * 12 + whiteKeys[x % 7]; + if (note > 127 || note < 0) { + noteMap[id] = 255; + } else { + noteMap[id] = note; + } + } + } + else { // Top row + for (int8_t x = 0; x < dimension.x; x++) { + uint8_t id = ui_y * dimension.x + x; + int8_t offset = blackKeys[x % 7]; + if(offset == -1) { + noteMap[id] = 255; + } + else { + int16_t note = (octave + (x / 7)) * 12 + offset; + if (note > 127 || note < 0) { + noteMap[id] = 255; + } else { + noteMap[id] = note; + } + } + } + } + } +} + +void NotePad::GenerateKeymap() { + c_aligned_scale_map = ((rt->config->scale << rt->config->rootKey) + ((rt->config->scale & 0xFFF) >> (12 - rt->config->rootKey % 12))) & 0xFFF; + switch (rt->config->mode) { + case OCTAVE_LAYOUT: + GenerateOctaveKeymap(); + break; + case OFFSET_LAYOUT: + GenerateOffsetKeymap(); + break; + case CHROMATIC_LAYOUT: + GenerateChromaticKeymap(); + break; + case PIANO_LAYOUT: + GeneratePianoKeymap(); + break; + } + // Send NoteOff for all active notes before clearing + for (uint8_t note = 0; note < 128; note++) { + if (GetActiveNoteCount(note) > 0) { + rt->midiPipeline.Send(MidiPacket::NoteOff(rt->config->channel, note, 0)); + } + } + memset(rt->activeNotes, 0, sizeof(rt->activeNotes)); // Initialize all counters to 0 + first_scan = true; + +} + +void NotePad::FirstScan(Point origin) +{ + for(const auto& activeKey : activeKeys) + { + uint8_t note = noteMap[activeKey.position.y * dimension.x + activeKey.position.x]; + + if(note == 255) { + continue; + } + + IncrementActiveNote(note); + Fract16 velocity = activeKey.velocity; + rt->midiPipeline.Send(MidiPacket::NoteOn(rt->config->channel, note, velocity.to7bits())); + } +} + +bool NotePad::RenderRootNScale(Point origin) { + uint8_t index = 0; + Color color_dim = rt->config->useWhiteAsOutOfScale ? Color(0x202020) : rt->config->color.Dim(32); + for (int8_t y = 0; y < dimension.y; y++) { + for (int8_t x = 0; x < dimension.x; x++) { + uint8_t note = noteMap[index]; + Point globalPos = origin + Point(x, y); + if (note == 255) { + MatrixOS::LED::SetColor(globalPos, Color(0)); + } + else if (IsNoteActive(note) || rt->midiPipeline.IsNoteActive(note)) { // If find the note is currently active. Show it as white + MatrixOS::LED::SetColor(globalPos, Color(0xFFFFFF)); + } + else { + uint8_t inScale = InScale(note); // Check if the note is in scale. + if (inScale == OFF_SCALE_NOTE) { + MatrixOS::LED::SetColor(globalPos, color_dim); + } + else if (inScale == SCALE_NOTE) { + MatrixOS::LED::SetColor(globalPos, rt->config->color); + } + else if (inScale == ROOT_NOTE) { + MatrixOS::LED::SetColor(globalPos, rt->config->rootColor); + } + } + index++; + } + } + return true; +} + +bool NotePad::RenderColorPerKey(Point origin) { + uint8_t index = 0; + const Color* colorMap; + if(rt->config->colorMode == COLOR_PER_KEY_POLY) { + colorMap = polyNoteColor; + } + else if(rt->config->colorMode == COLOR_PER_KEY_RAINBOW) { + colorMap = rainbowNoteColor; + } + else { + return false; + } + + for (int8_t y = 0; y < dimension.y; y++) { + for (int8_t x = 0; x < dimension.x; x++) { + uint8_t note = noteMap[index]; + Point globalPos = origin + Point(x, y); + if (note == 255) { + MatrixOS::LED::SetColor(globalPos, Color(0)); + } + else if (IsNoteActive(note)) { // If find the note is currently active. Show it as white + MatrixOS::LED::SetColor(globalPos, Color(0xFFFFFF)); + } + else { + uint8_t awayFromRoot = NoteFromRoot(note); + uint8_t inScale = InScale(note); + if (inScale == OFF_SCALE_NOTE) { + MatrixOS::LED::SetColor(globalPos, rt->config->useWhiteAsOutOfScale ? Color(0x202020) : Color(colorMap[awayFromRoot]).Dim(32)); + } + else { + MatrixOS::LED::SetColor(globalPos, colorMap[awayFromRoot]); + } + } + index++; + } + } + return true; +} + +bool NotePad::Render(Point origin) { + if(first_scan) { + FirstScan(origin); + first_scan = false; + } + switch(rt->config->colorMode) { + case ROOT_N_SCALE: + return RenderRootNScale(origin); + break; + case COLOR_PER_KEY_POLY: + case COLOR_PER_KEY_RAINBOW: + return RenderColorPerKey(origin); + break; + default: + return false; + } + return false; +} + +bool NotePad::KeyEvent(Point xy, KeyInfo* keyInfo) { + uint8_t note = noteMap[xy.y * dimension.x + xy.x]; + if (note == 255) { + return true; + } + if (keyInfo->State() == PRESSED) { + Fract16 velocity = rt->config->forceSensitive ? keyInfo->Force() : Fract16(0x7F << 9); + rt->midiPipeline.Send(MidiPacket::NoteOn(rt->config->channel, note, velocity.to7bits())); + AddActiveKey(xy, velocity); + IncrementActiveNote(note); + } + else if (rt->config->forceSensitive && keyInfo->State() == AFTERTOUCH) { + Fract16 velocity = keyInfo->Force(); + rt->midiPipeline.Send(MidiPacket::AfterTouch(rt->config->channel, note, velocity.to7bits())); + UpdateActiveKeyVelocity(xy, velocity); + } + else if (keyInfo->State() == RELEASED) { + rt->midiPipeline.Send(MidiPacket::NoteOff(rt->config->channel, note, 0)); + RemoveActiveKey(xy); + DecrementActiveNote(note); + } + return true; +} + +void NotePad::SetDimension(Dimension dimension) { + this->dimension = dimension; + GenerateKeymap(); +} + +void NotePad::SetPadRuntime(NotePadRuntime* rt) { + this->rt = rt; + GenerateKeymap(); +} + diff --git a/Applications/Note/NotePad.h b/Applications/Note/NotePad.h index 3d90b47b..bfe45c21 100644 --- a/Applications/Note/NotePad.h +++ b/Applications/Note/NotePad.h @@ -1,9 +1,12 @@ - #pragma once #include "MatrixOS.h" #include "Scales.h" #include "ui/UI.h" +#include "MidiPipeline.h" +#include "NoteLatch.h" +#include "ChordEffect.h" +#include "Arpeggiator.h" enum NoteLayoutMode : uint8_t { OCTAVE_LAYOUT, @@ -24,41 +27,20 @@ enum ColorMode : uint8_t { COLOR_PER_KEY_RAINBOW, }; -const Color polyNoteColor[12] = { - Color(0x00FFD9), - Color(0xFF0097), - Color(0xFFFB00), - Color(0x5D00FF), - Color(0xFF4B00), - Color(0x009BFF), - Color(0xFF003E), - Color(0xAEFF00), - Color(0xED00FF), - Color(0xFFAE00), - Color(0x1000FF), - Color(0xFF1D00) -}; +extern const Color polyNoteColor[12]; +extern const Color rainbowNoteColor[12]; -const Color rainbowNoteColor[12] = { - Color(0xFF0000), // Red - Color(0xFF4000), - Color(0xFFFF00), // Yellow - Color(0x80FF00), - Color(0x00FF00), // Green - Color(0x00FF80), - Color(0x00FFFF), // Cyan - Color(0x0080FF), - Color(0x0000FF), // Blue - Color(0x8000FF), - Color(0xFF00FF), // Magenta - Color(0xFF0080) -}; +struct ActiveKey { + Point position; + Fract16 velocity; + ActiveKey(Point pos, Fract16 vel) : position(pos), velocity(vel) {} +}; -struct NoteLayoutConfig { +struct NotePadConfig { uint8_t rootKey = 0; uint16_t scale = NATURAL_MINOR; - int8_t octave = 0; + int8_t octave = 4; uint8_t channel = 0; NoteLayoutMode mode = OCTAVE_LAYOUT; bool inKeyNoteOnly = true; @@ -82,369 +64,67 @@ struct NoteLayoutConfig { Color color = Color(0x00FFFF); ColorMode colorMode = ROOT_N_SCALE; bool useWhiteAsOutOfScale = false; + ArpeggiatorConfig arpConfig; +}; + +struct NotePadRuntime +{ + NotePadConfig* config = nullptr; + NoteLatch noteLatch; + ChordEffect chordEffect; + ArpeggiatorConfig* arpConfig = nullptr; + Arpeggiator arpeggiator; + MidiPipeline midiPipeline; + uint8_t activeNotes[64] = {0}; // Each uint8_t stores two 4-bit counters (upper/lower nibble) + + NotePadRuntime() : arpeggiator(nullptr) {} }; class NotePad : public UIComponent { public: Dimension dimension; - NoteLayoutConfig* config; std::vector noteMap; - uint8_t activeNotes[64]; // Each uint8_t stores two 4-bit counters (upper/lower nibble) uint16_t c_aligned_scale_map; + NotePadRuntime* rt; + bool first_scan = true; + std::vector activeKeys; - virtual Color GetColor() { return config->rootColor; } - virtual Dimension GetSize() { return dimension; } - - NoteType InScale(uint8_t note) { - note %= 12; - - if (note == config->rootKey) - return ROOT_NOTE; // It is a root key - return bitRead(c_aligned_scale_map, note) ? SCALE_NOTE : OFF_SCALE_NOTE; - } - - uint8_t NoteFromRoot(uint8_t note) { - return (note + config->rootKey) % 12; - } - - uint8_t GetNextInScaleNote(uint8_t note) { - for (int8_t i = 0; i < 12; i++) - { - note++; - if (InScale(note) == SCALE_NOTE || InScale(note) == ROOT_NOTE) - { - return note; - } - } - return UINT8_MAX; - } + NotePad(Dimension dimension, NotePadRuntime* data); + ~NotePad(); - uint8_t GetActiveNoteCount(uint8_t note, bool upper) { - if (note >= 128) return 0; - uint8_t index = note / 2; - if (upper) { - return (activeNotes[index] >> 4) & 0x0F; // Upper nibble - } else { - return activeNotes[index] & 0x0F; // Lower nibble - } - } + void Tick(); - void SetActiveNoteCount(uint8_t note, bool upper, uint8_t count) { - if (note >= 128 || count > 15) return; - uint8_t index = note / 2; - if (upper) { - activeNotes[index] = (activeNotes[index] & 0x0F) | ((count & 0x0F) << 4); // Set upper nibble - } else { - activeNotes[index] = (activeNotes[index] & 0xF0) | (count & 0x0F); // Set lower nibble - } - } + virtual Color GetColor(); + virtual Dimension GetSize(); - bool IsNoteActive(uint8_t note) { - if (note >= 128) return false; - bool upper = (note % 2) == 1; - return GetActiveNoteCount(note, upper) > 0; - } + NoteType InScale(int16_t note); + int16_t NoteFromRoot(int16_t note); + int16_t GetNextInScaleNote(int16_t note); - void IncrementActiveNote(uint8_t note) { - if (note >= 128) return; - bool upper = (note % 2) == 1; - uint8_t count = GetActiveNoteCount(note, upper); - if (count < 15) { - SetActiveNoteCount(note, upper, count + 1); - } - } + bool IsNoteActive(uint8_t note); + uint8_t GetActiveNoteCount(uint8_t note); + void SetActiveNoteCount(uint8_t note, uint8_t count); + void IncrementActiveNote(uint8_t note); + void DecrementActiveNote(uint8_t note); - void DecrementActiveNote(uint8_t note) { - if (note >= 128) return; - bool upper = (note % 2) == 1; - uint8_t count = GetActiveNoteCount(note, upper); - if (count > 0) { - SetActiveNoteCount(note, upper, count - 1); - } - } - - void GenerateOctaveKeymap() { - noteMap.reserve(dimension.Area()); - uint8_t root = 12 * config->octave + config->rootKey; - uint8_t nextNote = root; - uint8_t rootCount = 0; - for (int8_t y = 0; y < dimension.y; y++) - { - int8_t ui_y = dimension.y - y - 1; - - if (rootCount >= 2) - { - root += 12; - rootCount = 0; - nextNote = root; - } - - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t id = ui_y * dimension.x + x; - if (nextNote > 127) // If next note is out of range, fill with 255 - { - noteMap[id] = 255; - } - else if(!config->inKeyNoteOnly) // If enforce scale is false, just add the next note - { - noteMap[id] = nextNote; // Add to map - nextNote++; - } - else // If enforce scale is true, find the next note that is in scale - { - while (true) // Find next key that we should put in - { - uint8_t inScale = InScale(nextNote); - if (inScale == ROOT_NOTE) { rootCount++; } - if (inScale == SCALE_NOTE || inScale == ROOT_NOTE) - { - noteMap[id] = nextNote; // Add to map - nextNote++; - break; // Break from inf loop - } - else if (inScale == OFF_SCALE_NOTE) - { - nextNote++; - continue; // Check next note - } - } - } - } - } - - } - - void GenerateOffsetKeymap() { - noteMap.reserve(dimension.Area()); - uint8_t root = 12 * config->octave + config->rootKey; - if (!config->inKeyNoteOnly) - { - for (int8_t y = 0; y < dimension.y; y++) - { - int8_t ui_y = dimension.y - y - 1; - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t note = root + config->x_offset * x + config->y_offset * y; - noteMap[ui_y * dimension.x + x] = note; - } - } - } - else - { - for (uint8_t y = 0; y < dimension.y; y++) - { - int8_t ui_y = dimension.y - y - 1; - uint8_t note = root; - - for (uint8_t x = 0; x < dimension.x; x++) - { - noteMap[ui_y * dimension.x + x] = note; - for (uint8_t i = 0; i < config->x_offset; i++) - { - note = GetNextInScaleNote(note); - } - } - - for (uint8_t i = 0; i < config->y_offset; i++) - { - root = GetNextInScaleNote(root); - } - } - } - } - - void GenerateChromaticKeymap() { - noteMap.reserve(dimension.Area()); - uint8_t note = 12 * config->octave + config->rootKey; - for(uint8_t i = 0; i < dimension.Area(); i++) - { - uint8_t x = i % dimension.x; - uint8_t y = i / dimension.x; - int8_t ui_y = dimension.y - y - 1; - noteMap[ui_y * dimension.x + x] = note; - if(!config->inKeyNoteOnly) - { - note++; - } - else - { - note = GetNextInScaleNote(note); - } - } - } + void AddActiveKey(Point position, Fract16 velocity); + void RemoveActiveKey(Point position); + void UpdateActiveKeyVelocity(Point position, Fract16 velocity); + void GenerateOctaveKeymap(); + void GenerateOffsetKeymap(); + void GenerateChromaticKeymap(); + void GeneratePianoKeymap(); + void GenerateKeymap(); - void GeneratePianoKeymap() { - noteMap.reserve(dimension.Area()); - const int8_t blackKeys[7] = {-1, 1, 3, -1, 6, 8, 10}; - const int8_t whiteKeys[7] = {0, 2, 4, 5, 7, 9, 11}; - - for (int8_t y = 0; y < dimension.y; y++) - { - int8_t ui_y = dimension.y - y - 1; - uint8_t octave = config->octave + (y / 2); - - if(y % 2 == 0) // Bottom row - { - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t id = ui_y * dimension.x + x; - noteMap[id] = (octave + (x / 7)) * 12 + whiteKeys[x % 7]; - } - } - else // Top row - { - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t id = ui_y * dimension.x + x; - int8_t offset = blackKeys[x % 7]; - if(offset == -1) - { noteMap[id] = 255; } - else - { noteMap[id] = (octave + (x / 7)) * 12 + offset; } - } - } - } - } - + bool RenderRootNScale(Point origin); + bool RenderColorPerKey(Point origin); + void FirstScan(Point origin); - void GenerateKeymap() { - c_aligned_scale_map = ((config->scale << config->rootKey) + ((config->scale & 0xFFF) >> (12 - config->rootKey % 12))) & 0xFFF; - switch (config->mode) - { - case OCTAVE_LAYOUT: - GenerateOctaveKeymap(); - break; - case OFFSET_LAYOUT: - GenerateOffsetKeymap(); - break; - case CHROMATIC_LAYOUT: - GenerateChromaticKeymap(); - break; - case PIANO_LAYOUT: - GeneratePianoKeymap(); - break; - } - } - - bool RenderRootNScale(Point origin) - { - uint8_t index = 0; - Color color_dim = config->useWhiteAsOutOfScale ? Color(0x202020) : config->color.Dim(32); - for (int8_t y = 0; y < dimension.y; y++) - { - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t note = noteMap[index]; - Point globalPos = origin + Point(x, y); - if (note == 255) - { MatrixOS::LED::SetColor(globalPos, Color(0)); } - else if (IsNoteActive(note)) // If find the note is currently active. Show it as white - { MatrixOS::LED::SetColor(globalPos, Color(0xFFFFFF)); } - else - { - uint8_t inScale = InScale(note); // Check if the note is in scale. - if (inScale == OFF_SCALE_NOTE) - { MatrixOS::LED::SetColor(globalPos, color_dim); } - else if (inScale == SCALE_NOTE) - { MatrixOS::LED::SetColor(globalPos, config->color); } - else if (inScale == ROOT_NOTE) - { MatrixOS::LED::SetColor(globalPos, config->rootColor); } - } - index++; - } - } - return true; - } - - bool RenderColorPerKey(Point origin) { - uint8_t index = 0; - const Color* colorMap; - if(config->colorMode == COLOR_PER_KEY_POLY) - { - colorMap = polyNoteColor; - } - else if(config->colorMode == COLOR_PER_KEY_RAINBOW) - { - colorMap = rainbowNoteColor; - } - else - { - return false; - } - - for (int8_t y = 0; y < dimension.y; y++) - { - for (int8_t x = 0; x < dimension.x; x++) - { - uint8_t note = noteMap[index]; - Point globalPos = origin + Point(x, y); - if (note == 255) - { MatrixOS::LED::SetColor(globalPos, Color(0)); } - else if (IsNoteActive(note)) // If find the note is currently active. Show it as white - { MatrixOS::LED::SetColor(globalPos, Color(0xFFFFFF)); } - else - { - uint8_t awayFromRoot = NoteFromRoot(note); - uint8_t inScale = InScale(note); - if (inScale == OFF_SCALE_NOTE) - { MatrixOS::LED::SetColor(globalPos, config->useWhiteAsOutOfScale ? Color(0x202020) : Color(colorMap[awayFromRoot]).Dim(32)); } - else - { - MatrixOS::LED::SetColor(globalPos, colorMap[awayFromRoot]); - } - } - index++; - } - } - return true; - } - - virtual bool Render(Point origin) { - switch(config->colorMode) - { - case ROOT_N_SCALE: - return RenderRootNScale(origin); - break; - case COLOR_PER_KEY_POLY: - case COLOR_PER_KEY_RAINBOW: - return RenderColorPerKey(origin); - break; - default: - return false; - } - return false; - } + virtual bool Render(Point origin) override; + virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) override; - virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) { - uint8_t note = noteMap[xy.y * dimension.x + xy.x]; - if (note == 255) - { return true; } - if (keyInfo->State() == PRESSED) - { - MatrixOS::MIDI::Send(MidiPacket::NoteOn(config->channel, note, config->forceSensitive ? keyInfo->Force().to7bits() : 0x7F)); - IncrementActiveNote(note); - } - else if (config->forceSensitive && keyInfo->State() == AFTERTOUCH) - { MatrixOS::MIDI::Send(MidiPacket::AfterTouch(config->channel, note, keyInfo->Force().to7bits())); } - else if (keyInfo->State() == RELEASED) - { - MatrixOS::MIDI::Send(MidiPacket::NoteOff(config->channel, note, 0)); - DecrementActiveNote(note); - } - return true; - } - - NotePad(Dimension dimension, NoteLayoutConfig* config) { - this->dimension = dimension; - this->config = config; - memset(activeNotes, 0, sizeof(activeNotes)); // Initialize all counters to 0 - GenerateKeymap(); - } - - ~NotePad() { - MatrixOS::MIDI::Send(MidiPacket::ControlChange(config->channel, 123, 0)); // All notes off - } -}; + void SetDimension(Dimension dimension); + void SetPadRuntime(NotePadRuntime* rt); +}; \ No newline at end of file diff --git a/Applications/Note/OctaveShifter.h b/Applications/Note/OctaveShifter.h index d304cf49..448e4279 100644 --- a/Applications/Note/OctaveShifter.h +++ b/Applications/Note/OctaveShifter.h @@ -4,10 +4,10 @@ class OctaveShifter : public UIComponent { public: uint8_t count; - NoteLayoutConfig* configs; + NotePadConfig* configs; uint8_t* activeConfig; - OctaveShifter(uint8_t count, NoteLayoutConfig* configs, uint8_t* activeConfig) { + OctaveShifter(uint8_t count, NotePadConfig* configs, uint8_t* activeConfig) { this->count = count; this->configs = configs; this->activeConfig = activeConfig; diff --git a/Applications/Note/TimedDisplay.h b/Applications/Note/TimedDisplay.h new file mode 100644 index 00000000..5967fb0b --- /dev/null +++ b/Applications/Note/TimedDisplay.h @@ -0,0 +1,96 @@ +#pragma once +#include "MatrixOS.h" +#include "ui/UI.h" + + +class TimedDisplay : public UIComponent { + public: + uint32_t lastEnabledTime = 0; + uint32_t enableLength; + Dimension dimension; + std::unique_ptr> renderFunc; + + TimedDisplay(uint32_t enableLength = 500) { + this->lastEnabledTime = 0; + this->enableLength = enableLength; + this->renderFunc = nullptr; + } + + void SetRenderFunc(std::function renderFunc) { + this->renderFunc = std::make_unique>(renderFunc); + } + + void SetDimension(Dimension dimension) { + this->dimension = dimension; + } + + virtual Dimension GetSize() { return dimension; } + + bool IsEnabled() { + uint32_t currentTime = (uint32_t)MatrixOS::SYS::Millis(); + if (enableFunc) { + enabled = (*enableFunc)(); + } + + // If already timed out, force disabled + if(enabled && lastEnabledTime == UINT32_MAX) + { + enabled = false; + return enabled; + } + + // Detect rising edge: lastEnabledTime == 0 means was disabled, now enabled + if(enabled && lastEnabledTime == 0) + { + lastEnabledTime = currentTime; + return enabled; + } + + // If disabled externally, reset timer + if(!enabled) + { + lastEnabledTime = 0; + return enabled; + } + + // Check timeout - only if we have a valid start time + if(currentTime - lastEnabledTime > enableLength) + { + lastEnabledTime = UINT32_MAX; // Mark as timed out (not 0 to avoid re-triggering) + enabled = false; // Override to disabled due to timeout + } + + return enabled; + } + + void Disable() + { + lastEnabledTime = UINT32_MAX; + enabled = false; + } + + virtual bool Render(Point origin) { + for(int8_t y = 0; y < dimension.y; y++) + { + for(int8_t x = 0; x < dimension.x; x++) + { + MatrixOS::LED::SetColor(origin + Point(x, y), Color(0)); + } + } + + if (renderFunc) + { + (*renderFunc)(origin); + } + + return true; + } + + virtual bool KeyEvent(Point xy, KeyInfo* keyInfo) { + if(keyInfo->State() == PRESSED) + { + Disable(); + } + return true;// Block keypress + } +}; \ No newline at end of file diff --git a/Applications/Note/UnderglowLight.h b/Applications/Note/UnderglowLight.h index 2ba74a01..111ef1ce 100644 --- a/Applications/Note/UnderglowLight.h +++ b/Applications/Note/UnderglowLight.h @@ -11,6 +11,7 @@ class UnderglowLight : public UIComponent { virtual Color GetColor() { return color; } virtual Dimension GetSize() { return dimension; } + void SetColor(Color newColor) { this->color = newColor; } bool IsUnderglow(Point pos) { diff --git a/OS/Framework/Midi/MidiPacket.cpp b/OS/Framework/Midi/MidiPacket.cpp index 10ba358c..8d1fa099 100644 --- a/OS/Framework/Midi/MidiPacket.cpp +++ b/OS/Framework/Midi/MidiPacket.cpp @@ -161,7 +161,7 @@ MidiPacket MidiPacket::Reset() } // Status methods -EMidiStatus MidiPacket::Status() { +EMidiStatus MidiPacket::Status() const { if((uint8_t)status < EMidiStatus::SysExData) { return (EMidiStatus)data[0]; @@ -192,7 +192,7 @@ bool MidiPacket::SetStatus(EMidiStatus status) } // Port methods -uint16_t MidiPacket::Port() +uint16_t MidiPacket::Port() const { return port; } @@ -203,7 +203,7 @@ void MidiPacket::SetPort(uint16_t port_id) } // Channel methods -uint8_t MidiPacket::Channel() { +uint8_t MidiPacket::Channel() const { switch (status) { case EMidiStatus::NoteOn: @@ -239,7 +239,7 @@ bool MidiPacket::SetChannel(uint8_t channel) } // Note methods -uint8_t MidiPacket::Note() { +uint8_t MidiPacket::Note() const { switch (status) { case EMidiStatus::NoteOn: @@ -270,7 +270,7 @@ bool MidiPacket::SetNote(uint8_t note) } // Controller methods -uint8_t MidiPacket::Controller() // Just an alias for Note(), specially build for Program Change +uint8_t MidiPacket::Controller() const // Just an alias for Note(), specially build for Program Change { return Note(); } @@ -292,7 +292,7 @@ bool MidiPacket::SetController(uint8_t controller) } // Velocity methods -uint8_t MidiPacket::Velocity() { +uint8_t MidiPacket::Velocity() const { switch (status) { case EMidiStatus::NoteOn: @@ -326,7 +326,7 @@ bool MidiPacket::SetVelocity(uint8_t velocity) } // Value methods -uint16_t MidiPacket::Value() // Get value all type, basically a generic getter +uint16_t MidiPacket::Value() const // Get value all type, basically a generic getter { switch (status) { @@ -374,7 +374,7 @@ bool MidiPacket::SetValue(uint16_t value) } // Helper methods -uint8_t MidiPacket::Length() { +uint8_t MidiPacket::Length() const { switch (status) { case EMidiStatus::NoteOn: @@ -414,12 +414,12 @@ uint8_t MidiPacket::Length() { } } -bool MidiPacket::SysEx() // Terrible name +bool MidiPacket::SysEx() const // Terrible name { return status == EMidiStatus::SysExData || status == EMidiStatus::SysExEnd; } -bool MidiPacket::SysExStart() // Terrible name +bool MidiPacket::SysExStart() const // Terrible name { return status == EMidiStatus::SysExData && data[0] == MIDIv1_SYSEX_START; } diff --git a/OS/Framework/Midi/MidiPacket.h b/OS/Framework/Midi/MidiPacket.h index b4a8bf45..3f9c4b28 100644 --- a/OS/Framework/Midi/MidiPacket.h +++ b/OS/Framework/Midi/MidiPacket.h @@ -76,35 +76,35 @@ struct MidiPacket { static MidiPacket Reset(); // Status methods - EMidiStatus Status(); + EMidiStatus Status() const; bool SetStatus(EMidiStatus status); // Port methods - uint16_t Port(); + uint16_t Port() const; void SetPort(uint16_t port_id); // Channel methods - uint8_t Channel(); + uint8_t Channel() const; bool SetChannel(uint8_t channel); // Note methods - uint8_t Note(); + uint8_t Note() const; bool SetNote(uint8_t note); // Controller methods - uint8_t Controller(); // Just an alias for Note(), specially build for Program Change + uint8_t Controller() const; // Just an alias for Note(), specially build for Program Change bool SetController(uint8_t controller); // Velocity methods - uint8_t Velocity(); + uint8_t Velocity() const; bool SetVelocity(uint8_t velocity); // Value methods - uint16_t Value(); // Get value all type, basically a generic getter + uint16_t Value() const; // Get value all type, basically a generic getter bool SetValue(uint16_t value); // Helper methods - uint8_t Length(); - bool SysEx(); // Checks if packet is part of SysEx transfer - Terrible name - bool SysExStart(); // Checks if packet is start of SysEx transfer - Terrible name as well + uint8_t Length() const; + bool SysEx() const; // Checks if packet is part of SysEx transfer - Terrible name + bool SysExStart() const; // Checks if packet is start of SysEx transfer - Terrible name as well }; \ No newline at end of file diff --git a/OS/UI/Component/UIButton.h b/OS/UI/Component/UIButton.h index a94f60e0..d15afb38 100644 --- a/OS/UI/Component/UIButton.h +++ b/OS/UI/Component/UIButton.h @@ -90,7 +90,7 @@ class UIButton : public UIComponent { MatrixOS::KeyPad::Clear(); return true; } - else + else if(GetName() != "") { MatrixOS::UIUtility::TextScroll(GetName(), GetColor()); return true; diff --git a/OS/UI/Component/UIComponent.h b/OS/UI/Component/UIComponent.h index 5d1d9f3c..2a6f018e 100644 --- a/OS/UI/Component/UIComponent.h +++ b/OS/UI/Component/UIComponent.h @@ -17,10 +17,10 @@ class UIComponent { virtual bool Render(Point origin) { return false; } - void SetEnabled(bool enabled) { this->enabled = enabled; } - void SetEnableFunc(std::function enableFunc) { this->enableFunc = std::make_unique>(enableFunc); } + virtual void SetEnabled(bool enabled) { this->enabled = enabled; } + virtual void SetEnableFunc(std::function enableFunc) { this->enableFunc = std::make_unique>(enableFunc); } - bool IsEnabled() { + virtual bool IsEnabled() { if (enableFunc) { return (*enableFunc)(); } diff --git a/OS/UI/Component/UINumModifier.h b/OS/UI/Component/UINumModifier.h index d1a3137d..acfbcfcc 100644 --- a/OS/UI/Component/UINumModifier.h +++ b/OS/UI/Component/UINumModifier.h @@ -1,6 +1,8 @@ #pragma once #include "UIComponent.h" #include +#include +#include // TODO add negative support? class UINumberModifier : public UIComponent { @@ -12,6 +14,7 @@ class UINumberModifier : public UIComponent { uint8_t* controlGradient; int32_t lowerLimit; int32_t upperLimit; + std::unique_ptr> changeCallback; UINumberModifier() { this->color = Color(0); @@ -21,6 +24,7 @@ class UINumberModifier : public UIComponent { this->controlGradient = nullptr; this->lowerLimit = INT_MIN; this->upperLimit = INT_MAX; + this->changeCallback = nullptr; } virtual Dimension GetSize() { return Dimension(length, 1); } @@ -34,6 +38,14 @@ class UINumberModifier : public UIComponent { void SetLowerLimit(int32_t lowerLimit) { this->lowerLimit = lowerLimit; } void SetUpperLimit(int32_t upperLimit) { this->upperLimit = upperLimit; } + void OnChange(std::function changeCallback) { this->changeCallback = std::make_unique>(changeCallback); } + + virtual void OnChangeCallback(int32_t value) { + if (changeCallback != nullptr) { + (*changeCallback)(value); + } + } + virtual bool Render(Point origin) { for (uint8_t i = 0; i < length; i++) { MatrixOS::LED::SetColor(origin + Point(i, 0), color.Scale(controlGradient[i])); } @@ -50,7 +62,11 @@ class UINumberModifier : public UIComponent { else if (new_value < lowerLimit) { new_value = lowerLimit; } - *valuePtr = (int32_t)new_value; + int32_t final_value = (int32_t)new_value; + *valuePtr = final_value; + + // Trigger the callback if set + OnChangeCallback(final_value); } return true; // Prevent leak though to cause UI text scroll }