This section describes how the spectral envelope library was developed. First, the architecture will be described as the central design documentation in 7.4.1. Then, a few words will be said about how we got there from analysis, and some more words about how we're going from there to implementation in 7.4.2.
The architecture of the spectral envelope library is described using the notation introduced in section 7.2. The top-level data flow diagram in figure 7.7 shows the different transformations between sound data and the spectral envelope representation. Subsequently, the data and functional classes will be further decomposed into subclasses. An additional data class Control Data, which provides input to every functional class, is left out for the sake of lisibility.
The two main data classes, Sound Data and Spectral Envelope, the representation class, and their subclasses are shown in figures 7.8 and 7.9, respectively.
The diagrams in figures 7.10 to 7.15 show the functional classes Estimation, Synthesis, and Interpolation. They are grouped in pairs of two. The first diagram of each pair shows the subclasses of the respective functional class. The second diagram of each pair shows the input and output data flows of the subclasses. (The data flows of the base classes are all shown in the top-level diagram in figure 7.7.)
The architecture diagrams in figures 7.7 to 7.15 specify the design of the spectral envelope library. However, they are directly derived from the analysis of the requirements for the library. In fact, the diagrams that are the output of the analysis phase (the requirements engineering) look the same, except that some details like usage-relationships are missing. This is the reason that the analysis phase is not explicitly documented. This supports the claim, by the way, that the adapted notation is indeed useful, because it offers a transparent transition from the analysis of the software system to its design.
In going from design to implementation, however, it is impossible to make a transparent transition, since there we reach the boundary of the object-oriented paradigm . While in analysis and design, it was still possible to describe the world as consisting of interacting objects, each pertaining to one of several classes, in our implementation in non-object-oriented C, we have to talk about functions, modules, and data structures. Because we can't transfer the object-oriented paradigm completely to implementation, some desirable properties of object-oriented programming will not hold. Specifically, the aspects of encapsulation, inheritance and type-polymorphism will be lost, as explained in the following:
The C-structures that will be used instead of classes offer no compiler-enforced protection mechanism regarding the access to the data fields. However, for the data classes explained below, access functions can be provided, that set the value of one data field, or return its value. If the user of the library by convention uses these access functions, sufficient encapsulation between the interface and the implementation of the data structures is provided. This means that, for example in the case the internal data structures change, the code using these structures will not have to be changed.
For some data structures such as the one holding the estimation parameters, encapsulation is even undesirable, since
The inheritance relations (also called subtype or generalization /specialization relations) used in the analysis and design will not be visible in the implementation. Instead, the classes will be mapped to C-structures as shown in section 7.4.3.
In theory, it is always possible to replace inheritance by aggregation, i.e. the subclasses contain the base class, but for reasons explained below,
Polymorphism is closely linked to inheritance and means the access to a certain functionality that applies to all subclasses of a base class by one method name. This name maps to the appropriate method 8.3 of the actual subclass.
Polymorphism could be mimicked in C by storing pointers to member functions with each object. Apart from the complexity and usage of space of that procedure, type polymorphism is, on closer scrutiny, not really necessary for the spectral envelope library. This is because the number of generic functions (e.g. save) which have to be applied to all subtypes of one general type are fairly few. Instead, specialized functions for the classes exist. For example, the three different subclasses of sound data (Signal, Spectrum, Partials) are processed by three different estimation methods (LPC Analysis, Cepstrum, Discrete Cepstrum) to yield two different output data types (LPC Coefficients, Cepstral Coefficients). I.e. the data flows on parallel ``tracks'' that rarely cross. In another case, formant interpolation, it is necessary to know the actual subtype of spectral envelope representation (Sampled Spectral Envelope, Precise Formants, Fuzzy Formants) to choose the specialized method to perform the formant shift (see section 5.1.2). Therefore, it is advantageous to keep the subtype information explicit in the code and not generalize to a superclass.
As an example of the implementation of the data classes, the definition of the structures for the Spectral Envelope class, from the class diagram in figure 7.9, are given below:
The sampled spectral envelope, or spectral representation, is:
typedef struct { float maxfreq; /* upper border of env (maxfreq = sampling rate/2) */ int numenv; /* number of points in envelope */ float *env; /* numenv spectral envelope amplitude values */ float fstep; /* size of bin in env (fstep = sr / numenv) */ seScale scale; /* frequency scale for storage */ float breakfreq; /* break frequency for log scale */ } seSpecEnv;
Where seScale gives the type of the scaling and is defined as:
typedef enum { seScLinear, seScLog, seScNum } seScale;
For the spectral envelope representation as precise formants, we use a list of structures for formants, and a residual spectral envelope, which represents all components that are not of the shape of a formant:
typedef struct { float centerfreq; /* precise formant parameters */ float amplitude; float bandwidth; } seFormant;
typedef struct { int numformants; /* number of precise formants */ seFormant *formants; /* numformants precise formants */ seSpecEnv residual; /* residual spectral envelope */ } sePreciseFormants;
For the fuzzy formant representation, we use a list of formant regions within a sampled spectral envelope:
typedef struct { float lower; /* region boundary frequencies (Hz) */ float center; float upper; float salience; /* how sure we are it is a formant */ } seRegion;
typedef struct { int numformants; /* number of precise formants */ seRegion *formants; /* numformants precise formants */ seSpecEnv specenv; /* spectral envelope */ } sePreciseFormants;