Share Audio Clips Between Gateway and Clients

Didn't want to revive an old thread, but have the same question now.

The end goal is to repeatedly play this sound based on an expression tag, but for now I want to test it with a button. I've got the script:

from javafx.scene.media import AudioClip

def demoAlarmSound(audioPath):
	notify = AudioClip(audioPath)
	notify.play()

But I get the error that javafx is unknown.

How can I play mp3 in Ignition 8.1 ?

edit: this thread got pretty long on the first step of the process, so I've renamed it and will create other threads as needed.

We had to switch over to utilizing the system.util.playSoundClip function and only use WAV files.

So that's the only option?

No idea honestly, 99.9% of our alarm files were already WAV.
We just converted any that weren't.

Someone else might have another idea for how to play mp3 files.

Alright, storage space is not really a big concern so I'll just use WAV files to make my life easier.

I moved to using VLC for our andons.

JavaFX isn't bundled with the Java runtime anymore, so yeah, the javafx imports won't be an option on 8.x versions of Ignition.

Where does the wav file have to be? I have it on the gateway and get an unable to open file error when I try to play it.

Unable to open file/url: C:\SCADA\Alarm Sounds\Alarm.wav

Are you trying to play it in Vision? Just use the sound player component, it'll embed the audio clip.

There are 4 options for sounds and I'm trying to let the user test the different sounds with a button. I was going to create the correct path based on the selected option.

I'll also need to use the system function to repeatedly play the sound based on a tag, once I make it that far.

Nevermind, I'll just do it on the docked view that's always open. Quick question, what might happen if there are multiple instances of the same docked view with this sound component? I'd imagine I'll get duplicate sounds and it won't be pretty. How can I avoid that?

I would have to go back and look, but I believe I only trigger mine on "Desktop-0". The other thing you'll have an issue with most likely is if a user logs in and the sound is playing, it will trigger a second time and play over the top of itself. I had a fix for it too, but need to look up what I did again.

Edit: Just looked it up, and on the docked window, I have a custom property (could be anywehre really, that has a DesktopIndex custom property that gets set to the index of the desktop (0 being primary). Then on the Sound Player component, I created a custom property named playSound that is bound to the tag I want to trigger the sound to play on, then have a propertyChange script on the component with this text:

# Special script to only allow the sound player to play the sound once
# Issue with logging on/off causing it to trigger multiple times
# Also only allows the primary display to play the sound
if event.propertyName == 'playSound' and event.source.parent.DesktopIndex == 0:
	if event.source.trigger != event.newValue and event.newValue != None:
		event.source.trigger = event.newValue
elif event.propertyName == 'playSound':
	event.source.trigger = False

As a bonus, I actually have a script I call that lets each operator select from any number of wav files on the server, and when they select a different one, it pulls the wav file raw bytes from the server (my soundData property is bound to a runScript . I let technicians/admins upload sounds, and operators can then pick from the list of sounds they want for their alarms.

Well, in that case I might actually want to do it with the system function.

So, back to that question.

Can I play a WAV file at the client-level using a client event script to continuously play the WAV file that is hosted on the gateway?

Not without downloading it to the client in some way.

1 Like

That's doable. Can I script the "download" (transfer) to the client if the file doesn't exist?

No, not in either Vision or Perspective. (It sounds like you are using Perspective, but your earlier comments implied Vision... :man_shrugging: )

In Vision, the file needs to be present in the client filesystem. Vision can easily use system.util.sendRequest() to retrieve such from the gateway and write it locally. Perhaps cached.

In Perspective, the sound player needs a URL to the sound file. That URL cannot point at a local file. That means the WebDev module (if you must use a file), or perhaps my Blob Server module (if you can put the sound in a database as a blob.)

There are 4 files. Will I need to send a request for each one? What sort of data will be returned from the WAV files and how can I write it after receiving it?

Yes. As long as the alarm files are not very large (tens of megabytes), you can use system.util.globals as a JVM local cache of the audio bytes. You could asynchronously fetch and build that local cache on client startup. Or you could cache to a local file, but that does get more complex.

I'm not opposed to either solution, but I'm a bit lost on how to start in either case. I've not used the JVM local cache or system.util.globals before.

My original idea was to go the local file route. Right now I have a memory (gateway) tag that holds the name of the selected alarm sound. The idea was to truncate this at the end of the "sound path" (C:/SCADA/Alarm Sounds/) and play it that way.

I was going to try and make it as "automatic" as possible. Just add a file to the folder and it becomes available as an option for the alarm sound. Getting the file names into a dataset for use with a dropdown box using os.listDir().

That's very similar to what I'm doing, but not using system.util.globals (maybe I should be?)

I have a tag named BasePath that stores the base file path on the server C:\SCADA in your instance, then I have a few gateway message handlers in my gateway scripting project as follows:

GetAlarmSoundList

def handleMessage(payload):
	import os
	import errno
	
	folderPath = system.tag.readBlocking('BasePath')[0].value + '\AlarmSounds'
	try:
		os.makedirs(folderPath)
	except OSError as e:
		if errno.EEXIST != e.errno:
			raise
			
	fileList = []
	files = os.listdir(folderPath)
	for fileName in files:
		if fileName.upper().endswith('.WAV'):
			fileList.append(fileName)
			
	return fileList

GetAlarmSound

def handleMessage(payload):
	if payload['fileName']:
		filePath = system.tag.readBlocking('BasePath')[0].value + '\AlarmSounds\%s' % payload['fileName']
		if system.file.fileExists(filePath):
			bytes = system.file.readFileAsBytes(filePath)
			return bytes

On my project, I have a library:
Vision.Sound

def getSoundBytes(fileName):
	bytes = system.util.sendRequest('GatewayScripts', 'GetAlarmSound', {'fileName': fileName})
	return bytes
	
def getAvailableAlarmSounds():
	fileList = system.util.sendRequest('GatewayScripts', 'GetAlarmSoundList')
	fileDS = []
	for fileName in fileList:
		fileDS.append([fileName, fileName])
	
	return system.dataset.toDataSet(['Value', 'Label'], fileDS)

Then the binding on my Sound Player component's soundData property is:

runScript('Vision.Sound.getSoundBytes',0,{[client]AlarmSound})

For sound selection, I have a client settings popup that has a dropdown with the data property set to this binding:

runScript('Vision.Sound.getAvailableAlarmSounds', 0)

And the selectedStringValue property bound to my [client]AlarmSound tag.

I guess I maybe assumed that the Sound Player component essentially caches the sound bytes whenever the component is loaded or the sound file is changed. You could use a non-client tag also if you wanted it to change globally for everyone.

Thanks for the direction! I had actually just finished changing over to client tags instead of gateway tags, since the files have to be stored on the client anyway, the individual clients may as well be able to select individual sounds.