view yetilab/stream/framer.yeti @ 296:ec6a36d88f57

Move complex tests to complex package
author Chris Cannam
date Fri, 31 May 2013 22:02:03 +0100
parents 9bc985b602b3
children e8000dbab96e
line wrap: on
line source

module yetilab.stream.framer;

/**
 * Framer expresses a stream (or a file) as a lazy list of (possibly
 * overlapping) frames of data.
 */

vec = load yetilab.vector;
bf = load yetilab.vector.blockfuncs;
af = load yetilab.stream.audiofile;
win = load yetilab.signal.window;
fft = load yetilab.transform.fft;
mat = load yetilab.matrix;
ch = load yetilab.stream.channels;

//!!! todo: synchronized for everything with state assignment

blockList framesize stream =
    if stream.finished? then
        stream.close ();
        []
    else
        mat.resizedTo { rows = stream.channels, columns = framesize }
           (stream.read framesize)
            :. \(blockList framesize stream);
    fi;

overlappingBlockList size hop stream valid buffer =
   (
    m = stream.read hop;
    obtained = mat.width m;

    // Retain framesize - hop samples from old buffer, add hop samples
    // (zero-padded if necessary) just read
    buffer = map2
        do buf row:
            vec.concat
               [vec.slice buf hop size,
                vec.resizedTo hop (mat.getRow row m)];
        done buffer [0..stream.channels-1];

    // Number of "valid" elements (not tail-end zero-padding) left in buffer
    remaining = valid - (hop - obtained);

    if remaining <= 0 then
        stream.close ();
        [];
    else
        mat.newMatrix (RowMajor ()) buffer
            :. \(overlappingBlockList size hop stream remaining buffer);
    fi);

frames { framesize, hop } stream =
    if framesize == hop then
        blockList framesize stream
    else
        overlappingBlockList framesize hop stream 
            framesize (map \(vec.zeros framesize) [0..stream.channels-1]);
    fi;

streamContiguous rate framesize frames =
   (var remaining = frames;
    var buffered = mat.zeroSizeMatrix ();
    var position = 0;
    channels = mat.height (head frames); // so we don't need to keep a head ptr
    {
        get position () = position,
        get channels () = channels,
        get sampleRate () = rate,
        get finished? () = empty? remaining and mat.empty? buffered,
        get available () = 
            // Don't take length of frames -- we don't want to lose laziness.
            // If the caller cares that much, they can measure frames themselves
            if empty? remaining then
                Known (mat.width buffered) 
            else
                Unknown ()
            fi,
        read count =
           (framesFor samples acc =
                if samples <= 0 or empty? remaining then
                    acc
                else
                    this = head remaining;
                    remaining := tail remaining;
                    framesFor (samples - mat.width this) (acc ++ [this])
                fi;
            source = mat.concat (Horizontal ()) (framesFor count [buffered]);
            toReturn = mat.columnSlice source 0 count;
            position := position + mat.width toReturn;
            buffered := mat.columnSlice source count (mat.width source);
            toReturn),
        close = \(),
    });

overlapAdd overlap frames =
   (ola fr acc =
        case fr of
        first::rest:
           (w = mat.width acc;
            pre = mat.columnSlice acc 0 (w - overlap);
            added = mat.sum first
               (mat.resizedTo (mat.size first)
               (mat.columnSlice acc (w - overlap) w));
            pre :: ola rest added);
         _:
            [acc];
        esac;
    case frames of
    first::rest:
        mat.concat (Horizontal ()) (ola rest first);
     _: 
        mat.zeroSizeMatrix ();
    esac);

streamOverlapping rate framesize hop frames =
   (var remaining = frames;
    var buffered = mat.zeroSizeMatrix ();
    var position = 0;

    factor = hop / (framesize/2);
    w = bf.scaled factor (win.hann framesize); // periodic window, not symmetric
    channels = mat.height (head frames); // so we don't need to keep a head ptr

    syncd = synchronized remaining;

    finished' () = syncd \(empty? remaining and mat.empty? buffered);

    read' count = 
       (framesFor samples acc =
            if samples <= 0 or empty? remaining then
                acc
            else
                this = mat.resizedTo { columns = framesize, rows = channels }
                   (mat.newMatrix (RowMajor ())
                       (map (bf.multiply w) (mat.asRows (head remaining))));
                remaining := tail remaining;
                framesFor (samples - hop) (acc ++ [this])
            fi;
        source = overlapAdd (framesize - hop)
           (framesFor count [buffered]);
        buffered := mat.columnSlice source count (mat.width source);
        mat.columnSlice source 0 count);

    // lose initial padding
    \() (read' (framesize - hop));
    
    {
        get position () = syncd \(position),
        get channels () = channels,
        get sampleRate () = rate,
        get finished? () = finished' (),
        get available () = if finished' () then Known 0 else Unknown () fi,
        read count = syncd
          \(data = read' count;
            position := position + mat.width data;
            data),
        close = \(),
    });
    
//!!! doc: convert frames back to a stream
streamed rate { framesize, hop } frames =
    if framesize == hop then
        streamContiguous rate framesize frames
    else
        streamOverlapping rate framesize hop frames
    fi;

monoFrames params stream =
    map ch.mixedDown (frames params stream);

windowedFrames { framesize, hop, window } stream =
   (win = window framesize;
    map (bf.multiply win) (monoFrames { framesize, hop } stream));

frequencyDomainFrames parameters stream =
   (f = fft.realForward parameters.framesize;
    map f (windowedFrames parameters stream));

{ 
    frames,
    monoFrames,
    windowedFrames,
    frequencyDomainFrames,

    framesOfFile parameters filename =
        frames parameters (af.open filename),

    monoFramesOfFile parameters filename =
        monoFrames parameters (af.open filename),

    windowedFramesOfFile parameters filename = 
        windowedFrames parameters (af.open filename),

    frequencyDomainFramesOfFile parameters filename = 
        frequencyDomainFrames parameters (af.open filename),

    overlapAdd,

    streamed,
}