Music Programming with the new Features of Standard C++

Adrian Freed and Amar Chaudhary
CNMAT, 1750 Arch Street. Berkeley, CA 94709
(510) 643 9990, {adrian,amar}@cnmat.berkeley.edu

Abstract

Object-oriented programming using C++ classes is established practice in the general programming community and is beginning in computer music applications. However, large components of computer music systems are still commonly written in the C programming language, either because object-orientation is felt unnecessary or more often because of efficiency concerns. Such concerns are central to successful implementations of reactive performance-oriented computer music systems. By judicious use of new features of the recently established ISO Standard C++ , real-time computer music applications may be developed that are more efficient and reliable than typical C programs, easier to understand and write, and easier to optimize for a particular operating environment. This paper reviews new features of ISO C++ relevant to reactive music system programming and illustrates by example a new programming style for musical applications that exploits unique strengths of C++.

1. Introduction

The recently completed standardization effort for C++ was not a formal codification of existing practice with the language. Many years of the effort involved the introduction of entirely new features many of which directly address efficiency issues that have prevented C++ from use in reactive music software. Section 2 summarizes these new features and hints at their relevance to reactive music systems. References to particular sections of a readable description of the standard C++ language are offered since space limitations prevent a complete exposition of each new feature.

We are developing a new programming system for musical applications, the "Open Sound System" (OSS). We have rejected the approach of simply translating an existing library of primitives or building C++ wrappers around one of the C-based music languages for two reasons. Firstly, these legacy systems were designed for computer architectures very different from those in use today. In modern computers arithmetic is much faster than table indexing. The key to good performance these days is exploitation of parallelism, data and code locality and the multi-level register/primary/secondary/main memory hierarchy . The second reason to start from scratch is that by exploring the rich abstraction facilities of standard C++, we have identified a promising new approach for developing musical signal processing and synthesis applications that is fundamentally different from the traditional unit generator/wiring model. Section 3 illustrates this new approach by example.

2. Standard C++ features for Music Programming

2.1 C++ standard library

The C++ standard library now includes many features specifically designed for numerical programming which is required in the signal-processing component of computer music applications.

2.1.1 Complex numbers (Stroustrup 1997) 22.5

Complex variables of single, double and quadruple precision are supported with the full range of mathematical functions.

2.1.2 Valarray (Stroustrup 1997) 22.4

"Valarrays" are a low-level building block for floating point vectors and are optimized for performance. May apply aggressive optimization strategies on valarray arithmetic. "Valarray" data accesses and layout may be matched to according to algorithm access patterns, optimizing use of the memory hierarchy.

2.1.3 Limits (Stroustrup 1997) 22.2

There is now a standard way of asking for properties of the built-in numeric types such as the maximum and minimum values representable in variables of a particular type.

2.2 Function Objects (Stroustrup 1997) 18.4

Overloading the function call operator allows for explicit support in C++ of functors or "function objects". Functors are a natural choice for implementing unit generators and are very efficient since their calls can be "inlined", eliminating subroutine call overhead and optimizing register and data cache use.

2.3 Composition closure objects 22.4.7

It is common practice to use C++’s "operator overloading" feature to define matrix and vector operations resulting in high-level, compact, representations of signal processing algorithms. Unfortunately, the obvious way to do this results in horribly inefficient code with redundant creation of temporary objects for each vector operator. The solution to this problem is to use compile-time analysis and closure objects to transfer evaluation of sub-expressions into an object that implements a composite operation eliminating the call by value that creates temporaries. These "composition closure objects" may be used to expose parallelism that can be exploited by good compilers to generate optimal code sequences on modern, highly concurrent processor architectures.

2.4 Templates (Stroustrup 1997) 13 and Template Metaprograms (VeldHuizen 1996)

Templates are used for implementing polymorphic types in C++. Because template functions may be "inlined" they don’t incur the run-time overhead of virtual functions, C++’s other mechanism for polymorphism.

Template metaprograms exploit the computation engine required to implement standard C++’s template mechanism for compile-time computations. includes examples that may have application in signal processing algorithms.

2.5 Allocators (Stroustrup 1997) 19.4

Standard C++ "allocators" offer the programmer explicit control over how memory is allocated and freed on a per-object basis. For OSS we have experimented with shared, garbage collected and pre-allocated pools to achieve low latency memory management and improve cache hit rates.

2.6 Exceptions (Stroustrup 1997) 14

The new C++ exception mechanism can be used to manage unforeseen conditions in reactive systems without complete failure that would be disastrous in live musical performance.

3 Top-down Reactive Systems Programming in C++

The unit generator wiring paradigm pervades non real-time and reactive music programming software . In this paradigm, users assemble systems bottom-up from a large library of primitives. The approach described here is top-down from a high-level description of the desired function, through a series of refinements resulting in an efficient implementation.

3.1Example application

The impulse response of a second-order resonator system is e-ktsin2¹ft. We wish to compute a suitable approximation of this function as part of an implementation of the resonance synthesis model . The "literal translation" of the continuous-time representation above into C++ is :

double damped_sine(double frequency, double amplitude, double rate, Time &t)      {
     	return amplitude * exp(-rate*t) * sin(2.0*PI*frequency*t);
}

The following fragment will test this function for ten seconds:

for(Time t;t<10.0;++t)
     	cout<< damped_sine(440.0, 1.0, 0.1, t)<<endl;

This implementation of the Time iterator class implements regular sampling at a default rate of 44100kHz:

class Time {
     	double time; const double sampling_interval;
public:
     	Time(double srate=44100.0): time(0.0),sampling_interval(1.0/srate) {}
     	operator double() { return time; }
     	friend double operator *(double f, Time &t){ return f * t.time; }
     	Time& operator ++() { time += sampling_interval; return *this; }
};

The above implementation has the virtue of simplicity, but is too slow for real-time work since the library exponential and sine functions are computed for every sample point. The usual way to address this deficiency is to replace calls to the mathematical functions with calls to low-level optimized functions. We prefer another approach where we evolve the Time class to include formal descriptions of the mathematical identities behind the optimizations, leaving the compiler to deal with the details of exploiting the identities for the particular machine the code will run on.

The first step is to change the time representation to more honestly reflect that we are really computing discrete-time sequences:

class Time {
     int sample_count; const double sampling_interval;
public:
     Time(double srate=44100.0):sample_count(0), sampling_interval(1.0/srate) {}
     operator double() { return sample_count*sampling_interval; }
     friend double operator *(double f, Time &t) {
         return f * t.sample_count*t.sampling_interval;
     }
     Time& operator ++() { ++sample_count; return *this; }
};

Noting that the sequence computes for real and complex values of k, we can introduce these new classes to optimize computation of exponentials and sinusoids respectively:

class expstate {
     double value, factor;
  public:
     double exp(double k, int i) {
          return (i==0)? (factor = ::exp(k), value=1.0) :
                         (value *= factor) ; 
     }
};

class sinstate {
     complex<double> value, factor;
  public:
     double sin(double k, int i) {
     	 return (i==0) ? 
                (factor = ::exp(complex<double>(0.0,k)), value=1.0) :
                imag(value *= factor);
     }
};

For the compiler to use these optimization classes, we have to specify when they may substitute for sub-expressions involving exponential and sinusoid functions. We first use a composition closure object (defmul below) to defer multiplication of double constants and time variables. Then we can define exponential and sinusoid functions that operate on defmul objects:

class Time {
  private:
     mutable int sample_count;
     const double sampling_interval;
     mutable int expindex, sinindex;
     class expstate {
         mutable double value, factor;
       public:
         double exp(double k, int i) const{
             return (i==0) ? (factor = ::exp(k), value=1.0) :
                             (value *= factor); 
         }
     };
     vector<expstate> expstates;

     class sinstate {
         mutable complex<double> value, factor;
       public:
         double sin(double k, int i) const {
             return (i==0) ?
                    (factor = ::exp(complex<double>(0.0,k)), value=1.0) :
                    imag(value *= factor);
         }
     };
     vector<sinstate> sinstates;

  public:
    Time (double srate=44100.0, int depth=MAXDEPTH):
         sampling_interval(1.0/srate), sinstates(depth),expstates(depth),
         expindex(0),sinindex(0), sample_count(0) {}
    operator double() const { 
        return double(sample_count) *sampling_interval;
    }
    const Time& operator ++() const { 
        expindex=sinindex=0; ++sample_count; return *this;
    }
    struct defmul{ // deferred multiplication
        const Time& t; 
        const double& f;
        defmul(const Time& at, const double& af): t(at), f(af){}
        operator double() { return f * double(t); }
    };
    friend defmul operator *(const double f, const Time &t) { return defmul(t,f);}
    friend defmul operator *(const Time &t, const double f) { return defmul(t,f);}
    friend double exp(const defmul &dm) {
        return dm.t.expstates[dm.t.expindex++].exp(
            dm.f*dm.t.sampling_interval, dm.t.sample_count);
    }
    friend double sin(const defmul &dm) {
        return dm.t.sinstates[dm.t.sinindex++].sin(
            dm.f*dm.t.sampling_interval, dm.t.sample_count); 
    }
};

We compiled the above classes with several commercial C++ compilers. They all correctly interpreted the composition closure technique. Careful adjustment of optimization options is required to encourage compilers to "inline" implicit functions resulting from the composition closure. One notable compiler (http://www.kai.com) reasoned correctly that the complex and real exponentiation would only be computed once and that the arguments were known at compile time and thereby avoided compiling any calls to mathematical functions.

Conclusion

The optimization effort above did not involve changing the original function describing the computation required. This suggests that we could build a system where users define their needs in an expressive high-level form similar to a mathematical description. These descriptions would be compiled efficiently enough to run in real-time given a large enough family of optimization classes. Optimizations expressed in this form are available to the compiler as it compiles an entire application. By contrast optimized library functions are bound at the last moment by a linker program that does not have enough information to effectively exploit available computing resources. We are also encouraged by the fact that the optimizations themselves are expressed in a high-level form similar to a mathematical description making them easier to develop and debug.

References

Becker, P. (1998). C++ standard approved. C/C++ Users Journal. 16: 89.

Chaudhary, A. (1998). Band-Limited Simulation of Analog Synthesizer Modules by Additive Synthesis. AES 104th Convention, San Francisco, CA, AES.

Chaudhary, A., A. Freed, et al. (1998). OpenSoundEdit: An Interactive Visualization and Editing Framework for Timbral Resources. International Computer Music Conference, Ann, Arbor, Michigan.

Dowd, K. and M. K. Loukides (1993). High performance computing. Sebastopol, CA, O'Reilly & Associates.

Freed, A. (1994). Codevelopment of user interface, control and digital signal processing with the HTM environment. 5th International Conference on Signal Processing Applications and Technology, Dallas, TX, USA, DSP Associates.

Pope, S. T. (1993). "Machine tongues. XV. Three packages for software sound synthesis." Computer Music Journal 17(2): 23-54.

Potard, Y., P.-F. Baisnée, et al. (1986). Experimenting with Models of Resonance Produced by a New Technique for the Analysis of Impulsive Sounds. International Computer Music Conference, La Haye, CMA.

Stevens, A. (1998). "A C++ standard at last." Dr. Dobb's Journal 23(2): 115-17, 130-1.

Stroustrup, B. (1995). Standardization in The design and evolution of C++. ed. Reading, Mass., Addison-Wesley. 134-179.

Stroustrup, B. (1997). The C++ Programming Language. Reading, Mass., Addison-Wesley.

VeldHuizen, T. (1996). Using C++ Template Metaprograms in C++ Gems. ed. S. B. Lippman. New York, SIGS Books & Multimedia. 459-487.

Veldhuizen, T. and K. Pannambalam (1996). "Linear algebra with C++ template metaprograms." Dr. Dobb's Journal 21(8): 38, 40-2, 44.

Zicarelli, D. (1998). MSP. http://www.cycling74.com.