Capturing both input and output from the console

Hello all!

I’m a student at the University of Washington, and I’m working with the DXARTS department on developing a Quark for reading and working with hrtf data using the SOFA format. We’ve decided that the simplest way to do this would be to interface with the existing SOFA matlab/octave library. This is all well and good - I’ve been able to achieve some decent results by writing temporary source code files, capturing the output by calling octave from the console using Pipe, and then parsing the output string into relevant data structures in SuperCollider. Here’s an example of what that looks like:

var runSOFAroutine;
~sofaSrcDir = /* path to sofa matlab/octave repo ... */ ;

/** Run octave code on a SOFA object using the SOFA API.
 *
 * \param hrtfPath the path name to the sofa file to load
 * \param source the octave code to run
 *
 * The source code should refer to the SOFA object file as `hrtf`. Any output
 * by the source code will be captured and returned as a string.
 */
runSOFAroutine = { | hrtfPath, source |
    var sourceFile, octaveCmd;
    var allSourceCode, pipe, output, lastLine, nextLine;

    // the name of the octave command and the temporary source file
    octaveCmd = "octave";
    sourceFile = "temp.m";

    // define the source code to run:
    //   first we'll need to bootstrap some things
    allSourceCode = [
        // add the path to the sofa M/O repo and initialize
        "addpath('%');".format(~sofaSrcDir.fullPath),
        "SOFAstart;",

        // load the specified sofa file and mark the beginning of output
        "hrtf = SOFAload('%');".format(hrtfPath.fullPath),
        "printf('SuperCollider Data Interface\\n');",

    // now we can append the specified source code
    ] ++ source;

    // write the source code to the temporary file
    File.use(sourceFile, "w", { | fp |
        allSourceCode.do{ | line | fp.write(line ++ "\n"); };
    });

    // run the octave command, and pipe input into it
    pipe = Pipe.argv([octaveCmd, sourceFile], "r");

    // scan the output until coming across the output marker
    lastLine = pipe.getLine; nextLine = pipe.getLine;
    while ({ lastLine != "SuperCollider Data Interface" }, {
        lastLine = nextLine; nextLine = pipe.getLine;
    });
    // write the rest of the output data into an output string
    while ({ nextLine.notNil }, {
        output = output ++ nextLine ++ "\n";
        nextLine = pipe.getLine;
    });

    // clean up and return the output
    pipe.close;
    File.delete(sourceFile);

    output
};

/** Get the source vector of a given index from a SOFA object.
 *
 * \param hrtfPath the path name to the sofa file to load
 * \param index the index of the hrtf
 * \param precision the decimal precision of the output
 *
 * \return a vector of measurements (azimuth, elevation, radius)
 */
~sourceVectorAt = { | hrtfPath, index, precision = 10 |

    // compile the octave source code that computes the source vector
    runSOFAroutine.(hrtfPath, [
        // get a matrix of source vectors then grab the specified index
        "apv = SOFAcalculateAPV(hrtf);",
        "v = apv(%, :);".format(index),

        // print each element (azi, ele, r) separated by commas
        "printf('\\%.%f,\\%.%f,\\%.%f\\n', v(1), v(2), v(3));".format(
            precision, precision, precision),
    ])
    // split the output into list of values, collect as floats
    .split($,).collect({ | val | val.asFloat })
};

However, this is maybe a bit sloppy, since I actually have to write a file to the disk in order to run it with octave. Ideally, I’d be able to open an octave interpreter using a Pipe, so that I can write to the pipe, as well as redirect it’s output - something that I had hoped would have looked like this:

var pipe = Pipe.new("octave", "rw");    
pipe.write("printf('hello, world!\\n');");    
pipe.getLine().postln;    
pipe.close; 

However, it seems that I can only give one of "r" or "w" to the pipe. Is there another way that I can redirect input and output from the console?

Thanks!

Josie
They/them/theirs

Welcome!

I did take a quick look at this yesterday, but didn’t find a way… I was waiting to see if anyone else had some magic.

There’s a curious bit in the source code where a flag “twoway” gets set to true if the mode string contains a +, but Pipe resolutely doesn’t open for modes other than "r" or "w" (as stated in the help file)… so I’m not clear what popen() is doing there.

If this is a case where all the input is prepared in advance, you could write a method to manage the temp file (hide the implementation).

AFAICS SC doesn’t currently support using Pipe to interact with a child process.

hjh

Thanks for your effort! I figured it wasn’t supported, but I thought I’d ask here just in case!

You could use two pipes, one for reading and one for writing, but i think the real problem is there is no way to have non-blocking read or write, and no thread so you can’t do both read and write in SC. I think the only way is to write a little external script that run a command, and receive input by OSC and write output to OSC, this way you could communicate with any program in SC through OSC

I was thinking the same. With Java, I think you could achieve this easily.
Using JavaOSC for the OSC communication between your in-between module and something like Java ProcessBuilder to drive Octave.