non/mixer/src/Spatializer_Module.C

675 lines
19 KiB
C
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

/*******************************************************************************/
/* Copyright (C) 2013 Jonathan Moore Liles */
/* */
/* This program is free software; you can redistribute it and/or modify it */
/* under the terms of the GNU General Public License as published by the */
/* Free Software Foundation; either version 2 of the License, or (at your */
/* option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, but WITHOUT */
/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for */
/* more details. */
/* */
/* You should have received a copy of the GNU General Public License along */
/* with This program; see the file COPYING. If not,write to the Free Software */
/* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
/*******************************************************************************/
#include <FL/fl_draw.H>
#include <FL/Fl_Box.H>
#include "Spatializer_Module.H"
#include "dsp.h"
static const float max_distance = 15.0f;
static const float HIGHPASS_FREQ = 200.0f;
//static const float LOWPASS_FREQ = 70000.0f;
static const float LOWPASS_FREQ = 22000.0f;
#include <math.h>
class filter
{
protected:
float _sample_rate;
float _w;
float _last_output;
float _last_cutoff;
float _amount_of_current;
float _amount_of_last;
bool _bypass;
void recalculate ( float cutoff )
{
_last_cutoff = cutoff;
if (_last_cutoff <= 10 )
{
_bypass = true;
}
else if (_last_cutoff > _sample_rate * 0.5f )
{
_bypass = true;
}
else
{
const float c = 2.0f - cosf(_w * _last_cutoff);
_amount_of_last = c - sqrtf(c * c - 1.0f);
_amount_of_current = 1 - _amount_of_last;
_bypass = false;
}
}
public:
void sample_rate ( nframes_t srate )
{
_sample_rate = srate;
_w = (2 * M_PI) / (float)srate;
}
filter ()
{
_last_output = 0;
_last_cutoff = 0;
_w = 0;
_sample_rate = 0;
_amount_of_current = 0;
_amount_of_last = 0;
_bypass = false;
}
void
run_lowpass ( float *buf, float cutoff, nframes_t nframes )
{
if (cutoff != _last_cutoff)
{
recalculate( cutoff );
}
if ( !_bypass )
{
while ( nframes-- )
{
*buf = _last_output = (_amount_of_current * *buf + _amount_of_last * _last_output);
buf++;
}
}
}
void
run_highpass ( float *buf, float cutoff, nframes_t nframes )
{
if (cutoff != _last_cutoff)
{
recalculate( cutoff );
}
if ( !_bypass )
{
while ( nframes-- )
{
_last_output = ((_amount_of_current * *buf) + (_amount_of_last * _last_output));
*buf = *buf - _last_output;
buf++;
}
}
}
};
class delay
{
unsigned int _sample_rate;
float *_buffer;
long _write_index;
unsigned int _buffer_mask;
float _max_delay;
public:
void sample_rate ( float srate )
{
if ( _buffer )
free( _buffer );
unsigned int size, minsize;
minsize = (unsigned long)(srate * _max_delay);
size = 1;
while (size < minsize)
size <<= 1;
_buffer = (float *)calloc(size, sizeof(float));
_buffer_mask = size - 1;
_sample_rate = srate;
_write_index = 0;
}
delay ( float max_delay )
{
_max_delay = max_delay;
_write_index = 0;
_sample_rate = 0;
_buffer = 0;
_buffer_mask =0;
}
~delay ( )
{
if ( _buffer )
free( _buffer );
}
void run ( float *buf, float *delaybuf, float delay, nframes_t nframes )
{
const nframes_t min_delay_samples = 4;
if ( delaybuf )
{
for (nframes_t i = 0; i < nframes; i++ )
{
float delay_samples = delaybuf[i] * _sample_rate;
if ( delay_samples > _buffer_mask + 1 )
delay_samples = _buffer_mask;
else if ( delay_samples < min_delay_samples )
delay_samples = min_delay_samples;
long idelay_samples = (long)delay_samples;
const float frac = delay_samples - idelay_samples;
const long read_index = _write_index - idelay_samples;
_buffer[_write_index++ & _buffer_mask] = buf[i];
const float read = interpolate_cubic (frac,
_buffer[(read_index-1) & _buffer_mask],
_buffer[read_index & _buffer_mask],
_buffer[(read_index+1) & _buffer_mask],
_buffer[(read_index+2) & _buffer_mask]);
buf[i] = read;
}
}
else
{
float delay_samples = delay * _sample_rate;
if ( delay_samples > _buffer_mask + 1 )
delay_samples = _buffer_mask;
else if ( delay_samples < min_delay_samples )
delay_samples = min_delay_samples;
long idelay_samples = (long)delay_samples;
const float frac = delay_samples - idelay_samples;
for (nframes_t i = 0; i < nframes; i++ )
{
const long read_index = _write_index - idelay_samples;
_buffer[_write_index++ & _buffer_mask] = buf[i];
const float read = interpolate_cubic (frac,
_buffer[(read_index-1) & _buffer_mask],
_buffer[read_index & _buffer_mask],
_buffer[(read_index+1) & _buffer_mask],
_buffer[(read_index+2) & _buffer_mask]);
buf[i] = read;
}
}
}
};
class ambisonic_panner
{
/* last values */
float _x, _y, _z;
/* for stereo */
float _xr, _yr;
static inline void spherical_to_cartesian (float a, float e, float &x, float &y, float &z )
{
a *= DEG2RAD;
e *= DEG2RAD;
z = sinf(e);
const float ce = cosf(e);
x = ce * cosf(-a);
y = ce * sinf(-a);
}
public:
ambisonic_panner ( )
{
_x = _y = _z = _xr = _yr = 1.0f;
}
void
run_mono ( float *in,
float *out_w, float *out_x, float *out_y, float *out_z,
float a, float e,
nframes_t nframes )
{
float x = _x;
float y = _y;
float z = _z;
spherical_to_cartesian( a, e, _x, _y, _z );
const float c = 1.0f / (float)nframes;
/* calculate increment for linear interpolation */
const float dx = (_x - x) * c;
const float dy = (_y - y) * c;
const float dz = (_z - z) * c;
while ( nframes-- )
{
x += dx;
y += dy;
z += dz;
const float t = *in++;
*out_w++ = ONEOVERSQRT2 * t;
*out_x++ = x * t;
*out_y++ = y * t;
*out_z++ = z * t;
}
}
void
run_stereo ( float *in_l, float *in_r,
float *out_w, float *out_x, float *out_y, float *out_z,
float a, float e, float w,
nframes_t nframes )
{
float x = _x;
float y = _y;
float z = _z;
float xr = _xr;
float yr = _yr;
w *= 0.5f;
spherical_to_cartesian( a - w, e, _x, _y, _z );
spherical_to_cartesian( a + w, e, _xr, _yr, _z );
const float c = 1.0f / (float)nframes;
/* calculate increment for linear interpolation */
const float dx = (_x - x) * c;
const float dy = (_y - y) * c;
const float dz = (_z - z) * c;
const float dxr = (_xr - xr) * c;
const float dyr = (_yr - yr) * c;
while ( nframes-- )
{
x += dx;
y += dy;
z += dz;
xr += dxr;
yr += dyr;
const float L = *in_l++;
const float R = *in_r++;
const float LR = L + R;
*out_w++ = ONEOVERSQRT2 * LR;
*out_x++ = x * L + xr * R;
*out_y++ = y * L + yr * R;
*out_z++ = z * LR;
}
}
};
Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
{
is_default( false );
_panner = 0;
{
Port p( this, Port::INPUT, Port::CONTROL, "Azimuth" );
p.hints.type = Port::Hints::LINEAR;
p.hints.ranged = true;
p.hints.minimum = -180.0f;
p.hints.maximum = 180.0f;
p.hints.default_value = 0.0f;
p.connect_to( new float );
p.control_value( p.hints.default_value );
add_port( p );
}
{
Port p( this, Port::INPUT, Port::CONTROL, "Elevation" );
p.hints.type = Port::Hints::LINEAR;
p.hints.ranged = true;
p.hints.minimum = -90.0f;
p.hints.maximum = 90.0f;
p.hints.default_value = 0.0f;
p.connect_to( new float );
p.control_value( p.hints.default_value );
add_port( p );
}
{
Port p( this, Port::INPUT, Port::CONTROL, "Radius" );
p.hints.type = Port::Hints::LINEAR;
p.hints.ranged = true;
p.hints.minimum = 0.0f;
p.hints.maximum = max_distance;
p.hints.default_value = 1.0f;
p.connect_to( new float );
p.control_value( p.hints.default_value );
add_port( p );
}
{
Port p( this, Port::INPUT, Port::CONTROL, "Highpass (Hz)" );
p.hints.type = Port::Hints::LINEAR;
p.hints.ranged = true;
p.hints.minimum = 0.0f;
p.hints.maximum = 600.0f;
p.hints.default_value = 200.0f;
p.connect_to( new float );
p.control_value( p.hints.default_value );
add_port( p );
}
{
Port p( this, Port::INPUT, Port::CONTROL, "Width" );
p.hints.type = Port::Hints::LINEAR;
p.hints.ranged = true;
p.hints.minimum = -90.0f;
p.hints.maximum = 90.0f;
p.hints.default_value = 90.0f;
p.hints.visible = false;
p.connect_to( new float );
p.control_value( p.hints.default_value );
add_port( p );
}
log_create();
_panner = new ambisonic_panner();
labelsize(9);
color( FL_DARK1 );
copy_label( "Spatializer" );
align(FL_ALIGN_LEFT|FL_ALIGN_TOP|FL_ALIGN_INSIDE);
gain_smoothing.sample_rate( sample_rate() );
delay_smoothing.cutoff( 0.5f );
delay_smoothing.sample_rate( sample_rate() );
}
Spatializer_Module::~Spatializer_Module ( )
{
configure_inputs(0);
delete _panner;
delete (float*)control_input[0].buffer();
delete (float*)control_input[1].buffer();
delete (float*)control_input[2].buffer();
delete (float*)control_input[3].buffer();
delete (float*)control_input[4].buffer();
}
void
Spatializer_Module::handle_sample_rate_change ( nframes_t n )
{
gain_smoothing.sample_rate( n );
delay_smoothing.sample_rate( n );
for ( unsigned int i = 0; i < audio_input.size(); i++ )
{
_lowpass[i]->sample_rate( n );
_highpass[i]->sample_rate( n );
_delay[i]->sample_rate( n );
}
}
void
Spatializer_Module::draw ( void )
{
int W = 5;
child(0)->size( w() - W, h() );
Module::draw_box(x(),y(),w() - W,h());
Module::draw_label(x() + 4,y(),w() - W,h());
Module *m = this;
fl_color( fl_darker( FL_FOREGROUND_COLOR ) );
int spacing, offset;
int ni = jack_output.size();
spacing = h() / ni;
offset = spacing / 2;
for ( int i = ni; i--; )
{
int xi = offset + ( spacing * i );
fl_rectf( m->x() + m->w() - W, m->y() + xi, W, 2 );
}
}
void
Spatializer_Module::process ( nframes_t nframes )
{
if ( !bypass() )
{
float azimuth = control_input[0].control_value();
float elevation = control_input[1].control_value();
float radius = control_input[2].control_value();
float highpass_freq = control_input[3].control_value();
float width = control_input[4].control_value();
float delay_seconds = 0.0f;
if ( radius > 1.0f )
delay_seconds = ( radius - 1.0f ) / 340.29f;
/* direct sound follows inverse square law */
/* but it's just the inverse as far as SPL goes */
/* let's not go nuts... */
if ( radius < 0.01f )
radius = 0.01f;
float gain = 1.0f / radius;
float cutoff_frequency = gain * LOWPASS_FREQ;
sample_t gainbuf[nframes];
sample_t delaybuf[nframes];
bool use_gainbuf = gain_smoothing.apply( gainbuf, nframes, gain );
bool use_delaybuf = delay_smoothing.apply( delaybuf, nframes, delay_seconds );
for ( unsigned int i = 0; i < audio_input.size(); i++ )
{
sample_t *buf = (sample_t*) audio_input[i].buffer();
/* frequency effects */
_highpass[i]->run_highpass( buf, highpass_freq, nframes );
_lowpass[i]->run_lowpass( buf, cutoff_frequency, nframes );
/* send to late reverb */
if ( i == 0 )
buffer_copy( (sample_t*)jack_output[0].buffer(nframes), buf, nframes );
else
buffer_mix( (sample_t*)jack_output[0].buffer(nframes), buf, nframes );
/* /\* FIXME: use smoothed value... *\/ */
/* buffer_apply_gain( (sample_t*)jack_output[0].buffer(nframes), nframes, 1.0f / sqrt(D) ); */
if ( use_delaybuf )
_delay[i]->run( buf, delaybuf, 0, nframes );
else
_delay[i]->run( buf, 0, delay_seconds, nframes );
}
if ( audio_input.size() == 1 )
{
_panner->run_mono( (sample_t*)audio_input[0].buffer(),
(sample_t*)audio_output[0].buffer(),
(sample_t*)audio_output[1].buffer(),
(sample_t*)audio_output[2].buffer(),
(sample_t*)audio_output[3].buffer(),
azimuth,
elevation,
nframes );
}
else
{
_panner->run_stereo( (sample_t*)audio_input[0].buffer(),
(sample_t*)audio_input[1].buffer(),
(sample_t*)audio_output[0].buffer(),
(sample_t*)audio_output[1].buffer(),
(sample_t*)audio_output[2].buffer(),
(sample_t*)audio_output[3].buffer(),
azimuth,
elevation,
width,
nframes );
}
/* send to early reverb */
for ( int i = 4; i--; )
buffer_copy( (sample_t*)jack_output[1 + i].buffer(nframes),
(sample_t*)audio_output[0 + i].buffer(),
nframes );
/* gain effects */
if ( use_gainbuf )
{
for ( int i = 4; i--; )
buffer_apply_gain_buffer( (sample_t*)audio_output[i].buffer(), gainbuf, nframes );
}
else
{
for ( int i = 4; i--; )
buffer_apply_gain( (sample_t*)audio_output[i].buffer(), nframes, gain );
}
}
}
bool
Spatializer_Module::configure_inputs ( int n )
{
output_connection_handle->show();
output_connection_handle->tooltip( "Late Reverb" );
output_connection2_handle->show();
output_connection2_handle->tooltip( "Early Reverb" );
int on = audio_input.size();
if ( n > on )
{
for ( int i = n - on; i--; )
{
{ filter *o = new filter();
o->sample_rate( sample_rate() );
_lowpass.push_back( o );
}
{
filter *o = new filter();
o->sample_rate( sample_rate() );
_highpass.push_back( o );
}
{
delay *o = new delay( max_distance / 340.29f );
o->sample_rate( sample_rate() );
_delay.push_back( o );
}
add_port( Port( this, Port::INPUT, Port::AUDIO ) );
}
}
else if ( n < on )
{
for ( int i = on - n; i--; )
{
delete _lowpass.back();
_lowpass.pop_back();
delete _highpass.back();
_highpass.pop_back();
delete _delay.back();
_delay.pop_back();
audio_input.pop_back();
}
}
control_input[4].hints.visible = audio_input.size() == 2;
if ( n == 0 )
{
remove_jack_outputs();
audio_output.clear();
audio_input.clear();
}
else
{
if ( audio_output.size() != 4 )
{
for ( int i = 0; i < 4; i++ )
{
add_port( Port( this, Port::OUTPUT, Port::AUDIO ) );
}
}
if ( jack_output.size() != 5 )
{
add_jack_output( "late reverb", 0 );
add_jack_output( "early reverb", 0 );
add_jack_output( "early reverb", 1 );
add_jack_output( "early reverb", 2 );
add_jack_output( "early reverb", 3 );
}
}
_connection_handle_outputs[0][0] = 0;
_connection_handle_outputs[0][1] = 1;
_connection_handle_outputs[1][0] = 1;
_connection_handle_outputs[1][1] = jack_output.size();
return true;
}