ModalityTeam / Modality-toolkit

A SuperCollider toolkit to simplify the creation of personal (electronic) instruments utilising hardware and software controllers of any kind.
http://modalityteam.github.io/
87 stars 26 forks source link

PassSpec: ERROR: Message 'default' not understood. #385

Closed elgiano closed 2 years ago

elgiano commented 2 years ago

Hello! I'm trying to use PassSpec to get some string values from OSC messages (writing for the monome grid). Here is a desc snippet:

elementsDesc: (
    shared: ( ioType: \in, spec: PassSpec),
    elements: [( key: 'device',  oscPath: "/serialosc/device" )]
)

It fails on MKtl creation with this error

^^ ERROR: Message 'default' not understood.
RECEIVER: PassSpec

Proposed fix

On my machine I fix it like this:

PassSpec {
    var <>default = nil;

    *new { |default| ^this.newCopyArgs(default) }
    *asSpec { ^this.new }
    *map { |inval| ^inval }
    *unmap { |inval| ^inval }

    map { |inval| ^inval }
    unmap { |inval| ^inval }
    asSpec { ^this }
}

Because it looks like deviceSpec.default is created when MKtlElements are created (here MKtlElement.elemDesc_:)

if (deviceValue.isNil) {
    deviceValue = prevDeviceValue = this.defaultValue;
};
...
defaultValue {
    ^if (deviceSpec.notNil) { deviceSpec.default} { 0 };
}

So apparently PassSpec not having a *default method makes PassSpec unusable for MKtlElements.. or am I missing something?

Full error call stack

CALL STACK:
    DoesNotUnderstandError:reportError
        arg this = <instance of DoesNotUnderstandError>
    Nil:handleError
        arg this = nil
        arg error = <instance of DoesNotUnderstandError>
    Thread:handleError
        arg this = <instance of Thread>
        arg error = <instance of DoesNotUnderstandError>
    Object:throw
        arg this = <instance of DoesNotUnderstandError>
    Object:doesNotUnderstand
        arg this = <instance of Meta_PassSpec>
        arg selector = 'default'
        arg args = [*0]
    MKtlElement:elemDesc_
        arg this = <instance of MKtlElement>
        arg dict = <instance of Event>
        var mySpecOrName = nil
    Meta_MKtlElement:new
        arg this = <instance of Meta_MKtlElement>
        arg name = 'device'
        arg desc = <instance of Event>
        arg source = <instance of MKtl>
    Meta_MKtlElementGroup:fromDesc
        arg this = <instance of Meta_MKtlElementGroup>
        arg desc = <instance of Event>
        arg srcMktl = <instance of MKtl>
        var elems = nil
        var isGroup = false
        var group = nil
        var elemKey = 'device'
        var mktlElemDict = <instance of Event>
        var newElem = nil
    < FunctionDef in Method Collection:collectAs >
        arg elem = <instance of Event>
        arg i = 0
    ArrayedCollection:do
        arg this = [*3]
        arg function = <instance of Function>
        var i = 0
    Collection:collectAs
        arg this = [*3]
        arg function = <instance of Function>
        arg class = <instance of Meta_Array>
        var res = [*0]
    Meta_MKtlElementGroup:fromDesc
        arg this = <instance of Meta_MKtlElementGroup>
        arg desc = <instance of Event>
        arg srcMktl = <instance of MKtl>
        var elems = nil
        var isGroup = true
        var group = nil
        var elemKey = nil
        var mktlElemDict = <instance of Event>
        var newElem = nil
    MKtl:makeElements
        arg this = <instance of MKtl>
    MKtl:finishInit
        arg this = <instance of MKtl>
        arg lookForNew = false
        arg multiIndex = nil
        arg tryOpenDevice = true
    MKtl:init
        arg this = <instance of MKtl>
        arg argDesc = <instance of MKtlDesc>
        arg argLookupName = nil
        arg argLookupInfo = nil
        arg lookForNew = false
        arg multiIndex = nil
        arg tryOpenDevice = true
        var specsFromDesc = nil
    < closed FunctionDef >  (no arguments or variables)
adcxyz commented 2 years ago

You are right PassSpec should have a default method. PassSpec does not need to have instances, as it only passes values through, so this is enough:

PassSpec {
    *new { ^this }
    *asSpec { ^this }
    *map { |inval| ^inval }
    *unmap { |inval| ^inval }
    *default { ^nil }
}

I did some more tests, which should go in PassSpec help file:

(
d = (
    deviceName: "testa",
    protocol: \osc,
    idInfo: "testa",
    netAddrInfo: ( ipAddress: "169.254.1.1", srcPort: 9000, recvPort: 8000 ),

    elementsDesc: (
        shared: ( ioType: \in, spec: PassSpec, \type: \unknown),
        elements: [( key: 'device',  oscPath: "/serialosc/device" )]
    )
);
MKtl(\x).free;
MKtl(\x, d);

MKtl(\x).elAt(\device).dump;

)

// tests: setting and and getting non-number value works with PassSpec
MKtl(\x).elAt(\device).elemDesc.deviceSpec; // the class PassSpec

MKtl(\x).elAt(\device).value = "foo";
MKtl(\x).elAt(\device).value.cs;
MKtl(\x).elAt(\device).deviceValue.cs;

MKtl(\x).elAt(\device).deviceValue = \oof;
MKtl(\x).elAt(\device).value.cs
MKtl(\x).elAt(\device).deviceValue.cs

// showing it on a gui fails, because non-number elements not supported in MKtlGUI ...
MKtl(\x).gui;
elgiano commented 2 years ago

Great @adcxyz, thanks a lot! It works perfectly, we can close this issue for me, unless you need it for something else.

adcxyz commented 2 years ago

welcome @elgiano :-) I added a PassSpec help file and a new issue to keep track of the gui problem.

just for curiosity, what are the values of your device element?

elgiano commented 2 years ago

It's a heterogeneous list with strings and integers... full story below :)

I made a quick MKtl interface for the monome grid, I have the version with 64 buttons. Despite being the best candidate ever for using MIDI, it actually works via serial (USB). Monome provides a program, serialoscd, that reads serial from monome devices and acts as an OSC server... SC then needs to send a /serialosc/list message to serialoscd with its langPort, to get a reply with a list of connected devices and their ports (serialoscd opens a new one for each connected device)...

Here is the desc and usage code. Quick and dirty, it works only for 1 connected monome device, but it could be extended for more. Actually, it would be better to write MKtl descs only for devices and manage serialoscd from a class, with OSCFuncs...

(
idInfo: "monome_serialosc",
protocol: \osc,
netAddrInfo: (srcPort: 12002, recvPort: NetAddr.langPort),
specialMessages: (
    listDevices: [["/serialosc/list", "localhost", NetAddr.langPort]],
    notify: [["/serialosc/notify", "localhost", NetAddr.langPort]],
),
elementsDesc: (
    shared: ( ioType: \in, spec: PassSpec ),
    elements: [
        (
            key: 'device',
            valueAt: [1,2,3],
            oscPath: "/serialosc/device",
        ),
        ( key: 'add', oscPath: "/serialosc/add" ),
        ( key: 'remove', oscPath: "/serialosc/remove" ),
    ]
)
)
if ("pidof serialoscd".unixCmdGetStdOut == "") {
    "serialoscd".runInTerminal;
    1.wait;
};

MKtl(\serialosc, "monome-serialosc");

MKtl(\serialosc).elAt(\device).action = {|el|
    "[Monome Serial OSC] device id: % (%), port %".format(*el.value).postln;
    MKtl(\grid) ?? { MKtl(\grid, "monome-grid" ) };
    MKtl(\grid).sendSpecialMessage(\register); // this sends a '/sys/port' message to grid, with NetAddr.langPort
    MKtl(\grid).device.updateDstAddr("localhost", el.value.last);
    MKtl(\grid).device.updateSrcAddr("localhost", el.value.last);
};

MKtl(\serialosc).sendSpecialMessage(\listDevices);