Stagelinq protocol API availability? (Part 1)

Hello,

I’m the author of the StagelinQ Golang library! Awesome to see that people are picking up on the work! :slight_smile:

Unfortunately, after reverse-engineering the whole thing while kind of documenting my efforts on Twitter, I also have been a bit busy with other stuff, but I was intending on implementing a more user-friendly tool with the library that would either output to a text file or at least to the existing OBS plugin Tuna. Also a more machine-parsable command-line interface is definitely something on my mind as the example application that is shipped with the library is more intended for demo purposes. And lastly, I was also thinking of using this to automatically write a cuesheet for my recorded sessions so I can do easy timed tracklisting for sets I upload.

I’d be interested in the code you’ve written, so if you want you can either start a pull request against the repository on GitHub or send it to me directly. I suspect maybe SoundSwitch doesn’t like another application listening on the same port, even though my code sets the port up as shareable.

As for debugging the code, so far I am using Delve which integrates into the IDE I use (Visual Studio Code).

If you get into trouble understanding parts of the code, you can ask me, here or PM both. I’m sure the terminology or code logic isn’t always right or easy to understand, but I’m happy to share my knowledge!

EDIT: Btw the whole reason I jumped to working on the library was @SHAYDED mentioning that the protocol looked easy to analyze in Wireshark. And so I did just that and the whole rest is guesswork!

1 Like

This is fantastic @icedream

I was looking at a way to contact you on your GitHub and wasn’t sure if you was a Denon DJ forum member or not as I saw the work that you did and wanted to draw your attention to the efforts that the guys was doing on here. It seems that you have found your own way!

I have a Prime 4 too and was looking at a program to work in a way that ‘Serato Now Playing’ works, which I use and this shows potential. I had a PM chat with the Serato forum member (Lee Miran) who written that and he said it would be cool to incorporate any parts from or into Serato Now Playing.

His work is here: https://github.com/e1miran/Now-Playing-Serato/releases/tag/v1.4.0

If there is any usable code there, he said take a look at the read.me file and see if you can use anything. I’m a huge fan of this project and like the direction it’s going. Thanks!

2 Likes

hey thanks for your work - I’m a bit of a n00b when it comes to github and ended up clicking fork so I could upload my “I don’t understand variable handling in golang and or how to write to files”

So I janked together a PowerShell script that does the same (with a delay due to having to call the compiled exe, at the moment the “now playing” is about 9 seconds behind real time)

I edited the main.go in the sample stagelinq_discover to only provide Playstate / Play / Current BPM / Artist / Song name and fader position. which on windows I grabbed these and did some array / variable manipulation in PowerShell - First checking the fader is above 50% (I don’t ever use the crossfader) then checking if the deck is playing - if it is read the values into memory - format them for text and output them into something obs can use

go-stagelinq/GetDeckInfo.ps1 at master · djswolf/go-stagelinq (github.com)

in this example each deck outputs one text file with “Now Playing: Artist Name - Song Title BPM: (BPM rounded to 2dp)” (can be used with the OBS Text GDI source)

Then in this example

go-stagelinq/GetDeckInfo1.ps1 at master · djswolf/go-stagelinq (github.com)

I output 12 separate text files - 4 with the BPM of each deck - and then 2 for each deck one with the artist and one with the track title this I then use with

Free - Zyphen’s Now Playing overlay | OBS Forums (obsproject.com)

where I created 4 html files for Song.html - there’s a function called checkUpdate i just referenced each of the 4 separate artist / song title texts

you can put your logo as Snip_Artwork.jpg

this is then 4 separate browser sources for each deck and 4 Text GDI objects for BPM above it

I’ll look into Delve as i was only using the cmd line compiler out as a debug my “go to” editors are PowerShell ISE and Notepad ++ :laughing:

Hey @icedream

Great bit of code you put together…it served as a fantastic starting point and really helped.

This is the modified code I put together…its rough and ready as it was all edited in TextEdit on the Mac. I don’t have any GitHub experience I’m afraid.

The code below outputs to a text file which I use as a source in OBS. Works well…only issue as mentioned is the tendency to mess with Soundswitch.

I modified the string array for decks one and two to exclude the bits I wasn’t interested in, and added the external mixer float value so I could get the up fader positions. This means it will only write to the output file when the fader value is near 1 (up).

If I had more time I would probably tidy this up but it may be useful to you.

package main

import (
	"stagelinq"
	"log"
	"time"
	"sync"
	"fmt"
	"os"
	"bufio"

)

const (
	appName    = "Icedream StagelinQ Receiver"
	appVersion = "0.0.0"
	timeout    = 5 * time.Second
)

var stateValues = []string{
	stagelinq.EngineDeck1ExternalMixerVolume,
	stagelinq.EngineDeck1Play,
	stagelinq.EngineDeck1PlayState,
	stagelinq.EngineDeck1TrackArtistName,
	stagelinq.EngineDeck1TrackSongLoaded,
	stagelinq.EngineDeck1TrackSongName,


	stagelinq.EngineDeck2ExternalMixerVolume,
	stagelinq.EngineDeck2Play,
	stagelinq.EngineDeck2PlayState,
	stagelinq.EngineDeck2TrackArtistName,
	stagelinq.EngineDeck2TrackSongLoaded,
	stagelinq.EngineDeck2TrackSongName,

	
	stagelinq.EngineDeck3ExternalMixerVolume,
	stagelinq.EngineDeck3Play,
	stagelinq.EngineDeck3PlayState,
	stagelinq.EngineDeck3PlayStatePath,
	stagelinq.EngineDeck3TrackArtistName,
	stagelinq.EngineDeck3TrackTrackNetworkPath,
	stagelinq.EngineDeck3TrackSongLoaded,
	stagelinq.EngineDeck3TrackSongName,
	stagelinq.EngineDeck3TrackTrackData,
	stagelinq.EngineDeck3TrackTrackName,


	stagelinq.EngineDeck4ExternalMixerVolume,
	stagelinq.EngineDeck4Play,
	stagelinq.EngineDeck4PlayState,
	stagelinq.EngineDeck4PlayStatePath,
	stagelinq.EngineDeck4TrackArtistName,
	stagelinq.EngineDeck4TrackTrackNetworkPath,
	stagelinq.EngineDeck4TrackSongLoaded,
	stagelinq.EngineDeck4TrackSongName,
	stagelinq.EngineDeck4TrackTrackData,
	stagelinq.EngineDeck4TrackTrackName,


}

func makeStateMap() map[string]bool {
	retval := map[string]bool{}
	for _, value := range stateValues {
		retval[value] = false
	}
	return retval
}

func allStateValuesReceived(v map[string]bool) bool {
	for _, value := range v {
		if !value {
			return false
		}
	}
	return true
}

func connectDevice(device *stagelinq.Device, listener *stagelinq.Listener){

		log.Printf("%s %q",device.IP.String(), "Found StageLinQ Device on Network")
		
		// discover provided services
			log.Println("\tattempting to connect to this device…")
			deviceConn, err := device.Connect(listener.Token(), []*stagelinq.Service{})
			if err != nil {
				log.Printf("%s", "Failed to connect to device - Probably because no device but self found" + device.Name)
				log.Printf("WARNING: %s", err.Error())
				return
			} else {
				defer deviceConn.Close()
				log.Println("\trequesting device data services…")
				services, err := deviceConn.RequestServices()
				if err != nil {
					log.Printf("WARNING: %s", err.Error())
				}

				for _, service := range services {
					log.Printf("\toffers %s at port %d", service.Name, service.Port)
					switch service.Name {
					case "StateMap":
						stateMapTCPConn, err := device.Dial(service.Port)
						defer stateMapTCPConn.Close()
						if err != nil {
							log.Printf("WARNING: %s", err.Error())
						}
						stateMapConn, err := stagelinq.NewStateMapConnection(stateMapTCPConn, listener.Token())
						if err != nil {
							log.Printf("WARNING: %s", err.Error())
						}

						m := makeStateMap()
						for _, stateValue := range stateValues {
							stateMapConn.Subscribe(stateValue)
						}
						
						var artistName string
						var songName string
						//var strft string
						var count int	
						var fltft float64

										
						for state := range stateMapConn.StateC() {
							count = count + 1
							//log.Printf("%s", count)
							if state.Name == "/Engine/Deck1/ExternalMixerVolume" {
								//strft = fmt.Sprint(state.Value["value"].(float64))
								fltft = state.Value["value"].(float64)
								
								
								//log.Printf("%s %s %q",device.IP.String(), state.Name, strft)
							}else if state.Name == "/Engine/Deck1/Track/ArtistName" {

								artistName = state.Value["string"].(string)
								//log.Printf("%s %s %q",device.IP.String(), state.Name, state.Value["string"])
								}else if state.Name == "/Engine/Deck1/Track/SongName"{
									
									songName = state.Value["string"].(string)
									//log.Printf("%s %s %q",device.IP.String(), state.Name, state.Value["string"])
									//log.Printf("%s %s %q",device.IP.String(), state.Name, songName)
							}
							

							m[state.Name] = true
							if allStateValuesReceived(m) {
								log.Printf("%s", "Broken Out")
								break
							}
							
								//log.Printf("%s", count)
								if count == 12{
								//first connect gets all values
									if fltft > 0.99{
										//The fader is not a zero value and the index is 12 - Quick and Dirty
										log.Printf("%s %s %s",device.IP.String(),artistName, songName)
										
										writeFile(artistName + " - " + songName + "    ")
										
									}
									//count = count + 1
								}else if count > 12{
									//Already running so just check the fader positino
										if fltft > 0.99{
										//The fader is not a zero value
										log.Printf("%s %s %s",device.IP.String(),artistName, songName)
										
										writeFile(artistName + " - " + songName + "    ")
										
									}
								}
								

						}
						

						
						select {
						case err := <-stateMapConn.ErrorC():
							log.Printf("WARNING: %s", err.Error())
						default:
						}
						stateMapTCPConn.Close()
					}
				}

				log.Println("\tend of list of device data services")
			}



}

func writeFile(text string){

    file, err := os.OpenFile("/Users/Shared/OBS_Denon/output.txt", os.O_WRONLY|os.O_CREATE, 0666)
	file.Truncate(0)
    if err != nil {
        log.Printf("File does not exists or cannot be created")
        os.Exit(1)
    }
    defer file.Close()

    w := bufio.NewWriter(file)
    fmt.Fprintf(w, "%v\n", text)

    w.Flush()

}




func main() {

//Cleardown TextFile

writeFile("")

	listener, err := stagelinq.ListenWithConfiguration(&stagelinq.ListenerConfiguration{
		DiscoveryTimeout: timeout,
		SoftwareName:     appName,
		SoftwareVersion:  appVersion,
		Name:             "OBS_Plug",
	})
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	listener.AnnounceEvery(time.Second)

	deadline := time.After(timeout)
	foundDevices := []*stagelinq.Device{}

	log.Printf("Listening for devices for %s", timeout)

	var wg sync.WaitGroup

discoveryLoop:
	for {
		select {
		case <-deadline:
			break discoveryLoop
		default:
			device, deviceState, err := listener.Discover(timeout)
			if err != nil {
				log.Printf("WARNING: %s", err.Error())
				continue discoveryLoop
			}
			if device == nil {
				continue
			}
			// ignore device leaving messages since we do a one-off list
			if deviceState != stagelinq.DevicePresent {
				continue discoveryLoop
			}
			// check if we already found this device before
			for _, foundDevice := range foundDevices {
				if foundDevice.IsEqual(device) {
					continue discoveryLoop
				}
			}
			foundDevices = append(foundDevices, device)
			log.Printf("%s %q %q %q", device.IP.String(), device.Name, device.SoftwareName, device.SoftwareVersion)

			//WE NEED TO ADD A DIFFERENT METHOD HERE TO ITERATE THROUGH THE FOUNDDEVICES ARRAY AND CONNECT TO ALL - SG
			//BELOW METHOD ONLY CONNECTS TO A SINGLE DEVICE BASED ON THE FIRST DISCOVERY
			
			//USE OF GO CHANNELS WILL HELP CREATE A MULTI-THREAD
			
			//Structure of a printf command (<declare the types of string>, String A, String B.....etc )

		}
	}
	
	if foundDevices == nil {
		log.Printf("No Devices Found")
			} else {
				for _, dev := range foundDevices{
					if(dev.Name == "OBS_Plug"){
						//IGNORE LOCAL SERVICE
					}else if(dev.Name == "DN-X1800Prime"){

					}else if(dev.Name == "SoundSwitchPC"){
					}else{
					log.Printf("%s %q",dev.IP.String(), "Found Device")
					//writeFile(dev.IP.String())
					wg.Add(1)
					go func(dev *stagelinq.Device){
						connectDevice(dev, listener)
					}(dev)
					
					}
				}
			}
	
	wg.Wait()
	log.Printf("Found devices: %d", len(foundDevices))
}

Saw this online.

1 Like

It would be nice to get him on board as his overlay (via browser source) looks great.

I wonder if someone could reach out to him on his Discord? I’m not on that platform but would be cool if he could help incorporate even the layout.

EDIT: It’s pretty much perfect but for the wrong brand of players!

That’s a really grating phrase.

It’s wonderful that the code wasn’t built by a drunken ex-convict, ex-carpenter of no-fixed abode etc sure but that phrase is like the “Freeeessshhhhhhh” drop in turntablism, far too clichè and played out.

It may well be a great bit of code though. :slight_smile:

Ha ha.

Ok back tp practising my 2 click flares with the Freeeeeeeeeshhhhhhh sample :stuck_out_tongue: Hope I didn’t leave out an E

That’s why I suggested a StreamElements plug in. Its a real time browser source for OBS.

It will be easier if the Denon devs can throw streaming djs a quick bonus feature during this streaming era. It will show they have their fingers on the pulse of the customers.

I think Resolume displays this data via Engine Connect/Stagelinq!

2 Likes

This was my thinking too and would take an afternoon of coding by the Denon DJ team to get this to output to a text file.

It would compliment the new broadcast feature.

EDIT: If they did, maybe even a small “Played on Denon DJ” logo in the corner too to make the brand stand out on streams.

2 Likes

I’m on discord and just posted a link to this thread and a wee intro on behalf of youse! :stuck_out_tongue:

1 Like

Thanks @mufasa

Hopefully he’ll drop by and look at helping switch the Pioneer DJ commands out for the Denon DJ commands.

The hard work has been done on both sides. The Prolink app already exists (with awesome screen overlays) and the StagelinQ commands exist.

Now to marry them together!

Thanks for sharing this code, I’ll try to integrate this with a tool that I’ve built: https://github.com/erikrichardlarson/unbox

3 Likes

Okay, so I’ve downloaded and playing with this in Serato DJ. I really really like it.

Thanks @erikrichardlarson and I’ll be sure to drop you a beer your way if you manage to get the Prime Now Playing on screen!

EDIT: So I’ve downloaded unbox and think it really is fantastic. Very simple and uncomplicated while doing exactly what we want. The only thing it needs (for me) is a delay time in seconds as with the “Serato Now Playing” app you can set how many seconds the track has been written to the Serato History folder before it is written to the text file and displayed on screen. I have this set to between 60 seconds (sometimes longer as I’m a long transition house mixer). In unbox, the moment a track is loaded onto a deck in Serato DJ it appears as 'Now Playing" on screen. This is the time that I’m getting my beats lined up and they won’t hear the incoming track for a while yet! If I’ve missed this then sorry :slight_smile:

Other than this… literally PERFECT!

big thanks to @icedream and @crimsonsimon I’ve dumped my main.go demo app here: go-stagelinq/main.go at master · djswolf/go-stagelinq · GitHub does the same as the PowerShell GetDeckInfo1 but now in real time

1 Like

Thanks @MrWilks! I definitely want to get Denon gear working so stay tuned. As far as Serato, Serato writes to the history once a track is loaded which is causing this issue. My current plan is to allow for delay if you want to keep reading the Serato history directly, but I’m also adding a Serato Live Mode which will scrape the live playlist. The live playlist has some added benefits of Serato paying attention to the cross / line faders before setting a track to played. I’ll be sure to post here when the next release is out with these updates. Thanks again for the support!

1 Like

Hi @erikrichardlarson

This is great news. To have both the live playlists and local history text file is a great option. I feel a timed delay when writing to the txt file is a great way as it allows that timed delay to the OBS text input as well, should you choose that method. It is the only way I can do it at the moment.

The “Serato Now Playing” app writes a txt file at a predetermined time (I also use double line but that isn’t a deal-breaker). The dev of that suggested ways of getting his code into another app if anyone was interested.

I’m not sure if the ‘on air’ open/closed nature of the faders is useful from the commands too?

A fanatic effort and thank you for looking into it. Expect that beer!

Hey all,

So I am still working through the issue with the code I posted previously. The only major issue I can see is that when running soundswitch and the now playing app on the same machine, they tend to conflict, and this seems to be due to the udp port sharing as they are both listening on the stagelinq udp port.

If i run the now playing app on its own, it works perfectly.

I am still digging into the issue at the moment to see what I can do but will update once I have an idea. Have very limited time to put into this so sorry for the time it has taken to reply.

1 Like

Now, can I have a question to You coding guys here?

Is it possible to run that stable with devices that use ableton link somehow?

Sync the Ableton link enabled device to the Denon players…?

Is there a possibility for that in the code?

3 Likes

An official twitch plug in by Serato

2 Likes

Did you get anywhere with Denon integration? I love this tool btw!!!

1 Like