Made with JUCE: Elastik 2 from ueberschall


#1

We at zplane teamed up with ueberschall for the development of Elastik 2, a feature-rich loop player and browser for their sample libraries based on our elastiquePRO time-&pitch-shifting SDK, and we’re happy we can finally announce its release:

https://www.ueberschall.com/en/core/content/staticplayerelastik

You can download the player for free. It’s not limited in any way, but it won’t be too useful unless you already own ueberschall soundbanks. If you want to try it out, make sure to download the free demo soundbank as well, so you have a bit more samples to mix&match than the handful that are included in the player’s installer.

Now it’s time for a big THANK YOU. I can’t imagine how much time it would have taken us if we had to deal directly with all the IO-, GUI-, Audio-, and Plugin-APIs used by the player. With JUCE we spent our time actually developing and getting things done instead.

Extra credits for the excellent support here in the forum, not only to jules but also to all others who openly share their ideas, code & valuable knowledge.

Gruß und Dank aus Berlin,
Steffen
zplane.development | http://www.zplane.de


#2

Awesome stuff - cheers Steffen!


#3

I find that the representation of audio waveforms using polar coordinates is especially appropriate considering the circular nature of loops. Elastik 2 made me want to lick the screen.


#4

Out of interest, for the feature where you allow the whole plugin UI to be rescaled, did you use the new Component::setTransform() stuff?


#5

No, we had build most of the GUI before the transformation methods were added to the component class, so it’s basically just nesting components and re-calculating sub-components sizes in ::resized() using proportions of width/height.

If you look carefully (for example when licking the screen :smiley: ) the rounding errors sum up to create quite a few inconsistencies during rescaling or at small sizes. I’d love to give the component transformations a try in the future, but I didn’t dare to switch so close to our release date.


#6

Ah, that’s a pity - the setTransform method would have probably done the whole thing in one line of code! Sorry it arrived too late to make your job easier!


#7

Steffen have a look at this little example program

#include "juce.h"

//==============================================================================
/**
  Smoothly repositions and resizes child Components without rounding errors,
  according to caller provided stretching parameters.

  Usage:

  1) Derive your Component from ResizableLayout

  2) Give your Component a well defined default size

  3) Add child Components and position them according to how
     they should appear at the default.

  4) For each child Component, also call addToLayout() and specify what
     portion of the owner's growth or reduction should apply to each corner.

  5) At the end of your constructor, call activateLayout() to turn it on

  6) If you later manually reposition a control (for example, using a
     ComponentDragger) call updateLayoutFor() on the control that moved,
     or if you moved all the controls call updateLayout().
*/
class ResizableLayout : private ComponentListener
{
public:
	enum
	{
		anchorUnit=100
	};

	enum Style
	{
		styleStretch,
		styleFixedAspect
	};

	static const Point<int> anchorNone;
	static const Point<int> anchorTopLeft;
	static const Point<int> anchorTopCenter;
	static const Point<int> anchorTopRight;
	static const Point<int> anchorMidLeft;
	static const Point<int> anchorMidCenter;
	static const Point<int> anchorMidRight;
	static const Point<int> anchorBottomLeft;
	static const Point<int> anchorBottomCenter;
	static const Point<int> anchorBottomRight;

public:
	ResizableLayout (Component* owner);
	~ResizableLayout ();

	// Add a Component to the Layout.
	// topLeft and bottomRight are the percentages that the top left and bottom right of
	// the Component should move by, when the layout is resized.
	// So if you wanted to have the control take on the full width of the parent, and
	// half the height, you would use bottomRight.x=100, bottomRight.y=50. or
  // use the constant anchorMidRight
	void addToLayout (
		Component *component,
		const Point<int> &topLeft,
		const Point<int> &bottomRight=anchorNone,
		Style style = styleStretch );

	// Remove a Component from the Layout.
	void removeFromLayout (Component* component);

	// Activate (or deactivate) the Layout. The Layout is initially inactive,
	// to prevent spurious recalculation while a Component and its children are being
	// constructed (causing resized() messages). Activating the Layout for the
	// first time will cause an Update().
	void activateLayout (bool bActive=true);

	// Update the state information for all items. This is used on the first Activate(),
	// and can also be used if multiple controls are moved or resized from elsewhere.
	void updateLayout ();

	// Call this to manually update the state information for a single control
	// after it has been moved or resized from elsewhere.
	void updateLayoutFor (Component *component);

private:
  struct Rect
  {
	  Rect() {}
	  Rect( int top0, int left0, int bottom0, int right0 ) { top=top0; left=left0; bottom=bottom0; right=right0; }
	  Rect( const Rectangle<int> &r ) { top=int(r.getY()); left=int(r.getX()); bottom=int(r.getBottom()); right=int(r.getRight()); }
	  operator Rectangle<int>() const { return Rectangle<int>( left, top, Width(), Height() ); }
	  int Height( void ) const { return bottom-top; }
	  int Width( void ) const { return right-left; }
	  void Inset( int dx, int dy ) { top+=dy; left+=dx; bottom-=dy; right-=dx; }

	  int top;
	  int left;
	  int bottom;
	  int right;
  };

  struct Anchor
	{
		Style	style;
		Component* component;
		Point<int> topLeft;
		Point<int> bottomRight;

    Anchor (Component* component=0);
    bool operator== (const Anchor& rhs) const;
    bool operator>= (const Anchor& rhs) const;
	};

  struct State
	{
		Component* component;
		double aspect;
		Rect margin;

    State (Component* component=0);
    bool operator== (const State& rhs) const;
    bool operator>= (const State& rhs) const;
	};

  void addStateFor (const Anchor& anchor);

	void recalculateLayout ();

  void componentMovedOrResized (Component& component,
                                bool wasMoved,
                                bool wasResized);

  void componentBeingDeleted (Component& component);

private:
	Component* m_owner;

  SortedSet<Anchor> m_anchors;
  SortedSet<State> m_states;

	bool m_bFirstTime;
	bool m_bActive;
};

const Point<int> ResizableLayout::anchorNone			  ( -1, -1 );
const Point<int> ResizableLayout::anchorTopLeft		  ( 0, 0 );
const Point<int> ResizableLayout::anchorTopCenter		( anchorUnit/2, 0 );
const Point<int> ResizableLayout::anchorTopRight		( anchorUnit, 0 );
const Point<int> ResizableLayout::anchorMidLeft		  ( 0, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidCenter		( anchorUnit/2, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidRight		( anchorUnit, anchorUnit/2 );
const Point<int> ResizableLayout::anchorBottomLeft	( 0, anchorUnit );
const Point<int> ResizableLayout::anchorBottomCenter( anchorUnit/2, anchorUnit );
const Point<int> ResizableLayout::anchorBottomRight	( anchorUnit, anchorUnit );

ResizableLayout::Anchor::Anchor (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::Anchor::operator== (const Anchor& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::Anchor::operator>= (const Anchor& rhs) const
  { return component >= rhs.component; }

ResizableLayout::State::State (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::State::operator== (const State& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::State::operator>= (const State& rhs) const
  { return component >= rhs.component; }

//----

ResizableLayout::ResizableLayout (Component* owner)
: m_owner (owner)
{
  m_bFirstTime = true;
	m_bActive = false;

  m_owner->addComponentListener (this);
}


ResizableLayout::~ResizableLayout()
{
}

void ResizableLayout::addToLayout (Component* component,
                                   const Point<int> &topLeft,
                                   const Point<int> &bottomRight,
                                   Style style )
{
	jassert (topLeft!=anchorNone);

  Anchor anchor (component);
	anchor.style = style;
  anchor.topLeft = topLeft;
	anchor.bottomRight = bottomRight;

  m_anchors.add (anchor);

  //component->addComponentListener (this);
}

void ResizableLayout::removeFromLayout (Component* component)
{
  m_anchors.removeValue (component);
  m_states.removeValue (component);
}

void ResizableLayout::activateLayout( bool bActive )
{
	if( m_bActive!=bActive )
	{
		if( bActive && m_bFirstTime )
		{
			updateLayout();
			m_bFirstTime=false;
		}

		m_bActive=bActive;
	}
}

void ResizableLayout::updateLayout ()
{
  m_states.clearQuick();
	for( int i=0; i<m_anchors.size(); i++ )
		addStateFor (m_anchors[i]);
}

void ResizableLayout::updateLayoutFor (Component *component)
{
  m_states.removeValue (component);
  addStateFor (m_anchors[m_anchors.indexOf (Anchor(component))]);
}

void ResizableLayout::addStateFor (const Anchor& anchor)
{
	Rect rBounds = anchor.component->getBounds();

  // owner size
	Point<int> ptSize( int(m_owner->getWidth()), int(m_owner->getHeight()) );

  State state (anchor.component);

  // secret sauce
	state.margin.top = rBounds.top - (ptSize.getY() * anchor.topLeft.getY()) / anchorUnit;
	state.margin.left = rBounds.left - (ptSize.getX() * anchor.topLeft.getX()) / anchorUnit;
	state.margin.bottom = rBounds.bottom - (ptSize.getY() * anchor.bottomRight.getY()) / anchorUnit;
	state.margin.right = rBounds.right - (ptSize.getX() * anchor.bottomRight.getX()) / anchorUnit;

  state.aspect = double (rBounds.Width()) / rBounds.Height();

  m_states.add (state);
}

// Recalculate the position and size of all the controls
// in the layout, based on the owner Component size.
void ResizableLayout::recalculateLayout()
{
	if( m_bActive )
	{
		Rect rParent = m_owner->getBounds();
		
    for( int i=0; i<m_states.size(); i++ )
		{
      Anchor anchor = m_anchors[i];
      State state = m_states[i];
      jassert (anchor.component == state.component);

      Rect rBounds;

      // secret sauce
			rBounds.top = state.margin.top + (rParent.Height() * anchor.topLeft.getY()) / anchorUnit;
			rBounds.left = state.margin.left + (rParent.Width() * anchor.topLeft.getX()) / anchorUnit;
			if( anchor.bottomRight != anchorNone )
			{
				rBounds.bottom= state.margin.bottom + (rParent.Height() * anchor.bottomRight.getY()) / anchorUnit;
				rBounds.right = state.margin.right + (rParent.Width() * anchor.bottomRight.getX()) / anchorUnit;
			}
			else
			{
				rBounds.bottom = rBounds.top + anchor.component->getHeight();
				rBounds.right = rBounds.left + anchor.component->getWidth();
			}

			if (anchor.style == styleStretch)
			{
				anchor.component->setBounds (rBounds);
			}
			else if (anchor.style==styleFixedAspect)
			{
				Rect rItem;
				double aspect = double (rBounds.Width()) / rBounds.Height();

				if( aspect > state.aspect )
				{
					rItem.top = rBounds.top;
					rItem.bottom = rBounds.bottom;
					int width = int (state.aspect * rItem.Height());
					rItem.left = rBounds.left + (rBounds.Width()-width)/2;
					rItem.right = rItem.left + width;
				}
				else
				{
					rItem.left = rBounds.left;
					rItem.right = rBounds.right;
					int height = int (1. / state.aspect * rItem.Width());
					rItem.top = rBounds.top + (rBounds.Height() - height) / 2;
					rItem.bottom = rItem.top + height;
				}

				anchor.component->setBounds( rItem );
			}
		}
	}
}

void ResizableLayout::componentMovedOrResized (Component& component,
                                               bool wasMoved,
                                               bool wasResized)
{
  if( &component == m_owner )
  {
    if (wasResized)
    {
      recalculateLayout ();
    }
  }
}

void ResizableLayout::componentBeingDeleted (Component& component)
{
  m_anchors.removeValue (&component);
  m_states.removeValue (&component);
}

struct MainPanel : Component, ResizableLayout
{
  MainPanel() : ResizableLayout (this)
  {
    // create the initial layout

    // must have a defined initial size
    setSize(512, 384);

    TextButton* b;

    b = new TextButton ("Juce");
    b->setBounds (32, 32, 512-64, 32);
    addToLayout (b, anchorTopLeft, anchorTopRight);
    addAndMakeVisible (b);

    b = new TextButton ("One");
    b->setBounds (32, 96, 128, 32);
    addToLayout (b, Point<int>(0,0), Point<int>(33,0));
    addAndMakeVisible (b);

    b = new TextButton ("Two");
    b->setBounds (32+128+32, 96, 128, 32);
    addToLayout (b, Point<int>(33,0), Point<int>(66,0));
    addAndMakeVisible (b);

    b = new TextButton ("Three");
    b->setBounds (32+128+32+128+32, 96, 128, 32);
    addToLayout (b, Point<int>(66,0), Point<int>(100,0));
    addAndMakeVisible (b);

    b = new TextButton ("BR");
    b->setBounds (512-96, 384-64, 64, 32);
    addToLayout (b, anchorBottomRight);
    addAndMakeVisible (b);

    b = new TextButton ("Aspect");
    b->setBounds (32, 128+32, 96, 96);
    addToLayout (b, anchorTopLeft, anchorBottomRight, styleFixedAspect);
    addAndMakeVisible (b);

    // turn it on
    activateLayout ();
  }
  ~MainPanel() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour (Colours::grey);
    g.fillAll();
  }
};

struct MainWindow : DocumentWindow
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    MainPanel* p = new MainPanel;
    setResizable (true, false);
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  }
  ~MainWindow() {}

  void closeButtonPressed() { JUCEApplication::quit(); }
};

struct MainApp : JUCEApplication
{
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

#8

Thanks a lot for your suggestions. When we have time for a GUI brush-up I will definitely evaluate both approaches.