Detect Cue Tones and trigger OBS

So anyone who has had radio or TC experiecne know that some feeds come with DTMF tones, these tones are detected and then a advertisement is played.

been trying to do similar where OBS can detect it, with no real luck

one solution i have been working on is using multimon-ng and FFmpeg and Streamlink

I created a Youtube Live stream to emulate a live feed. I then use Streamlink to grab the youtube feed, then pipe that to FFMpeg.

for Multimon-mg to work it needs to be be a raw 16 bit wave file and 22.5k

so i grab the feed from streamlink -> ffmppeg --> Multimon-ng

multimon-ng detects the tone and writes the timestamp and DTMF to a .txt file

this is the code so far using a windows Batch File
@ECHO OFF title FF Streamlink viewer 1.0 color 4f cls setlocal enabledelayedexpansion ECHO. ECHO Type in Youtube URL set /p stream="" : path to ffmpeg set ffmpeg=bin\ffmpeg : path to ffplay set ffplay=bin\ffplay : path to streamlink set streamlink=C:\Program Files (x86)\Streamlink\bin\streamlink : path to multimon-ng set multimon-ng=bin\multimon-ng timeout /T 2 > nul echo. cls echo. echo. echo Sit back and relax and wait for the stream to start... echo. REM Pipe to check DTMF tones "%streamlink%" --quiet --twitch-disable-ads "%stream%" best -O | %ffmpeg% -hide_banner -loglevel 0 -i pipe: -f s16le -acodec pcm_s16le -ac 2 -ar 22050 -af "highpass=f=300, lowpass=f=800, volume=15,afftdn=nr=30:nf=-20" pipe:1 | %multimon-ng% -q -t wav --timestamp -a DTMF -

its working BUT even though i have tried to clean up the audio sometimes it is still reading the wrong DTMF or something else may set it off.

I want to set this up to detect the DTMF and then trigger a Macro in OBS
 
Last edited:
here is a example of what it looks like, as you can see it sometimes does NOT say the correct DTMF, these should all be "4"

example 2 post.png




may be possible to use in conjunction or combining with this

 

AaronD

Active Member
How noisy is the signal, and how clean/well-defined are the DTMF tones? A spectrograph might be useful here, as your ears probably have too much error-correction built-in.

Also compare the actual tones for the keys that it's reporting, not just the one that you want it to detect. Maybe there are some similarities?
For example, maybe some wow/flutter in the feed is causing a frequency/pitch shift that triggers the neighboring tone instead of the correct one?
Or maybe there's enough noise by the time it gets to the detector, that it's just guessing at random.

If nothing else, you could modify your "do it" command to replace multimon with a file, let that run while you send both the key you want and the keys that it's being mistaken for (one at a time, of course), and then load that file into Audacity. See what the spectrograph looks like in there.
(Be sure to select just one keypress in Audacity, and only have the spectrograph look at that one. If it looks at the entire file, it'll include all of them, both good and bad, indistinguishably on the same graph, which is not particularly helpful.)

Once you have a reliable detection, then yes, you should be able to use practically any trigger method to read and parse the file. THAT is where the Lua script, or the Adv-SS plugin, or whatever else, would come into play.
 
How noisy is the signal, and how clean/well-defined are the DTMF tones? A spectrograph might be useful here, as your ears probably have too much error-correction built-in.

Also compare the actual tones for the keys that it's reporting, not just the one that you want it to detect. Maybe there are some similarities?
For example, maybe some wow/flutter in the feed is causing a frequency/pitch shift that triggers the neighboring tone instead of the correct one?
Or maybe there's enough noise by the time it gets to the detector, that it's just guessing at random.

If nothing else, you could modify your "do it" command to replace multimon with a file, let that run while you send both the key you want and the keys that it's being mistaken for (one at a time, of course), and then load that file into Audacity. See what the spectrograph looks like in there.
(Be sure to select just one keypress in Audacity, and only have the spectrograph look at that one. If it looks at the entire file, it'll include all of them, both good and bad, indistinguishably on the same graph, which is not particularly helpful.)

Once you have a reliable detection, then yes, you should be able to use practically any trigger method to read and parse the file. THAT is where the Lua script, or the Adv-SS plugin, or whatever else, would come into play.
here is the test video i am using

its tough to say how noisy to be honest

do you know how to pipe the FFMEG to ffplay and Multimon?

i watch it thru ffplay using this command

C-like:
"%streamlink%" --quiet --twitch-disable-ads "%stream%" best -O | %ffmpeg% -hide_banner -loglevel 0 -i pipe: -f s16le -acodec pcm_s16le -ac 2 -ar 22050 -af "highpass=f=300, lowpass=f=800, volume=5,afftdn=nr=30:nf=-20"  pipe:1 | %ffplay% -f s16le -


it would be nice to monitor it with ffplay and still run multimon-ng

i think what happens is when it suddenly with no audio fadeout, it will think that sudden change is a dtmf going from video to the tone
 
Being able to parse the .txt file and look for the the tone "8" and only do something if that is the case not a "2" or a "#"

i saw in that lua script that you could set the parameters of what the file matches a pattern
 

AaronD

Active Member
here is the test video i am using

its tough to say how noisy to be honest
WOW! Hard cut from dialogue to a DTMF tone (/ad when you replace it), with no regard for the timing whatsoever, in the middle of a word even. Someone needs to get fish-slapped for wrecking the flow like that.
And good luck keeping your viewers from seeing the test card, without also cutting off that word. You need some wiggle-room!

do you know how to pipe the FFMEG to ffplay and Multimon?
I don't know how to pipe stdout of one program into stdin of two programs. *nix has a `tee` command, which copies its stdin to a file and stdout; maybe that could be a decent starting point? I see you're using some *nix-ism's already...

i think what happens is when it suddenly with no audio fadeout, it will think that sudden change is a dtmf going from video to the tone
I wonder if there's some auto-gain involved, so that the sudden jump in volume actually clips, and the added harmonics of clipping are getting it all confused. It's not a big jump, perceptively, but it could still be enough, especially since our hearing is logarithmic and the machine is linear.

Is the timing accurate at least? No false triggers when there wasn't a test card? And it always detects that there is *a* card, even if it gets the specific one wrong?
 
WOW! Hard cut from dialogue to a DTMF tone (/ad when you replace it), with no regard for the timing whatsoever, in the middle of a word even. Someone needs to get fish-slapped for wrecking the flow like that.
And good luck keeping your viewers from seeing the test card, without also cutting off that word. You need some wiggle-room!


I don't know how to pipe stdout of one program into stdin of two programs. *nix has a `tee` command, which copies its stdin to a file and stdout; maybe that could be a decent starting point? I see you're using some *nix-ism's already...


I wonder if there's some auto-gain involved, so that the sudden jump in volume actually clips, and the added harmonics of clipping are getting it all confused. It's not a big jump, perceptively, but it could still be enough, especially since our hearing is logarithmic and the machine is linear.

Is the timing accurate at least? No false triggers when there wasn't a test card? And it always detects that there is *a* card, even if it gets the specific one wrong?


and it works perfectly!
 

Attachments

  • Untitled.png
    Untitled.png
    16 KB · Views: 22
as yoiu can see it matches the tones in the comments... now to figure out how to take this info and trigger OBS...

i talked to my TV buddy and he told me most of the provides will user 3 or 4 tons like that.

so that lua txt trigger may not work as is...
 

AaronD

Active Member
So you'll need a state machine now, instead of just parsing each line individually. Still parse each line, but what the script does with that depends on what state it's in, and that state is also modified while it runs.

My first (formal) exposure to that (I had already reinvented something similar as a kid, but it didn't work as well), was in a college digital logic class. That is, hardware logic with physical circuitry, not software, although anything that hardware can do also has a software counterpart.

Anyway, to detect a sequence of 4 symbols, I might use 4 states:
  1. Looking for the first symbol. When found, record it, and go to state #2.
  2. Looking for the second symbol. When found, record it, and go to state #3.
  3. Looking for the third symbol. When found, record it, and go to state #4.
  4. Looking for the fourth symbol. When found, record it, compare the 4 recordings against a lookup table and issue that command, and go to state #1.
State #1 is also the error-recovery state. Go there on startup, or from any invalid state, or timeout. The logic to do that runs outside of the state machine itself.

In pseudo-C, this might be:
C-like:
char key;
char command[4];
uint8_t state = 1;
timer timeout;    //Fictional device that behaves like a variable, and starts running now.  Figure out your own way of actually doing this.
while(1)
{
    switch(state)
    {
        default:    //Invalid state
            timer_reset(timeout);
            state = 1;
            break;

        case 1:
            key = DTMF_decode();    //Non-blocking: if there's nothing to detect immediately, it returns an error value, which is used on the next line.
            if (is_valid(key))
            {
                command[0] = key;
                state = 2;
            }
            break;

        case 2:
            key = DTMF_decode();
            if (is_valid(key))
            {
                command[1] = key;
                state = 3;
            }
            break;

        case 3:
            key = DTMF_decode();
            if (is_valid(key))
            {
                command[2] = key;
                state = 4;
            }
            break;

        case 4:
            key = DTMF_decode();
            if (is_valid(key))
            {
                command[3] = key;
                send_command(lookup_command(command));
                timer_reset(timeout);
                state = 1;
            }
            break;
    }
    if (timer_expired(timeout))
    {
        timer_reset(timeout);
        state = 1;
    }
}

Of course, there are other ways to do it too, like using a blocking version of DTMF_decode(), and running the state machine only when it gets something. (don't forget the timeout, which will look a bit different from what I did here) This is also the way you'd do it with an event-driven model, with DTMF_decode() being that event, which contains a payload of what it decoded.
Practically, with an event-driven model, you end up with a function/method that gets called every time the event happens, and one of its arguments is that payload. So it's not hard at all to build that from scratch if it only has to do this one thing. (it gets a bit more complicated when you have to tell a mature operating system which of many pre-existing events you want it to call your function for, but you're not doing that here)

And if you have a mixture of different-length commands, you can put the lookup in several different states, cache it to a temporary variable like the key does, and check for an error value. If error, go to the next state; else, send the command, reset the timer, and go to state 1.
 
Last edited:
i have no idea how to use c programming :( sorry you did so much work on that

i was thinking of getting a hold of the guy who made the lua script and see if he can add to it or work on that some more.

not opposed to using on a linux box if need be
 

AaronD

Active Member
Even if you can't write it, you should still be able to figure out how to read it. Then you can see what it's actually doing and how. Once you understand that, you can write your own in your preferred language that does the same thing. (or close enough)

Just don't forget the error-correction! For example, there's nothing in what I wrote that could get it into a bad state, but if I do it for real without the default case, I can guarantee you it'll stop working at some point because it somehow got there anyway.

It can also help to step away for a while, then come back and see what bugs you find. For example, my code probably has a bug in it that relates to how the timer works. Most of the time, it'll work correctly, but every once in a while it'll miss a command because of the details in how it manages the timer. A good test of understanding might be for you to find that bug too, and then fix it in your version.
 
Last edited:
created another Batch file to parse out what the other one spits out

Code:
@echo off
setlocal ENABLEEXTENSIONS
setlocal ENABLEDELAYEDEXPANSION

echo.

ECHO Type in Log File to Monitor
set /p log=""


:start
REM checks that 4 tones are present
FOR /F %%i IN ('TYPE "%log%" ^| FIND /C /V ""') DO SET /A Lines=%%i
IF %Lines% GEQ 4 (

    GOTO startTones

) ELSE (

    GOTO start
)

:startTones

echo.
ECHO Type in Tone #1
set /p tone1=""
echo.
ECHO Type in Tone #2
set /p tone2=""
echo.
ECHO Type in Tone #3
set /p tone3=""
echo.
ECHO Type in Tone #4
set /p tone4=""
echo.


:check1
REM  gets 1st line from log
set firstLine=1
for /f "delims=" %%i in (%log%) do (
    if !firstLine!==1 echo %%i >nul
    set firstLine=0

)
REM  strips whitespaces
for /l %%a in (1,1,31) do if "!firstLine:~-1!"==" " set firstLine=!firstLine:~0,-1!

REM checks 1st tone is same as we set
echo %firstLine% | findstr "%tone1%" >nul

If %ERRORLEVEL% EQU 0 (

    GOTO check2
) ELSE (
    GOTO End
)

:check2
REM  gets 2nd line from log
for /f "skip=1 tokens=*" %%i in (%log%) do (
  set line2=%%i
  goto break2
)
:break2
REM  strips whitespaces
for /l %%a in (1,1,31) do if "!line2:~-1!"==" " set line2=!line2:~0,-1!

REM checks 2nd tone is same as we set
echo %line2% | findstr "%tone2%" >nul

If %ERRORLEVEL% EQU 0 (

    GOTO check3
) ELSE (
    GOTO End
)

:check3
REM  gets 3rd line from log
for /f "skip=2 tokens=*" %%i in (%log%) do (
  set line3=%%i
  goto break3
)
:break3
REM  strips whitespaces
for /l %%a in (1,1,31) do if "!line3:~-1!"==" " set line3=!line3:~0,-1!

REM checks 3rd tone is same as we set
echo %line3% | findstr "%tone3%" >nul

If %ERRORLEVEL% EQU 0 (

    GOTO check4
) ELSE (
    GOTO End
)

:check4
REM  gets 4thd line from log
for /f "skip=3 tokens=*" %%i in (%log%) do (
  set line4=%%i
  goto break4
)
:break4
REM  strips whitespaces
for /l %%a in (1,1,31) do if "!line4:~-1!"==" " set line4=!line4:~0,-1!

REM checks 4th tone is same as we set
echo %line4% | findstr "%tone4%" >nul

If %ERRORLEVEL% EQU 0 (

    GOTO success
) ELSE (
    GOTO End
)

:success
Echo Woooohoooooo it matches!

Pause

REM  now delete top 4 from Log




:End
echo Tone not found starting over
pause
 
now i can actually trigger from a Batch file anything i need into OBS...

i thought i saw a away to run macros from or a playlist from a batch file maybe i will check out


after dinner or after holiday
 
Top