diff options
author | John Glover <glover.john@gmail.com> | 2011-07-08 18:06:21 +0100 |
---|---|---|
committer | John Glover <glover.john@gmail.com> | 2011-07-08 18:06:21 +0100 |
commit | d6073e01c933c77f1e2bc3c3fe1126d617003549 (patch) | |
tree | 695d23677c5b84bf3a0f88fbd4959b4f7cbc0e90 /src/loris/SpcFile.C | |
parent | 641688b252da468eb374674a0dbaae1bbac70b2b (diff) | |
download | simpl-d6073e01c933c77f1e2bc3c3fe1126d617003549.tar.gz simpl-d6073e01c933c77f1e2bc3c3fe1126d617003549.tar.bz2 simpl-d6073e01c933c77f1e2bc3c3fe1126d617003549.zip |
Start adding Loris files
Diffstat (limited to 'src/loris/SpcFile.C')
-rw-r--r-- | src/loris/SpcFile.C | 1311 |
1 files changed, 1311 insertions, 0 deletions
diff --git a/src/loris/SpcFile.C b/src/loris/SpcFile.C new file mode 100644 index 0000000..66a7a17 --- /dev/null +++ b/src/loris/SpcFile.C @@ -0,0 +1,1311 @@ +/* + * This is the Loris C++ Class Library, implementing analysis, + * manipulation, and synthesis of digitized sounds using the Reassigned + * Bandwidth-Enhanced Additive Sound Model. + * + * Loris is Copyright (c) 1999-2010 by Kelly Fitz and Lippold Haken + * + * 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; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * + * SpcFile.C + * + * Implementation of SpcFile class for Partial import and export for + * real-time synthesis in Kyma. + * + * Spc files always represent a number of Partials that is a power of + * two. This is not necessary for purely-sinusoidal files, but might be + * (not clear) for enhanced data to be properly processed in Kyma. + * + * All of this is kind of disgusting right now. This code has evolved + * somewhat randomly, and we are awaiting full support for bandwidth- + * enhanced data in Kyma.. + * + * Kelly Fitz, 8 Jan 2003 + * loris@cerlsoundgroup.org + * + * http://www.cerlsoundgroup.org/Loris/ + * + */ + +#if HAVE_CONFIG_H + #include "config.h" +#endif + +#include "SpcFile.h" + +#include "AiffData.h" +#include "Breakpoint.h" +#include "BigEndian.h" +#include "LorisExceptions.h" +#include "Marker.h" +#include "Notifier.h" +#include "PartialUtils.h" + +#include <algorithm> +#include <cmath> +#include <fstream> + +#if defined(HAVE_M_PI) && (HAVE_M_PI) + const double Pi = M_PI; +#else + const double Pi = 3.14159265358979324; +#endif + + +// Define this if SpcFiles should always have a number of +// Partials that is a power of two. Cannot decide whether +// or not they should. Lip? +#define PO2 + + +using std::exp; +using std::log; +using std::sqrt; + +// begin namespace +namespace Loris { + +// --------------------------------------------------------------------------- +// constants, can, or should, these be made variable? +// --------------------------------------------------------------------------- + +// can't we change this? Seems like we could, but +// its part of the size of the magic junk in the +// Sose chunk. +const int LargestLabel = 256; // max number of partials for SPC file + + // 1 Mar 05 + // Found that this cannot actually be 512 for enhanced + // Partials, it crashes with a segmentation fault above 256. + // Unfortunately, whatever is causing the problem is not + // using this constant, but some other hardcoded thing. + // if I change this to 256, then I can still export + // 256 enhanced Partials, but I don't know what to change + // to allow 512! Ugh! + // -kel + + +// this used to be hard coded into Partial, don't know +// whether it is needed to make spc files work. +const double Fade = 0.001; + +// this always has to be 24 bits, 1 channel +const int Bps = 24; +const int Nchans = 1; + + +// --------------------------------------------------------------------------- +// static helper prototypes, defined at bottom +// --------------------------------------------------------------------------- +static void +configureEnvelopeDataCk( SoundDataCk & ck, const SpcFile::partials_type & partials ); + +static void +configureSosMarkerCk( MarkerCk & ck, const std::vector< Marker > & markers ); + +static void +configureSosEnvelopesCk( SosEnvelopesCk & ck ); + +static std::ostream & +writeSosEnvelopesChunk( std::ostream & s, const SosEnvelopesCk & ck ); + +static void +configureExportStruct( const SpcFile::partials_type & partials, double midipitch, + double endApproachTime, int enhanced ); + +static unsigned long getNumSampleFrames( void ); + +const int SpcFile::MinNumPartials = 32; +const double SpcFile::DefaultRate = 44100.; + +// -- construction -- +// --------------------------------------------------------------------------- +// SpcFile constructor from filename +// --------------------------------------------------------------------------- +// Initialize an instance of SpcFile by importing envelope parameter +// streams from the file having the specified filename or path. +// +SpcFile::SpcFile( const std::string & filename ) : + notenum_( 60 ), + rate_( DefaultRate ) +{ + readSpcData( filename ); +} + +// --------------------------------------------------------------------------- +// SpcFile constructor, empty. +// --------------------------------------------------------------------------- +// Initialize an instance of SpcFile having the specified fractional +// MIDI note number, and no Partials (or envelope parameter streams). +// +SpcFile::SpcFile( double midiNoteNum ) : + notenum_( midiNoteNum ), + rate_( DefaultRate ) +{ + growPartials( MinNumPartials ); +} + +// --------------------------------------------------------------------------- +// write +// --------------------------------------------------------------------------- +// Export the phase-correct bandwidth-enhanced envelope parameter +// streams represented by this SpcFile to the file having the specified +// filename or path. +// +// A nonzero endApproachTime indicates that the Partials do not include a +// release or decay, but rather end in a static spectrum corresponding to the +// final Breakpoint values of the partials. The endApproachTime specifies how +// long before the end of the sound the amplitude, frequency, and bandwidth +// values are to be modified to make a gradual transition to the static spectrum. +// +// If the endApproachTime is not specified, it is assumed to be zero, +// corresponding to Partials that decay or release normally. +// +void +SpcFile::write( const std::string & filename, double endApproachTime ) +{ + write( filename, true, endApproachTime ); +} + +// --------------------------------------------------------------------------- +// write +// --------------------------------------------------------------------------- +// Export the pure sinsoidal (omitting phase and bandwidth data) envelope +// parameter streams represented by this SpcFile to the file having the +// specified filename or path. +// +// A nonzero endApproachTime indicates that the Partials do not include a +// release or decay, but rather end in a static spectrum corresponding to the +// final Breakpoint values of the partials. The endApproachTime specifies how +// long before the end of the sound the amplitude, frequency, and bandwidth +// values are to be modified to make a gradual transition to the static spectrum. +// +// If the endApproachTime is not specified, it is assumed to be zero, +// corresponding to Partials that decay or release normally. +// +void +SpcFile::writeSinusoidal( const std::string & filename, double endApproachTime ) +{ + write( filename, false, endApproachTime ); +} + +// --------------------------------------------------------------------------- +// write +// --------------------------------------------------------------------------- +// Export the envelope parameter streams represented by this SpcFile to +// the file having the specified filename or path. Export phase-correct +// bandwidth-enhanced envelope parameter streams if enhanced is true +// (the default), or pure sinsoidal streams otherwise. +// +// +// A nonzero endApproachTime indicates that the Partials do not include a +// release or decay, but rather end in a static spectrum corresponding to the +// final Breakpoint values of the partials. The endApproachTime specifies how +// long before the end of the sound the amplitude, frequency, and bandwidth +// values are to be modified to make a gradual transition to the static spectrum. +// +// If the endApproachTime is not specified, it is assumed to be zero, +// corresponding to Partials that decay or release normally. +// +// This version of write is deprecated, but is handy for internal use, +// called by the above write and writeSiunsoidal members. +// +void +SpcFile::write( const std::string & filename, bool enhanced, double endApproachTime ) +{ + if ( endApproachTime < 0 ) + { + Throw( InvalidArgument, "End Approach Time may not be negative." ); + } + + std::ofstream s( filename.c_str(), std::ofstream::binary ); + if ( ! s ) + { + std::string s = "Could not create file \""; + s += filename; + s += "\". Failed to write Spc file."; + Throw( FileIOException, s ); + } + + // have to do this before trying to do anything else: + configureExportStruct( partials_, notenum_, endApproachTime, enhanced ); + + unsigned long dataSize = 0; + + CommonCk commonChunk; + configureCommonCk( commonChunk, getNumSampleFrames(), Nchans, Bps, rate_ ); + dataSize += commonChunk.header.size + sizeof(CkHeader); + + SoundDataCk soundDataChunk; + configureEnvelopeDataCk( soundDataChunk, partials_ ); + dataSize += soundDataChunk.header.size + sizeof(CkHeader); + + InstrumentCk instrumentChunk; + configureInstrumentCk( instrumentChunk, notenum_ ); + dataSize += instrumentChunk.header.size + sizeof(CkHeader); + + MarkerCk markerChunk; + if ( ! markers_.empty() ) + { + configureSosMarkerCk( markerChunk, markers_ ); + dataSize += markerChunk.header.size + sizeof(CkHeader); + } + + SosEnvelopesCk soseChunk; + configureSosEnvelopesCk( soseChunk ); + dataSize += soseChunk.header.size + sizeof(CkHeader); + + ContainerCk containerChunk; + configureContainer( containerChunk, dataSize ); + + try + { + writeContainer( s, containerChunk ); + writeCommonData( s, commonChunk ); + if ( ! markers_.empty() ) + writeMarkerData( s, markerChunk ); + writeInstrumentData( s, instrumentChunk ); + writeSosEnvelopesChunk( s, soseChunk ); + writeSampleData( s, soundDataChunk ); + + s.close(); + } + catch ( Exception & ex ) + { + ex.append( " Failed to write Spc file." ); + throw; + } +} + +// -- access -- + +// --------------------------------------------------------------------------- +// markers +// --------------------------------------------------------------------------- +// Return a reference to the Marker (see Marker.h) container +// for this SpcFile. +// +SpcFile::markers_type & +SpcFile::markers( void ) +{ + return markers_; +} + +const SpcFile::markers_type & +SpcFile::markers( void ) const +{ + return markers_; +} + +// --------------------------------------------------------------------------- +// midiNoteNumber +// --------------------------------------------------------------------------- +// Return the fractional MIDI note number assigned to this SpcFile. +// If the sound has no definable pitch, note number 60.0 is used. +// +double +SpcFile::midiNoteNumber( void ) const +{ + return notenum_; +} + +// --------------------------------------------------------------------------- +// partials +// --------------------------------------------------------------------------- +// Return a read-only (const) reference to the bandwidth-enhanced +// Partials represented by the envelope parameter streams in this SpcFile. +// +const SpcFile::partials_type & +SpcFile::partials( void ) const +{ + return partials_; +} + +// --------------------------------------------------------------------------- +// sampleRate +// --------------------------------------------------------------------------- +// Return the sampling freqency in Hz for the spc data in this +// SpcFile. This is the rate at which Kyma must be running to ensure +// proper playback of bandwidth-enhanced Spc data. +// +double +SpcFile::sampleRate( void ) const +{ + return rate_; +} + +// -- mutation -- + +// --------------------------------------------------------------------------- +// addPartial +// --------------------------------------------------------------------------- +// Add the specified Partial to the enevelope parameter streams +// represented by this SpcFile. +// +// A SpcFile can contain only one Partial having any given (non-zero) +// label, so an added Partial will replace a Partial having the +// same label, if such a Partial exists. +// +// This may throw an InvalidArgument exception if an attempt is made +// to add unlabeled Partials, or Partials labeled higher than the +// allowable maximum. +// +void +SpcFile::addPartial( const Partial & p ) +{ + addPartial( p, p.label() ); +} + +// --------------------------------------------------------------------------- +// addPartial +// --------------------------------------------------------------------------- +// Add a Partial, assigning it the specified label (and position in the +// Spc data). +// +// A SpcFile can contain only one Partial having any given (non-zero) +// label, so an added Partial will replace a Partial having the +// same label, if such a Partial exists. +// +// This may throw an InvalidArgument exception if an attempt is made +// to add unlabeled Partials, or Partials labeled higher than the +// allowable maximum. +// +void +SpcFile::addPartial( const Partial & p, int label ) +{ + if ( p.label() == 0 ) + { + Throw( InvalidArgument, "Spc Partials must be labeled." ); + } + if ( label < 1 ) + { + Throw( InvalidArgument, "Spc Partials must have positive labels." ); + } + if ( label > LargestLabel ) + { + Throw( InvalidArgument, "Spc Partial label is too large, cannot have more than 256." ); + } + + if ( label > partials_.size() ) + { + growPartials( label ); + } + + partials_[label - 1] = p; + partials_[label - 1].setLabel( label ); +} + +// --------------------------------------------------------------------------- +// setMidiNoteNumber +// --------------------------------------------------------------------------- +// Set the fractional MIDI note number assigned to this SpcFile. +// If the sound has no definable pitch, use note number 60.0 (the default). +// +void +SpcFile::setMidiNoteNumber( double nn ) +{ + if ( nn < 0 || nn > 128 ) + { + Throw( InvalidArgument, "MIDI note number outside of the valid range [1,128]" ); + } + notenum_ = nn; +} + +// --------------------------------------------------------------------------- +// setSampleRate +// --------------------------------------------------------------------------- +// Set the sampling freqency in Hz for the spc data in this +// SpcFile. This is the rate at which Kyma must be running to ensure +// proper playback of bandwidth-enhanced Spc data. +// +void +SpcFile::setSampleRate( double rate ) +{ + if ( rate <= 0 ) + { + Throw( InvalidArgument, "Sample rate must be positive." ); + } + rate_ = rate; +} + +// -- helpers -- + +// --------------------------------------------------------------------------- +// growPartials +// --------------------------------------------------------------------------- +// +void +SpcFile::growPartials( partials_type::size_type sz ) +{ + if ( partials_.size() < sz ) + { +#ifdef PO2 + partials_type::size_type po2sz = MinNumPartials; + while ( po2sz < sz ) + { + po2sz *= 2; + } + partials_.resize( po2sz ); +#else + partials_.resize( sz ); +#endif + for ( partials_type::size_type j = 0; j < partials_.size(); ++j ) + { + partials_[j].setLabel( j+1 ); + } + } +} + +// -- export structures -- + +// --------------------------------------------------------------------------- +// Export Structures +// --------------------------------------------------------------------------- +// + +// structure for export information +struct SpcExportInfo +{ + double midipitch; // note number (69.00 = A440) for spc file; + // this is the core parameter, others are, by default, + // computed from this one + double endApproachTime; // in seconds, this indicates how long before the end of the sound the + // amplitude, frequency, and bandwidth values are to be modified to + // make a gradual transition to the spectral content at the end, + // 0.0 indicates no such modifications are to be done + int numPartials; // number of partials in spc file + int fileNumPartials; // the actual number of partials plus padding to make a 2**n value + int enhanced; // true for bandwidth-enhanced spc file, false for pure sines + double startTime; // in seconds, time of first frame in spc file + double endTime; // in seconds, this indicates the time at which to truncate the end + // of the spc file, 0.0 indicates no truncation + double markerTime; // in seconds, this indicates time at which a marker is inserted in the + // spc file, 0.0 indicates no marker is desired + double sampleRate; // in hertz, intended sample rate for synthesis of spc file + double hop; // hop size, based on numPartials and sampleRate + double ampEpsilon; // small amplitude value (related to lsb value in spc file log-amp) +}; + +static struct SpcExportInfo spcEI; // yikky global spc Export information + + +// -- export helpers by Lippold -- + +// --------------------------------------------------------------------------- +// fileNumPartials +// --------------------------------------------------------------------------- +// Find number of partials in SOS file. This is the actual number of partials, +// plus padding to make a 2**n value. +// +static int fileNumPartials( int partials ) +{ + if ( partials <= 32 ) + return 32; + if ( partials <= 64 ) + return 64; + if ( partials <= 128 ) + return 128; + else if ( partials <= 256 ) + return 256; + else if ( partials <= LargestLabel ) + return LargestLabel; + + Throw( FileIOException, "Too many SPC partials!" ); + return LargestLabel; +} + +// --------------------------------------------------------------------------- +// envLog( ) +// --------------------------------------------------------------------------- +// For a range 0 to 1, this returns a log value, 0x0000 to 0xFFFF. +// +static unsigned long envLog( double floatingValue ) +{ + static double coeff = 65535.0 / log( 32768. ); + + return (unsigned long)( coeff * log( 32768.0 * floatingValue + 1.0 ) ); + +} // end of envLog( ) + +// --------------------------------------------------------------------------- +// envExp( ) +// --------------------------------------------------------------------------- +// For a range 0x0000 to 0xFFFF, this returns an exponentiated value in the range 0..1. +// This is the counterpart of SpcFile::envLog(). +// +static double envExp( long intValue ) +{ + static double coeff = 65535.0 / log( 32768. ); + + return ( exp( intValue / coeff ) - 1.0 ) / 32768.0; + +} // end of envExp( ) + +// --------------------------------------------------------------------------- +// getPhaseRefTime +// --------------------------------------------------------------------------- +// Find the time at which to reference phase. +// The time will be shortly after amplitude onset, if we are before the onset. +// +static double getPhaseRefTime( int label, const Partial & p, double time ) +{ +// Keep array of previous values to optimize spc export. +// This depends on this routine being called in increasing-time order. + static double prevPRT[LargestLabel + 1]; + if ( prevPRT[label] > time && time > spcEI.startTime ) + return prevPRT[ label ]; + +// Go forward to nonzero amplitude. + while ( p.amplitudeAt( time, Fade ) < spcEI.ampEpsilon && time < spcEI.endTime + spcEI.hop) + { + time += spcEI.hop; + } + + prevPRT[ label ] = time; + +// Use phase value at initial onset time. + return time; + +} + +// --------------------------------------------------------------------------- +// afbp +// --------------------------------------------------------------------------- +// Find amplitude, frequency, bandwidth, phase value. +// +static void afbp( const Partial & p, double time, double phaseRefTime, + double magMult, double freqMult, + double & amp, double & freq, double & bw, double & phase) +{ + +// Optional endApproachTime processing: +// Approach amp, freq, and bw values at endTime, and stick at endTime amplitude. +// We avoid a sudden transition when using stick-at-end-frame sustains. +// Compute weighting factor between "normal" envelope point and static point. + if ( spcEI.endApproachTime && time > spcEI.endTime - spcEI.endApproachTime ) + { + if ( time > p.endTime() && p.endTime() > spcEI.endTime - 2 * spcEI.hop) + time = p.endTime(); + double wt = ( spcEI.endTime - time ) / spcEI.endApproachTime; + amp = magMult * ( wt * p.amplitudeAt( time, Fade ) + + (1.0 - wt) * p.amplitudeAt( spcEI.endTime, Fade ) ); + freq = freqMult * ( wt * p.frequencyAt( time ) + (1.0 - wt) * p.frequencyAt( spcEI.endTime ) ); + bw = ( wt * p.bandwidthAt( time ) + (1.0 - wt) * p.bandwidthAt( spcEI.endTime ) ); + phase = p.phaseAt( time ); + } + +// If we are before the phase reference time, or on the final frame, +// use zero amp and offset phase. + else if ( time < phaseRefTime - spcEI.hop / 2 || time > spcEI.endTime - spcEI.hop / 2 ) + { + amp = 0.; + freq = freqMult * p.frequencyAt( phaseRefTime ); + bw = 0.; + phase = p.phaseAt( phaseRefTime ) - 2. * Pi * (phaseRefTime - time) * freq; + } + +// Use envelope values at "time". + else + { + amp = magMult * p.amplitudeAt( time, Fade ); + freq = freqMult * p.frequencyAt( time ); + bw = p.bandwidthAt( time ); + phase = p.phaseAt( time ); + } +} + +// --------------------------------------------------------------------------- +// pack +// --------------------------------------------------------------------------- +// Pack envelope breakpoint value for interpretation by Envelope Reader sounds +// in Kyma. The packed result is two 24-bit quantities, lval and rval. +// +// In lval, the log of the sine magnitude occupies the top 8 bits, the log of the +// frequency occupies the bottom 16 bits. +// +// In rval, the log of the noise magnitude occupies the top 8 bits, the scaled +// linear phase occupies the bottom 16 bits. +// +// lval and rval are pointers to 3-bytes each, filled in by this function. +// +static void pack( double amp, double freq, double bw, double phase, + Byte * lbytes, Byte * rbytes ) +{ + +// Set phase for one hop earlier, so that Kyma synthesis target phase is correct. +// Add offset to phase for difference between Kyma and Loris representation. + phase -= 2. * Pi * spcEI.hop * freq; + phase += Pi / 2; + +// Make phase into range 0..1. + phase = std::fmod( phase, 2. * Pi ); + while ( phase < 0. ) // used to be if, I think it should be while + { // -kel 12 May 2006 + phase += 2. * Pi; + } + double zeroToOnePhase = phase / (2. * Pi); + +// Make frequency into range 0..1. + double zeroToOneFreq = freq / 22050.0; // 0..1 , 1 is 22.050 kHz + +// Compute sine magnitude and noise magnitude from amp and bw. + double theSineMag = amp * sqrt( 1. - bw ); + double theNoiseMag = 64.0 * amp * sqrt( bw ); + if (theNoiseMag > 1.0) + theNoiseMag = 1.0; + +// Pack amp and freq into 24 least-significant bits of lval: +// 7 bits of log-sine-amplitude with 16 bits of zero to right. +// 16 bits of log-frequency with 0 bits of zero to right. + unsigned long lval; + lval = ( envLog( theSineMag ) & 0xFE00 ) << 7; + lval |= ( envLog( zeroToOneFreq ) & 0xFFFF ); + +// store in lbytes: +// store the sample bytes in big endian order, +// most significant byte first: + const int BytesPerSample = 3; + for ( int j = BytesPerSample; j > 0; --j ) + { + // mask the lowest byte after shifting: + *(lbytes++) = 0xFF & (lval >> (8*(j-1))); + } + +// Pack noise amp and phase into 24 least-significant bits of rval: +// 7 bits of log-noise-amplitude with 16 bits of zero to right. +// 16 bits of phase with 0 bits of zero to right. + unsigned long rval; + rval = ( envLog( theNoiseMag ) & 0xFE00 ) << 7; + rval |= ( (unsigned long) ( zeroToOnePhase * 0xFFFF ) ); + +// store in rbytes: +// store the sample bytes in big endian order, +// most significant byte first: + for ( int j = BytesPerSample; j > 0; --j ) + { + // mask the lowest byte after shifting: + *(rbytes++) = 0xFF & (rval >> (8*(j-1))); + } +} + +// --------------------------------------------------------------------------- +// packEnvelopes +// --------------------------------------------------------------------------- +// The partials should be labeled and distilled before this is called. +// +static bool notEmpty( const Partial & p ) { return p.size() > 0; } + +static void packEnvelopes( const SpcFile::partials_type & partials, + std::vector< Byte > & bytes ) +{ +// Assert( partials.size() == spcEI.fileNumPartials ); + + int frames = int( ( spcEI.endTime - spcEI.startTime ) / spcEI.hop ) + 1; + unsigned long dataSize = + frames * spcEI.fileNumPartials * ( 24 / 8 ) * (spcEI.enhanced ? 2 : 1); + bytes.clear(); + bytes.reserve( dataSize ); + + // get the reference partial; the lowest-nonzero-labeled partial with any breakpoints + SpcFile::partials_type::const_iterator pos = + std::find_if( partials.begin(), partials.end(), notEmpty ); + Assert( pos != partials.end() ); + const Partial & refPar = *pos; + int refLabel = refPar.label(); + Assert( (refLabel - 1) == (pos - partials.begin()) ); + + // write out one frame at a time: + for (double tim = spcEI.startTime; tim <= spcEI.endTime; tim += spcEI.hop ) + { + // for each frame, write one value for every partial: + // (this loop extends to the pad partials) + for (unsigned int label = 1; label <= spcEI.fileNumPartials; ++label ) + { + double amp, freq, bw, phase; + // find partial with the correct label + // if partial with the correct is empty, + // frequency-multiply the reference partial +#ifndef PO2 + if ( label > partials.size() || partials[ label - 1 ].size() == 0 ) +#else + if ( partials[ label - 1 ].size() == 0 ) +#endif + { + // find the reference time for the phase + double phaseRefTime = getPhaseRefTime( label, refPar, tim ); + + // find amplitude, frequency, bandwidth, phase value + double freqMult = (double) label / (double) refLabel; + double magMult = 0.0; + afbp( refPar, tim, phaseRefTime, magMult, freqMult, amp, freq, bw, phase ); + } + else + { + // find the reference time for the phase + double phaseRefTime = getPhaseRefTime( label, partials[ label - 1 ], tim ); + + // find amplitude, frequency, bandwidth, phase value + afbp( partials[ label - 1 ], tim, phaseRefTime, 1, 1, amp, freq, bw, phase ); + } + + // pack log amplitude and log frequency into 24-bit lval, + // log bandwidth and phase into 24-bit rval: + Byte leftbytes[3], rightbytes[3]; + pack( amp, freq, bw, phase, leftbytes, rightbytes); + + // pack integer samples into the Byte vector without + // byte swapping, they are already correctly packed + // (see pack above): + bytes.insert( bytes.end(), leftbytes, leftbytes + 3 ); + if ( spcEI.enhanced ) + { + bytes.insert( bytes.end(), rightbytes, rightbytes + 3 ); + } + } + } + + Assert( bytes.size() == dataSize ); +} + +// --------------------------------------------------------------------------- +// configureEnvelopeDataCk +// --------------------------------------------------------------------------- +// Configure a special SoundDataCk for exporting Spc envelopes. +// +static void +configureEnvelopeDataCk( SoundDataCk & ck, const SpcFile::partials_type & partials ) +{ + packEnvelopes( partials, ck.sampleBytes ); + + ck.header.id = SoundDataId; + + // size is everything after the header: + ck.header.size = sizeof(Uint_32) + // offset + sizeof(Uint_32) + // block size + ck.sampleBytes.size(); // sample data + + // no block alignment: + ck.offset = 0; + ck.blockSize = 0; +} + +// --------------------------------------------------------------------------- +// configureSosMarkerCk +// --------------------------------------------------------------------------- +// Spc needs a special version of this, because Marker times have to be +// rounded to the nearest frame. +// +void +configureSosMarkerCk( MarkerCk & ck, const std::vector< Marker > & markers ) +{ + ck.header.id = MarkerId; + + // accumulate data size + Uint_32 dataSize = sizeof(Uint_16); // num markers + + ck.numMarkers = markers.size(); + ck.markers.resize( markers.size() ); + for ( int j = 0; j < markers.size(); ++j ) + { + MarkerCk::Marker & m = ck.markers[j]; + m.markerID = j+1; + //m.position = Uint_32((markers[j].time() * srate) + 0.5); + + // align marker with nearest frame time: + m.position = Uint_32( markers[j].time() / spcEI.hop ) + * spcEI.fileNumPartials + * ( spcEI.enhanced ? 2 : 1 ); + + m.markerName = markers[j].name(); + + #define MAX_PSTRING_CHARS 254 + if ( m.markerName.size() > MAX_PSTRING_CHARS ) + m.markerName.resize( MAX_PSTRING_CHARS ); + + // the size of a pascal string is the number of + // characters plus the size byte, plus the terminal '\0': + // + // Actualy, at least one web source indicates that Pascal + // strings are not null-terminated, but that they _are_ + // padded with an extra (not part of the count) byte + // if necessary to ensure that the total length (including + // count) is even, and this seems to work better with other + // programs (e.g. Kyma) + if ( m.markerName.size()%2 == 0 ) + m.markerName += '\0'; + dataSize += sizeof(Uint_16) + sizeof(Uint_32) + (m.markerName.size() + 1); + } + + // must be an even number of bytes + if ( dataSize%2 ) + ++dataSize; + + ck.header.size = dataSize; +} + + +// --------------------------------------------------------------------------- +// configureSosEnvelopesCk +// --------------------------------------------------------------------------- +// Configure a the application-specific chunk for exporting Spc envelopes. +// +static void +configureSosEnvelopesCk( SosEnvelopesCk & ck ) +{ + ck.header.id = ApplicationSpecificId; + + // size is everything after the header: + ck.header.size = sizeof(Uint_32) + // signature + sizeof(Uint_32) + // enhanced + sizeof(Uint_32) + // validPartials + // this last bit is a big, obsolete array, that + // we now use two positions in, an they aren't + // even the first two! Truly nasty. + 4*LargestLabel + 8 * sizeof(Int_32);// initPhase[] et al + + ck.signature = SosEnvelopesId; + + ck.enhanced = spcEI.enhanced; + + // the number of partials is doubled in bandwidth-enhanced spc files + ck.validPartials = spcEI.numPartials * ( spcEI.enhanced ? 2 : 1 ); + + // resolution in microseconds + ck.resolution = long( 1000000.0 * spcEI.hop ); + + // all partials quasiharmonic + // the number of partials is doubled in bandwidth-enhanced spc files + ck.quasiHarmonic = spcEI.numPartials * ( spcEI.enhanced ? 2 : 1); + +} + +// --------------------------------------------------------------------------- +// writeSosEnvelopesChunk +// --------------------------------------------------------------------------- +// +std::ostream & +writeSosEnvelopesChunk( std::ostream & s, const SosEnvelopesCk & ck ) +{ + // write it out: + try + { + BigEndian::write( s, 1, sizeof(Int_32), (char *)&ck.header.id ); + BigEndian::write( s, 1, sizeof(Int_32), (char *)&ck.header.size ); + BigEndian::write( s, 1, sizeof(Int_32), (char *)&ck.signature ); + BigEndian::write( s, 1, sizeof(Int_32), (char *)&ck.enhanced ); + BigEndian::write( s, 1, sizeof(Int_32), (char *)&ck.validPartials ); + + // The SOSresultion and SOSquasiHarmonic fields are in the phase table memory. + //BigEndian::write( s, initPhaseLth, sizeof(Int_32), (char *)&ck.initPhase[0] ); + static const int InitPhaseLth = ( LargestLabel + 8 ); + Int_32 bogus[ InitPhaseLth ]; // obsolete initial phase array + std::fill( bogus, bogus + InitPhaseLth, 0 ); + bogus[ ck.validPartials ] = ck.resolution; + bogus[ ck.validPartials + 1 ] = ck.quasiHarmonic; + BigEndian::write( s, InitPhaseLth, sizeof(Int_32), (char *)bogus ); + } + catch( FileIOException & ex ) + { + ex.append( "Failed to write AIFF file Container chunk." ); + throw; + } + + return s; + +} + +// --------------------------------------------------------------------------- +// computeHop +// --------------------------------------------------------------------------- +// Find the hop size, based on number of partials and sample rate. +// +static double computeHop( int numPartials, double sampleRate ) +{ + return 2 * numPartials / sampleRate; +} + + +// --------------------------------------------------------------------------- +// computeStartTime +// --------------------------------------------------------------------------- +// Find the start time: the earliest time of any labeled partial. +// +static double computeStartTime( const SpcFile::partials_type & pars ) +{ + double startTime = 1000.; + for ( SpcFile::partials_type::const_iterator pIter = pars.begin(); pIter != pars.end(); ++pIter ) + if ( pIter->size() > 0 && startTime > pIter->startTime() && pIter->label() > 0 ) + startTime = pIter->startTime(); + return startTime; +} + + +// --------------------------------------------------------------------------- +// computeEndTime +// --------------------------------------------------------------------------- +// Find the end time: the latest time of any labeled partial. +// +static double computeEndTime( const SpcFile::partials_type & pars ) +{ + double endTime = -1000.; + for ( SpcFile::partials_type::const_iterator pIter = pars.begin(); pIter != pars.end(); ++pIter ) + if ( pIter->size() > 0 && endTime < pIter->endTime() && pIter->label() > 0 ) + endTime = pIter->endTime(); + return endTime; +} + + +// --------------------------------------------------------------------------- +// computeNumPartials +// --------------------------------------------------------------------------- +// Find the number of partials. +// +static long computeNumPartials( const SpcFile::partials_type & pars ) +{ + +// We purposely consider partials with no breakpoints, to allow +// a larger number of partials than actually have data. + int numPartials = 0; + for ( SpcFile::partials_type::const_iterator pIter = pars.begin(); pIter != pars.end(); ++pIter ) + if ( numPartials < pIter->label() ) + numPartials = pIter->label(); + +// To ensure a reasonable hop time, make at least 32 partials. + return numPartials ? std::max( 32, numPartials ) : 0; +} + +// --------------------------------------------------------------------------- +// configureExportStruct +// --------------------------------------------------------------------------- +static void +configureExportStruct( const SpcFile::partials_type & plist, double midipitch, + double endApproachTime, int enhanced ) +{ + // note number (69.00 = A440) for spc file + spcEI.midipitch = midipitch; + + // enhanced indicates a bandwidth-enhanced spc file; by default it is true. + // if enhanced is false, no bandwidth or noise information is exported. + spcEI.enhanced = enhanced; + + // endApproachTime is in seconds; by default it is zero (and has no effect). + // a nonzero endApproachTime indicates that the plist does not include a + // release, but rather ends in a static spectrum corresponding to the final + // breakpoint values of the partials. the endApproachTime specifies how + // long before the end of the sound the amplitude, frequency, and bandwidth + // values are to be modified to make a gradual transition to the static spectrum. + spcEI.endApproachTime = endApproachTime; + + // number of partials in spc file + spcEI.numPartials = computeNumPartials( plist ); + spcEI.fileNumPartials = fileNumPartials( spcEI.numPartials ); + + // start and end time of spc file + spcEI.startTime = computeStartTime( plist ); + spcEI.endTime = computeEndTime( plist ); + + // in seconds, this indicates time at which a marker is inserted + // in the spc file, 0.0 indicates no marker. this is not being used currently. + spcEI.markerTime = 0.; + + // in hertz, intended sample rate for synthesis of spc file + spcEI.sampleRate = 44100.; + + // compute hop size + spcEI.hop = computeHop( spcEI.numPartials, spcEI.sampleRate ); + + // compute ampEpsilon, a small amplitude value twice the lsb value + // of log amp in packed spc format. + spcEI.ampEpsilon = 2. * envExp( 0x200 ); + + // Max number of partials is due to (arbitrary) size of initPhase[]. + if ( spcEI.numPartials < 1 || spcEI.numPartials > LargestLabel ) + Throw( FileIOException, "Partials must be distilled and labeled between 1 and 512." ); + + debugger << "startTime = " << spcEI.startTime << " endTime = " << spcEI.endTime + << " hop = " << spcEI.hop << " partials = " << spcEI.numPartials << endl; +} + +// --------------------------------------------------------------------------- +// getNumSampleFrames +// --------------------------------------------------------------------------- +// The number of exported sample frames is computed from data stored in +// the icky global export struct. +// +static unsigned long getNumSampleFrames( void ) +{ + int frames = int( ( spcEI.endTime - spcEI.startTime ) / spcEI.hop ) + 1; + return frames * spcEI.fileNumPartials * ( spcEI.enhanced ? 2 : 1 ); +} + +// -- import helpers by Lippold -- +// --------------------------------------------------------------------------- +// processEnhancedPoint +// --------------------------------------------------------------------------- +// Add ehanced-spc breakpoint to existing Loris partials. +// +static void +processEnhancedPoint( Byte * leftbytes, Byte * rightbytes, + const double frameTime, + Partial & par ) +{ +// represent bytes as 24 bit integers: + const int BytesPerSample = 3; + + // assign the leading byte, so that the sign + // is preserved: + long left = static_cast<char>(*(leftbytes++)); + for ( int j = 1; j < BytesPerSample; ++j ) + { + // OR bytes after the most significant, so + // that their sign is ignored: + left = (left << 8) + (unsigned char)*(leftbytes++); + } + + long right = static_cast<char>(*(rightbytes++)); + for ( int j = 1; j < BytesPerSample; ++j ) + { + // OR bytes after the most significant, so + // that their sign is ignored: + right = (right << 8) + (unsigned char)*(rightbytes++); + } +// +// Unpack values. +// + double freq = envExp( left & 0xffff ) * 22050.0; + double sineMag = envExp( (left >> 7) & 0xfe00 ); + double noiseMag = envExp( (right >> 7) & 0xfe00 ) / 64.; + double phase = ( right & 0xffff ) * ( 2. * Pi / 0xffff ); + + double total = sineMag * sineMag + noiseMag * noiseMag; + + double amp = sqrt( total ); + + double noise = 0.; + if (total != 0.) + noise = noiseMag * noiseMag / total; + if (noise > 1.) + noise = 1.; + + phase -= Pi / 2.; + if (phase < 0.) + phase += 2. * Pi; + +// +// Create a new breakpoint and insert it. +// + Breakpoint newbp( freq, amp, noise, phase ); + par.insert( frameTime, newbp ); +} + +// --------------------------------------------------------------------------- +// processSineOnlyPoint +// --------------------------------------------------------------------------- +// Add sine-only spc breakpoint to existing Loris partials. +// +static void +processSineOnlyPoint( Byte * bytes, + const double frameTime, + Partial & par ) +{ +// represent bytes as 24 bit integers: + const int BytesPerSample = 3; + + // assign the leading byte, so that the sign + // is preserved: + long packed = static_cast<char>(*(bytes++)); + for ( int j = 1; j < BytesPerSample; ++j ) + { + // OR bytes after the most significant, so + // that their sign is ignored: + packed = (packed << 8) + (unsigned char)*(bytes++); + } + +// +// Unpack values. +// + double freq = envExp( packed & 0xffff ) * 22050.0; + double amp = envExp( (packed >> 7) & 0xfe00 ); + double noise = 0.; + double phase = 0.; + +// +// Create a new breakpoint and insert it. +// + Breakpoint newbp( freq, amp, noise, phase ); + par.insert( frameTime, newbp ); +} + +// --------------------------------------------------------------------------- +// readSpcData +// --------------------------------------------------------------------------- +// +void +SpcFile::readSpcData( const std::string & filename ) +{ + ContainerCk containerChunk; + CommonCk commonChunk; + SoundDataCk soundDataChunk; + InstrumentCk instrumentChunk; + MarkerCk markerChunk; + SosEnvelopesCk soseChunk; + + try + { + std::ifstream s( filename.c_str(), std::ifstream::binary ); + + // the Container chunk must be first, read it: + readChunkHeader( s, containerChunk.header ); + if( containerChunk.header.id != ContainerId ) + Throw( FileIOException, "Found no Container chunk." ); + readContainer( s, containerChunk, containerChunk.header.size ); + + // read other chunks, we are only interested in + // the Common chunk, the Sound Data chunk, the Markers: + CkHeader h; + while ( readChunkHeader( s, h ) ) + { + switch (h.id) + { + case CommonId: + readCommonData( s, commonChunk, h.size ); + if ( commonChunk.channels != 1 ) + { + Throw( FileIOException, + "Loris only processes single-channel AIFF samples files." ); + } + if ( commonChunk.bitsPerSample != 8 && + commonChunk.bitsPerSample != 16 && + commonChunk.bitsPerSample != 24 && + commonChunk.bitsPerSample != 32 ) + { + Throw( FileIOException, "Unrecognized sample size." ); + } + break; + case SoundDataId: + readSampleData( s, soundDataChunk, h.size ); + break; + case InstrumentId: + readInstrumentData( s, instrumentChunk, h.size ); + break; + case MarkerId: + readMarkerData( s, markerChunk, h.size ); + break; + case ApplicationSpecificId: + readApplicationSpecifcData( s, soseChunk, h.size ); + break; + default: + s.ignore( h.size ); + } + } + + if ( ! commonChunk.header.id || ! soundDataChunk.header.id ) + { + Throw( FileIOException, + "Reached end of file before finding both a Common chunk and a Sound Data chunk." ); + } + if ( soseChunk.signature != SosEnvelopesId ) + { + Throw( FileIOException, + "Reached end of file before finding a Spc Envelope Data chunk, this must not be a Spc file." ); + } + } + catch ( Exception & ex ) + { + ex.append( " Failed to read Spc file." ); + throw; + } + + // all the chunks have been read, use them to initialize + // the SpcFile members: + rate_ = commonChunk.srate; + + // why was this like this: + // double rate = commonChunk.srate; + + if ( instrumentChunk.header.id ) + { + notenum_ = instrumentChunk.baseNote; + notenum_ -= 0.01 * instrumentChunk.detune; + } + + // extract information from SOSe chunk: + // enhanced file format has number of partials doubled + // sine-only file format has proper number of partials + bool enhanced = soseChunk.enhanced != 0; + int numPartials = enhanced ? soseChunk.validPartials / 2 : soseChunk.validPartials; + int numFrames = commonChunk.sampleFrames / ( fileNumPartials( numPartials ) * ( enhanced ? 2 : 1 ) ); + double hop = soseChunk.resolution * 0.000001; // resolution is in microseconds + + // read markers, need to compute times corresponding to + // spc frames: + if ( markerChunk.header.id ) + { + for ( int j = 0; j < markerChunk.numMarkers; ++j ) + { + MarkerCk::Marker & m = markerChunk.markers[j]; + double markerTime = m.position * hop / ( fileNumPartials( numPartials ) * ( enhanced ? 2 : 1 ) ); + markers_.push_back( Marker( markerTime, m.markerName ) ); + } + } + + + // check for valid file + if ( numPartials == 0 || commonChunk.bitsPerSample != 24 ) + Throw( FileIOException, "Not an SPC file." ); + if ( numPartials < MinNumPartials || numPartials > LargestLabel ) + Throw( FileIOException, "Bad number of partials in SPC file." ); + + // check the number of bytes of Spc data: + const int BytesPerSample = 3; + const int PredictedNumBytes = + BytesPerSample * numFrames * fileNumPartials( numPartials ) * ( enhanced ? 2 : 1 ); + if ( soundDataChunk.sampleBytes.size() != PredictedNumBytes ) + { + notifier << "Found " << soundDataChunk.sampleBytes.size() << " bytes of " + << commonChunk.bitsPerSample << "-bit sample data." << endl; + notifier << "Header says there should be " << PredictedNumBytes + << "." << endl; + } + + // process SPC data points + partials_.clear(); + growPartials( numPartials ); + Byte * bytes = &soundDataChunk.sampleBytes.front(); + for ( int frame = 0; frame < numFrames; ++frame ) + { + for ( int partial = 0; partial < fileNumPartials( numPartials ); ++partial ) + { + if (enhanced) + { + Byte * lbytes = bytes; + bytes += BytesPerSample; + Byte * rbytes = bytes; + bytes += BytesPerSample; + if ( partial < partials_.size() ) + processEnhancedPoint( lbytes, rbytes, frame * hop, partials_[partial] ); + } + else + { + if ( partial < partials_.size() ) + processSineOnlyPoint( bytes, frame * hop, partials_[partial] ); + bytes += BytesPerSample; + } + } + } +} + +} // end of namespace Loris |