diff --git a/Makefile b/Makefile index b6e43c0..109308d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # If RACK_DIR is not defined when calling the Makefile, default to two directories above -RACK_DIR ?= C:\Users\Trevor\Documents\GitHub\Rack-SDK +RACK_DIR ?= C:\Rack-SDK # FLAGS will be passed to both the C and C++ compiler FLAGS += diff --git a/plugin.json b/plugin.json index d66863f..fe9deda 100644 --- a/plugin.json +++ b/plugin.json @@ -1,16 +1,16 @@ { "slug": "TMT", "name": "T's Musical Tools", - "version": "2.1.10", + "version": "2.2.0", "license": "MIT", "brand": "T", "author": "T", "authorEmail": "hillhand@gmail.com", - "authorUrl": "https://github.com/Jadael/", + "authorUrl": "http://hillhand.com", "pluginUrl": "https://github.com/Jadael/TMT", "manualUrl": "https://github.com/Jadael/TMT", "sourceUrl": "https://github.com/Jadael/TMT", - "donateUrl": "", + "donateUrl": "https://hillhand.itch.io/tmt", "changelogUrl": "", "modules": [ { @@ -75,6 +75,16 @@ "Sequencer", "Polyphonic" ] + }, + { + "slug": "Page", + "name": "Page", + "description": "Expander for Spellbook that outputs columns 17-32. Chain up to 31 Pages for 512 columns total.", + "tags": [ + "Sequencer", + "Polyphonic", + "Expander" + ] }, { "slug": "Stats", diff --git a/res/page.svg b/res/page.svg new file mode 100644 index 0000000..6293ed0 --- /dev/null +++ b/res/page.svg @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/src/page.cpp b/src/page.cpp new file mode 100644 index 0000000..6a71f2d --- /dev/null +++ b/src/page.cpp @@ -0,0 +1,199 @@ +/* +T's Musical Tools (TMT) - A collection of esoteric modules for VCV Rack, focused on manipulating RNG and polyphonic signals. +Copyright (C) 2024 T + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +#include "plugin.hpp" +#include "ports.hpp" +#include "spellbook_expander.hpp" + +struct Page : Module { + enum ParamId { + PARAMS_LEN + }; + enum InputId { + INPUTS_LEN + }; + enum OutputId { + POLY_OUTPUT, + OUT01_OUTPUT, OUT02_OUTPUT, OUT03_OUTPUT, OUT04_OUTPUT, + OUT05_OUTPUT, OUT06_OUTPUT, OUT07_OUTPUT, OUT08_OUTPUT, + OUT09_OUTPUT, OUT10_OUTPUT, OUT11_OUTPUT, OUT12_OUTPUT, + OUT13_OUTPUT, OUT14_OUTPUT, OUT15_OUTPUT, OUT16_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + LIGHTS_LEN + }; + + int position = 0; + int64_t baseID = -1; + int lastConfiguredPosition = -1; // Track when we last updated output labels + + // Expander message buffers (static allocation to avoid DLL issues) + // Allocate BOTH sides: Page receives from Spellbook (left) and sends to next Page (right) + SpellbookExpanderMessage leftMessages[2]; // To RECEIVE from Spellbook + SpellbookExpanderMessage rightMessages[2]; // To SEND to next Page + + Page() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configOutput(POLY_OUTPUT, "Polyphonic voltages from columns"); + + for (int i = 0; i < 16; ++i) { + configOutput(OUT01_OUTPUT + i, "Column " + std::to_string(i + 1)); + outputs[OUT01_OUTPUT + i].setVoltage(0.0f); + } + outputs[POLY_OUTPUT].setChannels(16); + + // Set up BOTH expander message buffers + // VCV Rack Engine will connect adjacent modules' buffers + leftExpander.producerMessage = &leftMessages[0]; + leftExpander.consumerMessage = &leftMessages[1]; + rightExpander.producerMessage = &rightMessages[0]; + rightExpander.consumerMessage = &rightMessages[1]; + + // Initialize message data to defaults + for (int i = 0; i < 2; i++) { + leftMessages[i].baseID = -1; + leftMessages[i].position = 0; + leftMessages[i].currentStep = 0; + leftMessages[i].totalSteps = 0; + leftMessages[i].totalColumns = 0; + for (int j = 0; j < MAX_EXPANDER_COLUMNS; j++) { + leftMessages[i].outputVoltages[j] = 0.0f; + } + } + for (int i = 0; i < 2; i++) { + rightMessages[i].baseID = -1; + rightMessages[i].position = 0; + rightMessages[i].currentStep = 0; + rightMessages[i].totalSteps = 0; + rightMessages[i].totalColumns = 0; + for (int j = 0; j < MAX_EXPANDER_COLUMNS; j++) { + rightMessages[i].outputVoltages[j] = 0.0f; + } + } + } + + void process(const ProcessArgs& args) override { + // Read message from left module (either Spellbook or another Page) + if (leftExpander.module && leftExpander.consumerMessage) { + // Verify it's a Spellbook or Page module by checking the message + SpellbookExpanderMessage* message = (SpellbookExpanderMessage*)leftExpander.consumerMessage; + + // Update our position and base ID + baseID = message->baseID; + position = message->position; + + if (message->totalColumns > 0) { + // Calculate which columns this expander handles + // Position 1 = columns 17-32 (indices 16-31) + // Position 2 = columns 33-48 (indices 32-47) + // etc. + int startColumn = SPELLBOOK_BASE_COLUMNS + (position - 1) * 16; + + // Only update output labels when position changes (not every process call!) + if (position != lastConfiguredPosition) { + std::string positionLabel = " (Page " + std::to_string(position) + ")"; + for (int i = 0; i < 16; ++i) { + int columnIndex = startColumn + i; + configOutput(OUT01_OUTPUT + i, "Column " + std::to_string(columnIndex + 1) + positionLabel); + } + lastConfiguredPosition = position; + } + + int activeChannels = 0; + + // Output the pre-calculated voltages for this expander's 16 columns + for (int i = 0; i < 16; i++) { + int columnIndex = startColumn + i; + float outputValue = 0.0f; + + // Only process if this column exists + if (columnIndex < message->totalColumns && columnIndex < MAX_EXPANDER_COLUMNS) { + // Simply read the pre-calculated voltage from Spellbook + outputValue = message->outputVoltages[columnIndex]; + activeChannels = i + 1; + } + + outputs[OUT01_OUTPUT + i].setVoltage(outputValue); + outputs[POLY_OUTPUT].setVoltage(outputValue, i); + } + + outputs[POLY_OUTPUT].setChannels(activeChannels); + + // Forward message to right expander (if any) + if (rightExpander.module && rightExpander.module->leftExpander.consumerMessage) { + SpellbookExpanderMessage* rightMessage = (SpellbookExpanderMessage*)rightExpander.module->leftExpander.consumerMessage; + rightMessage->baseID = baseID; + rightMessage->position = position + 1; // Increment position + rightMessage->currentStep = message->currentStep; + rightMessage->totalSteps = message->totalSteps; + rightMessage->totalColumns = message->totalColumns; + + // Copy the voltage array + for (int i = 0; i < MAX_EXPANDER_COLUMNS; i++) { + rightMessage->outputVoltages[i] = message->outputVoltages[i]; + } + + rightExpander.module->leftExpander.messageFlipRequested = true; + } + } else { + // No data from left module - output zeros + for (int i = 0; i < 16; i++) { + outputs[OUT01_OUTPUT + i].setVoltage(0.0f); + outputs[POLY_OUTPUT].setVoltage(0.0f, i); + } + } + } else { + // Not connected to anything - output zeros + position = 0; + baseID = -1; + for (int i = 0; i < 16; i++) { + outputs[OUT01_OUTPUT + i].setVoltage(0.0f); + outputs[POLY_OUTPUT].setVoltage(0.0f, i); + } + } + } +}; + +struct PageWidget : ModuleWidget { + PageWidget(Page* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/page.svg"))); + + // Poly output centered at top, then 16 outputs in two columns + addOutput(createOutputCentered(mm2px(Vec(15.993, 14.933)), module, Page::POLY_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 27.166)), module, Page::OUT01_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 27.166)), module, Page::OUT09_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 39.399)), module, Page::OUT02_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 39.399)), module, Page::OUT10_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 51.632)), module, Page::OUT03_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 51.632)), module, Page::OUT11_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 63.866)), module, Page::OUT04_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 63.866)), module, Page::OUT12_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 76.099)), module, Page::OUT05_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 76.099)), module, Page::OUT13_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 88.332)), module, Page::OUT06_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 88.332)), module, Page::OUT14_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 100.566)), module, Page::OUT07_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 100.566)), module, Page::OUT15_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(11.331, 112.799)), module, Page::OUT08_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(20.654, 112.799)), module, Page::OUT16_OUTPUT)); + } +}; + +Model* modelPage = createModel("Page"); diff --git a/src/plugin.cpp b/src/plugin.cpp index 9f9b3c7..863beda 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -31,6 +31,7 @@ void init(Plugin* p) { p->addModel(modelAppend); p->addModel(modelSight); p->addModel(modelSpellbook); + p->addModel(modelPage); p->addModel(modelStats); p->addModel(modelBlankt); p->addModel(modelSort); diff --git a/src/plugin.hpp b/src/plugin.hpp index 631ff6b..83bd00c 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -32,6 +32,7 @@ extern Model* modelOuroboros; extern Model* modelAppend; extern Model* modelSight; extern Model* modelSpellbook; +extern Model* modelPage; extern Model* modelStats; extern Model* modelBlankt; extern Model* modelSort; diff --git a/src/spellbook.cpp b/src/spellbook.cpp index 837fe8e..1f462ad 100644 --- a/src/spellbook.cpp +++ b/src/spellbook.cpp @@ -18,6 +18,7 @@ along with this program. If not, see . #include "plugin.hpp" #include "ports.hpp" +#include "spellbook_expander.hpp" #include #include #include @@ -127,8 +128,11 @@ C4 ? Pitches do NOT automatically create triggers..., ? ...you need a trigger co bool dirty = false; bool fullyInitialized = false; float lineHeight = 12; - - Spellbook() : lastValues(16, {0.0f, 'N'}) { // Some RhythML commands act differently based on the prior voltage of each channel, so assume all 0s for "before time began" + + // Expander message buffers (static allocation to avoid DLL issues) + SpellbookExpanderMessage rightMessages[2]; + + Spellbook() : lastValues(MAX_EXPANDER_COLUMNS, {0.0f, 'N'}) { // Support up to 128 columns for expanders config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); configInput(STEPFWD_INPUT, "Step Forward"); configInput(STEPBAK_INPUT, "Step Backward"); @@ -148,6 +152,23 @@ C4 ? Pitches do NOT automatically create triggers..., ? ...you need a trigger co outputs[POLY_OUTPUT].setChannels(16); // TODO: The compiler keeps complaining about array bounds, because we're basically just pinky-promising ourselves to never change the number of channels and a lot of places just ASSUME 16 channels. Need to change something about how we distribute all the right values to all the right channels to avoid that awkwardness. width = SPELLBOOK_DEFAULT_WIDTH; // Not sure this is needed, I just feel safer with it here. + + // Initialize expander messages + rightExpander.producerMessage = &rightMessages[0]; + rightExpander.consumerMessage = &rightMessages[1]; + + // Initialize message data to valid defaults + for (int i = 0; i < 2; i++) { + rightMessages[i].baseID = -1; + rightMessages[i].position = 1; + rightMessages[i].currentStep = 0; + rightMessages[i].totalSteps = 0; + rightMessages[i].totalColumns = 0; + for (int j = 0; j < MAX_EXPANDER_COLUMNS; j++) { + rightMessages[i].outputVoltages[j] = 0.0f; + } + } + fullyInitialized = true; } @@ -172,9 +193,9 @@ C4 ? Pitches do NOT automatically create triggers..., ? ...you need a trigger co } } configOutput(POLY_OUTPUT, polyOutputLabel); - - // Mono labels - for (size_t i = 0; i < labels.size(); ++i) { + + // Mono labels - only configure up to 16 outputs to prevent crash with 17+ columns + for (size_t i = 0; i < std::min(labels.size(), (size_t)16); ++i) { configOutput(OUT01_OUTPUT + i, labels[i]); } } @@ -410,11 +431,11 @@ C4 ? Pitches do NOT automatically create triggers..., ? ...you need a trigger co std::istringstream ss(text); std::string line; while (getline(ss, line)) { - std::vector stepData(16, StepData{0.0f, 'U'}); // Default all steps to 0.0 volts, "Unused" type + std::vector stepData(MAX_EXPANDER_COLUMNS, StepData{0.0f, 'U'}); // Support up to 128 columns std::istringstream lineStream(line); std::string cell; int index = 0; - while (getline(lineStream, cell, ',') && index < 16) { + while (getline(lineStream, cell, ',') && index < MAX_EXPANDER_COLUMNS) { size_t commentPos = cell.find('?'); if (commentPos != std::string::npos) { cell = cell.substr(0, commentPos); // Remove the comment part @@ -445,11 +466,27 @@ C4 ? Pitches do NOT automatically create triggers..., ? ...you need a trigger co index++; } + + // Trim unused columns - find the last non-unused column + int lastUsedColumn = 0; + for (int i = 0; i < (int)stepData.size(); i++) { + if (stepData[i].type != 'U') { + lastUsedColumn = i + 1; + } + } + + // Resize to only include used columns (or minimum 1) + if (lastUsedColumn > 0) { + stepData.resize(lastUsedColumn); + } else { + stepData.resize(1); // At least one column + } + steps.push_back(stepData); } if (steps.empty()) { - steps.push_back(std::vector(16, StepData{0.0f, 'U'})); + steps.push_back(std::vector(1, StepData{0.0f, 'U'})); } currentStep = currentStep % steps.size(); @@ -623,6 +660,84 @@ dtodt\dtodtod\odtodto\todtodt\dtodtod\odtodto\todtodt\dtodtod\odtodto\todtodt\ dirty = true; // Mark for re-parsing } + + // Send pre-calculated voltages to right expander (Page modules) + if (rightExpander.module && rightExpander.module->leftExpander.consumerMessage) { + SpellbookExpanderMessage* message = (SpellbookExpanderMessage*)rightExpander.module->leftExpander.consumerMessage; + + message->baseID = id; + message->position = 1; // First expander is position 1 + message->currentStep = currentStep; + message->totalSteps = steps.size(); + + // Get the total number of columns from current step + int totalColumns = 0; + if (currentStep < (int)steps.size()) { + totalColumns = steps[currentStep].size(); + } + message->totalColumns = totalColumns; + + // Calculate output voltages for ALL columns (up to MAX_EXPANDER_COLUMNS) + // This includes columns 1-16 (handled by Spellbook) and 17+ (handled by Page expanders) + for (int i = 0; i < MAX_EXPANDER_COLUMNS; i++) { + float outputValue = 0.0f; + + if (currentStep < (int)steps.size() && i < (int)steps[currentStep].size()) { + StepData& step = steps[currentStep][i]; + + // Use the same logic as the main output loop above + switch (step.type) { + case 'T': // Trigger + if (triggerTimer.check(0.002f)) { + outputValue = 0.0f; + } else if (triggerTimer.check(0.001f)) { + outputValue = 10.0f; + } else { + outputValue = 0.0f; + } + break; + case 'R': // Retrigger + if (!triggerTimer.check(0.001f)) { + outputValue = 0.0f; + } else { + outputValue = 10.0f; + } + break; + case 'G': // Full-width gate + outputValue = 10.0f; + break; + case 'N': // Normal pitch or CV + outputValue = step.voltage; + break; + case 'E': // Empty cells + if (i < (int)lastValues.size()) { + if (lastValues[i].type == 'G' || lastValues[i].type == 'T' || lastValues[i].type == 'R') { + outputValue = 0.0f; + } else { + outputValue = lastValues[i].voltage; + } + } + break; + case 'U': // Unused cells + outputValue = 0.0f; + break; + default: + outputValue = step.voltage; + break; + } + + // Update lastValues for this column + if (i < (int)lastValues.size()) { + lastValues[i].voltage = outputValue; + lastValues[i].type = step.type; + } + } + + message->outputVoltages[i] = outputValue; + } + + rightExpander.module->leftExpander.messageFlipRequested = true; + } } void overrideText(std::string newText) { diff --git a/src/spellbook_expander.hpp b/src/spellbook_expander.hpp new file mode 100644 index 0000000..f449195 --- /dev/null +++ b/src/spellbook_expander.hpp @@ -0,0 +1,33 @@ +/* +T's Musical Tools (TMT) - A collection of esoteric modules for VCV Rack, focused on manipulating RNG and polyphonic signals. +Copyright (C) 2024 T + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +#pragma once + +#define SPELLBOOK_BASE_COLUMNS 16 // Columns handled by base module +#define MAX_EXPANDER_COLUMNS 512 // Support up to 512 columns total (16 base + 31 Pages x 16 = 512) + +// Expander message structure shared between Spellbook and Page modules +// Spellbook sends pre-calculated voltages for all columns to Page expanders +struct SpellbookExpanderMessage { + int64_t baseID = -1; // ID of the base Spellbook module + int position = 0; // Position in the chain (1=first Page, 2=second, etc.) + int currentStep = 0; // Current step in the sequence + int totalSteps = 0; // Total number of steps in the sequence + int totalColumns = 0; // Total number of columns in current step + float outputVoltages[MAX_EXPANDER_COLUMNS]; // Pre-calculated output voltages for all columns +};