Change pitch of notes if PitchTrack is set

Now if a Gig file provides a few samples per octave, it'll change the
pitch of the sample specified for a note instead of just assuming it is
the right pitch.

Also, fixed issue where if attack length was zero the note would never
sound.
This commit is contained in:
Garrett
2014-11-17 16:55:38 -08:00
parent 2c0b1ef4b0
commit 71b6814729
2 changed files with 257 additions and 179 deletions

View File

@@ -84,7 +84,6 @@ struct GIGPluginData
GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) :
Instrument( _instrument_track, &gigplayer_plugin_descriptor ),
m_srcState( NULL ),
m_instance( NULL ),
m_instrument( NULL ),
m_filename( "" ),
@@ -112,11 +111,6 @@ GigInstrument::~GigInstrument()
{
engine::mixer()->removePlayHandles( instrumentTrack() );
freeInstance();
if( m_srcState != NULL )
{
src_delete( m_srcState );
}
}
@@ -319,7 +313,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * )
const uint velocity = _n->midiVelocity( baseVelocity );
QMutexLocker locker( &m_notesMutex );
m_notes.push_back( GigNote( midiNote, velocity ) );
m_notes.push_back( GigNote( midiNote, velocity, _n->unpitchedFrequency() ) );
}
}
@@ -331,6 +325,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * )
void GigInstrument::play( sampleFrame * _working_buffer )
{
const fpp_t frames = engine::mixer()->framesPerPeriod();
const int rate = engine::mixer()->processingSampleRate();
// Initialize to zeros
std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) );
@@ -345,18 +340,6 @@ void GigInstrument::play( sampleFrame * _working_buffer )
return;
}
// Data for converting sample rate
int oldRate = -1;
int newRate = engine::mixer()->processingSampleRate();
bool sampleError = false;
bool sampleConvert = false;
// How many frames we'll be grabbing from the sample
int samples = frames;
// How many we actually used from the sample
int used = frames;
for( QList<GigNote>::iterator it = m_notes.begin(); it != m_notes.end(); ++it )
{
// Process notes in the KeyUp state, adding release samples if desired
@@ -418,44 +401,8 @@ void GigInstrument::play( sampleFrame * _working_buffer )
break;
}
}
// Verify all the samples have the same rate
for( QList<GigSample>::iterator sample = it->samples.begin();
sample != it->samples.end(); ++sample )
{
int currentRate = sample->sample->SamplesPerSecond;
if( oldRate == -1 )
{
oldRate = currentRate;
}
else if( oldRate != currentRate )
{
qCritical() << "GigInstrument: not all samples are the same rate, not converting";
sampleError = true;
}
}
}
// If all samples have the same sample rate and it's not the output sample
// rate, then we'll convert sample rates
if( oldRate != -1 && sampleError != true && oldRate != newRate )
{
sampleConvert = true;
// Read a different number of samples depending on the sample rate, but
// resample it to always output the right number of frames
samples = frames * oldRate / newRate;
// We need a bit of margin so we don't get glitching
samples += MARGIN[m_interpolation];
}
// Create buffers
sampleFrame sampleData[samples]; // Individual note signal, overwritten for each note
sampleFrame convertBuf[samples]; // Sum of note signals
std::memset( &convertBuf[0][0], 0, DEFAULT_CHANNELS * samples * sizeof( float ) );
// Fill buffer with portions of the note samples
for( QList<GigNote>::iterator it = m_notes.begin(); it != m_notes.end(); ++it )
{
@@ -474,6 +421,35 @@ void GigInstrument::play( sampleFrame * _working_buffer )
continue;
}
// Will change if resampling
bool resample = false;
int samples = frames; // How many to grab
int used = frames; // How many we used
float freq_factor = 1.0; // How to resample
// Resample to be the correct pitch when the sample provided isn't
// solely for this one note (e.g. one or two samples per octave) or
// we are processing at a different sample rate
if( sample->pitchtrack == true || rate != sample->sample->SamplesPerSecond )
{
resample = true;
// Factor just for resampling
freq_factor = 1.0 * rate / sample->sample->SamplesPerSecond;
// Factor for pitch shifting as well as resampling
if( sample->pitchtrack == true )
{
freq_factor *= sample->freqFactor;
}
// We need a bit of margin so we don't get glitching
samples = frames / freq_factor + MARGIN[m_interpolation];
}
// This note's data
sampleFrame sampleData[samples];
// Set the position to where we currently are in the sample
sample->sample->SetPos( sample->pos ); // Note: not thread safe
@@ -544,18 +520,25 @@ void GigInstrument::play( sampleFrame * _working_buffer )
for( int i = 0; i < samples; ++i )
{
double amplitude = copy.value();
float amplitude = copy.value();
sampleData[i][0] *= amplitude;
sampleData[i][1] *= amplitude;
}
// Save to output buffer
if( sampleConvert == true )
// Output the data resampling if needed
if( resample == true )
{
for( int i = 0; i < samples; ++i )
sampleFrame convertBuf[frames];
// Only output if resampling is successful (note that "used" is output)
if( sample->convertSampleRate( *sampleData, *convertBuf, samples, frames,
freq_factor, used ) )
{
convertBuf[i][0] += sampleData[i][0];
convertBuf[i][1] += sampleData[i][1];
for( int i = 0; i < frames; ++i )
{
_working_buffer[i][0] += convertBuf[i][0];
_working_buffer[i][1] += convertBuf[i][1];
}
}
}
else
@@ -566,36 +549,10 @@ void GigInstrument::play( sampleFrame * _working_buffer )
_working_buffer[i][1] += sampleData[i][1];
}
}
}
}
// Convert sample rate if needed
if( sampleConvert == true )
{
// If an error occurred, it's better to render nothing than have some
// screeching high-volume noise
if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames,
oldRate, newRate, used ) )
{
std::memset( &_working_buffer[0][0], 0,
DEFAULT_CHANNELS * frames * sizeof( float ) );
}
}
// Update the note positions with how many samples we actually used
for( QList<GigNote>::iterator it = m_notes.begin(); it != m_notes.end(); ++it )
{
if( it->state == PlayingKeyDown || it->state == PlayingKeyUp )
{
for( QList<GigSample>::iterator sample = it->samples.begin();
sample != it->samples.end(); ++sample )
{
if( sample->sample != NULL )
{
sample->pos += used;
sample->adsr.inc( used );
}
}
// Update note position with how many samples we actually used
sample->pos += used;
sample->adsr.inc( used );
}
}
@@ -690,7 +647,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample )
if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh )
{
float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );;
float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate();
float length = (float) pSample->SamplesTotal / engine::mixer()->processingSampleRate();
// TODO: sample panning? looping? crossfade different layers?
@@ -704,8 +661,8 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample )
attenuation *= pDimRegion->SampleAttenuation;
}
gignote.samples.push_back( GigSample( pSample, attenuation,
ADSR( pDimRegion, pSample->SamplesPerSecond ) ) );
gignote.samples.push_back( GigSample( pSample, pDimRegion,
attenuation, m_interpolation, gignote.frequency ) );
}
}
@@ -829,67 +786,13 @@ void GigInstrument::getInstrument()
bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf,
int oldSize, int newSize, int oldRate, int newRate, int& used )
{
SRC_DATA src_data;
src_data.data_in = &oldBuf[0];
src_data.data_out = &newBuf[0];
src_data.input_frames = oldSize;
src_data.output_frames = newSize;
src_data.src_ratio = (double) newRate / oldRate;
src_data.end_of_input = 0;
m_srcMutex.lock();
int error = src_process( m_srcState, &src_data );
m_srcMutex.unlock();
used = src_data.input_frames_used;
if( error != 0 )
{
qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) );
return false;
}
if( src_data.output_frames_gen > newSize )
{
qCritical( "GigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, newSize );
return false;
}
if( oldSize != 0 && src_data.output_frames_gen == 0 )
{
qCritical( "GigInstrument: could not resample, no frames generated" );
return false;
}
return true;
}
// Recreate the libsamplerate instance each time the sample rate is changed.
// We could also create it once and then reset it whenever the sample rate
// changes. However, if we ever switch to changing interpolations, you have to
// recreate it.
// Since the sample rate changes when we start an export, clear all the
// currently-playing notes when we get this signal. Then, the export won't
// include leftover notes that were playing in the program.
void GigInstrument::updateSampleRate()
{
QMutexLocker locker( &m_srcMutex );
if( m_srcState != NULL )
{
src_delete( m_srcState );
}
int error;
m_srcState = src_new( m_interpolation, DEFAULT_CHANNELS, &error );
if( m_srcState == NULL || error != 0 )
{
qCritical( "error while creating libsamplerate data structure in GigInstrument::updateSampleRate()" );
}
QMutexLocker locker( &m_notesMutex );
m_notes.clear();
}
@@ -1095,11 +998,160 @@ void GigInstrumentView::showPatchDialog()
// Store information related to playing a sample from the GIG file
GigSample::GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr )
: sample( pSample ),
attenuation( attenuation ),
adsr( adsr ),
pos( 0 )
GigSample::GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion,
float attenuation, int interpolation, float desiredFreq )
: sample( pSample ), attenuation( attenuation ), pos( 0 ),
pitchtrack( false ), interpolation( interpolation ),
srcState( NULL ), sampleFreq( 0 ), freqFactor( 1 )
{
if( pSample != NULL && pDimRegion != NULL )
{
// Note: we don't create the libsamplerate object here since we always
// also call the copy constructor when appending to the end of the
// QList. We'll create it only in the copy constructor so we only have
// to create it once.
pitchtrack = pDimRegion->PitchTrack;
// Calculate note pitch and frequency factor only if we're actually
// going to be changing the pitch of the notes
if( pitchtrack == true )
{
// Calculate what frequency the provided sample is
float semitones = 1.0 * pSample->FineTune / 0xFFFFFFFF;
sampleFreq = 440.0 * powf( 2, 1.0 / 12 * (
1.0 * pDimRegion->UnityNote - 69 ) + semitones );
freqFactor = sampleFreq / desiredFreq;
}
// The sample rate we pass in is affected by how we are going to be
// resampling the note so that a 1.5 second release ends up being 1.5
// seconds after resampling
adsr = ADSR( pDimRegion, pSample->SamplesPerSecond / freqFactor );
}
}
GigSample::~GigSample()
{
if( srcState != NULL )
{
src_delete( srcState );
}
}
GigSample::GigSample( const GigSample& g )
: sample( g.sample ), attenuation( g.attenuation ), adsr( g.adsr ),
pos( g.pos ), pitchtrack( g.pitchtrack ), interpolation( g.interpolation ),
srcState( NULL ), sampleFreq( g.sampleFreq ), freqFactor( g.freqFactor )
{
// On the copy, we want to create the object
updateSampleRate();
}
GigSample& GigSample::operator=( const GigSample& g )
{
sample = g.sample;
attenuation = g.attenuation;
adsr = g.adsr;
pos = g.pos;
pitchtrack = g.pitchtrack;
interpolation = g.interpolation;
srcState = NULL;
sampleFreq = g.sampleFreq;
freqFactor = g.freqFactor;
if( g.srcState != NULL )
{
updateSampleRate();
}
return *this;
}
void GigSample::updateSampleRate()
{
if( srcState != NULL )
{
src_delete( srcState );
}
int error = 0;
srcState = src_new( interpolation, DEFAULT_CHANNELS, &error );
if( srcState == NULL || error != 0 )
{
qCritical( "error while creating libsamplerate data structure in GigSample" );
}
}
bool GigSample::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf,
int oldSize, int newSize, float freq_factor, int& used )
{
if( srcState == NULL )
{
return false;
}
SRC_DATA src_data;
src_data.data_in = &oldBuf[0];
src_data.data_out = &newBuf[0];
src_data.input_frames = oldSize;
src_data.output_frames = newSize;
src_data.src_ratio = freq_factor;
src_data.end_of_input = 0;
// We don't need to lock this assuming that we're only outputting the
// samples in one thread
int error = src_process( srcState, &src_data );
used = src_data.input_frames_used;
if( error != 0 )
{
qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) );
return false;
}
if( oldSize != 0 && src_data.output_frames_gen == 0 )
{
qCritical( "GigInstrument: could not resample, no frames generated" );
return false;
}
if( src_data.output_frames_gen > 0 && src_data.output_frames_gen < newSize )
{
qCritical() << "GigInstrument: not enough frames, wanted"
<< newSize << "generated" << src_data.output_frames_gen;
return false;
}
return true;
}
ADSR::ADSR()
: preattack( 0 ), attack( 0 ), decay1( 0 ), decay2( 0 ), infiniteSustain( false ),
sustain( 0 ), release( 0 ),
amplitude( 0 ), isAttack( true ), isRelease( false ), isDone( false ),
attackPosition( 0 ), attackLength( 0 ), decayLength( 0 ),
releasePosition( 0 ), releaseLength( 0 )
{
}
@@ -1130,6 +1182,12 @@ ADSR::ADSR( gig::DimensionRegion * region, int sampleRate )
attackLength = attack * sampleRate;
decayLength = decay1 * sampleRate; // TODO: ignoring decay2 for now
releaseLength = release * sampleRate;
// If there is no attack, start at the full amplitude
if( attackLength == 0 )
{
amplitude = 1.0;
}
}
}
@@ -1155,9 +1213,9 @@ bool ADSR::done()
// Return the current amplitude and increment internal positions
double ADSR::value()
float ADSR::value()
{
double currentAmplitude = amplitude;
float currentAmplitude = amplitude;
// If we're done, don't output any signal
if( isDone == true )

View File

@@ -91,16 +91,16 @@ struct Dimension
class ADSR
{
// From the file
double preattack; // initial amplitude (0-1)
double attack; // 0-60s
double decay1; // 0-60s
double decay2; // 0-60s
float preattack; // initial amplitude (0-1)
float attack; // 0-60s
float decay1; // 0-60s
float decay2; // 0-60s
bool infiniteSustain; // i.e., no decay2
double sustain; // sustain amplitude (0-1)
double release; // 0-60s
float sustain; // sustain amplitude (0-1)
float release; // 0-60s
// Used to calculate current amplitude
double amplitude;
float amplitude;
bool isAttack;
bool isRelease;
bool isDone;
@@ -111,10 +111,11 @@ class ADSR
int releaseLength;
public:
ADSR();
ADSR( gig::DimensionRegion * region, int sampleRate );
void keyup(); // We will begin releasing starting now
bool done(); // Is this sample done playing?
double value(); // What's the current amplitude
float value(); // What's the current amplitude
void inc( int num ); // Increment internal positions by num
} ;
@@ -126,12 +127,38 @@ public:
class GigSample
{
public:
GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr );
GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion,
float attenuation, int interpolation, float desiredFreq );
~GigSample();
// Needed when initially creating in QList
GigSample( const GigSample& g );
GigSample& operator=( const GigSample& g );
// Needed since libsamplerate stores data internally between calls
void updateSampleRate();
bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf,
int oldSize, int newSize, float freq_factor, int& used );
gig::Sample * sample;
float attenuation;
ADSR adsr;
int pos; // Position in sample
// The position in sample
int pos;
// Whether to change the pitch of the samples, e.g. if there's only one
// sample per octave and you want that sample pitch shifted for the rest of
// the notes in the octave, this will be true
bool pitchtrack;
// Used to convert sample rates
int interpolation;
SRC_STATE * srcState;
// Used changing the pitch of the note if desired
float sampleFreq;
float freqFactor;
} ;
@@ -163,11 +190,12 @@ public:
int velocity;
bool release; // Whether to trigger a release sample on key up
GigState state;
float frequency;
QList<GigSample> samples;
GigNote( int midiNote, int velocity )
GigNote( int midiNote, int velocity, float frequency )
: midiNote( midiNote ), velocity( velocity ),
release( false ), state( KeyDown )
release( false ), state( KeyDown ), frequency( frequency )
{
}
} ;
@@ -226,9 +254,6 @@ public slots:
private:
// Used to convert sample rates
SRC_STATE * m_srcState;
// The GIG file and instrument we're using
GigInstance * m_instance;
gig::Instrument * m_instrument;
@@ -243,7 +268,6 @@ private:
// Locking for the data
QMutex m_synthMutex;
QMutex m_srcMutex;
QMutex m_notesMutex;
// Used for resampling
@@ -271,10 +295,6 @@ private:
// samples
void addSamples( GigNote & gignote, bool wantReleaseSample );
// Convert sample rates
bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf,
int oldSize, int newSize, int oldRate, int newRate, int& used );
friend class GigInstrumentView;
signals: