How to multiply audio signals

I just tried to create a simple audio signal processing chain with ci::audio. I want to multiply two signals. I created the following tree:

-- class cinder::audio::GainNode	[ enabled, ch: 1, ch mode: matches input, sum ]
    -- -- class cinder::audio::MultiplyNode	[ enabled, ch: 1, ch mode: matches input, sum ]
        -- -- -- class cinder::audio::GenTriangleNode	[ enabled, ch: 1, ch mode: specified, in-place ]
        -- -- -- class cinder::audio::GenOscNode	[ enabled, ch: 1, ch mode: specified, in-place ]

However, if I replace the MultiplyNode (initial value 1) with an AddNode (initial value 0), I get the same result - both different from a prototype in Max/MSP where I multiply the signals with a *~ node. Have I misunderstood the way the audio nodes are designed? How can I multiply or add two signals with each other, rather than multiply both with the same constant?

As far as I know the cinder::audio::MathNodes are scalar operations (they only have 1 parameter). So the Multiply node will multiply the whole buffer by the single parameter.

If you want to multiply two buffers together I would probably create a new audioNode that receives 2 channels and multiplies the left and right buffers to the stereo output.

Here is a simple (and naive) implementation that seems to work:

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"

#include "cinder/audio/Context.h"
#include "cinder/audio/GenNode.h"
#include "cinder/audio/ChannelRouterNode.h"

using namespace ci;
using namespace ci::app;
using namespace std;

typedef std::shared_ptr<class SignalMultiplyNode>	SignalMultiplyNodeRef;

class SignalMultiplyNode    : public audio::Node
{
public:
    SignalMultiplyNode( const Format &format = Format() )
        : Node( format )
    {}
    
    void process( ci::audio::Buffer *buffer ) override
    {
        const size_t numFrames = buffer->getNumFrames();
        
        float* leftData = buffer->getChannel( 0 );
        float* rightData = buffer->getChannel( 1 );
        
        // Multiply left and right channels into both outputs;
        for( size_t i = 0; i < numFrames; i++ )
        {
            const float multOut = leftData[i] * rightData[i];
            
            leftData[i] = multOut;
            rightData[i] = multOut;
        }
    }
};


class MultTestApp : public App {
  public:
	void setup() override;
	void update() override;
	void draw() override;
    
    audio::GenTriangleNodeRef       mTriangleNode;
    audio::GenOscNodeRef            mGenOscNode;
    audio::ChannelRouterNodeRef     mRouterNode;
    SignalMultiplyNodeRef           mMultiplyNode;
    audio::OutputNodeRef            mOutputNode;
};

void MultTestApp::setup()
{
    auto* ctx = audio::Context::master();
    auto format = audio::Node::Format().channels( 2 );
    
    // Create Nodes
    mOutputNode = ctx->getOutput();
    mRouterNode = ctx->makeNode( new audio::ChannelRouterNode( format ) );
    mTriangleNode = ctx->makeNode( new audio::GenTriangleNode() );
    mGenOscNode = ctx->makeNode( new audio::GenOscNode() );
    mMultiplyNode = ctx->makeNode( new SignalMultiplyNode( format ) );
    
    // Pipe both sources into the left and right channels
    // of a router node
    mTriangleNode >> mRouterNode->route(0, 0);
    mGenOscNode >> mRouterNode->route(0, 1);
    
    // Pipe the router node into our signal multiply node
    // And then pipe that into the output
    mRouterNode >> mMultiplyNode >> mOutputNode;
    
    // Enable some sound from the gen nodes
    mTriangleNode->setFreq( 220 );
    mTriangleNode->enable();
    
    mGenOscNode->setFreq( 440 );
    mGenOscNode->enable();
    
    ctx->enable();
}

void MultTestApp::update()
{
}

void MultTestApp::draw()
{
	gl::clear( Color( 0, 0, 0 ) ); 
}

CINDER_APP( MultTestApp, RendererGl )

There will almost certainly be more optimal solutions (using the audio::dsp::multiply for example) but you get the gist.

perhaps @rich.e has a simpler solution?

F

1 Like

@pholz as per your arranged audio graph, both GenTriangleNode and GenOscNode are inputs to MultiplyNode, so they are summed into MultiplyNode's first channel in this case, which is then multiplied by 1 (the value of it’s audio::Param). ci::audio departs from what systems like Max / MSP or Super Collider do here, in that we specify params that are manipulable by audio signals with a second class, called audio::Param (this is similar to Web Audio, however).

MathNodes do have only one audio::Param on it, which provides the value for the specific operation such as ‘multiply’ or ‘add’, however it doesn’t have to be a scalar. You can provide ramps to it, or in your case set a different Node as its ‘processor’. So, to make a triangle waveform modulate an sinewave oscillator (which is the same as multiplying them together in Max / MSP), you’d do something like:

oscNode >> multiplyNode >> gainNode;
multiplyNode->getParam()->setProcessor( triangleNode );

Documentation in the guide is unfortunately lacking on this feature, but you can find it used in the DelayFeedback sample, and in various places by searching in the crude audio test suite.

And I also highly recommend @felixfaire suggestion of making custom nodes, when in doubt. :slight_smile: Much easier to do this in ci::audio / C++ compared to Max like environments.

cheers,
Rich

2 Likes

Aha! I thought I was missing a trick. Good to know.

F

I should also mention, that the frequency property on GenNode, the parent class for all waveform generators, is a param, so if you’re looking to do frequency modulation (as opposed to the above configuration, which is amplitude modulation), you could set your processor Node directly on that. The usual configuration would be something like:

modSineNode->setFreq( freq );
modGainNode->setValue( modAmount );

modSineNode >> modGainNode;
oscNode->getParamFreq()->setProcessor( modGainNode );

Great! This does exactly what I want. I completely missed that. It seems, however, that setProcessor() has to be called on the Param pointer, i.e.

multiplyNode->getParam()->setProcessor(triangleNode);

Ah good catch, thanks. I’ve updated my snippets above.