Experimental

The experimental module is a placeholder for modules which haven't yet been approved for release, but might eventually be incorporated into the library. By default the experimental module is disabled and none of its modules are compiled or installed. It is enabled using the configure flag --enable-experimental and includes the internal header file include/liquid.experimental.h .

fbasc (filterbank audio synthesizer codec)

The fbasc audio codec implements an AAC-like compression algorithm, using the modified discrete cosine transform as a loss-less channelizer. The resulting channelized data are then quantized based on their spectral energy levels and then packed into a frame which the decoder can then interpret. The result is a lossy encoder (as a result of quantization) whose compression/quality levels can be easily varied.

Specifically, fbasc uses sub-band coding to allocate quantization bits to each channel in order to minimize distortion of the reconstructed signal. Sub-bands with higher variance (signal 'energy') are assigned more bits. This is the heart of the codec, which exploits several components typical of audio signals and aspects of human hearing and perception:

  • The majority of audio signals (including music and voice) have a strong time-frequency localization; that is, they only occupy a small fraction of audible frequencies for a short duration. This is particularly true for voiced signals (e.g. vowel sounds).
  • The human ear (and brain) tends to be quite forgiving of spectral compression and often cannot easily distinguish between neighboring frequency components.

There are several benefits to using fbasc over other compression algorithms such as CVSD and auto-regressive models, the main being that the algorithm is theoretically lossless (i.e. perfect reconstruction) as the bit rate increases. As a result, the codec is limited only by the quantization noise on each channel.

Here are some useful definitions, as used in the fbasc code:

  • MDCT the modified discrete cosine transform is a lapped discrete cosine transform which uses a special windowing function to ensure perfect reconstruction on its inverse. The transform operates on \(2M\) time-domain samples (overlapped by \(M\) ) to produce \(M\) frequency-domain samples. Conversely, the inverse MDCT accepts \(M\) frequency-domain samples and produces \(2M\) time-domain samples which are windowed and then overlapped to reconstruct the original signal. For convenience, we may refer to \(M\) time-domain samples as a 'symbol.'
  • symbol one block of \(M\) time-domain samples upon which the MDCT operates.
  • channel one of the \(M\) frequency-domain components as a result of applying the MDCT. This is somewhat equivalent to a discrete Fourier transform 'bin.' Note than \(M\) is equal to the number of channels in analysis.
  • frame a set of MDCT symbols upon which the fbasc codec runs its analysis. Because the codec uses time-frequency localization for its encoding, it is necessary for the codec to gain enough statistical information about the original signal without losing temporal stationarity. The codec typically operates on several symbols, however, the exact number depends on the application.

Interface

  • fbasc_create() creates an fbasc encoder/decoder object, allocating memory as necessary, and computing internal parameters appropriately.
  • fbasc_destroy() destroys an fbasc encoder/decoder object, freeing internally-allocated memory.
  • fbasc_encode() encode a frame of data, storing the header and frame data separately. This separation allows the user to use different forward error-correction codes (if desired) to protect the header differently than the rest of the frame. It is important to keep the two together, however, as the header is a description of how to decode the frame.
  • fbasc_decode() decodes a frame of data, generating the reconstructed time series.

Useful properties

  • Because of the nature of the MDCT, frames will overlap by \(M\) samples (one symbol). This introduces a reconstruction delay of \(M\) samples, noticeable at the decoder.

gport

The gport object implements a generic port to share data between asynchronous threads. The port itself is really just a circular (ring) buffer containing a mutually-exclusive locking mechanism to allow processes running on independent threads to access its data. Because no other modules rely on the gport object and because it requires the pthread library, it is likely to be removed from liquid in the near future and likely put into another library, e.g. liquid-threads .

There are two ways to access the data in the gport object: direct memory access and indirect (copied) memory access, each with distinct advantages and disadvantages. Regardless of which interface you use, the model is equivalent: a buffer of data (initially empty) is created. The producer is the method in charge of writing to the buffer (or "producing" the data). The consumer is the method in charge of reading the data from the buffer (or "consuming" it). The producer and consumer methods can exist on completely separate threads, and do not need to be externally synchronized. The gport object synchronizes the data between the ports.

Direct Memory Access

Using gport via direct memory access is a multi-step process, equivalent for both the producer and consumer threads. For the sake of simplicity, we will describe the process for writing data to the port on the producer side; the consumer process is identical.

  • the producer requests a lock on the port of a certain number of samples.
  • once the request is serviced, the port returns a pointer to an array of data allocated internally by the port itself.
  • the producer writes its data at this location, not exceeding the original number of samples requested.
  • the producer then unlocks the port, indicating how many samples were actually written to the buffer. This allows the consumer thread to read data from the buffer.
  • this process is repeated as necessary.

Listed below is a minimal example demonstrating the direct memory access method for the gport object.

Indirect/Copied Memory Access

Indirect (or "copied") memory access appears similar...

Key differences between memory access modes

While the direct memory access method provides a simpler interface--in the sense that no external buffers are required--the user must take care in not writing outside the bounds of the memory requested. That is, if 256 samples are locked, only 256 values are available. Writing more data will produce unexpected results, and could likely result in a segmentation fault. Furthermore, the buffer must wait until the entire requested block is available before returning. This could potentially increase the amount of time that each process is waiting on the port. Additionally, if one requests too many samples on both the producer and consumer sides, the port could wait forever. For example, assume one initially creates a gport with 100 elements and the producer initially writes 30 samples. Immediately following, the consumer requests a lock for 100 samples which isn't serviced because only 30 are available. Following that, the producer requests a lock for 100 samples which isn't serviced because only 70 are available. This is a deadlock condition where both threads are waiting for data, and neither request will be serviced. The solution to this problem is actually fairly simple; the port should be initially created as the sum of maximum size of the producer's and consumer's requests. That is, if the producer will at most ever request a lock on 50 samples and the consumer will at most request a lock of 70 samples, then the port should be initially created with a buffer size of 120. This guarantees that the deadlock condition will never occur.

Alternatively one may use the indirect memory access method which guarantees that the deadlock condition will never occur, even if the buffer size is 1 and the producer writes 1000 samples while the consumer reads 1000. This is because both the internal producer and consumer methods will write the data as it becomes available, and do not have to wait internally until an entire block of the requested size is ready. This is the benefit of using the indirect memory access interface of the gport object. Indirect memory access, however, requires the use of memory allocated externally to the port.

It is important to stay consistent with the memory access mode used within a thread, however mixed memory access modes can be used between threads on the same port. For example, the producer thread may use the direct memory access mode while the consumer uses the indirect memory access mode.

Interface

  • gport_create() creates a new gport object with an internal buffer of a certain length.
  • gport_destroy() destroys a gport object, signaling end of message to any connected ports.
  • gport_producer_lock() locks a requested number of samples for producing, returning a void pointer to the locked data array directly. Invoking this method can be thought of as asking the port to allocate a certain number of samples for writing. Special care must be taken by the user not to write more elements to the buffer than were requested. This function is a blocking call and waits for the data to become available or an end of message signal to be received. The data are locked until gport_producer_unlock() is invoked. The number of unlocked samples does not have to match but cannot exceed those which are locked.
  • gport_producer_unlock() unlocks a requested number of samples from the port. Use in conjunction with gport_producer_lock() . Invoking this method can be thought of as telling the port "I have written \(n\) samples to the buffer you gave me earlier; release them to the consumer for reading." The number of samples written to the port cannot exceed the initial request (e.g. if you request a lock for 100 samples, you should never try to unlock more than 100). There is no internal error-checking to ensure this. Failure to comply could result in over-writing data internally, and corrupt the consumer side.
  • gport_produce() produces \(n\) samples to the port from an external buffer. This method is a blocking call and waits for the requested data to become available or an end of message signal to be received.
  • gport_produce_available() operates just like gport_produce() except will write as many samples as are available when the function is called. Invoking this method is like telling the buffer "I have \(n\) samples, so write as many as you can right now." It will always wait for at least one sample to become available and blocks until this condition is met.
  • gport_consumer_lock() locks a requested number of samples for consuming, returning a void pointer to the locked data array directly. Invoking this method can be thought of as asking the port to wait for a certain number of samples to be read. Special care must be taken by the user not to read more elements to the buffer than were requested. This function is a blocking call and waits for enough samples to become available or an end of message signal to be received. The data will be locked until gport_consumer_unlock() is invoked. The number of unlocked samples does not have to match but cannot exceed those which are locked.
  • gport_consumer_unlock() unlocks a requested number of samples from the port. Use in conjunction with gport_consumer_lock() . Invoking this method can be though of as telling the port "I have read \(n\) samples from the buffer you gave me earlier; release them to the producer for writing." The number of samples read from the port cannot exceed the initial request (e.g. if you request a lock for 100 samples, you should never try to unlock more than 100).
  • gport_consume() consumes \(n\) samples from the port and writes to an external buffer. This method is a blocking call and waits for the requested data to become available or an end of message signal to be received.
  • gport_consume_available() operates just like gport_consume() except will read as many samples as are available when the function is called. Invoking this method is like telling the buffer "I have a buffer of \(n\) samples, so write to it as many as you can right now." It will always wait for at least one sample to become available and blocks until this condition is met.
  • gport_signal_eom() signals end of message to any connected gport . This tells the port to stop waiting for data (on both the producer and consumer side) and return. This method prevents lock conditions where, e.g., the producer is waiting for several samples to become available, but the consumer has finished its process. This method is normally invoked only during gport_destroy() .
  • gport_clear_eom() ( untested ) clears the end of message signal.

Problem areas

When using the direct memory access method, the size of the data request during lock is limited by the size of the port. (race/lock conditions?)

dds (direct digital synthesizer)

qmfb (quadrature mirror filter bank)

qnsearch (Quasi-Newton Search)

The qnsearch object in liquid implements the Quasi-Newton search algorithm which uses the first- and second-order derivatives (gradient vector and Hessian matrix) in its update equation. Newtonian-based search algorithms approximate the function to be nearly quadratic near its optimum which requires the second partial derivative (Hessian matrix) to be computed or estimated at each iteration. Quasi-Newton methods circumvent this by approximating the Hessian with successive gradient computations (or estimations) with each step. The Quasi-Newton method is usually faster than the gradient search due in part to its second-order (rather than a first-order) Taylor series expansion about the function of interest, however its update criteria is significantly more involved. In particular the step size must be sufficiently conditioned otherwise the algorithm can result in instability.

An example of the qnsearch interface is listed below. Notice that its interface is virtually identical to that of gradient_search .


#include <liquid/liquid.h>

int main() {
    unsigned int num_parameters = 8;    // search dimensionality
    unsigned int num_iterations = 100;  // number of iterations to run
    float target_utility = 0.01f;       // target utility

    float optimum_vect[num_parameters];

    // ...

    // create qnsearch object
    qnsearch q = qnsearch_create(NULL,
                                 optimum_vect,
                                 num_parameters,
                                 &liquid_rosenbrock,
                                 LIQUID_OPTIM_MINIMIZE);

    // execute batch search
    qnsearch_execute(q, num_iterations, target_utility);

    // execute search one iteration at a time
    unsigned int i;
    for (i=0; i<num_iterations; i++)
        qnsearch_step(q);

    // clean it up
    qnsearch_destroy(q);
}