~hecanjog/pippi

ee0bb902de20419b0d6d899d6ea714d5b55c1939 — He Can Jog 16 days ago 4a99a09
Flailing around
6 files changed, 63 insertions(+), 232 deletions(-)

M README.md
M astrid/orc/pulsar.c
M astrid/src/astrid.c
M astrid/src/astrid.h
M pippi/renderer.pxd
M pippi/renderer.pyx
M README.md => README.md +0 -163
@@ 1,170 1,7 @@
# Pippi: Computer Music With Python

v2.0.0 - Beta 5 (In Development)

Source code: [https://git.sr.ht/~hecanjog/pippi](https://git.sr.ht/~hecanjog/pippi)

Documentation: [https://pippi.world](https://pippi.world)

![Pippi: Computer music with python](banner.png)

## What is this?

Pippi is a library of computer music modules for python. 

It's what I use to make my music, and I hope it'll be useful to others in some way, too. It's a work in progress.

It includes a few handy data structures for music like
`SoundBuffer` & `Wavetable`, which are operator-overloaded 
to make working with sounds and control structures simpler.

It also includes a lot of useful methods for doing common and 
not-so-common transformations to sounds and control structures. 

``` python
from pippi import dsp

sound1 = dsp.read('sound1.wav')
sound2 = dsp.read('sound2.flac')

# Mix two sounds
both = sound1 & sound2

# Apply a skewed hann Wavetable as an envelope to a sound
enveloped = sound * dsp.win('hann').skewed(0.6)

# Or just a sine envelope via a shortcut method on the `SoundBuffer`
enveloped = sound.env('sine')

# Synthesize a 10 second graincloud from the sound, 
# with grain length modulating between 20ms and 2s 
# over a triangle shaped curve.
cloudy = enveloped.cloud(10, grainlength=dsp.win('tri', dsp.MS*20, 2))
```

It comes with several oscs:

- `Alias` - a highly aliased pulse train osc
- `Bar` - a bar physical model (from Soundpipe)
- `Drunk` - does a drunk walk on the y axis over a fixed set of random points w/hermite interpolation for smooth waveshapes (kind of like dynamic stochastic synthesis in one dimension)
- `DSS` - a basic implementation of dynamic stochastic synthesis that does a drunk walk in two dimensions over a random set of breakpoints
- `FM` - a basic two operator FM synth w/harmonicity ratio & modulation index controls
- `Fold` - an infinite folding wavetable osc
- `Osc` - an everyday wavetable osc
- `Osc2d` - a 2d morphing wavetable osc
- `Pluck` - a plucked string physical model (adapted from JOS)
- `Pulsar` - a pulsar synthesis engine
- `Pulsar2d` - a 2d morphing pulsar synthesis engine (pairs well with a stack of wavetables extracted with the `Waveset` module)
- `SineOsc` - a simple sinewave osc (doesn't use wavetables)
- `Tukey` - a tukey-window-based osc with waveshape modulation between square-like and sine-like

And many built-in effects and transformations:

- Easy independent control over pitch and speed for any `SoundBuffer`
- Paulstretch
- Several forms of waveshaping and distortion including a crossover distortion ported from supercollider
- Sweapable highpass, lowpass, bandpass and band reject butterworth filters from Soundpipe
- Lots more!

As well as support for pitch and harmony transformations and non-standard tuning systems
``` python
from pippi import tune

# Get a list of frequencies from a list of scale degrees
frequencies = tune.degrees([1,3,5,9], octave=3, root='a', scale=tune.MINOR, ratios=tune.JUST)

# Get a list of frequencies from a chord symbol using a tuning system devised by Terry Riley
frequencies = tune.chord('ii69', key='g#', octave=5, ratios=tune.TERRY)

# Convert MIDI note to frequency
freq = tune.mtof(60)

# Convert frequency to MIDI note
note = tune.ftom(440.0)

# Convert a pitch to a frequency
freq = tune.ntf('C#3')
```
And basic graphing functionality for any `SoundBuffer` or `Wavetable` -- some dumb examples pictured in the banner above.

``` python
from pippi import dsp

sound = dsp.read('sound.wav')

# Render an image of this sound's waveform
sound.graph('mysound.png')

# Render an image of a sinc wavetable with a label and scaled range
dsp.win('sinc').graph('sinc.png', label='A sinc wavetable', y=(-.25, 1))
```

As well as other neat stuff like soundfont rendering support via tinysf!

``` python
from pippi import dsp, soundfont

# Play a piano sound from a soundfont with general MIDI support (program change is zero-indexed)
tada = soundfont.play('my-cool-soundfont.sf2', length=30, freq=345.9, amp=0.5, voice=0)

# Save copy to your hard disk
tada.write('tada.wav')
```

## Tutorials

There are annotated example scripts in the [tutorials](docs/tutorials) directory which introduce some of pippi's functionality.

Beyond arriving at a good-enough stable API for the 2.x series of releases (and fixing bugs), my goal during the 
beta phase of development is to deal with the lack of documentation for this project.


## Installation

Pippi requires python 3.6+ which can be found here:

    https://www.python.org/downloads/

To install pippi:

- Clone this repository locally: `git clone https://github.com/luvsound/pippi.git`
- _(Optional but recommended)_ Create a virtualenv somewhere where you want to work: `cd /my/pippi/projects; python3 -m venv venv; source venv/bin/activate`
- _(With your virtualenv active)_ Go back to the pippi source directory `cd /path/to/pippi` and run `make install` 

> **Raspberry Pi OS:**
> 
> Use the same steps as above, but create your virtualenv with `python3 -m venv --system-site-packages venv` and run `make rpi-install`.

The final command does a few things:

- Installs python deps, so *make sure you're inside a virtual environment* if you want to be!
- Sets up git submodules for external libs
- Builds and installs Soundpipe
- Builds and installs pippi & cython extensions

Please let me know if you run into problems!

### From pypi

At the moment the best place to get pippi is using the method described above. Because of some packaging issues that need to be worked out, the version on pypi is quite a bit older and does not include most of the fun stuff.

## To run tests

    make test

In many cases, this will produce a soundfile in the `tests/renders` directory for the corresponding test. (Ear-driven regression testing...)
During the beta I like to keep failing tests in the main repo, so... most tests will be passing but if they *all* are passing, probably you are living in the future and are looking at the first stable release.

There are also shortcuts to run only certain groups of tests, like `test-wavesets` -- check out the `Makefile` for a list of them all.

## Hacking

> NOTE: the default branch is now called `main`. Run `bash scripts/rename_default_branch.sh` to update your local clone if needed.

While hacking on pippi itself, running `make build` will recompile the cython extensions.

If you need to build sources from a clean slate (sometimes updates to `pxd` files require this) then run `make clean build` instead.

## Thanks

[Astrid Lindgren](https://en.wikipedia.org/wiki/Astrid_Lindgren) who wrote inspiring stories about Pippi Longstocking, this library's namesake.

M astrid/orc/pulsar.c => astrid/orc/pulsar.c +5 -4
@@ 58,17 58,18 @@ void param_update_callback(void * arg) {
    astrid_instrument_set_param_float(instrument, PARAM_PW, LPRand.rand(0.05f, 1.f));
}

#if 0
lpbuffer_t * renderer_callback(void * arg) {
    lpbuffer_t * out;
    lpinstrument_t * instrument = (lpinstrument_t *)arg;

    out = LPBuffer.cut(instrument->adcbuf, LPRand.randint(0, instrument->adcbuf->length/2), LPRand.randint(SR, instrument->adcbuf->length-2));
    out = LPBuffer.create(LPRand.randint(0, SR), instrument->channels, SR);
    if(lpsampler_read_ringbuffer_block("pulsar-adc", 0, out->length, instrument->channels, out->data) < 0) {
        return NULL;
    }
    LPFX.norm(out, LPRand.rand(0.26f, 0.5f));

    return out;
}
#endif

void audio_callback(size_t blocksize, __attribute__((unused)) float ** input, float ** output, void * arg) {
    size_t i;


@@ 149,7 150,7 @@ int main() {

    // Set the callbacks for streaming, async renders and param updates
    if((instrument = astrid_instrument_start(NAME, CHANNELS, ADC_LENGTH, (void*)ctx, 
                    audio_callback, NULL, param_update_callback)) == NULL) {
                    audio_callback, renderer_callback, param_update_callback)) == NULL) {
        fprintf(stderr, "Could not start instrument: (%d) %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }

M astrid/src/astrid.c => astrid/src/astrid.c +45 -56
@@ 912,11 912,6 @@ int lpsampler_get_path(char * name, char * path) {
    return 0;
}

int lpsampler_get_data_path(char * name, char * path) {
    snprintf(path, PATH_MAX, "/astrid-sampler-%s-data", name);
    return 0;
}

int lpsampler_create(char * name, double length_in_seconds, int channels, int samplerate) {
    int semvalue, shmfd;
    sem_t * sem;


@@ 929,7 924,7 @@ int lpsampler_create(char * name, double length_in_seconds, int channels, int sa

    /* Determine the size of the shared memory segment */
    bufsize = sizeof(lpbuffer_t) + (length * channels * sizeof(lpfloat_t));
    printf("bufsize=%ld samplerate=%d length=%ld channels=%d\n", 
    syslog(LOG_DEBUG, "bufsize=%ld samplerate=%d length=%ld channels=%d\n", 
            bufsize, samplerate, length, channels);

    /* Create the POSIX semaphore and initialize it to 1 */


@@ 1064,14 1059,18 @@ int lpsampler_destroy(char * name) {
    return 0;
}

int lpsampler_write_ringbuffer_block(char * name, float ** block, int channels, size_t blocksize_in_samples) {
    size_t write_pos, insert_pos, i, boundry;
int lpsampler_write_ringbuffer_block(
        char * name, 
        float ** block, 
        int channels, 
        size_t blocksize_in_frames
    ) {
    size_t insert_pos, i;
    int c;
    lpbuffer_t * buf;
    float sample = 0;
    char path[PATH_MAX] = {0};

    boundry = blocksize_in_samples * channels;
    lpsampler_get_path(name, path);

    /* Aquire a lock on the buffer */


@@ 1080,29 1079,23 @@ int lpsampler_write_ringbuffer_block(char * name, float ** block, int channels, 
        return -1;
    }

    printf("pos: %ld\n", buf->pos);
    write_pos = buf->pos;
    assert(buf->channels == channels);

    /* Copy the block of samples */
    for(i=0; i < blocksize_in_samples; i++) {
    /* Copy the block */
    for(i=0; i < blocksize_in_frames; i++) {
        for(c=0; c < channels; c++) {
            insert_pos = ((write_pos+i) * channels + c) % boundry;
            sample = *block[c]++;
            insert_pos = ((buf->pos+i) * channels + c) % buf->length;
            sample = *block[i]++;
            buf->data[insert_pos] = sample;
            printf("sample: %f\n", buf->data[insert_pos]);
        }
    }

    /* Increment the write position */
    write_pos += blocksize_in_samples;
    while(write_pos >= boundry) {
        write_pos -= boundry;
    buf->pos += blocksize_in_frames;
    while(buf->pos >= buf->length) {
        buf->pos -= buf->length;
    }

    /* Store the new write position */
    buf->pos = write_pos;
    printf("pos: %ld\n", buf->pos);

    /* Release the lock on the ADC buffer shm */
    if(lpsampler_release(name) < 0) {
        syslog(LOG_ERR, "lpsampler_write_ringbuffer_block: Could not release buffer shm after update\n");


@@ 1113,13 1106,19 @@ int lpsampler_write_ringbuffer_block(char * name, float ** block, int channels, 
}


int lpsampler_read_block_of_samples(char * name, size_t offset, size_t size, lpfloat_t * out) {
    size_t read_pos, start, end, readsize, lastreadsize, boundry;
int lpsampler_read_ringbuffer_block(
        char * name, 
        size_t offset_in_frames, 
        size_t length_in_frames, 
        int channels, 
        lpfloat_t * out
    ) {
    size_t start, i, bufidx;
    int c;
    lpbuffer_t * buf;
    char path[PATH_MAX] = {0};

    lpsampler_get_path(name, path);
    boundry = buf->length * buf->channels;

    /* Aquire a lock on the buffer */
    if(lpsampler_aquire(name, &buf) < 0) {


@@ 1127,30 1126,22 @@ int lpsampler_read_block_of_samples(char * name, size_t offset, size_t size, lpf
        return -1;
    }

    /* Maximum read size == buffer length */
    if(size > boundry) size = boundry;
    assert(buf->channels == channels);

    /* Get the read position with the offset and read as far 
     * to the start of the buffer before wrapping as possible */
    if(buf->pos >= offset) {
        read_pos = (buf->pos - offset) % boundry;
    } else {
        read_pos = (buf->pos + (boundry - offset)) % boundry;
    }
    /*
     * buf->pos is the last frame written to the circular buffer
     * offset is the number of frames backward from that point to start reading
     * start is buf->pos - offset, wrapped to the length of the circular buffer
     */

    start = read_pos >= size ? read_pos - size : 0;
    end = read_pos;
    readsize = end - start;
    memcpy(out, buf->data + start, readsize * sizeof(lpfloat_t));
    start = (buf->pos - offset_in_frames) % buf->length;
    syslog(LOG_DEBUG, "buffer read start: %ld\n", start);

    /* If there are remaining samples to be read on the other end of 
     * the buffer, read those samples back from the end. */
if (readsize < size) {
        lastreadsize = readsize;
        start = boundry - (size - readsize);
        end = boundry;
        readsize = end - start;
        memcpy(out + lastreadsize, buf->data + start, readsize * sizeof(lpfloat_t));
    for(i=0; i < length_in_frames; i++) {
        bufidx = (start + i) % buf->length;
        for(c=0; c < buf->channels; c++) {
            out[i * buf->channels + c] = buf->data[bufidx * buf->channels + c];
        }
    }

    /* Release the lock on the buffer shm */


@@ 1706,7 1697,6 @@ lpbuffer_t * deserialize_buffer(char * buffer_code, lpmsg_t * msg) {
    int channels, samplerate, is_looping;
    char * str; // bufstr
    lpbuffer_t * buf;
    lpfloat_t * audio;
    struct stat statbuf;
    int fd;
    sem_t * sem;


@@ 1765,20 1755,17 @@ lpbuffer_t * deserialize_buffer(char * buffer_code, lpmsg_t * msg) {
    memcpy(&onset, str + offset, sizeof(size_t));
    offset += sizeof(size_t);

    audio = calloc(1, audiosize);
    memcpy(audio, str + offset, audiosize);
    buf = (lpbuffer_t *)LPMemoryPool.alloc(1, sizeof(lpbuffer_t) + audiosize);
    memcpy(buf->data, str + offset, audiosize);
    offset += audiosize;

    memcpy(msg, str + offset, sizeof(lpmsg_t));
    offset += sizeof(lpmsg_t);

    buf = (lpbuffer_t *)LPMemoryPool.alloc(1, sizeof(lpbuffer_t));

    buf->length = length;
    buf->channels = channels;
    buf->samplerate = samplerate;
    buf->is_looping = is_looping;
    memcpy(buf->data, audio, audiosize);
    buf->onset = onset;

    buf->phase = 0.f;


@@ 2673,11 2660,13 @@ int astrid_instrument_jack_callback(jack_nframes_t nframes, void * arg) {
        memset(output_channels[c], 0, nframes * sizeof(float));
    }

#if 0
    /* write the block into the adc ringbuffer */
    if(lpsampler_write_ringbuffer_block(path, input_channels, instrument->channels, nframes) < 0) {
        syslog(LOG_ERR, "Error writing into adc ringbuf\n");
        return 0;
    }
#endif

    /* mix in async renders */
    if(instrument->async_mixer != NULL) {


@@ 3179,8 3168,8 @@ lpinstrument_t * astrid_instrument_start(
    syslog(LOG_DEBUG, "Opened message relay queue for %s with fd %d\n", instrument->name, instrument->exmsgq);

    // Write the instrument name into msg structs
    snprintf(instrument->msg.instrument_name, strlen(instrument->name)+1, instrument->name);
    snprintf(instrument->cmd.instrument_name, strlen(instrument->name)+1, instrument->name);
    strncpy(instrument->msg.instrument_name, instrument->name, strlen(instrument->name)+1);
    strncpy(instrument->cmd.instrument_name, instrument->name, strlen(instrument->name)+1);

    // Start the message sequencer
    if(astrid_instrument_seq_start(instrument) < 0) {


@@ 3450,7 3439,7 @@ int astrid_instrument_console_readline(char * instrument_name) {
    char * line;
    lpmsg_t cmd;

    snprintf(cmd.instrument_name, strlen(instrument_name)+1, instrument_name);
    strncpy(cmd.instrument_name, instrument_name, strlen(instrument_name)+1);

    line = linenoise("^_- ");
    if(line != NULL) {

M astrid/src/astrid.h => astrid/src/astrid.h +2 -2
@@ 314,8 314,8 @@ int lpadc_read_block_of_samples(size_t offset, size_t size, lpfloat_t * out);

int lpsampler_aquire(char * name, lpbuffer_t ** buf);
int lpsampler_release(char * name);
int lpsampler_read_block_of_samples(char * name, size_t offset, size_t size, lpfloat_t * out);
int lpsampler_write_ringbuffer_block(char * name, float ** block, int channels, size_t blocksize_in_samples);
int lpsampler_read_ringbuffer_block(char * name, size_t offset_in_frames, size_t length_in_frames, int channels, lpfloat_t * out);
int lpsampler_write_ringbuffer_block(char * name, float ** block, int channels, size_t blocksize_in_frames);
int lpsampler_create(char * name, double length_in_seconds, int channels, int samplerate);
int lpsampler_destroy(char * name);


M pippi/renderer.pxd => pippi/renderer.pxd +1 -1
@@ 116,7 116,7 @@ cdef extern from "astrid.h":

        lpscheduler_t * async_mixer

    int lpsampler_read_block_of_samples(char * name, size_t offset, size_t size, lpfloat_t * out)
    int lpsampler_read_ringbuffer_block(char * name, size_t offset_in_frames, size_t length_in_frames, int channels, lpfloat_t * out)

    int lpipc_getid(char * path)
    ssize_t astrid_get_voice_id()

M pippi/renderer.pyx => pippi/renderer.pyx +10 -6
@@ 18,6 18,8 @@ import sys
import time
import warnings

cimport cython

from pippi import dsp, midi, ugens
from pippi.soundbuffer cimport SoundBuffer



@@ 36,7 38,6 @@ if not logger.handlers:
    logger.setLevel(logging.DEBUG)
    warnings.simplefilter('always')

cdef lpfloat_t[LPADCBUFSAMPLES] adc_block

cdef bytes serialize_buffer(SoundBuffer buf, int is_looping, lpmsg_t msg):
    cdef bytearray strbuf


@@ 406,22 407,25 @@ cdef class Instrument:
    cdef SoundBuffer read_from_adc(Instrument self, double length, double offset=0, int channels=2, int samplerate=48000):
        cdef size_t i
        cdef int c
        cdef double[:,:] block
        cdef double * blockp
        name_byte_string = ("%s-adc" % self.name).encode('UTF-8')
        cdef char * _ascii_name = name_byte_string

        cdef SoundBuffer snd = SoundBuffer(length=length, channels=channels, samplerate=samplerate)
        cdef size_t length_in_frames = len(snd)
        cdef size_t offset_in_frames = <size_t>(offset * samplerate)
        block = snd.frames
        if not block.flags['C_CONTIGUOUS']:
            block = block.ascontiguousarray(block)

        block_memview: cython.double[::1] = block.reshape(-1).flatten()

        # fixme add dcblock
        if lpsampler_read_block_of_samples(_ascii_name, offset_in_frames * channels, length_in_frames * channels, adc_block) < 0:
        if lpsampler_read_ringbuffer_block(_ascii_name, offset_in_frames, length_in_frames, channels, cython.address(block_memview[0])) < 0:
            logger.error('pippi.renderer ADC read: failed to read %d frames at offset %d from ADC' % (length_in_frames, offset_in_frames))
            return snd

        for i in range(length_in_frames):
            for c in range(channels):
                snd.frames[i,c] = adc_block[i * channels + c]

        return snd

    def reload(self):