Sampler

From CSWiki
Revision as of 18:15, 7 February 2009 by Atte (talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
/*
Copyright (c) 2009 Atte André Jensen.  All rights reserved.
http://atte.dk

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.

General purpose sampler class for ChucK (http://chuck.cs.princeton.edu/).
Version 1.1
For examples of usage, see the comments at the end of this file.

Features:
* Polyphony, with dynamic voice allocation (stealing of oldest note)
* Optional envelope
* Automatic handling of stereo samples
* Supports multi-samples (zones)
* Build-in midi keyboard listener (all devices, all channels)

My main motivation for writing this class, was to not have to solve
the same problems (polyphony, handling of stereo) over and over again.
I tried to make it easy to use and useable in a wide range of situations,
while still keeping it fast and economic on the cpu.

Suggestions, feedback, bugfixes or bugreports are welcome:
atte (dot) jensen (at) gmail (dot) com
*/

class SamplerVoice {
    int debug;

    SndBuf s_left => ADSR e_left;
    SndBuf s_right => ADSR e_right;
    Pan2 out;

    
    0 => float root_key;
    0 => int lo_key;
    127 => int hi_key;

    0 => int loaded;
    0 => int playing;
    0 => int envelope_set;
    0 => int loop_enabled;
    0 => int enabled;
    0 => int channels;
    int voice_number;
    -1 => float current_note;

    
    time stamp;
    string filename;


    fun void connect(UGen stereo){
        out => stereo;
    }


    fun void load(string sample, float root){
        root => root_key;
        load(sample);
    }

    fun void load(string sample){
        sample => filename;
        s_left.read(sample);
        s_left.channels() => channels;
        s_left.samples() => s_left.pos;
        if(channels == 2){
            s_right.read(sample);
            s_right.samples() => s_right.pos;
            0 => s_left.channel;
            1 => s_right.channel;
        }
        1 => loaded;
    }

    fun void enable(){
        if(channels == 1){
            e_left => out;
        } else
        if(channels == 2){
            e_left => out.left;
            e_right => out.right;
        }
        1 => enabled;
    }

    fun void disable(){
        if(channels == 1){
            e_left =< out;
        } else
        if(channels == 2){
            e_left =< out.left;
            e_right =< out.right;
        }
        0 => enabled;
        0 => playing;
        -1 => current_note;
    }
    
    fun void on(float note){
        if(debug){
            <<<"in on, note: " + note>>>;
        }
        if(note >= lo_key && note <= hi_key){
            if(root_key > 0){
                note => current_note;
                Std.mtof(note) / Std.mtof(root_key) => s_left.rate => s_right.rate;
            }
            on();
        }
    }
    fun void on(){
        if(debug){
            <<<"in on, voice: " + voice_number>>>;
        }
        
        //1 => playing;
        now => stamp => time my_stamp;

        if(envelope_set){
            e_left.keyOn();
            if(channels == 2){
                e_right.keyOn();
            }
        }
        
        if(loop_enabled){
            1 => s_left.loop;
            if(channels == 2){
                1 => s_right.loop;
            }
        }

        0 => s_left.pos;
        if(channels == 2){
            0 => s_right.pos;
        }
        
        if(!enabled){
            enable();
        }
        if(envelope_set){
            if(debug){
                <<<filename + ": envelope set - should be ok">>>;
            }
        } else
        if(!s_left.loop()){
            if(debug){
                <<<filename + ": no envelope, no looping - should be ok, will let sample run out">>>;
            }
            s_left.length() => now;
            if(stamp == my_stamp){
                disable();
            }
        }
        else {
            if(debug){
                <<<filename + ": looping without envelope - stealing voices's gonna sound bad!">>>;
            }
        }
    }

    fun void off(){
        if(debug){
            <<<"in SampleVoice.off()">>>;
        }
        time old_stamp;
        if(envelope_set){
            e_left.keyOff();
            if(channels == 2){
                e_right.keyOff();
            }
            stamp => old_stamp;
            e_left.releaseTime() => now;
            if(stamp == old_stamp){
                disable();
            }
        }
        else {
            if(debug){
                <<<"cutting off note">>>;
            }
            0 => s_left.loop;
            s_left.samples() => s_left.pos;
            if(channels == 2){
                0 => s_right.loop;
                s_right.samples() => s_right.pos;
            }
            disable();
        }
        
    }
    
    fun void enable_loop(){
        1 => loop_enabled;
    }

    fun void set_gain(float gain){
        gain => out.gain;
    }

    fun void set_envelope(dur attack, dur decay, float sustain, dur release){
        e_left.set(attack, decay, sustain, release);
        if(channels == 2){
            e_right.set(attack, decay, sustain, release);
        }
        1 => envelope_set;
    }
}


public class Sampler {
    0 => int debug;

    Pan2 out;
    
    [
    // currently 16 voices, with little or no overhead
    // copy the following line to add more
    new SamplerVoice, new SamplerVoice, new SamplerVoice, new SamplerVoice,
    new SamplerVoice, new SamplerVoice, new SamplerVoice, new SamplerVoice, 
    new SamplerVoice, new SamplerVoice, new SamplerVoice, new SamplerVoice,
    new SamplerVoice, new SamplerVoice, new SamplerVoice, new SamplerVoice
    ] @=> SamplerVoice voices[];

    fun void on(){
        1 => int must_steal;
        -1 => int voice_to_steal;
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded && !voices[i].playing){
                spork ~ voices[i].on();
                //voices[i].on();
                1 => voices[i].playing;
                0 => must_steal;
                voices.cap() => i;
            }
        }
        if(must_steal){
            now + 100::week => time oldest;
            for(0 => int i; i < voices.cap(); i++){
                if(voices[i].loaded){
                    if(oldest > voices[i].stamp){
                        voices[i].stamp => oldest;
                        i => voice_to_steal;
                    }
                }
            }
            if(voice_to_steal != -1){
                spork ~ voices[voice_to_steal].on();
                1 => voices[voice_to_steal].playing;
            }
            if(debug){
                <<<"must steal " + voice_to_steal>>>;
            }
        }
    }

    fun int already_playing(float note){
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded && voices[i].playing && voices[i].current_note == note){
                return 1;
                
            }
        }   
    }
    
    fun void on(float note){
        if(already_playing(note)){
            if(debug){
                <<<"already playing note: " + note>>>;
            }
            //return;
        }

        if(debug){
            <<<"in Sampler.on">>>;
        }
        1 => int must_steal;
        -1 => int voice_to_steal;
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded && !voices[i].playing && note >= voices[i].lo_key && note <= voices[i].hi_key){
                spork ~ voices[i].on(note);
                1 => voices[i].playing;
                0 => must_steal;
                voices.cap() => i;
            }
        }
        if(must_steal){
            now + 100::week => time oldest;

            for(0 => int i; i < voices.cap(); i++){
                if(voices[i].loaded && note >= voices[i].lo_key && note <= voices[i].hi_key){
                    if(oldest > voices[i].stamp){
                        voices[i].stamp => oldest;
                        i => voice_to_steal;
                    }
                }
            }
            if(voice_to_steal != -1){
                spork ~ voices[voice_to_steal].on(note);
                1 => voices[voice_to_steal].playing;
            }
            if(debug){
                <<<"must steal " + voice_to_steal>>>;
            }
        }
    }

    fun void off(float note){
        if(debug){
            <<<"in sampler.off(), note: " + note>>>;
        }
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded && voices[i].playing && voices[i].current_note == note){
                spork ~ voices[i].off();
            }
        }
    }

    fun void off(){
        if(debug){
            <<<"in sampler.off()">>>;
        }
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded && voices[i].playing){
                spork ~ voices[i].off();
            }
        }
    }

    
    fun void connect(UGen stereo){
        out => stereo;
        /*
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded){
                voices[i].connect(stereo);
            }
        }
        */
    }


    fun int load(string sample){
        1 => int error;
        for(0 => int i; i< voices.cap(); i++){
            if(!voices[i].loaded){
                voices[i].load(sample);
                0 => error;
                voices[i].connect(out);
                i => voices[i].voice_number;
                if(debug){
                    <<<"loaded " + sample + " as voice " + i>>>;
                }
                debug => voices[i].debug;
                voices.cap() => i;
            }
        }
        if(error){
            <<<"out of polyphony, not loaded " + sample>>>;
        }
        return error;
    }

    fun int load(string sample, float root, int polyphony){
        for(0 => int i; i < polyphony; i++){
            load(sample,root);
        }
    }
    


    
    fun int load(string sample, float root){
        1 => int error;
        for(0 => int i; i< voices.cap(); i++){
            if(!voices[i].loaded){
                voices[i].load(sample,root);
                0 => error;
                voices[i].connect(out);
                i => voices[i].voice_number;
                if(debug){
                    <<<"loaded " + sample + " as voice " + i>>>;
                }
                debug => voices[i].debug;
                voices.cap() => i;
            }
        }
        if(error){
            <<<"out of polyphony, not loaded " + sample>>>;
        }
        return error;
    }

    fun int load(string sample, float root, int lo_key, int hi_key, int polyphony){
        for(0 => int i; i < polyphony; i++){
            load(sample,root,lo_key,hi_key);
        }
    }

    fun int load(string sample, float root, int lo_key, int hi_key){
        1 => int error;
        for(0 => int i; i< voices.cap(); i++){
            if(!voices[i].loaded){
                voices[i].load(sample,root);
                0 => error;
                voices[i].connect(out);
                if(debug){
                    <<<"loaded " + sample + " as voice " + i>>>;
                }
                debug => voices[i].debug;
                lo_key => voices[i].lo_key;
                hi_key => voices[i].hi_key;
                voices.cap() => i;
            }
        }
        if(error){
            <<<"out of polyphony, not loaded " + sample>>>;
        }
        return error;
    }

    fun void enable_loop(){
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded){
                spork ~ voices[i].enable_loop();
            }
        }
    }
    
    fun void set_gain(float gain){
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded){
                voices[i].set_gain(gain);
            }
        }
    }

    fun void set_envelope(dur attack, dur decay, float sustain, dur release){
        for(0 => int i; i < voices.cap(); i++){
            if(voices[i].loaded){
                voices[i].set_envelope(attack, decay, sustain, release);
            }
        }
    }
    

    fun void listen_midi(){
        16 => int max_devices;

        for(0 => int i; i < max_devices; i++){
            spork ~ listen_midi(i);
        }
        1::week => now;
    }
        


    fun void listen_midi(int keyboard_device){
        if(keyboard_device == -1){
            return;
        }
        MidiIn midi_in;
        MidiMsg midi_msg;

        int event_channel, event_type, note, velocity;
        
        int type;
        if(!midi_in.open(keyboard_device)){
            me.exit;
            if(debug){
                <<<"couldnt open", keyboard_device>>>;
            }
        }
        while(true){
            midi_in => now;
            while(midi_in.recv(midi_msg)){
                midi_msg.data1 & 0x0f => event_channel;
                ((midi_msg.data1 & 0xf0) >> 4) => event_type;
                if(debug){
                    <<<"----------">>>;
                    <<<midi_msg.data1>>>;
                    <<<midi_msg.data2>>>;
                    <<<midi_msg.data3>>>;
                    <<<"channel: " + event_channel>>>;
                    <<<"type: " + event_type>>>;
                }

                // note on
                if(event_type == 9){
                    midi_msg.data2 => note;
                    midi_msg.data3 => velocity;

                    if(velocity == 0){
                        off(note);
                    }
                    else {
                        on(note);
                    }
                } else
                // note off
                if(event_type == 8){
                    midi_msg.data2 => note;

                    off(note);
                }
}
        }  
    }

}

// usage examples:

/*
Sampler bd;
bd.load("mens_alt_endnu_er_tyst/samples/bd.wav");
bd.connect(dac);
while(true){
    bd.on();
    500::ms => now;
}
*/

/*
Sampler strings;
// load low sample with root 48, between midinote 0 and 56, with polyphony of 8
strings.load("mens_alt_endnu_er_tyst/samples/warm_strings_c4_loop.wav",48,0,56,8);
// load hi sample with root 60, between midinote 57 and 120, with polyphony of 8
strings.load("mens_alt_endnu_er_tyst/samples/warm_strings_c5_loop.wav",60,57,120,8);

strings.connect(dac);
strings.set_envelope(500::ms,10::ms,1,500::ms);
strings.enable_loop();
strings.set_gain(.2);

// listen to midi on all channels, all devices...
spork ~ strings.listen_midi();

// ...while playing random notes
int noteA, noteB;
while(true){
    
    strings.off(noteA);
    Std.rand2(30,61) => noteA;
    strings.on(noteA);
    2::second => now;

    strings.off(noteB);
    Std.rand2(66,67) => noteB;
    strings.on(noteB);
    2::second => now;
}
*/