From 5789e8f431722795f0907843b2bbc56e0c242e30 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 27 Feb 2026 08:53:13 -0500 Subject: [PATCH 01/37] Implemented drag to reorder tracks in media clipboard --- src/widgets/TrackAreaWidget.h | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 0fcd8c27..b890ed0d 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -27,6 +27,7 @@ class TrackAreaWidget : public Component, TrackAreaWidget(DisplayMode mode = DisplayMode::Input, int trackHeight = 0) : displayMode(mode), fixedTrackHeight(trackHeight) { + addMouseListener(this, true); } ~TrackAreaWidget() { resetState(); } @@ -34,6 +35,24 @@ class TrackAreaWidget : public Component, void paint(Graphics& g) override { g.fillAll(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); + + // Draws drop line for track reordering + if (isDraggingTrack && dragInsertIndex >= 0) + { + int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + int lineY = dragInsertIndex * trackSlotHeight; + + g.setColour(Colours::white); + g.fillRect(0, lineY - 1, getWidth(), 3); + } + + // Makes the dragged track look visually distinct + if (isDraggingTrack && draggedTrack != nullptr) + { + Rectangle draggedBounds = draggedTrack->getBounds(); + g.setColour(Colours::black.withAlpha(0.3f)); + g.fillRect(draggedBounds); + } } void resized() override @@ -328,6 +347,25 @@ class TrackAreaWidget : public Component, resized(); } + void reorderTrack(MediaDisplayComponent* draggedDisplay, int newIndex) + { + auto it = + std::find_if(mediaDisplays.begin(), + mediaDisplays.end(), + [draggedDisplay](const auto& ptr) { return ptr.get() == draggedDisplay; }); + + if (it == mediaDisplays.end()) return; + + auto draggedPtr = std::move(*it); + mediaDisplays.erase(it); + + newIndex = jlimit(0, (int)mediaDisplays.size(), newIndex); + + mediaDisplays.insert(mediaDisplays.begin() + newIndex, std::move(draggedPtr)); + + resized(); + } + void filesDropped(const StringArray& files, int /*x*/, int /*y*/) override { for (String f : files) @@ -357,6 +395,86 @@ class TrackAreaWidget : public Component, } } + int getInsertIndexAtY(int y) + { + int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + int index = y / trackSlotHeight; + return jlimit(0, getNumTracks(), index); + } + + void mouseDown(const MouseEvent& e) override + { + if (!isThumbnailWidget()) return; + + Component* clicked = e.eventComponent; + + MediaDisplayComponent* clickedDisplay = nullptr; + Component* c = clicked; + while (c != nullptr && c != this) + { + if (auto* md = dynamic_cast(c)) + { + clickedDisplay = md; + break; + } + c = c->getParentComponent(); + } + + if (clickedDisplay) + { + draggedTrack = clickedDisplay; + isDraggingTrack = false; + } + } + + void mouseDrag(const MouseEvent& e) override + { + if (!isThumbnailWidget() || draggedTrack == nullptr) return; + + Point posInThis = e.getEventRelativeTo(this).getPosition(); + + if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) + { + isDraggingTrack = true; + } + + if (isDraggingTrack) + { + dragInsertIndex = getInsertIndexAtY(posInThis.y); + // Draws the drop indicator line + repaint(); + } + } + + void mouseUp(const MouseEvent& e) override + { + if (!isThumbnailWidget()) return; + + if (isDraggingTrack && draggedTrack != nullptr) + { + int currentIndex = -1; + for (int i = 0; i < (int)mediaDisplays.size(); i++) + { + if (mediaDisplays[i].get() == draggedTrack) + { + currentIndex = i; + break; + } + } + + if (dragInsertIndex != currentIndex && dragInsertIndex != currentIndex + 1) + { + reorderTrack(draggedTrack, dragInsertIndex); + } + } + + // Reset the drag state + draggedTrack = nullptr; + dragInsertIndex = -1; + isDraggingTrack = false; + repaint(); + } + const DisplayMode displayMode; const int fixedTrackHeight = 0; @@ -364,5 +482,10 @@ class TrackAreaWidget : public Component, int fixedTotalWidth = 0; int minTotalHeight = 0; + // For reordering tracks via dragging + MediaDisplayComponent* draggedTrack = nullptr; + int dragInsertIndex = -1; + bool isDraggingTrack = false; + std::vector> mediaDisplays; }; From 216e96c507ab9fea7207df66c38ca17db102135e Mon Sep 17 00:00:00 2001 From: Elek Yuhas Date: Wed, 4 Mar 2026 11:56:03 -0500 Subject: [PATCH 02/37] Initial copy-paste implementation for MacOS. --- CMakeLists.txt | 2 ++ src/media/MediaDisplayComponent.cpp | 51 ++++++++++++++++++++++++++++- src/media/MediaDisplayComponent.h | 6 ++++ src/media/copy.h | 4 +++ src/media/copy.mm | 20 +++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/media/copy.h create mode 100644 src/media/copy.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index c2efdf26..c9f9e63a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,8 @@ target_sources(${PROJECT_NAME} src/media/MidiDisplayComponent.cpp src/media/OutputLabelComponent.cpp + src/media/copy.mm + src/media/pianoroll/KeyboardComponent.cpp src/media/pianoroll/NoteGridComponent.cpp src/media/pianoroll/PianoRollComponent.cpp diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 111a311c..7c0c096f 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,6 +1,7 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" +#include "copy.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} @@ -11,6 +12,7 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW deviceManager.initialise(0, 2, nullptr, true, {}, nullptr); deviceManager.addAudioCallback(&sourcePlayer); + sourcePlayer.setSource(&transportSource); @@ -99,6 +101,24 @@ void MediaDisplayComponent::initializeButtons() saveFileButton.addMode(saveFileButtonInactiveInfo); headerComponent.addAndMakeVisible(saveFileButton); + // Mode when an copyable file is loaded + copyFileButtonActiveInfo = MultiButton::Mode { "Copy-Active", + "Click to copy the media file.", + [this] { copyFileCallback(); }, + MultiButton::DrawingMode::IconOnly, + Colours::lightblue, + fontawesome::Save }; + // Mode when there is nothing to copy + copyFileButtonInactiveInfo = + MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", + [this] {}, MultiButton::DrawingMode::IconOnly, + Colours::lightgrey, fontawesome::Save }; + copyFileButton.addMode(copyFileButtonActiveInfo); + copyFileButton.addMode(copyFileButtonInactiveInfo); + headerComponent.addAndMakeVisible(copyFileButton); + + + resetButtonState(); } @@ -244,7 +264,7 @@ void MediaDisplayComponent::resized() if (isOutputTrack()) { buttonsFlexBox.items.add( - FlexItem(saveFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(copyFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); } buttonsFlexBox.performLayout(buttonsComponent.getBounds()); @@ -428,6 +448,7 @@ void MediaDisplayComponent::resetButtonState() playStopButton.setMode(playButtonInactiveInfo.displayLabel); chooseFileButton.setMode(chooseFileButtonInfo.displayLabel); saveFileButton.setMode(saveFileButtonInactiveInfo.displayLabel); + copyFileButton.setMode(copyFileButtonInactiveInfo.displayLabel); } void MediaDisplayComponent::initializeDisplay(const URL& filePath) @@ -460,6 +481,7 @@ void MediaDisplayComponent::updateDisplay(const URL& filePath) playStopButton.setMode(playButtonActiveInfo.displayLabel); saveFileButton.setMode(saveFileButtonActiveInfo.displayLabel); + copyFileButton.setMode(copyFileButtonActiveInfo.displayLabel); } void MediaDisplayComponent::setOriginalFilePath(URL filePath) @@ -740,6 +762,33 @@ void MediaDisplayComponent::saveFileCallback() } } + +void MediaDisplayComponent::copyFileCallback() +{ + if (!isFileLoaded()) + { + if (statusMessage != nullptr) + statusMessage->setMessage("No file loaded."); + return; + } + + // If you're copying the original file path: + juce::File file = getOriginalFilePath().getLocalFile(); + + if (file.exists()) + { + copyFileToClipboard(file); + + if (statusMessage != nullptr) + statusMessage->setMessage("File path copied to clipboard."); + } + else + { + if (statusMessage != nullptr) + statusMessage->setMessage("File does not exist."); + } +} + float MediaDisplayComponent::getPixelsPerSecond() { if (visibleRange.getLength()) diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 312d9410..9cf6a55e 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -110,6 +110,8 @@ class MediaDisplayComponent : public Component, void saveFileCallback(); + void copyFileCallback(); + virtual double getTotalLengthInSecs() = 0; virtual double getTimeAtOrigin() { return visibleRange.getStart(); } virtual float getPixelsPerSecond(); @@ -236,6 +238,7 @@ class MediaDisplayComponent : public Component, // Media + overhead panel (if any) Component mediaAreaContainer; + // Header sub-components Label trackNameLabel; MultiButton playStopButton; @@ -247,6 +250,9 @@ class MediaDisplayComponent : public Component, MultiButton saveFileButton; MultiButton::Mode saveFileButtonActiveInfo; MultiButton::Mode saveFileButtonInactiveInfo; + MultiButton copyFileButton; + MultiButton::Mode copyFileButtonActiveInfo; + MultiButton::Mode copyFileButtonInactiveInfo; // Panel displaying overhead labels ColorablePanel overheadPanel { overheadPanelColor }; diff --git a/src/media/copy.h b/src/media/copy.h new file mode 100644 index 00000000..d23614b4 --- /dev/null +++ b/src/media/copy.h @@ -0,0 +1,4 @@ +#pragma once +#include + +void copyFileToClipboard (const juce::File& file); \ No newline at end of file diff --git a/src/media/copy.mm b/src/media/copy.mm new file mode 100644 index 00000000..d4f5dd68 --- /dev/null +++ b/src/media/copy.mm @@ -0,0 +1,20 @@ +#import + +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + NSPasteboard* pb = [NSPasteboard generalPasteboard]; + + [pb declareTypes: [NSArray arrayWithObject: NSPasteboardTypeFileURL] + owner: nil]; + + NSString* path = [NSString stringWithUTF8String: file.getFullPathName().toUTF8()]; + NSURL* fileURL = [NSURL fileURLWithPath: path]; + + [pb setString: [fileURL absoluteString] + forType: NSPasteboardTypeFileURL]; +} \ No newline at end of file From ed56787bb71ae36b3655ace548d76b54d2fe3463 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 5 Mar 2026 12:52:46 -0500 Subject: [PATCH 03/37] Upgraded to latest version of JUCE. --- JUCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JUCE b/JUCE index 9f643254..501c0767 160000 --- a/JUCE +++ b/JUCE @@ -1 +1 @@ -Subproject commit 9f64325446ec0baf11f0f5f99c8484a15bbd1ab0 +Subproject commit 501c07674e1ad693085a7e7c398f205c2677f5da From 3cd192f400c025646a5f37f941636150efa11233 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 6 Mar 2026 13:29:07 -0500 Subject: [PATCH 04/37] Fixed downward drag offset and out-of-bounds drag. --- src/widgets/TrackAreaWidget.h | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index b890ed0d..7a60f06a 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -356,9 +356,17 @@ class TrackAreaWidget : public Component, if (it == mediaDisplays.end()) return; + // Track the old index for downward reordering + int oldIndex = std::distance(mediaDisplays.begin(), it); + auto draggedPtr = std::move(*it); mediaDisplays.erase(it); + + // Decrement index if moving downward to account for shifting indicies + if (newIndex > oldIndex) + newIndex--; + newIndex = jlimit(0, (int)mediaDisplays.size(), newIndex); mediaDisplays.insert(mediaDisplays.begin() + newIndex, std::move(draggedPtr)); @@ -440,8 +448,18 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { - dragInsertIndex = getInsertIndexAtY(posInThis.y); - // Draws the drop indicator line + // Only update the drop index while inside the widget + if (getLocalBounds().contains(posInThis)) + { + dragInsertIndex = getInsertIndexAtY(posInThis.y); + } + else + { + // Hides the indicator line when outside the widget + dragInsertIndex = -1; + } + + // Updates the drop indicator line repaint(); } } @@ -450,7 +468,11 @@ class TrackAreaWidget : public Component, { if (!isThumbnailWidget()) return; - if (isDraggingTrack && draggedTrack != nullptr) + // Tracks the release psoition of the mouse + Point releasePos = e.getEventRelativeTo(this).getPosition(); + + // Only reorders if the mouse was released inside the widget + if (isDraggingTrack && draggedTrack != nullptr && getLocalBounds().contains(releasePos)) { int currentIndex = -1; for (int i = 0; i < (int)mediaDisplays.size(); i++) From 790ca8ea155104799aa018d60e9d4c3b2b573dc2 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 16 Mar 2026 20:14:28 -0400 Subject: [PATCH 05/37] Initial attempt to implement time axis --- src/media/MediaDisplayComponent.cpp | 124 ++++++++++++++++++++++++++-- src/media/MediaDisplayComponent.h | 5 ++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 91178c12..7142ae06 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -2,6 +2,82 @@ #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" +#include + +namespace +{ +class MediaDisplayComponent::TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override + { + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } + } + +private: + MediaDisplayComponent* owner = nullptr; +}; +} // namespace + MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) @@ -36,8 +112,11 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW horizontalScrollBar.setAutoHide(false); horizontalScrollBar.addListener(this); + timeAxisStrip = std::make_unique(this); + mediaAreaContainer.addAndMakeVisible(overheadPanel); mediaAreaContainer.addAndMakeVisible(contentComponent); + mediaAreaContainer.addAndMakeVisible(*timeAxisStrip); mediaAreaContainer.addAndMakeVisible(horizontalScrollBar); addAndMakeVisible(mediaAreaContainer); @@ -277,6 +356,20 @@ void MediaDisplayComponent::resized() // Media component takes remaining space mediaAreaFlexBox.items.add(FlexItem(contentComponent).withFlex(1)); + if (timeAxisStrip != nullptr) + { + timeAxisStrip->setVisible(horizontalScrollBar.isVisible()); + if (timeAxisStrip->isVisible()) + { + mediaAreaFlexBox.items.add(FlexItem(*timeAxisStrip) + .withHeight(timeAxisHeight) + .withMargin({ 0, + getVerticalControlsWidth(), + static_cast(controlSpacing), + getMediaXPos() })); + } + } + if (horizontalScrollBar.isVisible()) { // Add horizontal scrollbar with fixed height @@ -823,6 +916,9 @@ void MediaDisplayComponent::updateVisibleRange(Range r) updateCursorPosition(); repositionLabels(); + if (timeAxisStrip != nullptr) + timeAxisStrip->repaint(); + visibleRangeCallback(); } @@ -839,16 +935,30 @@ void MediaDisplayComponent::horizontalMove(double deltaT) void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { - horizontalZoomFactor = jlimit(1.0, 2.0, horizontalZoomFactor + deltaZoom); + const float mediaWidth = getMediaWidth(); + const double totalLength = getTotalLengthInSecs(); - double newScale = jmax(0.05, getTotalLengthInSecs() * (2.0 - horizontalZoomFactor)); + if (mediaWidth <= 0.0f || totalLength <= 0.0) + return; - double visibleStart = visibleRange.getStart(); - double visibleEnd = visibleRange.getEnd(); - double visibleLength = visibleRange.getLength(); + const float pps = getPixelsPerSecond(); + if (pps <= 0.0f) + return; + + // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. + const float minPps = 20.0f; + const float maxPps = static_cast(mediaWidth / totalLength); + + float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); + newPps = jlimit(minPps, maxPps, newPps); + + double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); + if (newVisibleLength > totalLength) + newVisibleLength = totalLength; - double newStart = scrollPosT - newScale * (scrollPosT - visibleStart) / visibleLength; - double newEnd = scrollPosT + newScale * (visibleEnd - scrollPosT) / visibleLength; + double newStart = scrollPosT - newVisibleLength * 0.5; + newStart = jlimit(0.0, totalLength - newVisibleLength, newStart); + double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index a4020658..5a57c0b8 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -115,6 +115,7 @@ class MediaDisplayComponent : public Component, virtual double getTotalLengthInSecs() = 0; virtual double getTimeAtOrigin() { return visibleRange.getStart(); } virtual float getPixelsPerSecond(); + const Range& getVisibleRange() const { return visibleRange; } virtual void setPlaybackPosition(double t) { transportSource.setPosition(t); } virtual double getPlaybackPosition() { return transportSource.getCurrentPosition(); } @@ -151,6 +152,7 @@ class MediaDisplayComponent : public Component, const int controlSpacing = 1; const int scrollBarSize = 8; + const int timeAxisHeight = 20; // Media (audio or MIDI) content area Component contentComponent; @@ -159,6 +161,9 @@ class MediaDisplayComponent : public Component, Range visibleRange; + class TimeAxisStrip; + std::unique_ptr timeAxisStrip; + AudioFormatManager formatManager; AudioDeviceManager deviceManager; From 4441387d732f8981b521adf304e4ba0c8ad8f514 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 17 Mar 2026 10:06:43 -0400 Subject: [PATCH 06/37] Second attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 74 ----------------------------- src/media/MediaDisplayComponent.h | 74 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 76 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7142ae06..8b842711 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,80 +4,6 @@ #include -namespace -{ -class MediaDisplayComponent::TimeAxisStrip : public Component -{ -public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override - { - if (owner == nullptr || ! owner->isFileLoaded()) - return; - - const auto& visibleRange = owner->getVisibleRange(); - const float pps = owner->getPixelsPerSecond(); - const double totalLength = owner->getTotalLengthInSecs(); - - if (pps <= 0.0f || visibleRange.getLength() <= 0.0) - return; - - const double visibleStart = visibleRange.getStart(); - const double visibleEnd = visibleRange.getEnd(); - const int w = getWidth(); - const int h = getHeight(); - - g.setColour(Colours::darkgrey); - g.fillRect(getLocalBounds()); - - g.setColour(Colours::lightgrey.withAlpha(0.8f)); - - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) - { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); - } - - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); - - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) - { - const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) - continue; - - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; - - g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); - } - } - -private: - MediaDisplayComponent* owner = nullptr; -}; -} // namespace - MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 5a57c0b8..58c4151b 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -161,7 +161,77 @@ class MediaDisplayComponent : public Component, Range visibleRange; - class TimeAxisStrip; + class TimeAxisStrip : public Component + { + public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override + { + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } + } + + private: + MediaDisplayComponent* owner = nullptr; + }; + std::unique_ptr timeAxisStrip; AudioFormatManager formatManager; @@ -296,4 +366,4 @@ class MediaDisplayComponent : public Component, SharedResourcePointer instructionsMessage; SharedResourcePointer statusMessage; -}; +}; \ No newline at end of file From 0aea58f8126c02699d96146f3daf26afaf42c990 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Thu, 19 Mar 2026 18:58:30 -0400 Subject: [PATCH 07/37] Third attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 73 +++++++++++++++++++++++-- src/media/MediaDisplayComponent.h | 84 +++++------------------------ 2 files changed, 83 insertions(+), 74 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 8b842711..a46c3ba9 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,6 +4,68 @@ #include +void TimeAxisStrip::paint(Graphics& g) +{ + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } +} + MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) @@ -853,8 +915,10 @@ void MediaDisplayComponent::horizontalMove(double deltaT) double visibleStart = visibleRange.getStart(); double visibleLength = visibleRange.getLength(); + const double totalLength = getTotalLengthInSecs(); + const double maxStart = jmax(0.0, totalLength - visibleLength); double newStart = visibleStart - deltaT * visibleLength / 10.0; - newStart = jlimit(0.0, jmax(0.0, getTotalLengthInSecs() - visibleLength), newStart); + newStart = jlimit(0.0, maxStart, newStart); updateVisibleRange({ newStart, newStart + visibleLength }); } @@ -873,7 +937,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. const float minPps = 20.0f; - const float maxPps = static_cast(mediaWidth / totalLength); + float maxPps = static_cast(mediaWidth / totalLength); + if (maxPps < minPps) + maxPps = minPps; float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); @@ -882,8 +948,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (newVisibleLength > totalLength) newVisibleLength = totalLength; + const double maxStart = jmax(0.0, totalLength - newVisibleLength); double newStart = scrollPosT - newVisibleLength * 0.5; - newStart = jlimit(0.0, totalLength - newVisibleLength, newStart); + newStart = jlimit(0.0, maxStart, newStart); double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 58c4151b..225c9698 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -17,6 +17,8 @@ using namespace juce; +class MediaDisplayComponent; + enum class DisplayMode { Input, @@ -46,6 +48,17 @@ class ColorablePanel : public Component Colour backgroundColor; }; +class TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override; + +private: + MediaDisplayComponent* owner = nullptr; +}; + class MediaDisplayComponent : public Component, public ChangeListener, public ChangeBroadcaster, @@ -161,77 +174,6 @@ class MediaDisplayComponent : public Component, Range visibleRange; - class TimeAxisStrip : public Component - { - public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override - { - if (owner == nullptr || ! owner->isFileLoaded()) - return; - - const auto& visibleRange = owner->getVisibleRange(); - const float pps = owner->getPixelsPerSecond(); - const double totalLength = owner->getTotalLengthInSecs(); - - if (pps <= 0.0f || visibleRange.getLength() <= 0.0) - return; - - const double visibleStart = visibleRange.getStart(); - const double visibleEnd = visibleRange.getEnd(); - const int w = getWidth(); - const int h = getHeight(); - - g.setColour(Colours::darkgrey); - g.fillRect(getLocalBounds()); - - g.setColour(Colours::lightgrey.withAlpha(0.8f)); - - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) - { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); - } - - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); - - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) - { - const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) - continue; - - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; - - g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); - } - } - - private: - MediaDisplayComponent* owner = nullptr; - }; - std::unique_ptr timeAxisStrip; AudioFormatManager formatManager; From d33ab53a672c3949cd2d4e2addb3b74c9d3a6c0c Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Thu, 19 Mar 2026 23:42:02 -0400 Subject: [PATCH 08/37] Add cross-platform copy to clipboard implementation --- CMakeLists.txt | 10 +++++-- src/media/MediaDisplayComponent.cpp | 2 +- src/{media => utils/Copy}/copy.h | 0 src/utils/Copy/copyLinux.cpp | 17 +++++++++++ src/{media/copy.mm => utils/Copy/copyMac.mm} | 2 +- src/utils/Copy/copyWindows.cpp | 31 ++++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) rename src/{media => utils/Copy}/copy.h (100%) create mode 100644 src/utils/Copy/copyLinux.cpp rename src/{media/copy.mm => utils/Copy/copyMac.mm} (99%) create mode 100644 src/utils/Copy/copyWindows.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c9f9e63a..b778a4eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,8 @@ juce_generate_juce_header(${PROJECT_NAME}) # Finally, we supply a list of source files that will be built into the target. This is a standard # CMake command. + + target_sources(${PROJECT_NAME} PRIVATE src/Main.cpp @@ -99,8 +101,6 @@ target_sources(${PROJECT_NAME} src/media/MidiDisplayComponent.cpp src/media/OutputLabelComponent.cpp - src/media/copy.mm - src/media/pianoroll/KeyboardComponent.cpp src/media/pianoroll/NoteGridComponent.cpp src/media/pianoroll/PianoRollComponent.cpp @@ -132,6 +132,12 @@ target_sources(${PROJECT_NAME} src/external/fontaudio/data/FontAudioIcons.h ) +if(APPLE) +target_sources(HARP PRIVATE src/utils/Copy/copyMac.mm) +elseif(WIN32) +target_sources(HARP PRIVATE src/utils/Copy/copyWindows.cpp) +endif() + # `target_compile_definitions` adds some preprocessor definitions to our target. In a Projucer # project, these might be passed in the 'Preprocessor Definitions' field. JUCE modules also make use # of compile definitions to switch certain features on/off, so if there's a particular feature you diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7c0c096f..89b78c52 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,7 +1,7 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -#include "copy.h" +#include "../utils/Copy/copy.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} diff --git a/src/media/copy.h b/src/utils/Copy/copy.h similarity index 100% rename from src/media/copy.h rename to src/utils/Copy/copy.h diff --git a/src/utils/Copy/copyLinux.cpp b/src/utils/Copy/copyLinux.cpp new file mode 100644 index 00000000..685436b4 --- /dev/null +++ b/src/utils/Copy/copyLinux.cpp @@ -0,0 +1,17 @@ +#include +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + const juce::String uri = "file://" + file.getFullPathName(); + + GtkClipboard* clipboard = + gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + + gtk_clipboard_set_text(clipboard, + uri.toRawUTF8(), + -1); +} \ No newline at end of file diff --git a/src/media/copy.mm b/src/utils/Copy/copyMac.mm similarity index 99% rename from src/media/copy.mm rename to src/utils/Copy/copyMac.mm index d4f5dd68..63058284 100644 --- a/src/media/copy.mm +++ b/src/utils/Copy/copyMac.mm @@ -17,4 +17,4 @@ void copyFileToClipboard (const juce::File& file) [pb setString: [fileURL absoluteString] forType: NSPasteboardTypeFileURL]; -} \ No newline at end of file +} diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/Copy/copyWindows.cpp new file mode 100644 index 00000000..2537fc82 --- /dev/null +++ b/src/utils/Copy/copyWindows.cpp @@ -0,0 +1,31 @@ +#include +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + if (!OpenClipboard(nullptr)) + return; + + EmptyClipboard(); + + std::wstring path = file.getFullPathName().toWideCharPointer(); + + size_t size = sizeof(DROPFILES) + (path.size() + 2) * sizeof(wchar_t); + + HGLOBAL hMem = GlobalAlloc(GHND, size); + DROPFILES* df = (DROPFILES*)GlobalLock(hMem); + + df->pFiles = sizeof(DROPFILES); + df->fWide = TRUE; + + wchar_t* data = (wchar_t*)((BYTE*)df + sizeof(DROPFILES)); + wcscpy(data, path.c_str()); + + GlobalUnlock(hMem); + + SetClipboardData(CF_HDROP, hMem); + CloseClipboard(); +} \ No newline at end of file From 2f6196304023572cbf1f7b7c7d563b0b6c0cfff4 Mon Sep 17 00:00:00 2001 From: ella-granger <44934397+ella-granger@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:31:10 -0400 Subject: [PATCH 09/37] Update host.md --- website/content/pyharp_docs/host.md | 116 +++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 54ccb031..270ed0c7 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -1,3 +1,117 @@ # Hosting PyHARP Apps in the Cloud -(Coming soon) \ No newline at end of file +Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: + +## Gradio Endpoints +Gradio endpoints are the most convenient solution for a PyHARP application. It will allow you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, which is free from additional charges. + +1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. +2. Choose the Blank template. +3. You can choose ZeroGPU hardware if you are a PRO subscriber and your application requires GPU computation. You can also customize the hardware. +4. Clone the initialized repository locally: +```bash +git clone https://huggingface.co/spaces// +``` +5. Add your files to the repository, commit, then push to the `main` branch: +```bash +git add . +git commit -m "initial commit" +git push -u origin main +``` +6. Here are the suggested configurations in the related files. + - `README.md` + + Set __sdk_version__ to __5.28.0__. This is the recommended version, as HARP may not work with the very latest or earlier versions of Gradio. + + - `requirements.txt` + + This is the place where you put all your required **pip** packages. You should put in the latest PyHARP: + ``` + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` + If you are using Gradio endpoints, you don't have to include the gradio package here. + + - `packages.txt` + + This is the place to put **apt-get install** debian packages, which are required by some models. + +## Docker Endpoints +PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or prior version, which may cause issues when upgrading python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. + +Using Docker endpoints can help you fix this issue. The Docker will allow you to customize the deployment, which will make room for potential patches and fixes. However, the ZeroGPU hardware will not be available for Docker spaces and you need to pay for the GPU usage. + +1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. +2. Choose the Blank template. +3. Clone the initialized repository locally: +```bash +git clone https://huggingface.co/spaces// +``` +4. Add your files to the repository, commit, then push to the `main` branch: +```bash +git add . +git commit -m "initial commit" +git push -u origin main +``` +5. Configurations + - `README.md` + + Set **app_port** to ``. + + - `requirements.txt` and `packages.txt` + + Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile`, which will be introduced in the next step. + + For `requirements.txt`, you should include: + ``` + gradio==5.28.0 + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` + + - `Dockerfile` + + Here is an example `Dockerfile` configuration, which will install the general packages and fix the `madmom` package. + + ```Docker + FROM python:3.10-slim # Set python version + + WORKDIR /app + COPY packages.txt /app/packages.txt + + # System deps for building packages from source + RUN apt-get update + RUN xargs apt-get install -y --no-install-recommends < /app/packages.txt + RUN rm -rf /var/lib/apt/lists/* + + COPY requirements.txt /app/requirements.txt + # disable build isolation so Cython installed in the environment is visible at build time + ENV PIP_NO_BUILD_ISOLATION=1 + RUN pip install --no-cache-dir -U pip wheel Cython + RUN pip install --no-cache-dir setuptools==80.9.0 + RUN pip install --no-cache-dir -r /app/requirements.txt + RUN pip install --no-cache-dir --no-build-isolation madmom + + # madmom patch + COPY patch_madmom.py /app/scripts/patch_madmom.py # A patch fix in the repo + RUN python /app/scripts/patch_madmom.py + RUN python -c "import madmom; print('madmom import OK')" + + # Copy the rest of the repo + COPY . /app + + # HF Spaces routes traffic to $PORT. Gradio should listen on it. + ENV PORT= # in README.md + EXPOSE + + # Run the app + CMD ["python", "app.py"] + ``` + +--- +Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed by the two methods will have the same UI and functionality. + +Here are a few tips and best-practices when dealing with HuggingFace Spaces: +- Spaces operate based off of the files in the `main` branch +- An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces +- A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) + +For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). From 5cba27a20fcb66df54f10569d4c8702a4ed2013d Mon Sep 17 00:00:00 2001 From: Derek LLanes Date: Wed, 25 Mar 2026 01:00:05 -0400 Subject: [PATCH 10/37] Fix Windows runtime DLL path for VS BuildTools installs --- CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2efdf26..a21ebd92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,14 +212,16 @@ if (WIN32) ) # Set the base path for Visual Studio 2022 redistributable DLLs - set(VS_CRT_BASE_PATH "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT") + set(VS_CRT_BASE_PATH + "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" + "C:/Program Files (x86)/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" + ) # Search for the DLLs in the specified base path file(GLOB_RECURSE all_runtime_dlls - LIST_DIRECTORIES false - "${VS_CRT_BASE_PATH}/*.dll" + LIST_DIRECTORIES false + ${VS_CRT_BASE_PATH}/*.dll ) - set(runtime_dlls) foreach(dll ${all_runtime_dlls}) From 02330b7d8729e39f2f14a9e5f88f004b505ecd14 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 27 Mar 2026 00:09:50 -0400 Subject: [PATCH 11/37] Implemented visual drag and drop with gap logic using an overlay that spans the entire application. --- src/MainComponent.cpp | 5 + src/MainComponent.h | 3 +- src/widgets/MediaClipboardWidget.h | 6 +- src/widgets/TrackAreaWidget.h | 219 +++++++++++++++++++++++------ 4 files changed, 189 insertions(+), 44 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index b7402618..08c52816 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -15,9 +15,12 @@ MainComponent::MainComponent() addAndMakeVisible(mainModelTab); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); + addAndMakeVisible(dragOverlay); showStatusArea = Settings::getBoolValue("view.showStatusArea", true); showMediaClipboard = Settings::getBoolValue("view.showMediaClipboard", false); + dragOverlay.setVisible(false); + dragOverlay.toFront(false); requiredWindowWidth = minimumWindowWidth; requiredWindowHeight = minimumWindowHeight; @@ -126,6 +129,8 @@ void MainComponent::resized() { welcomeWindow->refreshHighlightForCurrentStep(); } + + dragOverlay.setBounds(getLocalBounds()); } void MainComponent::updateWindowConstraints() diff --git a/src/MainComponent.h b/src/MainComponent.h index 2acb5841..d7ebde43 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -152,7 +152,8 @@ class MainComponent : public Component, ModelTab mainModelTab; StatusAreaWidget statusAreaWidget; - MediaClipboardWidget mediaClipboardWidget; + DragOverlayComponent dragOverlay; + MediaClipboardWidget mediaClipboardWidget { &dragOverlay }; bool isTutorialActive = false; Rectangle tutorialHighlightRect; diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 3753cbfd..5a2b551b 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -19,7 +19,9 @@ using namespace juce; class MediaClipboardWidget : public Component, public ChangeListener { public: - MediaClipboardWidget() + MediaClipboardWidget(DragOverlayComponent* overlay) + : dragOverlay(overlay), + trackAreaWidget(DisplayMode::Thumbnail, 75, overlay) { selectionTextBox.onReturnKey = [this] { renameSelectionCallback(); }; controlsComponent.addAndMakeVisible(selectionTextBox); @@ -625,6 +627,8 @@ class MediaClipboardWidget : public Component, public ChangeListener MultiButton::Mode sendToDAWButtonInactiveInfo1; MultiButton::Mode sendToDAWButtonInactiveInfo2; + DragOverlayComponent* dragOverlay = nullptr; + Viewport trackArea; TrackAreaWidget trackAreaWidget { DisplayMode::Thumbnail, 75 }; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 7a60f06a..c6b59e5e 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -18,14 +18,85 @@ using namespace juce; +class DragOverlayComponent : public Component +{ +public: + DragOverlayComponent() + { + setInterceptsMouseClicks(false, false); + } + + void startDrag(Image snapshot, Point offset) + { + if (isActive) return; + dragImage = snapshot; + dragOffset = offset; + isActive = true; + setVisible(true); + repaint(); + } + + // Called every mouseDrag event with the current mouse position + void updatePosition(Point newPos) + { + if (newPos == currentPos) return; + + // Clear old position + repaint(currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight()); + + currentPos = newPos; + + // Repaint only new position + repaint(currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight()); + } + + void stopDrag() + { + isActive = false; + dragImage = Image(); // Releases the image data + setVisible(false); + repaint(); + } + + void paint(Graphics& g) override + { + if (!isActive || dragImage.isNull()) + return; + + g.drawImage(dragImage, + currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight(), + 0, + 0, + dragImage.getWidth(), + dragImage.getHeight()); + } + +private: + Image dragImage; + Point currentPos; + Point dragOffset; + bool isActive = false; +}; + class TrackAreaWidget : public Component, public ChangeListener, public ChangeBroadcaster, public FileDragAndDropTarget { public: - TrackAreaWidget(DisplayMode mode = DisplayMode::Input, int trackHeight = 0) - : displayMode(mode), fixedTrackHeight(trackHeight) + TrackAreaWidget(DisplayMode mode = DisplayMode::Input, + int trackHeight = 0, + DragOverlayComponent* overlay = nullptr) + : displayMode(mode), fixedTrackHeight(trackHeight), dragOverlay(overlay) { addMouseListener(this, true); } @@ -35,24 +106,6 @@ class TrackAreaWidget : public Component, void paint(Graphics& g) override { g.fillAll(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); - - // Draws drop line for track reordering - if (isDraggingTrack && dragInsertIndex >= 0) - { - int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); - int lineY = dragInsertIndex * trackSlotHeight; - - g.setColour(Colours::white); - g.fillRect(0, lineY - 1, getWidth(), 3); - } - - // Makes the dragged track look visually distinct - if (isDraggingTrack && draggedTrack != nullptr) - { - Rectangle draggedBounds = draggedTrack->getBounds(); - g.setColour(Colours::black.withAlpha(0.3f)); - g.fillRect(draggedBounds); - } } void resized() override @@ -66,8 +119,32 @@ class TrackAreaWidget : public Component, if (getNumTracks() > 0) { + int draggedIndex = isDraggingTrack ? getDraggedTrackIndex() : -1; + + int visualGapIndex = dragInsertIndex; + if (isDraggingTrack && draggedIndex >= 0 && dragInsertIndex > draggedIndex) + visualGapIndex = dragInsertIndex - 1; + + int layoutIndex = 0; + for (auto& m : mediaDisplays) { + // Don't draw dragged track + if (m.get() == draggedTrack && isDraggingTrack) + continue; + + if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) + { + FlexItem gap; + + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + + mainBox.items.add(gap); + } + FlexItem i = FlexItem(*m); if (fixedTrackHeight) @@ -80,6 +157,21 @@ class TrackAreaWidget : public Component, } mainBox.items.add(i.withMargin(marginSize)); + + layoutIndex++; + } + + // If the drag gap is at the end of the list + if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) + { + FlexItem gap; + + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + + mainBox.items.add(gap); } if (fixedTrackHeight) @@ -362,7 +454,6 @@ class TrackAreaWidget : public Component, auto draggedPtr = std::move(*it); mediaDisplays.erase(it); - // Decrement index if moving downward to account for shifting indicies if (newIndex > oldIndex) newIndex--; @@ -406,14 +497,42 @@ class TrackAreaWidget : public Component, int getInsertIndexAtY(int y) { int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + + if (isDraggingTrack && draggedTrack != nullptr) + { + int draggedIndex = getDraggedTrackIndex(); + + int draggedSlotTop = draggedIndex * trackSlotHeight; + + if (draggedIndex >= 0 && y > draggedSlotTop) + y += trackSlotHeight; + } + int index = y / trackSlotHeight; return jlimit(0, getNumTracks(), index); } + int getDraggedTrackIndex() const + { + if (draggedTrack == nullptr) return -1; + + for (int i = 0; i < (int)mediaDisplays.size(); i++) + { + if (mediaDisplays[i].get() == draggedTrack) + return i; + } + + return -1; + } + void mouseDown(const MouseEvent& e) override { if (!isThumbnailWidget()) return; + // Reset drag state at the start of every click + draggedTrack = nullptr; + isDraggingTrack = false; + Component* clicked = e.eventComponent; MediaDisplayComponent* clickedDisplay = nullptr; @@ -431,7 +550,10 @@ class TrackAreaWidget : public Component, if (clickedDisplay) { draggedTrack = clickedDisplay; - isDraggingTrack = false; + + Point mouseInThis = e.getEventRelativeTo(this).getPosition(); + Point trackTopLeft = draggedTrack->getBounds().getTopLeft(); + dragClickOffset = mouseInThis - trackTopLeft; } } @@ -444,23 +566,35 @@ class TrackAreaWidget : public Component, if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) { isDraggingTrack = true; + + // Takes a snapshot of the track at the moment dragging starts + if (dragOverlay != nullptr) + { + Image snapshot = draggedTrack->createComponentSnapshot(draggedTrack->getLocalBounds()); + dragOverlay->startDrag(snapshot, dragClickOffset); + draggedTrack->setVisible(false); + } } if (isDraggingTrack) { - // Only update the drop index while inside the widget + int newInsertIndex = -1; + if (getLocalBounds().contains(posInThis)) + newInsertIndex = getInsertIndexAtY(posInThis.y); + + if (newInsertIndex != dragInsertIndex) { - dragInsertIndex = getInsertIndexAtY(posInThis.y); + dragInsertIndex = newInsertIndex; + resized(); } - else + + // Converts the position to the overlay's coordinate space + if (dragOverlay != nullptr) { - // Hides the indicator line when outside the widget - dragInsertIndex = -1; + Point posInOverlay = dragOverlay->getLocalPoint(this, posInThis); + dragOverlay->updatePosition(posInOverlay); } - - // Updates the drop indicator line - repaint(); } } @@ -468,33 +602,32 @@ class TrackAreaWidget : public Component, { if (!isThumbnailWidget()) return; - // Tracks the release psoition of the mouse + // Tracks the release position of the mouse Point releasePos = e.getEventRelativeTo(this).getPosition(); // Only reorders if the mouse was released inside the widget if (isDraggingTrack && draggedTrack != nullptr && getLocalBounds().contains(releasePos)) { - int currentIndex = -1; - for (int i = 0; i < (int)mediaDisplays.size(); i++) - { - if (mediaDisplays[i].get() == draggedTrack) - { - currentIndex = i; - break; - } - } + int currentIndex = getDraggedTrackIndex(); - if (dragInsertIndex != currentIndex && dragInsertIndex != currentIndex + 1) + if (dragInsertIndex != currentIndex) { reorderTrack(draggedTrack, dragInsertIndex); } } + // Restore the track's appearance and stop the overlay + if (draggedTrack != nullptr) + draggedTrack->setVisible(true); + if (dragOverlay != nullptr) + dragOverlay->stopDrag(); + // Reset the drag state draggedTrack = nullptr; dragInsertIndex = -1; isDraggingTrack = false; - repaint(); + + resized(); } const DisplayMode displayMode; @@ -506,8 +639,10 @@ class TrackAreaWidget : public Component, // For reordering tracks via dragging MediaDisplayComponent* draggedTrack = nullptr; + DragOverlayComponent* dragOverlay = nullptr; int dragInsertIndex = -1; bool isDraggingTrack = false; + Point dragClickOffset { 0, 0 }; std::vector> mediaDisplays; }; From 58987dcb3eebc71d3eac1a49d150fa3f1efc1666 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 30 Mar 2026 11:04:58 -0400 Subject: [PATCH 12/37] Fourth attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 129 ++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index a46c3ba9..7380f0b4 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,6 +4,67 @@ #include +namespace +{ +struct TickScheme +{ + double majorStep; + double minorStep; + int minorCount; +}; + +TickScheme chooseTickScheme(double visibleLength) +{ + static const TickScheme schemes[] = { + { 0.1, 0.02, 5 }, + { 0.5, 0.1, 5 }, + { 1.0, 0.25, 4 }, + { 2.0, 0.5, 4 }, + { 5.0, 1.0, 5 }, + { 15.0, 5.0, 3 }, + { 30.0, 10.0, 3 }, + { 60.0, 15.0, 4 }, + { 120.0, 30.0, 4 }, + { 300.0, 60.0, 5 }, + { 600.0, 120.0, 5 }, + }; + + // Pick the scheme that gives roughly 4–12 major ticks across the visible range + for (const auto& s : schemes) + { + double numMajor = visibleLength / s.majorStep; + if (numMajor >= 2.0 && numMajor <= 15.0) + return s; + } + + // Fallback for very long files + double majorStep = std::pow(10.0, std::floor(std::log10(visibleLength / 5.0))); + majorStep = std::max(0.01, majorStep); + return { majorStep, majorStep / 5.0, 5 }; +} + +String formatTime(double t, double step) +{ + if (t >= 3600.0) + { + int hrs = static_cast(t / 3600.0); + int mins = static_cast(std::fmod(t, 3600.0) / 60.0); + int secs = static_cast(std::fmod(t, 60.0)); + return String(hrs) + "h " + String(mins) + "m " + String(secs) + "s"; + } + if (t >= 60.0) + { + int mins = static_cast(t / 60.0); + int secs = static_cast(std::fmod(t, 60.0)); + return String(mins) + "m " + String(secs) + "s"; + } + if (step >= 1.0) + return String(static_cast(t)) + "s"; + + return String(t, 2) + "s"; +} +} // namespace + void TimeAxisStrip::paint(Graphics& g) { if (owner == nullptr || ! owner->isFileLoaded()) @@ -24,45 +85,57 @@ void TimeAxisStrip::paint(Graphics& g) g.setColour(Colours::darkgrey); g.fillRect(getLocalBounds()); - g.setColour(Colours::lightgrey.withAlpha(0.8f)); + const double visibleLength = visibleRange.getLength(); + const auto scheme = chooseTickScheme(visibleLength); + const double majorStep = scheme.majorStep; + const double minorStep = scheme.minorStep; - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) + const float majorTickTop = 0.0f; + const float majorTickBot = static_cast(h); + const float minorTickTop = static_cast(h) * 0.55f; + const float minorTickBot = static_cast(h); + + // --- Minor ticks --- + g.setColour(Colours::grey.withAlpha(0.5f)); + + const double firstMinor = std::ceil(visibleStart / minorStep) * minorStep; + for (double t = firstMinor; t <= visibleEnd && t <= totalLength; t += minorStep) { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); + // Skip positions that coincide with major ticks + double remainder = std::fmod(t, majorStep); + if (remainder < minorStep * 0.1 || (majorStep - remainder) < minorStep * 0.1) + continue; + + const float x = static_cast((t - visibleStart) * pps); + if (x < 0.0f || x > static_cast(w)) + continue; + + g.drawVerticalLine(static_cast(x), minorTickTop, minorTickBot); } - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); + // --- Major ticks and labels --- + g.setColour(Colours::lightgrey.withAlpha(0.9f)); - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + const int labelH = jmin(13, h - 2); + g.setFont(static_cast(labelH)); + + const double firstMajor = std::ceil(visibleStart / majorStep) * majorStep; + for (double t = firstMajor; t <= visibleEnd && t <= totalLength; t += majorStep) { const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) + if (x < -60.0f || x > static_cast(w) + 60.0f) continue; - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; + g.drawVerticalLine(static_cast(x), majorTickTop, majorTickBot); + String label = formatTime(t, majorStep); g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); + static_cast(x) + 3, + 0, + jmin(90, w - static_cast(x)), + h, + Justification::centredLeft, + true); } } From 4e5fd2e8780c813973bcf369b858d1ae9f12a97c Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 30 Mar 2026 11:47:12 -0400 Subject: [PATCH 13/37] Fifth attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7380f0b4..e4508b41 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1008,9 +1008,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (pps <= 0.0f) return; - // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. - const float minPps = 20.0f; - float maxPps = static_cast(mediaWidth / totalLength); + const double minVisibleSeconds = 5.0; + const float minPps = static_cast(mediaWidth / totalLength); + float maxPps = static_cast(mediaWidth / minVisibleSeconds); if (maxPps < minPps) maxPps = minPps; From 557ecd8f02dfc7f482d8f3805849d61aef6ad332 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Wed, 1 Apr 2026 11:29:10 -0400 Subject: [PATCH 14/37] Sixth attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 16 ++++++++++++---- src/media/MediaDisplayComponent.h | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index e4508b41..7b351529 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -996,18 +996,19 @@ void MediaDisplayComponent::horizontalMove(double deltaT) updateVisibleRange({ newStart, newStart + visibleLength }); } -void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) +bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { const float mediaWidth = getMediaWidth(); const double totalLength = getTotalLengthInSecs(); if (mediaWidth <= 0.0f || totalLength <= 0.0) - return; + return false; const float pps = getPixelsPerSecond(); if (pps <= 0.0f) - return; + return false; + // minPps = zoomed all the way out; maxPps = zoomed all the way in (~5 seconds visible). const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); @@ -1017,6 +1018,10 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); + // If pps didn't change, zoom has hit a limit + if (std::abs(newPps - pps) < 0.01f) + return false; + double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); if (newVisibleLength > totalLength) newVisibleLength = totalLength; @@ -1027,6 +1032,7 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); + return true; } void MediaDisplayComponent::scrollBarMoved(ScrollBar* scrollBarThatHasMoved, @@ -1070,7 +1076,9 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe } else { - horizontalZoom(static_cast(wheel.deltaY), scrollTime); + bool zoomed = horizontalZoom(static_cast(wheel.deltaY), scrollTime); + if (! zoomed) + horizontalMove(static_cast(wheel.deltaY)); } } else diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 225c9698..154b698f 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -216,7 +216,7 @@ class MediaDisplayComponent : public Component, int correctMediaXBounds(float mX, float width); void horizontalMove(double deltaT); - void horizontalZoom(double deltaZoom, double scrollPosT); + bool horizontalZoom(double deltaZoom, double scrollPosT); void scrollBarMoved(ScrollBar* scrollBarThatHasMoved, double scrollBarRangeStart) override; From 12ece4036eeddf8eb239b38be5fe808bcc82fe61 Mon Sep 17 00:00:00 2001 From: ella-granger Date: Fri, 3 Apr 2026 09:50:25 -0400 Subject: [PATCH 15/37] Update TIGER document and the hosting document. --- website/content/pyharp_docs/example.md | 130 ++++++++++++++++++------- website/content/pyharp_docs/host.md | 22 ++--- 2 files changed, 104 insertions(+), 48 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index 97b14144..a1c212e3 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -10,7 +10,13 @@ To get started, we'll install [the code](https://github.com/JusperLee/TIGER) nee ``` git clone https://github.com/JusperLee/TIGER.git -cd TIGER && pip install -r requirements.txt +cd TIGER +``` + +In the `requirements.txt`, remove `triton==3.1.0` to avoid dependency issues. Then pip install: + +``` +pip install -r requirements.txt ``` Next, we'll look at how TIGER processes audio files to separate speech. From [`TIGER/inference_speech.py`](https://github.com/JusperLee/TIGER/blob/main/inference_speech.py), we can see that TIGER requires audio files to be resampled to 16kHz and formatted as a PyTorch tensor of shape `[1, C, T]` where `C` is the number of channels and `T` is the number of audio samples; then, the model can be applied in one line of code: @@ -21,11 +27,12 @@ Next, we'll look at how TIGER processes audio files to separate speech. From [`T ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] ``` -Here, `num_spk` is the estimated number of speakers in the recording. For our example application, we'll only return audio of the first speaker. +Here, `num_spk` is the estimated number of speakers in the recording. The TIGER model sets `num_spk` to two. We are going to take both outputs from the model: ``` # Select audio of first speaker -output = ests_speech[:, 0, :] # Expected output shape: [1, T] +output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] +output_2 = ests_speech[:, 1, :] # Expected output shape: [1, T] ``` And that's it -- to deploy TIGER in HARP, we need to write a Gradio application for processing audio files like this, sprinkle in some PyHARP functions, and then run our application in the background to handle any audio sent by HARP. @@ -42,17 +49,28 @@ Now that we have a grip on how to run TIGER, let's revisit the [elements](/conte name="TIGER", description="The TIGER speech separation model of Xu et al. (https://arxiv.org/abs/2410.01469)", author="Your name", - tags=["example", "speech separation"], - midi_in=False, - midi_out=False + tags=["example", "speech separation"] ) ``` -* We need to define a list of __Gradio interactive components__ specifying the interface. In our case, because we only require audio input/output, we'll leave this list empty: +* We need to define a list of __Gradio interactive components__ specifying the interface. In our case, we need one audio input and two audio outputs: ``` # Define Gradio Components - components = [] - ``` -* We need to define a __processing function__ for handling file input and output. This function will load an audio file from a given path, format it for TIGER and run separation as discussed above, save the output to a new file, and return the path of this output file. + # Input + input_audio = gr.Audio( + label="Input Audio", + type="filepath", + sources=["upload", "microphone"] + ) + # Outputs + output_audio_1 = gr.Audio( + type="filepath", + label="Output Audio 1" + ) + output_audio_2 = gr.Audio( + type="filepath", + label="Output Audio 2" + ) +* Then we define a __processing function__ for handling file input and output. This function will load an audio file from a given path, format it for TIGER and run separation as discussed above, save the outputs to new files, and return the paths of the output files. Note that the parameters of the function correspond to the input components and the return values correspond to the output components. ``` # Define the processing function @torch.inference_mode() @@ -61,31 +79,49 @@ Now that we have a grip on how to run TIGER, let's revisit the [elements](/conte # By default, load audio as a Descript-AudioTools `AudioSignal` object sig = load_audio(input_audio_path) # Wraps a tensor of shape [1, C, T] - audio_input = sig.resample(16_000).audio_data # Tensor of shape [1, C, T] + audio_input = sig.resample(16_000).audio_data.to(device) # Tensor of shape [1, C, T] # Apply TIGER ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] - output = ests_speech[:, 0, :] # Expected output shape: [1, T] + output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] + output_2 = ests_speech[:, 1, :] - sig.audio_data = output - output_audio_path = save_audio(sig) + # Create two new audio + sig_1 = AudioSignal(output_1.cpu().numpy().astype("float32"), sample_rate=16000) + sig_2 = AudioSignal(output_2.cpu().numpy().astype("float32"), sample_rate=16000) - # Because this application does not apply labels to audio, return an empty - # label list - output_labels = LabelList() + # save to files + output_dir = Path("_outputs").resolve() + output_dir.mkdir(exist_ok=True, parents=True) - return output_audio_path, output_labels + output_audio_path_1 = output_dir / "sig1.wav" + output_audio_path_2 = output_dir / "sig2.wav" + + save_audio(sig_1, output_audio_path_1) + save_audio(sig_2, output_audio_path_2) + + return output_audio_path_1, output_audio_path_2 + ``` +* After we define the __model card__, __I/O components__ and the __processing function__, we aggregate them into an endpoint: + ``` + # Build Endpoint + app = build_endpoint( + model_card=model_card, + input_components = [input_audio], + output_components = [output_audio_1, output_audio_2], + process_fn=process_fn + ) ``` -Adding imports and some model-loading code, our final `app.py` should look like this: +Adding imports and model-loading code, our final `app.py` should look like this: ``` from pyharp import * +from audiotools import AudioSignal -import yaml import os +from pathlib import Path import gradio as gr -import torchaudio import torch import look2hear.models @@ -107,9 +143,7 @@ model_card = ModelCard( name="TIGER", description="The TIGER speech separation model of Xu et al. (https://arxiv.org/abs/2410.01469)", author="Your name", - tags=["example", "speech separation"], - midi_in=False, - midi_out=False + tags=["example", "speech separation"] ) # Define the processing function @@ -123,25 +157,47 @@ def process_fn(input_audio_path): # Apply TIGER ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] - output = ests_speech[:, :1, :] # Expected output shape: [1, 1, T] - - sig.audio_data = output - output_audio_path = save_audio(sig.cpu()) # Ensure our output signal is on CPU + output_1 = ests_speech[:, 0, :] # Expected output shape: [1, 1, T] + output_2 = ests_speech[:, 1, :] + + sig_1 = AudioSignal(output_1.cpu().numpy().astype("float32"), sample_rate=16000) + sig_2 = AudioSignal(output_2.cpu().numpy().astype("float32"), sample_rate=16000) + + output_dir = Path("_outputs").resolve() + output_dir.mkdir(exist_ok=True, parents=True) + + output_audio_path_1 = output_dir / "sig1.wav" + output_audio_path_2 = output_dir / "sig2.wav" - # Because this application does not apply labels to audio, return an empty - # label list - output_labels = LabelList() + save_audio(sig_1, output_audio_path_1) + save_audio(sig_2, output_audio_path_2) - return output_audio_path, output_labels + return output_audio_path_1, output_audio_path_2 # Build Gradio endpoint with gr.Blocks() as demo: # Define Gradio Components - components = [] - - app = build_endpoint(model_card=model_card, - components=components, - process_fn=process_fn) + input_audio = gr.Audio( + label="Input Audio", + type="filepath", + sources=["upload", "microphone"] + ) + output_audio_1 = gr.Audio( + type="filepath", + label="Output Audio 1" + ) + output_audio_2 = gr.Audio( + type="filepath", + label="Output Audio 2" + ) + # output_labels = gr.JSON(label="Separation") + + app = build_endpoint( + model_card=model_card, + input_components = [input_audio], + output_components = [output_audio_1, output_audio_2], # , output_labels], + process_fn=process_fn + ) demo.queue() demo.launch(share=True, show_error=True) diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 270ed0c7..26a970d7 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -3,7 +3,7 @@ Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: ## Gradio Endpoints -Gradio endpoints are the most convenient solution for a PyHARP application. It will allow you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, which is free from additional charges. +Gradio endpoints are the most convenient solution for a PyHARP application. It will give you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, at no additional charge. 1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. 2. Choose the Blank template. @@ -25,7 +25,7 @@ git push -u origin main - `requirements.txt` - This is the place where you put all your required **pip** packages. You should put in the latest PyHARP: + This is the place where you put all the **pip** packages. You should also put in the latest PyHARP: ``` git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 ``` @@ -33,12 +33,12 @@ git push -u origin main - `packages.txt` - This is the place to put **apt-get install** debian packages, which are required by some models. + This is there to put **apt-get install** Debian package list, which are required by some models. ## Docker Endpoints -PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or prior version, which may cause issues when upgrading python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. +PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or a prior version, which may cause issues when upgrading Python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. -Using Docker endpoints can help you fix this issue. The Docker will allow you to customize the deployment, which will make room for potential patches and fixes. However, the ZeroGPU hardware will not be available for Docker spaces and you need to pay for the GPU usage. +Using Docker endpoints can help you fix this issue. Docker will allow you to customize deployments, making room for potential patches and fixes. However, the ZeroGPU hardware will not be available in Docker spaces, and you will need to pay for GPU usage. 1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. 2. Choose the Blank template. @@ -59,7 +59,7 @@ git push -u origin main - `requirements.txt` and `packages.txt` - Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile`, which will be introduced in the next step. + Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile` introduced in the next step. For `requirements.txt`, you should include: ``` @@ -69,7 +69,7 @@ git push -u origin main - `Dockerfile` - Here is an example `Dockerfile` configuration, which will install the general packages and fix the `madmom` package. + Here is an example `Dockerfile` configuration that installs general packages and fixes the `madmom` package. ```Docker FROM python:3.10-slim # Set python version @@ -107,11 +107,11 @@ git push -u origin main ``` --- -Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed by the two methods will have the same UI and functionality. +Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed using two methods will have the same UI and functionality. -Here are a few tips and best-practices when dealing with HuggingFace Spaces: -- Spaces operate based off of the files in the `main` branch +Here are a few tips and best practices when dealing with HuggingFace Spaces: +- Spaces operate based on the files in the `main` branch - An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces - A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) -For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). +For more information, please refer to the offical Hugging Face document on [Spaces](https://huggingface.co/docs/hub/spaces). From 194565ff171acbf113e83c24f2265016cb98cdef Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Fri, 3 Apr 2026 14:45:58 -0400 Subject: [PATCH 16/37] Add header to copyWindows --- src/utils/Copy/copyWindows.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/Copy/copyWindows.cpp index 2537fc82..2ab58b61 100644 --- a/src/utils/Copy/copyWindows.cpp +++ b/src/utils/Copy/copyWindows.cpp @@ -1,4 +1,5 @@ #include +#include #include "copy.h" void copyFileToClipboard (const juce::File& file) @@ -28,4 +29,4 @@ void copyFileToClipboard (const juce::File& file) SetClipboardData(CF_HDROP, hMem); CloseClipboard(); -} \ No newline at end of file +} From 81d1606a8dc0e52e937dc15eef36d9345aaf7016 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Fri, 3 Apr 2026 14:48:01 -0400 Subject: [PATCH 17/37] Add save AND copy buttons to GUI Also Increased Size of Buttons --- src/media/MediaDisplayComponent.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 89b78c52..c8a0e539 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -255,16 +255,18 @@ void MediaDisplayComponent::resized() // Add buttons to flex with equal height buttonsFlexBox.items.add( - FlexItem(playStopButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(playStopButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); if (isInputTrack()) { buttonsFlexBox.items.add( - FlexItem(chooseFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(chooseFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); } if (isOutputTrack()) { buttonsFlexBox.items.add( - FlexItem(copyFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(saveFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); + buttonsFlexBox.items.add( + FlexItem(copyFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); } buttonsFlexBox.performLayout(buttonsComponent.getBounds()); From 7ee2d9514fa111a799fe701ba0b09afbff4b0895 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 09:59:12 -0400 Subject: [PATCH 18/37] Working copy/paste on linux using xclip and reorganization. --- CMakeLists.txt | 6 ++++-- src/media/MediaDisplayComponent.cpp | 3 ++- src/utils/Copy/copy.h | 4 ---- src/utils/Copy/copyLinux.cpp | 17 ---------------- src/utils/Interface.h | 4 +++- src/utils/copy/CopyLinux.cpp | 20 +++++++++++++++++++ .../{Copy/copyMac.mm => copy/CopyMacOS.mm} | 8 +++++++- .../copyWindows.cpp => copy/CopyWindows.cpp} | 19 ++++++++++++------ 8 files changed, 49 insertions(+), 32 deletions(-) delete mode 100644 src/utils/Copy/copy.h delete mode 100644 src/utils/Copy/copyLinux.cpp create mode 100644 src/utils/copy/CopyLinux.cpp rename src/utils/{Copy/copyMac.mm => copy/CopyMacOS.mm} (80%) rename src/utils/{Copy/copyWindows.cpp => copy/CopyWindows.cpp} (60%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b778a4eb..390be5fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,9 +133,11 @@ target_sources(${PROJECT_NAME} ) if(APPLE) -target_sources(HARP PRIVATE src/utils/Copy/copyMac.mm) + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyMacOS.mm) elseif(WIN32) -target_sources(HARP PRIVATE src/utils/Copy/copyWindows.cpp) + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyWindows.cpp) +else() + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyLinux.cpp) endif() # `target_compile_definitions` adds some preprocessor definitions to our target. In a Projucer diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index c8a0e539..95b7ce5b 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,7 +1,8 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -#include "../utils/Copy/copy.h" + +#include "../utils/Interface.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} diff --git a/src/utils/Copy/copy.h b/src/utils/Copy/copy.h deleted file mode 100644 index d23614b4..00000000 --- a/src/utils/Copy/copy.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once -#include - -void copyFileToClipboard (const juce::File& file); \ No newline at end of file diff --git a/src/utils/Copy/copyLinux.cpp b/src/utils/Copy/copyLinux.cpp deleted file mode 100644 index 685436b4..00000000 --- a/src/utils/Copy/copyLinux.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include "copy.h" - -void copyFileToClipboard (const juce::File& file) -{ - if (! file.existsAsFile()) - return; - - const juce::String uri = "file://" + file.getFullPathName(); - - GtkClipboard* clipboard = - gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); - - gtk_clipboard_set_text(clipboard, - uri.toRawUTF8(), - -1); -} \ No newline at end of file diff --git a/src/utils/Interface.h b/src/utils/Interface.h index a44effa1..620c9f25 100644 --- a/src/utils/Interface.h +++ b/src/utils/Interface.h @@ -1,7 +1,7 @@ /** * @file Interface.h * @brief Simple helper functions for interface. - * @author hugofloresgarcia + * @author hugofloresgarcia, JEYuhas */ #pragma once @@ -20,3 +20,5 @@ inline Colour getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour uiCo return fallback; } + +void copyFileToClipboard(const File& file); diff --git a/src/utils/copy/CopyLinux.cpp b/src/utils/copy/CopyLinux.cpp new file mode 100644 index 00000000..aab8d157 --- /dev/null +++ b/src/utils/copy/CopyLinux.cpp @@ -0,0 +1,20 @@ +/** + * @file CopyLinux.cpp + * @brief Copy file path to clipboard on Linux. + * @author JEYuhas, cwitkowitz + */ + +#include "../Interface.h" + +void copyFileToClipboard(const File& file) +{ + if (! file.existsAsFile()) + return; + + String cmd; + + cmd << "printf \"file://" << file.getFullPathName() + << "\\n\" | xclip -selection clipboard -t text/uri-list"; + + std::system(cmd.toRawUTF8()); +} diff --git a/src/utils/Copy/copyMac.mm b/src/utils/copy/CopyMacOS.mm similarity index 80% rename from src/utils/Copy/copyMac.mm rename to src/utils/copy/CopyMacOS.mm index 63058284..d501d6c6 100644 --- a/src/utils/Copy/copyMac.mm +++ b/src/utils/copy/CopyMacOS.mm @@ -1,6 +1,12 @@ +/** + * @file CopyMacOS.mm + * @brief Copy file path to clipboard on MacOS. + * @author JEYuhas + */ + #import -#include "copy.h" +#include "../Interface.h" void copyFileToClipboard (const juce::File& file) { diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/copy/CopyWindows.cpp similarity index 60% rename from src/utils/Copy/copyWindows.cpp rename to src/utils/copy/CopyWindows.cpp index 2ab58b61..973def2b 100644 --- a/src/utils/Copy/copyWindows.cpp +++ b/src/utils/copy/CopyWindows.cpp @@ -1,13 +1,20 @@ -#include +/** + * @file CopyWindows.cpp + * @brief Copy file path to clipboard on Windows. + * @author JEYuhas + */ + #include -#include "copy.h" +#include + +#include "../Interface.h" -void copyFileToClipboard (const juce::File& file) +void copyFileToClipboard(const juce::File& file) { if (! file.existsAsFile()) return; - if (!OpenClipboard(nullptr)) + if (! OpenClipboard(nullptr)) return; EmptyClipboard(); @@ -17,12 +24,12 @@ void copyFileToClipboard (const juce::File& file) size_t size = sizeof(DROPFILES) + (path.size() + 2) * sizeof(wchar_t); HGLOBAL hMem = GlobalAlloc(GHND, size); - DROPFILES* df = (DROPFILES*)GlobalLock(hMem); + DROPFILES* df = (DROPFILES*) GlobalLock(hMem); df->pFiles = sizeof(DROPFILES); df->fWide = TRUE; - wchar_t* data = (wchar_t*)((BYTE*)df + sizeof(DROPFILES)); + wchar_t* data = (wchar_t*) ((BYTE*) df + sizeof(DROPFILES)); wcscpy(data, path.c_str()); GlobalUnlock(hMem); From 998c804b968729b455017a2496312acdd82ca6c7 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 10:08:15 -0400 Subject: [PATCH 19/37] Copy icon, removed non-loaded file status message, and formatting. --- CMakeLists.txt | 2 -- src/media/MediaDisplayComponent.cpp | 43 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 2 -- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 390be5fa..09de67b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,8 +64,6 @@ juce_generate_juce_header(${PROJECT_NAME}) # Finally, we supply a list of source files that will be built into the target. This is a standard # CMake command. - - target_sources(${PROJECT_NAME} PRIVATE src/Main.cpp diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 95b7ce5b..f386fef8 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -13,7 +13,6 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW deviceManager.initialise(0, 2, nullptr, true, {}, nullptr); deviceManager.addAudioCallback(&sourcePlayer); - sourcePlayer.setSource(&transportSource); @@ -108,18 +107,16 @@ void MediaDisplayComponent::initializeButtons() [this] { copyFileCallback(); }, MultiButton::DrawingMode::IconOnly, Colours::lightblue, - fontawesome::Save }; + fontawesome::Copy }; // Mode when there is nothing to copy copyFileButtonInactiveInfo = MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", [this] {}, MultiButton::DrawingMode::IconOnly, - Colours::lightgrey, fontawesome::Save }; + Colours::lightgrey, fontawesome::Copy }; copyFileButton.addMode(copyFileButtonActiveInfo); copyFileButton.addMode(copyFileButtonInactiveInfo); headerComponent.addAndMakeVisible(copyFileButton); - - resetButtonState(); } @@ -765,30 +762,28 @@ void MediaDisplayComponent::saveFileCallback() } } - void MediaDisplayComponent::copyFileCallback() { - if (!isFileLoaded()) + if (isFileLoaded()) { - if (statusMessage != nullptr) - statusMessage->setMessage("No file loaded."); - return; - } - - // If you're copying the original file path: - juce::File file = getOriginalFilePath().getLocalFile(); + File file = getOriginalFilePath().getLocalFile(); - if (file.exists()) - { - copyFileToClipboard(file); + if (file.exists()) + { + copyFileToClipboard(file); - if (statusMessage != nullptr) - statusMessage->setMessage("File path copied to clipboard."); - } - else - { - if (statusMessage != nullptr) - statusMessage->setMessage("File does not exist."); + if (statusMessage != nullptr) + { + statusMessage->setMessage("File copied to clipboard."); + } + } + else + { + if (statusMessage != nullptr) + { + statusMessage->setMessage("Failed to copy file to clipboard."); + } + } } } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 9cf6a55e..6e68ba47 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -109,7 +109,6 @@ class MediaDisplayComponent : public Component, bool isDuplicateFile(const URL& fileParth); void saveFileCallback(); - void copyFileCallback(); virtual double getTotalLengthInSecs() = 0; @@ -238,7 +237,6 @@ class MediaDisplayComponent : public Component, // Media + overhead panel (if any) Component mediaAreaContainer; - // Header sub-components Label trackNameLabel; MultiButton playStopButton; From 600e3a7f9d792ae3c8493bf2e211f0a0992b2b18 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 10:56:06 -0400 Subject: [PATCH 20/37] More general X11 copy implementation for linux. --- CMakeLists.txt | 7 +- src/utils/copy/CopyLinux.cpp | 163 ++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09de67b4..a2669bef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,7 +134,7 @@ if(APPLE) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyMacOS.mm) elseif(WIN32) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyWindows.cpp) -else() +elseif(LINUX) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyLinux.cpp) endif() @@ -204,6 +204,11 @@ target_link_libraries(${PROJECT_NAME} juce::juce_recommended_warning_flags ) +if(LINUX) + find_package(X11 REQUIRED) + target_link_libraries(${PROJECT_NAME} PRIVATE X11::X11) +endif() + # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\msvcp140.dll # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140_1.dll # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140.dll diff --git a/src/utils/copy/CopyLinux.cpp b/src/utils/copy/CopyLinux.cpp index aab8d157..ba0d1542 100644 --- a/src/utils/copy/CopyLinux.cpp +++ b/src/utils/copy/CopyLinux.cpp @@ -4,17 +4,172 @@ * @author JEYuhas, cwitkowitz */ +extern "C" +{ +#include +#include +} + +#include +#include + #include "../Interface.h" +namespace +{ +struct Clipboard +{ + Display* display = nullptr; + Window window = 0; + + Atom clipboard; + Atom targets; + Atom uriList; + Atom textPlain; + Atom utf8; + Atom gnome; + + std::string dataUri; + std::string dataPlain; + std::string dataGnome; + + std::atomic running { true }; +}; + +Clipboard* clipboardState = nullptr; + +void handleRequest(XEvent& e) +{ + auto* req = &e.xselectionrequest; + + XEvent res {}; + res.xselection.type = SelectionNotify; + res.xselection.display = req->display; + res.xselection.requestor = req->requestor; + res.xselection.selection = req->selection; + res.xselection.target = req->target; + res.xselection.time = req->time; + res.xselection.property = None; + + if (req->target == clipboardState->targets) + { + Atom list[] = { clipboardState->targets, + clipboardState->uriList, + clipboardState->textPlain, + clipboardState->utf8, + clipboardState->gnome }; + + XChangeProperty(req->display, + req->requestor, + req->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*) list, + 5); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->uriList) + { + XChangeProperty(req->display, + req->requestor, + req->property, + clipboardState->uriList, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataUri.c_str(), + (int) clipboardState->dataUri.size()); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->textPlain || req->target == clipboardState->utf8) + { + XChangeProperty(req->display, + req->requestor, + req->property, + req->target, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataPlain.c_str(), + (int) clipboardState->dataPlain.size()); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->gnome) + { + XChangeProperty(req->display, + req->requestor, + req->property, + clipboardState->gnome, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataGnome.c_str(), + (int) clipboardState->dataGnome.size()); + + res.xselection.property = req->property; + } + + XSendEvent(req->display, req->requestor, False, 0, &res); + XFlush(req->display); +} + +void runLoop() +{ + while (clipboardState->running) + { + while (XPending(clipboardState->display)) + { + XEvent e; + XNextEvent(clipboardState->display, &e); + + if (e.type == SelectionRequest) + handleRequest(e); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} +} // namespace + void copyFileToClipboard(const File& file) { if (! file.existsAsFile()) return; - String cmd; + if (clipboardState) + { + clipboardState->running = false; + delete clipboardState; + clipboardState = nullptr; + } + + clipboardState = new Clipboard(); + + clipboardState->display = XOpenDisplay(nullptr); + if (! clipboardState->display) + return; + + clipboardState->window = XCreateSimpleWindow( + clipboardState->display, DefaultRootWindow(clipboardState->display), 0, 0, 1, 1, 0, 0, 0); + + clipboardState->clipboard = XInternAtom(clipboardState->display, "CLIPBOARD", False); + clipboardState->targets = XInternAtom(clipboardState->display, "TARGETS", False); + clipboardState->uriList = XInternAtom(clipboardState->display, "text/uri-list", False); + clipboardState->textPlain = XInternAtom(clipboardState->display, "text/plain", False); + clipboardState->utf8 = XInternAtom(clipboardState->display, "UTF8_STRING", False); + clipboardState->gnome = + XInternAtom(clipboardState->display, "x-special/gnome-copied-files", False); + + std::string path = file.getFullPathName().toStdString(); + std::string uri = "file://" + path; + + clipboardState->dataUri = uri + "\r\n"; + clipboardState->dataPlain = path; + clipboardState->dataGnome = "copy\n" + uri + "\n"; - cmd << "printf \"file://" << file.getFullPathName() - << "\\n\" | xclip -selection clipboard -t text/uri-list"; + XSetSelectionOwner( + clipboardState->display, clipboardState->clipboard, clipboardState->window, CurrentTime); - std::system(cmd.toRawUTF8()); + std::thread(runLoop).detach(); } From aa80e334843b03825b9651c58d860c15fa954e1f Mon Sep 17 00:00:00 2001 From: Derek LLanes Date: Wed, 8 Apr 2026 12:42:50 -0400 Subject: [PATCH 21/37] Implement Ticket 377 drum MIDI visualization --- src/media/MidiDisplayComponent.cpp | 10 ++--- src/media/pianoroll/NoteGridComponent.cpp | 46 ++++++++++++++++------- src/media/pianoroll/NoteGridComponent.hpp | 6 ++- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/media/MidiDisplayComponent.cpp b/src/media/MidiDisplayComponent.cpp index 2c13d614..8c8a7cb4 100644 --- a/src/media/MidiDisplayComponent.cpp +++ b/src/media/MidiDisplayComponent.cpp @@ -96,10 +96,9 @@ void MidiDisplayComponent::loadMediaFile(const URL& filePath) int channel = midiMessage.getChannel(); - if (channel == 10) - { - // TODO - handle drums channel - } + // Handle drums channel + bool isDrum = (channel == 10); + if (midiMessage.isProgramChange()) { @@ -137,7 +136,8 @@ void MidiDisplayComponent::loadMediaFile(const URL& filePath) startTime, duration, static_cast(velocity), - static_cast(instrument)); + static_cast(instrument), + isDrum); pianoRoll.insertNote(n); } } diff --git a/src/media/pianoroll/NoteGridComponent.cpp b/src/media/pianoroll/NoteGridComponent.cpp index 3fc574c7..c7e15637 100644 --- a/src/media/pianoroll/NoteGridComponent.cpp +++ b/src/media/pianoroll/NoteGridComponent.cpp @@ -27,26 +27,46 @@ void NoteGridComponent::paint(Graphics& g) Rectangle bounds(noteXPos, noteYPos, jmax(3.0f, noteWidth), noteHeight - 1.0f); + float maxDrumWidth = 0.01f * getParentComponent()->getWidth(); + float hue = static_cast(n->instrument) / 127.0f; Colour color = Colour::fromHSV(hue, 1.0f, 1.0f, 1.0f).brighter(); // Note fill - g.setColour(color.withAlpha(0.75f)); - g.fillRect(bounds.toNearestInt()); + + if (n->isDrum) + { + g.setColour(color); + + const float thickness = jmax(2.0f, noteHeight * 0.18f); + const float pad = jmax(1.0f, thickness * 0.5f); - if ((noteWidth >= 5) & (noteHeight >= 8)) + const float x1 = noteXPos - maxDrumWidth/2; + const float y1 = bounds.getY() + pad; + const float x2 = noteXPos + maxDrumWidth/2; + const float y2 = bounds.getBottom() - pad; + + g.drawLine(x1, y1, x2, y2, thickness); + g.drawLine(x2, y1, x1, y2, thickness); + } + else { - const float maxVelocityWidth = static_cast(noteWidth - 4); - const float verticalOffset = static_cast(noteHeight) * 0.5f - 2.0f; - const float velocityHeight = 4.0f; - - // Velocity fill - g.setColour(color.brighter()); - g.fillRect(bounds.translated(2, verticalOffset) - .withWidth(maxVelocityWidth * n->velocity / 127.0f) - .withHeight(velocityHeight) - .toNearestInt()); + g.setColour(color.withAlpha(0.75f)); + g.fillRect(bounds.toNearestInt()); + + if ((noteWidth >= 5) && (noteHeight >= 8)) + { + const float maxVelocityWidth = static_cast(noteWidth - 4); + const float verticalOffset = static_cast(noteHeight) * 0.5f - 2.0f; + const float velocityHeight = 4.0f; + + g.setColour(color.brighter()); + g.fillRect(bounds.translated(2, verticalOffset) + .withWidth(maxVelocityWidth * n->velocity / 127.0f) + .withHeight(velocityHeight) + .toNearestInt()); + } } } } diff --git a/src/media/pianoroll/NoteGridComponent.hpp b/src/media/pianoroll/NoteGridComponent.hpp index 7a3eb19a..d4f8e13e 100644 --- a/src/media/pianoroll/NoteGridComponent.hpp +++ b/src/media/pianoroll/NoteGridComponent.hpp @@ -15,8 +15,8 @@ using namespace juce; struct MidiNote { public: - MidiNote(unsigned char n, double s, double d, unsigned char v, unsigned char i = 0) - : noteNumber(n), startTime(s), duration(d), velocity(v), instrument(i) + MidiNote(unsigned char n, double s, double d, unsigned char v, unsigned char i = 0, bool drum = false) + : noteNumber(n), startTime(s), duration(d), velocity(v), instrument(i), isDrum(drum) { } @@ -27,6 +27,7 @@ struct MidiNote duration = other.duration; velocity = other.velocity; instrument = other.instrument; + isDrum = other.isDrum; } unsigned char noteNumber; @@ -34,6 +35,7 @@ struct MidiNote double duration; unsigned char velocity; unsigned char instrument; + bool isDrum; }; class NoteGridComponent : public KeyboardComponent From 53fa7ef01e69d189ef1a9ccc35e0053ec7bd0e7e Mon Sep 17 00:00:00 2001 From: Richard Zhu Date: Wed, 8 Apr 2026 23:11:16 -0400 Subject: [PATCH 22/37] Add audio format conversion when sending to DAW When the selected file's format doesn't match the DAW-linked file's format, attempt conversion using JUCE's AudioFormatManager rather than showing an error. Supports WAV, AIFF (.aif/.aiff), and FLAC. --- src/widgets/MediaClipboardWidget.h | 128 ++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 3753cbfd..0004af60 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,7 +133,10 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } + Rectangle getClipboardControlsBounds() const + { + return controlsComponent.getBounds().expanded(2); + } Rectangle getClipboardNameBoxBounds() const { @@ -273,20 +276,49 @@ class MediaClipboardWidget : public Component, public ChangeListener + validExtensions.joinIntoString(", ") + ".", "OK"); }*/ - + // RZ if (originalFile.getFileExtension() != selectedFile.getFileExtension()) { - AlertWindow::showMessageBoxAsync( - AlertWindow::WarningIcon, - "File Type Mismatch", - "Cannot overwrite file of type \"" - + originalFile.getFileExtension() - + "\" with file of type \"" - + selectedFile.getFileExtension() + "\".", - "OK"); + File tempFile = selectedFile.getSiblingFile( + selectedFile.getFileNameWithoutExtension() + + "_converted" + originalFile.getFileExtension()); - // TODO - perform conversion if possible + if (convertAudioFile(selectedFile, tempFile)) + { + if (tempFile.copyFileTo(originalFile)) + { + DBG_AND_LOG( + "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote " + << originalFile.getFullPathName() << " with " + << selectedFile.getFullPathName() << "."); + + tempFile.deleteFile(); + + linkedDisplays[selectedIndex]->initializeDisplay( + URL(originalFile)); + removeSelectionCallback(); + linkedDisplays[selectedIndex]->selectTrack(); + } + else + { + tempFile.deleteFile(); + DBG_AND_LOG( + "MediaClipboardWidget::sendToDAWCallback: Conversion succeeded " + "but failed to copy to " + << originalFile.getFullPathName()); + } + } + else + { + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, + "File Type Mismatch", + "Cannot convert file of type \"" + + selectedFile.getFileExtension() + "\" to \"" + + originalFile.getFileExtension() + "\".", + "OK"); + } } else { @@ -544,6 +576,80 @@ class MediaClipboardWidget : public Component, public ChangeListener } } + // RZ edit + bool convertAudioFile(const File& source, const File& target) + { + AudioFormatManager formatManager; + formatManager.registerBasicFormats(); + + std::unique_ptr reader(formatManager.createReaderFor(source)); + + if (! reader) + { + DBG_AND_LOG( + "convertAudioFile: Could not read source file: " << source.getFullPathName()); + return false; + } + + String ext = target.getFileExtension().toLowerCase(); + AudioFormat* format = nullptr; + + if (ext == ".wav") + format = formatManager.findFormatForFileExtension("wav"); + else if (ext == ".aiff" || ext == ".aif") + format = formatManager.findFormatForFileExtension("aiff"); + else if (ext == ".flac") + format = formatManager.findFormatForFileExtension("flac"); + else + { + DBG_AND_LOG("convertAudioFile: Unsupported target format: " << ext); + return false; + } + + if (! format) + { + DBG_AND_LOG("convertAudioFile: Format not available: " << ext); + return false; + } + + target.deleteFile(); + auto outputStream = target.createOutputStream(); + if (! outputStream) + { + DBG_AND_LOG( + "convertAudioFile: Could not create output file: " << target.getFullPathName()); + return false; + } + + std::unique_ptr writer(format->createWriterFor( + outputStream.release(), reader->sampleRate, reader->numChannels, 16, {}, 0)); + + if (! writer) + { + DBG_AND_LOG( + "convertAudioFile: Could not create writer for: " << target.getFullPathName()); + return false; + } + + const int blockSize = 4096; + AudioBuffer buffer((int) reader->numChannels, blockSize); + int64 totalFrames = (int64) reader->lengthInSamples; + int64 position = 0; + + while (position < totalFrames) + { + int64 framesToRead = jmin((int64) blockSize, totalFrames - position); + reader->read(&buffer, 0, (int) framesToRead, position, true, true); + writer->writeFromAudioSampleBuffer(buffer, 0, (int) framesToRead); + position += framesToRead; + } + + DBG_AND_LOG("convertAudioFile: Converted " << source.getFullPathName() << " -> " + << target.getFullPathName()); + + return true; + } + void resetState() { selectionTextBox.clear(); From 103cebc1df059fbb88df753af17e122e9f3956cf Mon Sep 17 00:00:00 2001 From: Richard Zhu Date: Wed, 8 Apr 2026 23:20:10 -0400 Subject: [PATCH 23/37] Add audio format conversion when sending to DAW When the selected file's format doesn't match the DAW-linked file's format, attempt conversion using JUCE's AudioFormatManager rather than showing an error. Supports WAV, AIFF (.aif/.aiff), and FLAC. --- src/widgets/MediaClipboardWidget.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 0004af60..29541ceb 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,10 +133,7 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const - { - return controlsComponent.getBounds().expanded(2); - } + Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } Rectangle getClipboardNameBoxBounds() const { From 2b5622f9965f3cdb2fe9922532d7256dd065f0d0 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 12:35:48 -0400 Subject: [PATCH 24/37] Added initial check for attempt to convert audio to midi and some minor cleanup. --- src/widgets/MediaClipboardWidget.h | 112 ++++++++++++++++------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 29541ceb..4dd5d940 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,7 +133,10 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } + Rectangle getClipboardControlsBounds() const + { + return controlsComponent.getBounds().expanded(2); + } Rectangle getClipboardNameBoxBounds() const { @@ -261,7 +264,7 @@ class MediaClipboardWidget : public Component, public ChangeListener File selectedFile = selectedTrack->getOriginalFilePath().getLocalFile(); - /*StringArray validExtensions = + StringArray validExtensions = originalTrack->getInstanceExtensions(); if (! validExtensions.contains(selectedFile.getFileExtension())) @@ -269,13 +272,19 @@ class MediaClipboardWidget : public Component, public ChangeListener AlertWindow::showMessageBoxAsync( AlertWindow::WarningIcon, "Invalid File", - "This track can only be overwritten with data of the following file types: " - + validExtensions.joinIntoString(", ") + ".", + "This track cannot be overwritten with the selected file.", "OK"); - }*/ - // RZ - if (originalFile.getFileExtension() - != selectedFile.getFileExtension()) + + return; + } + + bool successfulOverwrite = false; + + String ext = originalFile.getFileExtension().toLowerCase(); + + if (AudioDisplayComponent::getSupportedExtensions().contains( + ext) + && ext != selectedFile.getFileExtension().toLowerCase()) { File tempFile = selectedFile.getSiblingFile( selectedFile.getFileNameWithoutExtension() @@ -286,25 +295,22 @@ class MediaClipboardWidget : public Component, public ChangeListener if (tempFile.copyFileTo(originalFile)) { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); - - tempFile.deleteFile(); + "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote \"" + << originalFile.getFullPathName() + << "\" with \"" + << selectedFile.getFullPathName() << "\"."); - linkedDisplays[selectedIndex]->initializeDisplay( - URL(originalFile)); - removeSelectionCallback(); - linkedDisplays[selectedIndex]->selectTrack(); + successfulOverwrite = true; } else { - tempFile.deleteFile(); DBG_AND_LOG( "MediaClipboardWidget::sendToDAWCallback: Conversion succeeded " - "but failed to copy to " - << originalFile.getFullPathName()); + "but failed to copy to \"" + << originalFile.getFullPathName() << "\"."); } + + tempFile.deleteFile(); } else { @@ -322,28 +328,33 @@ class MediaClipboardWidget : public Component, public ChangeListener if (selectedFile.copyFileTo(originalFile)) { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Overwriting file " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); + "MediaClipboardWidget::sendToDAWCallback: Overwriting file \"" + << originalFile.getFullPathName() << "\" with \"" + << selectedFile.getFullPathName() << "\"."); - // Update display with overwritten media - linkedDisplays[selectedIndex]->initializeDisplay( - URL(originalFile)); - - // Remove selected track - removeSelectionCallback(); - - // Select overwritten track - linkedDisplays[selectedIndex]->selectTrack(); + successfulOverwrite = true; } else { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Failed to overwrite file " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); + "MediaClipboardWidget::sendToDAWCallback: Failed to overwrite file \"" + << originalFile.getFullPathName() << "\" with \"" + << selectedFile.getFullPathName() << "\"."); } } + + if (successfulOverwrite) + { + // Update display with overwritten media + linkedDisplays[selectedIndex]->initializeDisplay( + URL(originalFile)); + + // Remove selected track + removeSelectionCallback(); + + // Select overwritten track + linkedDisplays[selectedIndex]->selectTrack(); + } } } }), @@ -573,7 +584,7 @@ class MediaClipboardWidget : public Component, public ChangeListener } } - // RZ edit + // TODO - move to utils/? bool convertAudioFile(const File& source, const File& target) { AudioFormatManager formatManager; @@ -583,8 +594,9 @@ class MediaClipboardWidget : public Component, public ChangeListener if (! reader) { - DBG_AND_LOG( - "convertAudioFile: Could not read source file: " << source.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Could not read source file \"" + << source.getFullPathName() << "\"."); + return false; } @@ -599,22 +611,21 @@ class MediaClipboardWidget : public Component, public ChangeListener format = formatManager.findFormatForFileExtension("flac"); else { - DBG_AND_LOG("convertAudioFile: Unsupported target format: " << ext); - return false; - } + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Unsupported target format \"" + << ext << "\"."); - if (! format) - { - DBG_AND_LOG("convertAudioFile: Format not available: " << ext); return false; } target.deleteFile(); + auto outputStream = target.createOutputStream(); + if (! outputStream) { - DBG_AND_LOG( - "convertAudioFile: Could not create output file: " << target.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Could not create output file \"" + << target.getFullPathName() << "\"."); + return false; } @@ -623,8 +634,9 @@ class MediaClipboardWidget : public Component, public ChangeListener if (! writer) { - DBG_AND_LOG( - "convertAudioFile: Could not create writer for: " << target.getFullPathName()); + DBG_AND_LOG("convertAudioFile: Could not create writer for \"" + << target.getFullPathName() << "\"."); + return false; } @@ -641,8 +653,8 @@ class MediaClipboardWidget : public Component, public ChangeListener position += framesToRead; } - DBG_AND_LOG("convertAudioFile: Converted " << source.getFullPathName() << " -> " - << target.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Converted and saved \"" + << source.getFullPathName() << "\" to \"" << target.getFullPathName() << "\"."); return true; } From c7d277e89b12064c05d0b1d9e1ef5324cf7869be Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 13:15:29 -0400 Subject: [PATCH 25/37] Attempt to generalize Windows-specific build steps. --- CMakeLists.txt | 65 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a21ebd92..bf7ee352 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,61 +196,74 @@ target_link_libraries(${PROJECT_NAME} juce::juce_recommended_warning_flags ) -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\msvcp140.dll -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140_1.dll -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140.dll if (WIN32) # Function to find the specific runtime DLLs function(find_vc_runtime_dlls out_var) - message(STATUS "Searching for specific runtime DLLs...") + message(STATUS "Locating MSVC runtime DLLs...") # Define the target DLLs set(target_dlls - "msvcp140.dll" - "vcruntime140_1.dll" - "vcruntime140.dll" + msvcp140.dll + vcruntime140.dll + vcruntime140_1.dll ) - # Set the base path for Visual Studio 2022 redistributable DLLs - set(VS_CRT_BASE_PATH - "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" - "C:/Program Files (x86)/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" - ) + # Get the MSVC toolchain root from the compiler path + get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) - # Search for the DLLs in the specified base path - file(GLOB_RECURSE all_runtime_dlls - LIST_DIRECTORIES false - ${VS_CRT_BASE_PATH}/*.dll + # Walk up to: .../VC/Tools/MSVC/ + get_filename_component(_msvc_root "${_compiler_dir}/../../.." ABSOLUTE) + + # Construct Redist path dynamically + file(GLOB _redist_dirs + "${_msvc_root}/Redist/MSVC/*" ) + set(runtime_dlls) - foreach(dll ${all_runtime_dlls}) - get_filename_component(dll_name ${dll} NAME) - if(dll_name IN_LIST target_dlls) - list(APPEND runtime_dlls ${dll}) - endif() + foreach(redist_dir ${_redist_dirs}) + # Handle both x64 and x86 automatically + foreach(arch x64 x86) + set(crt_dir "${redist_dir}/${arch}/Microsoft.VC*.CRT") + + file(GLOB dlls + "${crt_dir}/*.dll" + ) + + foreach(dll ${dlls}) + get_filename_component(name "${dll}" NAME) + if(name IN_LIST target_dlls) + list(APPEND runtime_dlls "${dll}") + endif() + endforeach() + endforeach() endforeach() + list(REMOVE_DUPLICATES runtime_dlls) + if(runtime_dlls) - message(STATUS "Found specific runtime DLLs:") + message(STATUS "Found MSVC runtime DLLs:") foreach(dll ${runtime_dlls}) - message(STATUS "${dll}") + message(STATUS " ${dll}") endforeach() set(${out_var} ${runtime_dlls} PARENT_SCOPE) else() - message(FATAL_ERROR "Required specific runtime DLLs not found") + message(FATAL_ERROR "MSVC runtime DLLs not found") endif() endfunction() # Call the function to find the specific DLLs find_vc_runtime_dlls(RUNTIME_DLLS) - # Add a post-build step to copy the specific DLLs to the output directory + # Add a post-build step to copy the DLLs to the output directory add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${RUNTIME_DLLS} - $) + $ + ) + endif() + # copy the pyinstaller tools to the bundle # if (APPLE) # add_custom_command(TARGET ${PROJECT_NAME} From bb4d68c77fa4893c894e634cbcc684c360e4fe56 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 13:23:17 -0400 Subject: [PATCH 26/37] Attempt to simplify Windows-specific build steps. --- CMakeLists.txt | 67 +++----------------------------------------------- 1 file changed, 4 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf7ee352..b14b3ff8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,71 +197,12 @@ target_link_libraries(${PROJECT_NAME} ) if (WIN32) - # Function to find the specific runtime DLLs - function(find_vc_runtime_dlls out_var) - message(STATUS "Locating MSVC runtime DLLs...") - - # Define the target DLLs - set(target_dlls - msvcp140.dll - vcruntime140.dll - vcruntime140_1.dll - ) - - # Get the MSVC toolchain root from the compiler path - get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) - - # Walk up to: .../VC/Tools/MSVC/ - get_filename_component(_msvc_root "${_compiler_dir}/../../.." ABSOLUTE) - - # Construct Redist path dynamically - file(GLOB _redist_dirs - "${_msvc_root}/Redist/MSVC/*" - ) - - set(runtime_dlls) - - foreach(redist_dir ${_redist_dirs}) - # Handle both x64 and x86 automatically - foreach(arch x64 x86) - set(crt_dir "${redist_dir}/${arch}/Microsoft.VC*.CRT") - - file(GLOB dlls - "${crt_dir}/*.dll" - ) - - foreach(dll ${dlls}) - get_filename_component(name "${dll}" NAME) - if(name IN_LIST target_dlls) - list(APPEND runtime_dlls "${dll}") - endif() - endforeach() - endforeach() - endforeach() - - list(REMOVE_DUPLICATES runtime_dlls) - - if(runtime_dlls) - message(STATUS "Found MSVC runtime DLLs:") - foreach(dll ${runtime_dlls}) - message(STATUS " ${dll}") - endforeach() - set(${out_var} ${runtime_dlls} PARENT_SCOPE) - else() - message(FATAL_ERROR "MSVC runtime DLLs not found") - endif() - endfunction() - - # Call the function to find the specific DLLs - find_vc_runtime_dlls(RUNTIME_DLLS) - - # Add a post-build step to copy the DLLs to the output directory add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${RUNTIME_DLLS} - $ + COMMAND ${CMAKE_COMMAND} -E copy + $ + $ + COMMAND_EXPAND_LISTS ) - endif() # copy the pyinstaller tools to the bundle From e42e81d042657f8a29fa33e1bbe6cfa1846b86ed Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Thu, 9 Apr 2026 19:56:03 -0400 Subject: [PATCH 27/37] Seventh attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7b351529..e7d57142 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -29,7 +29,6 @@ TickScheme chooseTickScheme(double visibleLength) { 600.0, 120.0, 5 }, }; - // Pick the scheme that gives roughly 4–12 major ticks across the visible range for (const auto& s : schemes) { double numMajor = visibleLength / s.majorStep; @@ -37,7 +36,6 @@ TickScheme chooseTickScheme(double visibleLength) return s; } - // Fallback for very long files double majorStep = std::pow(10.0, std::floor(std::log10(visibleLength / 5.0))); majorStep = std::max(0.01, majorStep); return { majorStep, majorStep / 5.0, 5 }; @@ -95,13 +93,12 @@ void TimeAxisStrip::paint(Graphics& g) const float minorTickTop = static_cast(h) * 0.55f; const float minorTickBot = static_cast(h); - // --- Minor ticks --- + // Minor ticks g.setColour(Colours::grey.withAlpha(0.5f)); const double firstMinor = std::ceil(visibleStart / minorStep) * minorStep; for (double t = firstMinor; t <= visibleEnd && t <= totalLength; t += minorStep) { - // Skip positions that coincide with major ticks double remainder = std::fmod(t, majorStep); if (remainder < minorStep * 0.1 || (majorStep - remainder) < minorStep * 0.1) continue; @@ -113,7 +110,7 @@ void TimeAxisStrip::paint(Graphics& g) g.drawVerticalLine(static_cast(x), minorTickTop, minorTickBot); } - // --- Major ticks and labels --- + // Major ticks and labels g.setColour(Colours::lightgrey.withAlpha(0.9f)); const int labelH = jmin(13, h - 2); @@ -1008,7 +1005,6 @@ bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (pps <= 0.0f) return false; - // minPps = zoomed all the way out; maxPps = zoomed all the way in (~5 seconds visible). const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); @@ -1018,7 +1014,6 @@ bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); - // If pps didn't change, zoom has hit a limit if (std::abs(newPps - pps) < 0.01f) return false; @@ -1076,9 +1071,7 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe } else { - bool zoomed = horizontalZoom(static_cast(wheel.deltaY), scrollTime); - if (! zoomed) - horizontalMove(static_cast(wheel.deltaY)); + horizontalZoom(static_cast(wheel.deltaY), scrollTime); } } else From cfa52948a7e851ad946c28f59706ddafd45452c5 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:14:14 -0400 Subject: [PATCH 28/37] Fixed merge conflict, improved feel of zoom anchoring, and some minor cleanup. --- src/media/MediaDisplayComponent.cpp | 48 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 2703c1be..7d9c9023 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -2,7 +2,8 @@ #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -<<<<<<< feature/375-time-axis-horizontal-zoom +#include "../utils/Interface.h" + #include namespace @@ -17,17 +18,9 @@ struct TickScheme TickScheme chooseTickScheme(double visibleLength) { static const TickScheme schemes[] = { - { 0.1, 0.02, 5 }, - { 0.5, 0.1, 5 }, - { 1.0, 0.25, 4 }, - { 2.0, 0.5, 4 }, - { 5.0, 1.0, 5 }, - { 15.0, 5.0, 3 }, - { 30.0, 10.0, 3 }, - { 60.0, 15.0, 4 }, - { 120.0, 30.0, 4 }, - { 300.0, 60.0, 5 }, - { 600.0, 120.0, 5 }, + { 0.1, 0.02, 5 }, { 0.5, 0.1, 5 }, { 1.0, 0.25, 4 }, { 2.0, 0.5, 4 }, + { 5.0, 1.0, 5 }, { 15.0, 5.0, 3 }, { 30.0, 10.0, 3 }, { 60.0, 15.0, 4 }, + { 120.0, 30.0, 4 }, { 300.0, 60.0, 5 }, { 600.0, 120.0, 5 }, }; for (const auto& s : schemes) @@ -136,9 +129,6 @@ void TimeAxisStrip::paint(Graphics& g) true); } } -======= -#include "../utils/Interface.h" ->>>>>>> develop MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} @@ -1042,41 +1032,47 @@ void MediaDisplayComponent::horizontalMove(double deltaT) updateVisibleRange({ newStart, newStart + visibleLength }); } -bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) +void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { const float mediaWidth = getMediaWidth(); const double totalLength = getTotalLengthInSecs(); if (mediaWidth <= 0.0f || totalLength <= 0.0) - return false; + return; const float pps = getPixelsPerSecond(); if (pps <= 0.0f) - return false; + return; const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); - if (maxPps < minPps) - maxPps = minPps; + maxPps = jmax(maxPps, minPps); float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); if (std::abs(newPps - pps) < 0.01f) - return false; + return; double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); - if (newVisibleLength > totalLength) - newVisibleLength = totalLength; + newVisibleLength = jmin(newVisibleLength, totalLength); + + double anchorRatio = 0.5; + double visibleStart = visibleRange.getStart(); + double visibleLength = visibleRange.getLength(); + + if (visibleLength > 0.0) + anchorRatio = (scrollPosT - visibleStart) / visibleLength; + + anchorRatio = jlimit(0.0, 1.0, anchorRatio); const double maxStart = jmax(0.0, totalLength - newVisibleLength); - double newStart = scrollPosT - newVisibleLength * 0.5; + double newStart = scrollPosT - anchorRatio * newVisibleLength; newStart = jlimit(0.0, maxStart, newStart); double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); - return true; } void MediaDisplayComponent::scrollBarMoved(ScrollBar* scrollBarThatHasMoved, @@ -1105,7 +1101,7 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe double scrollTime = mediaXToTime(evt.position.getX()); - if (! commandMod) + if (! commandMod && evt.eventComponent == getMediaComponent()) { if (std::abs(wheel.deltaX) > 2 * std::abs(wheel.deltaY)) { diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 9ca18b66..39b91e58 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -217,7 +217,7 @@ class MediaDisplayComponent : public Component, int correctMediaXBounds(float mX, float width); void horizontalMove(double deltaT); - bool horizontalZoom(double deltaZoom, double scrollPosT); + void horizontalZoom(double deltaZoom, double scrollPosT); void scrollBarMoved(ScrollBar* scrollBarThatHasMoved, double scrollBarRangeStart) override; From 1e9b57f697b9b9957ee51f8a42a288b151822f95 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:36:51 -0400 Subject: [PATCH 29/37] Protect against $ returning empty. --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b14b3ff8..6e5c13df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,10 +198,12 @@ target_link_libraries(${PROJECT_NAME} if (WIN32) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy + COMMAND ${CMAKE_COMMAND} -E echo "Copying runtime DLLs (if any)" + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ COMMAND_EXPAND_LISTS + VERBATIM ) endif() From 9f6df123c71ec4b3408fd8cdc9fc1b855fe690e2 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:49:12 -0400 Subject: [PATCH 30/37] Switch to install-time packaging. --- .github/workflows/cmake_ctest.yml | 7 +++++-- CMakeLists.txt | 12 +++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cmake_ctest.yml b/.github/workflows/cmake_ctest.yml index 25a7d9e9..a28a97f9 100644 --- a/.github/workflows/cmake_ctest.yml +++ b/.github/workflows/cmake_ctest.yml @@ -149,6 +149,9 @@ jobs: run: cmake --build ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --parallel 4 # dummy build # run: touch dummy + - name: Install + shell: bash + run: cmake --install ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --prefix install # why the python3 -m pip install packaging? see this issue: https://github.com/nodejs/node-gyp/issues/2869 - name: Install Python dependencies for MACOS dmg @@ -167,14 +170,14 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ env.PRODUCT_NAME }} - path: ${{ env.ARTIFACTS_PATH }} + path: ${{ env.BUILD_DIR }}/install - name: Upload Zip (Linux) if: ${{ matrix.name == 'Linux' }} uses: actions/upload-artifact@v4 with: name: ${{ env.PRODUCT_NAME }} - path: ${{ env.ARTIFACTS_PATH }} + path: ${{ env.BUILD_DIR }}/install - name: Upload DMG (macOS) if: ${{ matrix.name == 'macOS' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e5c13df..2f0744c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,13 +197,11 @@ target_link_libraries(${PROJECT_NAME} ) if (WIN32) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E echo "Copying runtime DLLs (if any)" - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND_EXPAND_LISTS - VERBATIM + install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION . + RUNTIME_DEPENDENCIES + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" + POST_EXCLUDE_REGEXES "system32" ) endif() From d3827a65870841b225f1f2caa8c8157896396ca8 Mon Sep 17 00:00:00 2001 From: Huiran Yu Date: Fri, 17 Apr 2026 10:50:50 -0400 Subject: [PATCH 31/37] CMakeLists.txt update. #288 --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f0744c1..c45547b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,10 +198,11 @@ target_link_libraries(${PROJECT_NAME} if (WIN32) install(TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION . RUNTIME_DEPENDENCIES PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" POST_EXCLUDE_REGEXES "system32" + RUNTIME DESTINATION . + RESOURCE DESTINATION . ) endif() From 4a47f179ad1d904c3eba232734d1a93f5c2e757a Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 17 Apr 2026 14:05:41 -0400 Subject: [PATCH 32/37] Track gap remains and resets to origin position if track dragged outside media clipboard --- src/widgets/TrackAreaWidget.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index c6b59e5e..51f3e2b8 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -550,6 +550,7 @@ class TrackAreaWidget : public Component, if (clickedDisplay) { draggedTrack = clickedDisplay; + dragOriginIndex = getDraggedTrackIndex(); Point mouseInThis = e.getEventRelativeTo(this).getPosition(); Point trackTopLeft = draggedTrack->getBounds().getTopLeft(); @@ -581,7 +582,13 @@ class TrackAreaWidget : public Component, int newInsertIndex = -1; if (getLocalBounds().contains(posInThis)) + { newInsertIndex = getInsertIndexAtY(posInThis.y); + } + else + { + newInsertIndex = dragOriginIndex; + } if (newInsertIndex != dragInsertIndex) { @@ -625,6 +632,7 @@ class TrackAreaWidget : public Component, // Reset the drag state draggedTrack = nullptr; dragInsertIndex = -1; + dragOriginIndex = -1; isDraggingTrack = false; resized(); @@ -641,6 +649,7 @@ class TrackAreaWidget : public Component, MediaDisplayComponent* draggedTrack = nullptr; DragOverlayComponent* dragOverlay = nullptr; int dragInsertIndex = -1; + int dragOriginIndex = -1; bool isDraggingTrack = false; Point dragClickOffset { 0, 0 }; From 2c9ef2659a09a2cff54685fa39e5cb91804b7d99 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 17 Apr 2026 14:21:56 -0400 Subject: [PATCH 33/37] ghost track when dragging outside of media clipboard implemented --- src/widgets/TrackAreaWidget.h | 76 +++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 51f3e2b8..43a7d148 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -87,6 +87,29 @@ class DragOverlayComponent : public Component bool isActive = false; }; +class GhostTrackComponent : public Component +{ +public: + void setImage(const Image& img) + { + ghostImage = img; + repaint(); + } + + void paint(Graphics& g) override + { + if (ghostImage.isNull()) return; + + g.setOpacity(0.4f); + g.drawImage(ghostImage, + getLocalBounds().toFloat(), + RectanglePlacement::stretchToFit); + } + +private: + Image ghostImage; +}; + class TrackAreaWidget : public Component, public ChangeListener, public ChangeBroadcaster, @@ -99,6 +122,7 @@ class TrackAreaWidget : public Component, : displayMode(mode), fixedTrackHeight(trackHeight), dragOverlay(overlay) { addMouseListener(this, true); + addChildComponent(ghostTrack); } ~TrackAreaWidget() { resetState(); } @@ -136,12 +160,22 @@ class TrackAreaWidget : public Component, if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) { FlexItem gap; - - if (fixedTrackHeight) - gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); - else - gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); - + if (isDraggingOutside) + { + ghostTrack.setVisible(true); + if (fixedTrackHeight) + gap = FlexItem(ghostTrack).withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem(ghostTrack).withFlex(1).withMinHeight(50).withMargin(marginSize); + } + else + { + ghostTrack.setVisible(false); + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + } mainBox.items.add(gap); } @@ -165,12 +199,22 @@ class TrackAreaWidget : public Component, if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) { FlexItem gap; - - if (fixedTrackHeight) - gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + if (isDraggingOutside) + { + ghostTrack.setVisible(true); + if (fixedTrackHeight) + gap = FlexItem(ghostTrack).withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem(ghostTrack).withFlex(1).withMinHeight(50).withMargin(marginSize); + } else - gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); - + { + ghostTrack.setVisible(false); + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + } mainBox.items.add(gap); } @@ -572,6 +616,7 @@ class TrackAreaWidget : public Component, if (dragOverlay != nullptr) { Image snapshot = draggedTrack->createComponentSnapshot(draggedTrack->getLocalBounds()); + ghostTrack.setImage(snapshot); dragOverlay->startDrag(snapshot, dragClickOffset); draggedTrack->setVisible(false); } @@ -580,17 +625,20 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { int newInsertIndex = -1; + bool wasOutside = isDraggingOutside; if (getLocalBounds().contains(posInThis)) { newInsertIndex = getInsertIndexAtY(posInThis.y); + isDraggingOutside = false; } else { newInsertIndex = dragOriginIndex; + isDraggingOutside = true; } - if (newInsertIndex != dragInsertIndex) + if (newInsertIndex != dragInsertIndex || isDraggingOutside != wasOutside) { dragInsertIndex = newInsertIndex; resized(); @@ -634,6 +682,8 @@ class TrackAreaWidget : public Component, dragInsertIndex = -1; dragOriginIndex = -1; isDraggingTrack = false; + isDraggingOutside = false; + ghostTrack.setVisible(false); resized(); } @@ -648,9 +698,11 @@ class TrackAreaWidget : public Component, // For reordering tracks via dragging MediaDisplayComponent* draggedTrack = nullptr; DragOverlayComponent* dragOverlay = nullptr; + GhostTrackComponent ghostTrack; int dragInsertIndex = -1; int dragOriginIndex = -1; bool isDraggingTrack = false; + bool isDraggingOutside = false; Point dragClickOffset { 0, 0 }; std::vector> mediaDisplays; From a59c70948f14a16db320a30d0350fb1a7e3b6f74 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Mon, 27 Apr 2026 23:51:21 -0400 Subject: [PATCH 34/37] Fix Ticket 378: Add banner for optional input tracks --- src/media/MediaDisplayComponent.cpp | 39 +++++++++++++++++++++++++++++ src/media/MediaDisplayComponent.h | 1 + 2 files changed, 40 insertions(+) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..b42b38ff 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -315,10 +315,49 @@ void MediaDisplayComponent::paint(Graphics& g) } } +void MediaDisplayComponent::paintOverChildren(Graphics& g) +{ + // Detect optional AND input track AND not a thumbnail + if (!isRequired() && isInputTrack() && !isThumbnailTrack()) + { + // Grab that 24-pixel vertical slice on the far left that we reserved in resized() + auto bannerArea = getLocalBounds().removeFromLeft(24); + + // Draw the solid orange background + g.setColour(Colours::darkorange); + g.fillRect(bannerArea); + + // Setup text formatting + g.setColour(Colours::white); + g.setFont(12.0f); + + // Save the graphics state before rotating + Graphics::ScopedSaveState state(g); + + // Rotate the graphics context -90 degrees around the center of our banner + float cx = static_cast(bannerArea.getCentreX()); + float cy = static_cast(bannerArea.getCentreY()); + g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); + + // Create a rotated bounding box for the text + Rectangle textBounds(0, 0, static_cast(bannerArea.getHeight()), static_cast(bannerArea.getWidth())); + textBounds.setCentre(cx, cy); + + // Draw the text inside our rotated box + g.drawText("OPTIONAL INPUT TRACK", textBounds, Justification::centred, false); + } +} + void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); + // Reserve 24 pixels on the left edge for the vertical "Optional Input" banner. + if (!isRequired() && isInputTrack() && !isThumbnailTrack()) + { + totalBounds.removeFromLeft(24); + } + // Remove existing items in main flex mainFlexBox.items.clear(); diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..7e4c9a84 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -79,6 +79,7 @@ class MediaDisplayComponent : public Component, virtual StringArray getInstanceExtensions() = 0; void paint(Graphics& g) override; + void paintOverChildren(Graphics& g) override; virtual void resized() override; void repositionLabels(); From 61de9c815f9a9aeff51b4fdd09c81cc0f1981790 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 30 Apr 2026 12:03:54 -0400 Subject: [PATCH 35/37] Minor updates to pyharp web documentation. --- website/content/pyharp_docs/example.md | 4 +- website/content/pyharp_docs/host.md | 90 +++++++++++++------------ website/content/pyharp_docs/install.md | 2 + website/content/pyharp_docs/overview.md | 2 +- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index a1c212e3..a0246d63 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -30,7 +30,7 @@ ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] Here, `num_spk` is the estimated number of speakers in the recording. The TIGER model sets `num_spk` to two. We are going to take both outputs from the model: ``` -# Select audio of first speaker +# Select audio of each speaker independently output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] output_2 = ests_speech[:, 1, :] # Expected output shape: [1, T] ``` @@ -205,7 +205,7 @@ demo.launch(share=True, show_error=True) ### Deploying Our App -For now, we'll put `app.py` inside the `TIGER/` directory to avoid further installation steps. Note that while PyHARP's utilities use [Descript-AudioTools](https://github.com/descriptinc/audiotools) under the hood to handle audio loading and saving, you're free to use whichever libraries you want as long as they can read and produce valid audio files. +For now, we'll put `app.py` inside the cloned `TIGER/` directory to avoid further installation steps. Note that while PyHARP's utilities use [Descript-AudioTools](https://github.com/descriptinc/audiotools) under the hood to handle audio loading and saving, you're free to use whichever libraries you want as long as they can read and produce valid audio files. With our application up and running, it's time to link it to HARP. Run: diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 26a970d7..24e18bee 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -1,14 +1,13 @@ -# Hosting PyHARP Apps in the Cloud +# Hosting PyHARP Apps in the Cloud (HuggingFace Spaces) +Automatically generated Gradio endpoints are only available for a maximum of 72 hours. If you'd like to keep an endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely. -Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: +### Gradio Endpoints +This is the most convenient solution for hosting a PyHARP app. If you are a Hugging Face PRO subscriber, you can use [ZeroGPU](https://huggingface.co/docs/hub/spaces-zerogpu) to dynamically allocate GPU resources according to user requests without any additional charges. Non-PRO users can select from CPU environments or paid GPU options. -## Gradio Endpoints -Gradio endpoints are the most convenient solution for a PyHARP application. It will give you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, at no additional charge. - -1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. -2. Choose the Blank template. -3. You can choose ZeroGPU hardware if you are a PRO subscriber and your application requires GPU computation. You can also customize the hardware. -4. Clone the initialized repository locally: +1. Create a new [HuggingFace Space](https://huggingface.co/new-space). +2. Choose Gradio as the SDK along with the blank template. +3. Select the desired hardware option. +4. Create the space and clone the initialized repository locally: ```bash git clone https://huggingface.co/spaces// ``` @@ -18,87 +17,90 @@ git add . git commit -m "initial commit" git push -u origin main ``` -6. Here are the suggested configurations in the related files. +6. Configure the following repository files: - `README.md` - Set __sdk_version__ to __5.28.0__. This is the recommended version, as HARP may not work with the very latest or earlier versions of Gradio. + Set __sdk_version__ to __5.28.0__, the recommended version of `gradio`. HARP may not work with the very latest or earlier versions. - `requirements.txt` - This is the place where you put all the **pip** packages. You should also put in the latest PyHARP: + Place all of the required **pip** packages in this file. It should also include the latest version of `pyharp`: ``` git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 ``` - If you are using Gradio endpoints, you don't have to include the gradio package here. + Note that you do not have to include the `gradio` package in this file. - `packages.txt` - This is there to put **apt-get install** Debian package list, which are required by some models. + Place any necessary **apt-get install** debian packages in this file. Some models may require these. -## Docker Endpoints -PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or a prior version, which may cause issues when upgrading Python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. +### Docker Endpoints +Some models may have been developed with older versions of Python, and attempting to deploy them would lead to dependency issues. For example, the `numpy.float` and `numpy.int` deprecation in `numpy==1.24` breaks older packages such as `madmom`. Therefore, we may need to patch any corresponding source files during the deployment process. However, this is not supported by the highly-modularized Gradio SDK. -Using Docker endpoints can help you fix this issue. Docker will allow you to customize deployments, making room for potential patches and fixes. However, the ZeroGPU hardware will not be available in Docker spaces, and you will need to pay for GPU usage. +Using Docker endpoints can help circumvent these issues. Docker will allow you to customize the deployment, which makes room for any necessary patches. Note however that ZeroGPU is not available for Docker spaces, meaning you must pay to use GPU resources with this option. -1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. -2. Choose the Blank template. -3. Clone the initialized repository locally: +1. Create a new [HuggingFace Space](https://huggingface.co/new-space). +2. Choose Docker as the SDK along with the blank template. +3. Select the desired hardware option. +4. Create the space and clone the initialized repository locally: ```bash git clone https://huggingface.co/spaces// ``` -4. Add your files to the repository, commit, then push to the `main` branch: +5. Add your files to the repository, commit, then push to the `main` branch: ```bash git add . git commit -m "initial commit" git push -u origin main ``` -5. Configurations +6. Configure the following repository files: - `README.md` - Set **app_port** to ``. + Set **app_port** to any valid ``. - - `requirements.txt` and `packages.txt` + - `requirements.txt` - Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile` introduced in the next step. - - For `requirements.txt`, you should include: - ``` - gradio==5.28.0 - git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 - ``` + Place all of the required **pip** packages in this file. It should also include the recommended version of `gradio` and the latest version of `pyharp`: + ``` + gradio==5.28.0 + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` - - `Dockerfile` + - `packages.txt` + + Place any necessary **apt-get install** debian packages in this file. Some models may require these. - Here is an example `Dockerfile` configuration that installs general packages and fixes the `madmom` package. + - `Dockerfile` + Installs the required **pip** and **apt-get** packages, and supports manual patching (_e.g._ of `madmom` in the following example): ```Docker FROM python:3.10-slim # Set python version WORKDIR /app COPY packages.txt /app/packages.txt - # System deps for building packages from source + # System dependencies for building packages from source RUN apt-get update RUN xargs apt-get install -y --no-install-recommends < /app/packages.txt RUN rm -rf /var/lib/apt/lists/* COPY requirements.txt /app/requirements.txt - # disable build isolation so Cython installed in the environment is visible at build time + # Disable build isolation so Cython installed in the environment is visible at build time ENV PIP_NO_BUILD_ISOLATION=1 RUN pip install --no-cache-dir -U pip wheel Cython RUN pip install --no-cache-dir setuptools==80.9.0 RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir --no-build-isolation madmom - # madmom patch - COPY patch_madmom.py /app/scripts/patch_madmom.py # A patch fix in the repo + # Patch madmom package + COPY patch_madmom.py /app/scripts/patch_madmom.py # Script to patch madmom source files RUN python /app/scripts/patch_madmom.py RUN python -c "import madmom; print('madmom import OK')" - # Copy the rest of the repo + # Copy remainder of the repo COPY . /app - # HF Spaces routes traffic to $PORT. Gradio should listen on it. + # HF Spaces route traffic to + # Gradio should listen accordingly ENV PORT= # in README.md EXPOSE @@ -107,11 +109,13 @@ git push -u origin main ``` --- -Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed using two methods will have the same UI and functionality. - Here are a few tips and best practices when dealing with HuggingFace Spaces: -- Spaces operate based on the files in the `main` branch +- Spaces operate based off of the files in the `main` branch - An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces - A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) +- Pin versions for `numpy` (_e.g._, `<2`), `torch` (_e.g._, `==2.2.2`), and `torchaudio` (_e.g._, `==2.2.2`) to avoid unexpected build issues caused by the latest versions of these packages + +For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). -For more information, please refer to the offical Hugging Face document on [Spaces](https://huggingface.co/docs/hub/spaces). +## Accessing Within HARP +PyHARP apps deployed to HuggingFace will begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The two deployment methods above produce identical UIs and functionality. diff --git a/website/content/pyharp_docs/install.md b/website/content/pyharp_docs/install.md index b888406f..4ff988ed 100644 --- a/website/content/pyharp_docs/install.md +++ b/website/content/pyharp_docs/install.md @@ -4,3 +4,5 @@ PyHARP can be installed using `pip`: - **Clone the PyHarp repository:** `git clone https://github.com/TEAMuP-dev/pyharp` - **Install:** `pip install -e pyharp && cd pyharp` + +Note that PyHARP depends on [Gradio](https://www.gradio.app/). We recommend installing `gradio==5.28.0`, which requires `python>=3.10`. diff --git a/website/content/pyharp_docs/overview.md b/website/content/pyharp_docs/overview.md index 76ee1b03..df900688 100644 --- a/website/content/pyharp_docs/overview.md +++ b/website/content/pyharp_docs/overview.md @@ -6,7 +6,7 @@ We can use a variety of [state-of-the-art models within HARP](/content/usage/mod Under the hood, HARP simply routes data (audio, MIDI, etc.) to Python applications for processing and renders the results in the editor. To make this work, we require (1) that these applications are built using [Gradio](https://www.gradio.app/), and (2) that these applications follow a simple set of specifications defined in the [PyHARP](https://github.com/TEAMuP-dev/pyharp) library. Crucially, PyHARP wraps a number of interactive components from Gradio (e.g. knobs, sliders, text boxes) to allow for rendering application-defined interfaces within the HARP editor. -Within these constraints, developers can build a wide variety of audio and MIDI-processing applications. For instance, HARP is compatible with any Python deep learning framework because it only requires that applications consume and produce audio files, MIDI files, or structrued labels -- it places no limits on how these objects are created or modified. +Within these constraints, developers can build a wide variety of audio and MIDI-processing applications. For instance, HARP is compatible with any Python deep learning framework because it only requires that applications consume and produce audio files, MIDI files, or structured labels -- it places no limits on how these objects are created or modified. ### Host Your Endpoint Locally or in the Cloud From 8f6649758d52d3abe3f982e9d3c4e053c5b72206 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Fri, 8 May 2026 13:46:51 -0400 Subject: [PATCH 36/37] Refine optional input banner text and color --- src/media/MediaDisplayComponent.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index b42b38ff..5444deef 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -323,8 +323,8 @@ void MediaDisplayComponent::paintOverChildren(Graphics& g) // Grab that 24-pixel vertical slice on the far left that we reserved in resized() auto bannerArea = getLocalBounds().removeFromLeft(24); - // Draw the solid orange background - g.setColour(Colours::darkorange); + // Draw the background color + g.setColour(Colour::fromRGB(90, 105, 105)); g.fillRect(bannerArea); // Setup text formatting @@ -344,7 +344,7 @@ void MediaDisplayComponent::paintOverChildren(Graphics& g) textBounds.setCentre(cx, cy); // Draw the text inside our rotated box - g.drawText("OPTIONAL INPUT TRACK", textBounds, Justification::centred, false); + g.drawText("OPTIONAL", textBounds, Justification::centred, false); } } @@ -352,7 +352,7 @@ void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); - // Reserve 24 pixels on the left edge for the vertical "Optional Input" banner. + // Reserve 24 pixels on the left edge for the vertical "Optional" banner. if (!isRequired() && isInputTrack() && !isThumbnailTrack()) { totalBounds.removeFromLeft(24); From 585f1f1ccf50345e154235ff28def617af149744 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 15 May 2026 14:08:00 -0400 Subject: [PATCH 37/37] Adapted banner to current flexbox layout. --- src/media/MediaDisplayComponent.cpp | 72 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 32 ++++++++----- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 5444deef..2a1765b0 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -6,6 +6,27 @@ #include +void OptionalBannerComponent::paint(Graphics& g) +{ + const float cx = static_cast(getWidth()) / 2.0f; + const float cy = static_cast(getHeight()) / 2.0f; + + g.setColour(Colour::fromRGB(90, 105, 105)); + g.fillAll(); + + g.setColour(Colours::white); + g.setFont(12.0f); + + Graphics::ScopedSaveState state(g); + g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); + + Rectangle textBounds( + 0, 0, static_cast(getHeight()), static_cast(getWidth())); + textBounds.setCentre(cx, cy); + + g.drawText("OPTIONAL", textBounds, Justification::centred, false); +} + namespace { struct TickScheme @@ -164,6 +185,8 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW horizontalScrollBar.setAutoHide(false); horizontalScrollBar.addListener(this); + addAndMakeVisible(optionalBanner); + timeAxisStrip = std::make_unique(this); mediaAreaContainer.addAndMakeVisible(overheadPanel); @@ -315,49 +338,10 @@ void MediaDisplayComponent::paint(Graphics& g) } } -void MediaDisplayComponent::paintOverChildren(Graphics& g) -{ - // Detect optional AND input track AND not a thumbnail - if (!isRequired() && isInputTrack() && !isThumbnailTrack()) - { - // Grab that 24-pixel vertical slice on the far left that we reserved in resized() - auto bannerArea = getLocalBounds().removeFromLeft(24); - - // Draw the background color - g.setColour(Colour::fromRGB(90, 105, 105)); - g.fillRect(bannerArea); - - // Setup text formatting - g.setColour(Colours::white); - g.setFont(12.0f); - - // Save the graphics state before rotating - Graphics::ScopedSaveState state(g); - - // Rotate the graphics context -90 degrees around the center of our banner - float cx = static_cast(bannerArea.getCentreX()); - float cy = static_cast(bannerArea.getCentreY()); - g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); - - // Create a rotated bounding box for the text - Rectangle textBounds(0, 0, static_cast(bannerArea.getHeight()), static_cast(bannerArea.getWidth())); - textBounds.setCentre(cx, cy); - - // Draw the text inside our rotated box - g.drawText("OPTIONAL", textBounds, Justification::centred, false); - } -} - void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); - // Reserve 24 pixels on the left edge for the vertical "Optional" banner. - if (!isRequired() && isInputTrack() && !isThumbnailTrack()) - { - totalBounds.removeFromLeft(24); - } - // Remove existing items in main flex mainFlexBox.items.clear(); @@ -374,6 +358,16 @@ void MediaDisplayComponent::resized() { // Place header beside media mainFlexBox.flexDirection = FlexBox::Direction::row; + + if (! isRequired() && isInputTrack() && ! isThumbnailTrack()) + { + mainFlexBox.items.add(FlexItem(optionalBanner).withWidth(24)); + } + else + { + optionalBanner.setBounds(0, 0, 0, 0); + } + // Fixed area for track label and buttons mainFlexBox.items.add(FlexItem(headerComponent).withFlex(1).withMaxWidth(40).withMargin(4)); } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 7e4c9a84..7769861b 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -27,6 +27,23 @@ enum class DisplayMode Thumbnail // Reduced functionality }; +class OptionalBannerComponent : public Component +{ +public: + void paint(Graphics& g) override; +}; + +class TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override; + +private: + MediaDisplayComponent* owner = nullptr; +}; + class ColorablePanel : public Component { public: @@ -48,17 +65,6 @@ class ColorablePanel : public Component Colour backgroundColor; }; -class TimeAxisStrip : public Component -{ -public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override; - -private: - MediaDisplayComponent* owner = nullptr; -}; - class MediaDisplayComponent : public Component, public ChangeListener, public ChangeBroadcaster, @@ -79,7 +85,6 @@ class MediaDisplayComponent : public Component, virtual StringArray getInstanceExtensions() = 0; void paint(Graphics& g) override; - void paintOverChildren(Graphics& g) override; virtual void resized() override; void repositionLabels(); @@ -273,6 +278,9 @@ class MediaDisplayComponent : public Component, MultiButton::Mode copyFileButtonActiveInfo; MultiButton::Mode copyFileButtonInactiveInfo; + // Banner shown on left edge of optional input tracks + OptionalBannerComponent optionalBanner; + // Panel displaying overhead labels ColorablePanel overheadPanel { overheadPanelColor };