• ddw_music

    @impression I think it's not possible to avoid the central problem, which is associating a note-off message with the prior matching note-on message. Unfortunately, this problem takes some careful thinking. It's tempting to avoid doing this careful thinking at the incoming-message stage and try to push the problem off to another part of the patch. But, I think everything is going to be easier if the note parsing is done correctly in the first place.

    I think you will need one [text] just to parse the messages into chords, and then another [text] as a chord repository.

    The basic flow I'm thinking of is:

    • Use [poly] to assign an index to incoming note numbers. (Why? Because, when a note-off message comes in, you have to find the matching note-on record and look up the time. Then you can use that time to calculate the duration. There is no way to do this without matching note-on and note-off, and [poly] does this for you.)

    • Note on: Some jiggery-pokery to rearrange the note-on data into the format "poly-index timer-value note-num velocity" and then write this list into a [text] at the row number where that index lives.

    • Note off: Find the [text] entry matching index, unpack it, subtract the current time from the message's time (to get duration), pack into the desired format "note-num vel duration' and add to the current chord.

    I had to make a decision about how to know when the current chord is finished. In this patch, it's when all notes have been released -- [pd notecount] gives a bang when a note-off decrements the active note count down to 0. That may or may not be right for your use case, but that part can probably be adjusted without requiring too much surgery on the rest.

    In any case, in the end I get messages such as the following:

    chord: 69 101 394.739 62 101 394.739 60 101 394.739 64 101 394.739 67 101 394.739 71 101 394.739
    chord: 65 101 394.739 72 101 417.959 67 101 394.739 69 101 394.739
    chord: 71 101 394.739 65 101 394.739 60 101 394.739 62 101 394.739 64 101 394.739 72 101 394.739
    chord: 65 101 394.739 64 101 394.739 67 101 417.959 72 101 394.738 71 101 394.739

    ... which could be read out in groups of three for playback. (The durations and velocities are all the same because I was testing with MIDI notes from SuperCollider -- I had specified a duration of 400 ms for each note and Pd is pretty close to that. So I think that confirms the approach.)

    (Note that it's a good idea to reset the [timer] at the beginning of a run -- I set up MIDI controller 37 as a trigger for that but you could do it any other way you want.)

    hjh

    pd-midi-notes.png

    midi-parsing.pd

    posted in technical issues read more
  • ddw_music

    @impression What if you store your chords in the format:

    note0 vel0 dur0 note1 vel1 dur1 ...

    That is, don't bother with clumping all the note numbers together at all -- interleave them from the start.

    Then your iterator can "get $1 3" and increment by 3 each time.

    Maybe an example later when I have a bit more time.

    hjh

    posted in technical issues read more
  • ddw_music

    @impression said:

    I can’t really use [clone] for my synth setup.

    I'm curious, why is that? There's probably a solution. If it's the first time you're using [clone], I can see how it might be tricky to figure out, but "haven't figured it out" may not be the same as "can't use it."

    I’d basically need to generate my own note on and note off messages manually.

    That's probably the [makenote] object.

    hjh

    posted in technical issues read more
  • ddw_music

    For computer-to-computer communication, Open Sound Control over a LAN may be easier than MIDI (as a transport device).

    hjh

    posted in technical issues read more
  • ddw_music

    BTW though: I hadn't looked closely at the sources earlier, so I was conjecturing. Looked just now. If we take, say, "unpack" as an example of an object that typically has multiple outlets, and see how it responds to a list:

    in m.connective.c:

    static void unpack_list(t_unpack *x, t_symbol *s, int argc, t_atom *argv)
    {
        t_atom *ap;
        t_unpackout *u;
        int i;
        if (argc > x->x_n) argc = (int)x->x_n;
        for (i = argc, u = x->x_vec + i, ap = argv + i; u--, ap--, i--;)
        {
            t_atomtype type = u->u_type;
            if (type != ap->a_type)
                pd_error(x, "unpack: type mismatch");
            else if (type == A_FLOAT)
                outlet_float(u->u_outlet, ap->a_w.w_float);
            else if (type == A_SYMBOL)
                outlet_symbol(u->u_outlet, ap->a_w.w_symbol);
            else outlet_pointer(u->u_outlet, ap->a_w.w_gpointer);
        }
    }
    

    ... where each output value calls either outlet_float() or outlet_symbol() or outlet_pointer(). It'll move to the next outlet when the outlet_ function returns.

    m_obj.c:

    void outlet_float(t_outlet *x, t_float f)
    {
        t_outconnect *oc;
        if(!stackcount_add())
            outlet_stackerror(x);
        else
            for (oc = x->o_connections; oc; oc = oc->oc_next)
                pd_float(oc->oc_to, f);
        stackcount_release();
    }
    

    OK, check for stack overflow and then call pd_float() for each connection from that outlet. (_bang, _symbol, _pointer are similar.)

    m_pd.c:

    void pd_float(t_pd *x, t_float f)
    {
        if (x == &pd_objectmaker)
            ((t_floatmethodr)(*(*x)->c_floatmethod))(x, f);
        else
            (*(*x)->c_floatmethod)(x, f);
    }
    

    Get a reference to the next object's floatmethod function, e.g. and call it: a method handler --> an iterator over connections --> a connection handler --> another object's method handler, at which point the cycle recurs.

    So the way that it backtracks seems to be simply that e.g. outlet_float() returns and the method handler resumes where it left off: normal tree-traversal recursion using C's call stack (without data return, as these are all void).

    Gotos do appear in the Pd source but it looks like the vast majority of these are used for early-exit or for input validation before performing some operation -- goto error, goto fail, goto doit, etc. I don't see them in the message-passing functions. (What I also don't see in the message-passing functions is an explicit stack of objects/connections -- stackcount_add() is just a global counter, no real push/pop -- so if I guessed that pd keeps its own stack of objects visited, that was a wrong conjecture.)

    hjh

    posted in technical issues read more
  • ddw_music

    @impression said:

    Lets say I want 32 polyphony, that would be an insane effort.

    IMO this all comes down to data structure design. A bad design will require insane effort. A good design will scale up to any amount of polyphony you need.

    What's the input? From your MIDI keyboard?

    What do you need to do with the notes afterward? Just spit the MIDI events back out, or quantize them, etc etc?

    [text sequence] is a nice way to store note sequences. Each entry starts with the time delta before this note, followed by the note data, which may be anything you want (e.g., note number, sustain time, velocity, suitable for [makenote] when playing back).

    It would really help if you could explain the goal, to help design a way to un-insane the insane effort.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid Agreed, yes, let's move on.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid Thanks. I think this confirms that you and I are talking at orthogonal purposes. What I've been trying to say is that one can use a linked list to implement a stack and use that stack to organize gotos into procedure-call behavior, or one could use a dedicated Stack data type and implement the same behavior using only structured control flow, and the behavior in both cases is that of a procedure call. Pointing out that the one is just a linked list and gotos doesn't make it not procedure-call behavior.

    I.e. we're focusing on different levels of abstraction. Or, in answer to your earlier question, "are they really procedure calls or is that just conceptual?" -- I'm mainly interested in the conceptual aspect, yes, and you're mainly interested in the concrete implementation. So any confusion/frustration stems from talking past each other.

    hjh

    posted in technical issues read more
  • ddw_music

    @jameslo said:

    Hmm, that's interesting, have you considered Java's use of gotos? :)

    Nice one :grin: well, there's a time and place for everything. There are gotos in SuperCollider's C++ codebase, iirc mainly in the long switch blocks in the bytecode interpreter. I guess... it's like second inversion triads. In conventional harmony, there are a few legit ways to use them, but otherwise, don't; same for gotos.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid Perhaps I owe an apology for an excessively critical post -- I can see that I was a bit too heated.

    Perhaps part of the problem in this thread is trying to find terminology without agreeing on terminology -- I'm using some terms in a way that you disagree with, or might be misunderstanding to some extent, and vice versa.

    I think some of the things that have been said in this thread are misleading in some ways. Message-passing along patch cables isn't a goto -- it just isn't. Gotos have been discouraged for a long time because they're messy. Overuse produces spaghetti code, and goto-heavy code is hard to debug because gotos are history-less -- there's no backtrace to see how you got into a bad place. Pd message passing keeps all the history until you reach the end of the chain. Stack traces are a characteristic of procedure/function calls -- not a characteristic of gotos.

    I do see that Pd doesn't use a stack for returning data to a caller. But there have been some statements that seem to go beyond that, for instance, "without a return stack we can't actually return" -- that's true, we can't return data but execution can and does return back to an object that was previously touched.

    Again, probably a lot of this is about terminology -- for example, when I used the term "procedure call," that isn't the normal way that Pd users talk about message passing, so there's a bit of "well that doesn't make sense"... but I think the behavior of Pd control objects lines up very closely with the behavior of procedure calls, and I think some of the resistance to this idea has been overstated to some extent or another. (Likewise, to suggest that you totally rejected the idea of a stack is a considerable overstatement -- which I certainly have to retract.)

    So I am sorry about getting a bit punchy there -- at the same time, I'm a bit bewildered why so many things I've tried to say in this thread seem to have gotten a "well, no, that's not it" where I guess I hoped for a "hmm, that's interesting, have you considered?"

    hjh

    posted in technical issues read more
  • ddw_music

    @impression The first step is to work out the data structure. If it's my project, I'd go with one of these approaches:

    • Save MIDI note on and note off directly, without much analysis. Playback logic would then be very similar to MIDI polyphonic playback.
    • Or, use note-off to calculate the sustain duration of the preceding note that matches the note-off pitch. It's harder to calculate that, but then you get a list of notes with parameters "note number, velocity, sustain duration" -- may be more useful for algorithmically manipulating the notes.

    What are your thoughts about how to play back the sequence?

    hjh

    posted in technical issues read more
  • ddw_music

    @oid said:

    @ddw_music In pd there is no instruction pointer so never any need to return or backtrack or goto.

    You absolutely must backtrack in the case of any object that has multiple outputs. Backtracking is most straightforwardly done with a stack.

    A stack is only a data structure that defines two operations: add something new at the end (push) and take the last item off of the end (pop) -- last in, first out. "Stack" doesn't define what it should be used for. It may be data, as in FORTH; it may be execution frames; can be anything.

    If necessary, I could find it in the source code later, but let's assume that Miller Puckette is no fool, and he knows that tree traversal is best done with a stack, and that he isn't going to waste time trying to implement some alternative to a stack because of a preconception that stacks are for x or y but never z (and further, that he wouldn't post "stack overflow" errors to the console if that was misleading).

    I am not quite sure how it deals with reaching the end of the branch...

    I'm also not sure of the implementation details in C (don't really need to be), but if each object gets pushed onto a message-passing stack, then it's easy: when this object is done, pop an object off the stack and see if it has outputs that haven't been issued yet; then issue them, or if there are none, pop again and repeat until the stack is empty. Because a stack is LIFO, this is always (literally!) backtracking.

    If you've ever used Max's graphical debug mode: it literally shows you the message-passing stack in a window. I'll admit that I don't have the line-number evidence to show exactly how pd does it in C, but I'm not making this up...

    Trigger is unused at run time, it's only used at "flatten" time... However audio tree is flatten to create a DSP chain but maybe the control tree is not…

    I'm quite sure there's no flattening for control objects. (See Max's debugger, or I bet pd's trace mode will show the same.)

    Edit:

    pd-trace-tbb.png

    trace: bang 
    trigger: bang      -- not flattened
    message: One 
    print: One
    trigger: bang      -- and, after doing "One," it backtracked to here
    message: Two 
    print: Two
    backtrace:  -- what happened before the [trace] object?
    bng: bang 
    

    Note that you don't know "what happened before the [trace] object" without a stack :wink:

    Again, if I'm really saying anything controversial, when time permits I can go diving into pd's sources and find the evidence. But IMO the [trace] output is enough.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid said:

    @ddw_music said:

    In Pd, wires are procedure calls, not gotos.

    Are they actually procedures or just conceptually?

    The distinction I'm thinking of is that gotos have no history, while procedure calls do. When a chain of objects runs out of connections, pd and max do backtrack, so there must be a call stack for the objects that have been traversed so far (this, I'm certain, is not conceptual in pd -- there are stack overflow errors, for one thing). A goto is just a jump, with no memory of how it got there.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid said:

    Edit2: I guess wires are also gotos then.

    I realized a bit later what the difference is -- In Pd, wires are procedure calls, not gotos.

    Continuing the tangent, feel free to disregard :wink:

    Pascal distinguishes between procedures and functions, where a function has a return value and a procedure does not. (C gets rid of the term "procedure," but you can still have void functions.) In SmallTalk (and SC inherits this), there is always a return value, which may be ignored -- hence no such thing as a procedure. The last thing a function or method does is to return a result and drop its own execution frame off the stack. In rand(20) * 2, after rand returns the random number, there is only the result -- the fact that rand was called at all is forgotten. (Returning from a procedure just drops the execution frame, with no return value.)

    In dataflow, there are only procedures and the last thing an object does is to procedure-call the next object -- the entire history stays on the stack. In [random 20] --> [* 2] --> [print], "random" is more like: 1. Get from the RNG; 2. Call descendants with this result; 3. Then backtrack up the stack.

    It's kind of like, Random(20, descendants: [Multiply(_, 2, descendants: [Print(_)])]). I could hypothetically create these objects in SC and it would work (but, not much point lol).

    The two approaches are mirror images: lisp would write (print (* (random 20) 2)) where the last operation to be performed ("print") is at the head of the tree. Structurally SC is the same. Syntactically it may be different, since a method selector may be either pre- or post-fixed, but (rand(20) * 2).postln still parses as follows. Then it renders bytecodes depth first! So internally it ends up like FORTH ("push 20, random, push 2, *, postln").

       postln
         |
         *
        / \
    rand   2
     |
     20
    

    But in Pd, the last operation is structurally the innermost leaf, not the head.

    I think this explains a bit why it took me a good couple of years to wrap my head around dataflow, even though (I thought) I knew a thing or two. It's literally upside-down world for me. Still struggle with that sometimes.

    hjh

    posted in technical issues read more
  • ddw_music

    @FFW said:

    Nevertheless I'd like to note that even in written languages the return to caller process at low level is coded by giving the called method an address to store the result in and then the caller can read it.

    I think (not a major expert here) that most text languages do this on the stack: an operator or function leaves its result on the stack, and returns to the point in the code that called it. Nested calls like myFunction(random(20) * 2, random(20) * 3) are easy for an interpreter to handle if they're:

    1. Push 20
    2. Do "random" on the top value.
    3. Push 2.
    4. Do * on the top 2 values.
    5. 6, 7, 8. Similar 4 steps.
    6. Do "myFunction" on the 2 values left on the stack (and leave its result there).

    In that sense, high level languages are just a less mind-bending way to write FORTH.

    (oid is right that Pd uses a stack for something quite different, such that returning data back upstream really can't be a thing. But workarounds aren't all that painful in the end.)

    hjh

    posted in technical issues read more
  • ddw_music

    @porres

    for now it's just quite abstract and over my head

    I made a concrete example -- with scores! :wink: If the programming-ese terminology is too difficult, maybe focus on that example.

    I hear that it's impossible to do something "x", or too hard, and I hear about complex programming paradigms, and then it comes down to a simple solution like using [value].

    That's a good point. Sometimes it takes effort to find the simplest way.

    @oid

    I don't think extending pd would solve this issue

    Yeah, the core of the issue is that Pd's stack works differently from most text languages since Fortran.

    Tbh I think variable-target [send] is fine, once it's been established that [send] is going to be involved in pretty much any Pd solution. "Return to caller" as in any structured or object-oriented language is nicer, but [send] is not bad.

    what I think @ddw_music refers to as "spray everywhere,"

    Here, I'm referring specifically to https://forum.pdpatchrepo.info/topic/15085/return-to-caller-followup-from-symbols-explicit/8 , where the [send] pushes the value out to all matching receivers -- to every potential caller -- where it's swallowed by an [f]'s cold inlet, and only the specific caller pushes the value forward. If you have 50 potential callers, then the data have to be replicated 50 times.

    If the data are sub-lists instead of single floats, that would be a lot of multiple allocation. My/jameslo's solution doesn't do that; nor does yours.

    to get the equivalent of abstractions calling abstractions would require you to fill your abstractions with [v ]s and [send]s and [receives]s in the vain attempt to cover every possible situation

    [list-map] in list-abs shows a nice approach, though, decoupling the iteration logic from the user-operation: the abstraction outputs a trigger to perform the operation, and then the result of that operation is put back into the abstraction to finish the job. That could inspire a super-minimalist approach (which has the benefit of assuming nothing about the returned value's type, whereas most other solutions posted here seem to assume it will always be a single float):

    pd-call-return.png

    Now, I didn't think of this until the discussion went through a few iterations... see porres's comment about the eventual solution often being much simpler than the problem seems at first.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid said:

    @ddw_music Value has been mentioned multiple times in both threads :)

    I know, and I've used value in other places. I hadn't thought to connect it to the current index, though.

    Not sure what you mean by "spray everywhere," these are targeted approaches and there is no copying.

    Sure, that's a misreading. I see also (on closer inspection) that folding the metro into the stream abstraction makes the stream effectively its own caller, sidestepping the return to caller problem. Though this means that your approach could be used only in self-timed situations, where SC streams and Python iterators are more general.

    Haskell people like to say that a programming language isn't worth learning if it doesn't change the way you think. Pd has actually done that for me, to some extent.

    hjh

    posted in technical issues read more
  • ddw_music

    @oid the [v] for the index is interesting. I wouldn't have thought of that.

    (I guess?) I still lean slightly toward the targeted-send approach, over the spray-everywhere approach. One reason is, if you're using lists, sending a sub-list to one place is likely to be more efficient than sending it to potentially dozens of places. It's a valid concern, about redundant list allocations; one way to deal with that is to be less promiscuous about sending data to places where they won't be used.

    I do appreciate everyone's indulgence. I've certainly gotten more comfortable with patching over the years, but I'm never quite sure if what I'm doing is really idiomatic.

    hjh

    posted in technical issues read more
  • ddw_music

    @jameslo said:

    @ddw_music Does this behave like a shared stateful function that returns to the caller?

    It does (and is basically identical to the approach in my example patch above).

    So perhaps this really is the best way.

    hjh

    posted in technical issues read more
Internal error.

Oops! Looks like something went wrong!