Sending OSC `/hush` to Tidal needs two consecutive OSC hushes

To silence patterns via OSC is a feature I really like. I think I have configured all the stuff properly and I can send /hush to Tidal so that it stops playing (or /silence 1, etc.).

However, I have to send two /hush OSC messages quickly after each other so that the hush has an effect. Just sending one has no effect.

I use TouchOSC to send OSC messages and so far I can inspect everything, it sends OSC messages reliably. And it also does not matter whether I send the OSC to localhost or from another machine to Tidal. It does not react on only one /hush.

Does someone has a hint how to dig into the details? Is there an option so that Tidal logs messages in a verbose style so that I can inspect where the single /hush might be swallowed?

Is this unique to /hush, or does sending /silence 1 also require sending two messages in quick succession?

There aren't really any options for extra logging here, but another thing to try is sending a message that's not implemented (such as /hsuh or something). Tidal should print a message saying that the message isn't supported or something.

It happens for /hush and /silence 1 as well.

When I send /foohush, which is an invalid command, I get what you expect

Warning: GHCi | Unhandled OSC: Message {messageAddress = "/foohush", messageDatum = []}

And I do get it not every time I send /foohush. So some messages are swallowed somewhere before. Later today I'll inspect with Wireshark (running out of time :smile:).

Any other ideas why not all /foohush propagate to the warning message?

Definitely check with wire shark whether there’s something happening with the network.

I’m terms of the Tidal side, are you doing anything strange with how you’re configuring Tidal in your boot script? I can’t think of how it could get this specific behavior, but it might be worth trying to reproduce by sending messages to a Tidal instance running with the default BootTidal.hs.

Wireshark shows all the OSC messages and they are all identical (up to the timestamps):

Frame 1716: 44 bytes on wire (352 bits), 44 bytes captured (352 bits) on interface \Device\NPF_Loopback, id 0
    Section number: 1
    Interface id: 0 (\Device\NPF_Loopback)
        Interface name: \Device\NPF_Loopback
    Encapsulation type: NULL/Loopback (15)
    Arrival Time: Sep 18, 2023 17:36:30.694392000 W. Europe Daylight Time
    [Time shift for this packet: 0.000000000 seconds]
    Epoch Time: 1695051390.694392000 seconds
    [Time delta from previous captured frame: 0.021546000 seconds]
    [Time delta from previous displayed frame: 0.134128000 seconds]
    [Time since reference or first frame: 26.400535000 seconds]
    Frame Number: 1716
    Frame Length: 44 bytes (352 bits)
    Capture Length: 44 bytes (352 bits)
    [Frame is marked: False]
    [Frame is ignored: False]
    [Protocols in frame: null:ip:udp:osc]
    [Coloring Rule Name: UDP]
    [Coloring Rule String: udp]
Null/Loopback
    Family: IP (2)
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
        0000 00.. = Differentiated Services Codepoint: Default (0)
        .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
    Total Length: 40
    Identification: 0x35c2 (13762)
    000. .... = Flags: 0x0
    ...0 0000 0000 0000 = Fragment Offset: 0
    Time to Live: 128
    Protocol: UDP (17)
    Header Checksum: 0x0000 [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 127.0.0.1
    Destination Address: 127.0.0.1
User Datagram Protocol, Src Port: 6061, Dst Port: 6060
    Source Port: 6061
    Destination Port: 6060
    Length: 20
    Checksum: 0x998e [unverified]
    [Checksum Status: Unverified]
    [Stream index: 13]
    [Timestamps]
    UDP payload (12 bytes)
Open Sound Control Encoding
    Message: /hush ,
        Header
            Path: /hush
            Format: ,

This is my BootTidal.hs

:set -XOverloadedStrings
:set prompt ""

import Sound.Tidal.Context

import System.IO (hSetEncoding, stdout, utf8)
hSetEncoding stdout utf8

musicTarget = superdirtTarget {oSchedule = Pre BundleStamp, oLatency = 0.0451, oAddress = "127.0.0.1", oPort = 57120}
config = defaultConfig {cProcessAhead = 0.1, cVerbose = True, cFrameTimespan = 1/20, cCtrlAddr = "0.0.0.0", cCtrlPort = 6060}
tidal <- startTidal musicTarget config

:{
let only = (hush >>)
    p = streamReplace tidal
    hush = streamHush tidal
    panic = do hush
               once $ sound "superpanic"
    list = streamList tidal
    mute = streamMute tidal
    unmute = streamUnmute tidal
    unmuteAll = streamUnmuteAll tidal
    unsoloAll = streamUnsoloAll tidal
    solo = streamSolo tidal
    unsolo = streamUnsolo tidal
    once = streamOnce tidal
    first = streamFirst tidal
    asap = once
    nudgeAll = streamNudgeAll tidal
    all = streamAll tidal
    resetCycles = streamResetCycles tidal
    setCycle = streamSetCycle tidal
    setcps = asap . cps
    getcps = streamGetcps tidal
    getnow = streamGetnow tidal
    xfade i = transition tidal True (Sound.Tidal.Transition.xfadeIn 4) i
    xfadeIn i t = transition tidal True (Sound.Tidal.Transition.xfadeIn t) i
    histpan i t = transition tidal True (Sound.Tidal.Transition.histpan t) i
    wait i t = transition tidal True (Sound.Tidal.Transition.wait t) i
    waitT i f t = transition tidal True (Sound.Tidal.Transition.waitT f t) i
    jump i = transition tidal True (Sound.Tidal.Transition.jump) i
    jumpIn i t = transition tidal True (Sound.Tidal.Transition.jumpIn t) i
    jumpIn' i t = transition tidal True (Sound.Tidal.Transition.jumpIn' t) i
    jumpMod i t = transition tidal True (Sound.Tidal.Transition.jumpMod t) i
    jumpMod' i t p = transition tidal True (Sound.Tidal.Transition.jumpMod' t p) i
    mortal i lifespan release = transition tidal True (Sound.Tidal.Transition.mortal lifespan release) i
    interpolate i = transition tidal True (Sound.Tidal.Transition.interpolate) i
    interpolateIn i t = transition tidal True (Sound.Tidal.Transition.interpolateIn t) i
    clutch i = transition tidal True (Sound.Tidal.Transition.clutch) i
    clutchIn i t = transition tidal True (Sound.Tidal.Transition.clutchIn t) i
    anticipate i = transition tidal True (Sound.Tidal.Transition.anticipate) i
    anticipateIn i t = transition tidal True (Sound.Tidal.Transition.anticipateIn t) i
    forId i t = transition tidal False (Sound.Tidal.Transition.mortalOverlay t) i
    d1 = p 1 . (|< orbit 0)
    d2 = p 2 . (|< orbit 1)
    d3 = p 3 . (|< orbit 2)
    d4 = p 4 . (|< orbit 3)
    d5 = p 5 . (|< orbit 4)
    d6 = p 6 . (|< orbit 5)
    d7 = p 7 . (|< orbit 6)
    d8 = p 8 . (|< orbit 7)
    d9 = p 9 . (|< orbit 8)
    d10 = p 10 . (|< orbit 9)
    d11 = p 11 . (|< orbit 10)
    d12 = p 12 . (|< orbit 11)
    d13 = p 13
    d14 = p 14
    d15 = p 15
    d16 = p 16
:}

:{
let getState = streamGet tidal
    setI = streamSetI tidal
    setF = streamSetF tidal
    setS = streamSetS tidal
    setR = streamSetR tidal
    setB = streamSetB tidal
:}

:set prompt "tidal> "
:set prompt-cont ""

default (Pattern String, Integer, Double)

I captured the UDP packet and replayed it with Packet Sender to have another sender than TouchOSC.

Getting the same results :confused: Also on another system (windows too).

To reproduce:

I started some debugging by adding print messages to the execution. There seems to be a timeout while receiving messages.

I can confirm that the hushes do not have an effect, when I end in this branch of the conditional block: https://github.com/tidalcycles/Tidal/blob/3ac320020d809ea626fc283839fecb10c9da4ce2/src/Sound/Tidal/Stream.hs#L681

When the /hush has an effect, the program goes through the else branch, if it does not work, it goes trough the primary branch.

HOT FIX: Setting the timeout in https://github.com/tidalcycles/Tidal/blob/3ac320020d809ea626fc283839fecb10c9da4ce2/src/Sound/Tidal/Stream.hs#L678 to 10 seconds or (-1) for infinite prevents the timeout in reproducible way (is the interpretation really in seconds?).

I never experienced time spans of 2 seconds until a reaction of the system showed up that lead to the timeout branch mentioned above. I see two options: either the timeout value is interpreted not as seconds, or the recvMessagesTimeout function returns Nothing before the timeout expires (my basic haskell knowledge tells me that this can't be the case since recvPacket in hosc package returns IO Packet).

I struggle reading through the fmap in recvMessagesTimeout. Maybe something is wrong there? Or the timeout interpretation :blush: