Skip to content

Instantly share code, notes, and snippets.

@bitingsock
Last active April 11, 2024 20:45
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bitingsock/5e9714efff963c9689b0671d68f195ad to your computer and use it in GitHub Desktop.
Save bitingsock/5e9714efff963c9689b0671d68f195ad to your computer and use it in GitHub Desktop.
Runs a child process to play a second audio stream. Synced through named pipe. Recommend using with [cycle-adevice](https://gist.github.com/bitingsock/ad58ee5da560ecb922fa4a867ac0ecfd) to change the output device of the parent process.
--New child with start with the current 'aid' and 'audio-device' of the parent
-- non-Windows environments require the use of the 'socat' package
local platform_is_windows = (package.config:sub(1, 1) == "\\")
local options = require 'mp.options'
local o = {
pipe_template = platform_is_windows and "\\\\.\\pipe\\mpvDupedAudio" or "~/mpvDupedAudio", --windows format
new_child_key = 'Ctrl+A', --Start a new child: 'Ctrl+Shift+a'
cycle_child_control_key = 'Ctrl+Alt+A', --Cycle child control: 'Ctrl+Shift+Alt+a'
cycle_child_aid_key = 'Ctrl+Alt+a', --Cycle 'aid' of child: 'Ctrl+Alt+a'
child_increase_volume_key = 'Ctrl+Alt+WHEEL_UP', --Change volume of child: 'Ctrl+Alt+wheel'
child_decrease_volume_key = 'Ctrl+Alt+WHEEL_DOWN', --Change volume of child: 'Ctrl+Alt+wheel'
}
options.read_options(o)
local piper = platform_is_windows and " >" or " | socat - "
local mpvexe = "mpv" .. (platform_is_windows and ".com" or "")
local pipeInstance = {}
local control = 0
local function sync(prop, newVal)
local timestamp = mp.get_property("audio-pts") or mp.get_property("time-pos")
for k, v in pairs(pipeInstance) do
if v then
os.execute("echo no-osd set time-pos " .. timestamp .. piper .. o.pipe_template .. k)
if prop == "speed" then os.execute("echo no-osd set speed " .. newVal .. piper .. o.pipe_template .. k) end
if prop == "pause" then os.execute("echo no-osd set pause " .. newVal .. piper .. o.pipe_template .. k) end
end
end
end
local function cycle_aid()
os.execute("echo cycle aid" .. piper .. o.pipe_template .. control)
end
local function switchControl()
local lastInstance = 0
for k, v in ipairs(pipeInstance) do
if v then
lastInstance = k
end
end
if lastInstance == 0 then
control = 0
return
end
if control >= lastInstance then control = 0 end
for k, v in ipairs(pipeInstance) do
if k > control and v then
control = k
return
end
end
end
local controlling = false
local function quit()
os.execute("echo quit" .. piper .. o.pipe_template .. control)
pipeInstance[control] = false
switchControl()
print(control)
if control == 0 then
print(">>> Removed last child <<<")
mp.remove_key_binding("quit")
mp.remove_key_binding("cycle_aid")
mp.remove_key_binding("volumeUP")
mp.remove_key_binding("volumeDOWN")
mp.unobserve_property(cycle_pause)
mp.unobserve_property(syncSpeed)
mp.unregister_event(sync)
controlling = false
else
print(">>> Now controlling child: " .. control .. " <<<")
end
end
local function start_child()
table.insert(pipeInstance, true)
control = #pipeInstance
print(">>> Now controlling child: " .. control .. " <<<")
mp.commandv("set", "msg-level", mp.get_script_name() .. "=warn")
if controlling == false then
mp.register_event("playback-restart", sync)
mp.add_forced_key_binding("q", "quit", quit)
mp.add_forced_key_binding(o.cycle_child_aid_key, "cycle_aid", cycle_aid)
mp.add_forced_key_binding(o.child_increase_volume_key, "volumeUP", function() os.execute("echo add volume 5" .. piper .. o.pipe_template .. control) end)
mp.add_forced_key_binding(o.child_decrease_volume_key, "volumeDOWN", function() os.execute("echo add volume -5" .. piper .. o.pipe_template .. control) end)
mp.add_timeout(0.25, function()
mp.observe_property("pause", "string", sync)
mp.observe_property("speed", "string", sync)
end)
controlling = true
end
mp.command_native_async({
name = "subprocess",
args = {
mpvexe,
mp.get_property("path"),
"--speed=" .. mp.get_property("speed"),
"--pause=" .. mp.get_property("pause"),
"--no-video",
"--no-sub",
"--osd-level=1",
"--aid=" .. mp.get_property("aid"),
"--audio-device=" .. mp.get_property("audio-device"),
"--input-ipc-server=" .. o.pipe_template .. control,
"--start=" .. (mp.get_property("audio-pts") or mp.get_property("time-pos"))
},
playback_only = false
}, function() end)
mp.add_timeout(0.25, function()
mp.commandv("set", "msg-level", mp.get_script_name() .. "=status")
end)
end
mp.add_forced_key_binding(o.cycle_child_control_key, "switchControl", function()
switchControl()
if control == 0 then
print(">>> No active children <<<")
else
print(">>> Now controlling child: " .. control .. " <<<")
end
end)
mp.add_forced_key_binding(o.new_child_key, "start", start_child)
mp.register_event("shutdown", function()
for k, _ in pairs(pipeInstance) do
os.execute("echo quit" .. piper .. o.pipe_template .. k)
end
end)
@Hrxn
Copy link

Hrxn commented Sep 17, 2020

Is this link in the description (for cycle-adevice) correct?

@bitingsock
Copy link
Author

sorry, fixed it

@dran1x
Copy link

dran1x commented Feb 5, 2022

How is this suppose to play two channels at once?

@bitingsock
Copy link
Author

@dran1x
"Ctrl+Alt+a" cycles the audio stream of the second instance

@Andreadj
Copy link

Andreadj commented Jan 23, 2023

@bitingsock
I can run 3 audio process in 3 different languages at the same time without any sync problem!!!
Amazing work bitingsock!!!
You made it!

Thanks a lot

@nicko88
Copy link

nicko88 commented Feb 2, 2023

What is the sync accuracy of this?

I am looking to do this to route one audio stream as bit-stream to my AVR and a second audio render as decoded so I can isolate the LFE track and send out separately to my tactile device.

I am hoping for ~millisecond accuracy, but I suppose up to 5ms, maybe 10ms at absolute most could be acceptable enough to try this.

Any ideas before I try?

Thanks!

@bitingsock
Copy link
Author

@nicko88
Depending on the responsiveness of the system's IPC it gets pretty dang close. I have not measured it but considering that (i think) the event loop in mpv is throttled to 50ms there is no way to be sure.
However, if you have a way to measure it you could add or subtract from timestamp on line 16.
That said, your worst case requirement seems a bit overkill. Here is a test of 150ms silence - 10ms chirp at 1000hz - 10ms silence - 10ms chirp at 800hz - 150ms silence
test.webm

if you were listening to two stream in their entirety you could notice some slight reverb but since it's just LFE I doubt you'd be able to tell

@nicko88
Copy link

nicko88 commented Feb 2, 2023

10ms is not overkill I can assure you (unfortunately).

I can adjust my tactile bass shakers by 10ms now (with my minidsp), and once I'm more than about 10ms off my audio I can start to feel the tactile impulses are "off" from the sound I'm hearing.

Like gunshots, etc feel off.

I will have to play around. I came across this in my search for dual audio. Though if this works I may try to rewrite this for MPC player because I use madVR, and I can't use madVR with mpv unfortunately.

But this concept seemed promising to me.

MPC-BE can already do dual audio out, but I cannot find a way to have one output be bit-stream for Atmos and DTS:X to my AVR with the second audio out be 8 channel PCM decoded so that I can isolate the LFE track and send that out to my "bass shaker" amps.

@bitingsock
Copy link
Author

@NetherSleep
that is because the main process is the control for the dupes. I've added a catch for the shutdown event to close all dupes. keep in mind this still only works if mpv exits "normally". if the process is killed unexpectedly, we may still lose control of the dupes

@HairyOtter
Copy link

Has anyone managed to get this to work on *nix?
I've tried to get it running on Ubuntu desktop but I can't seem to get it working. (I'm a complete noob at lua and named pipes.)
local pipe = "\\\\.\\pipe\\mpvDupedAudio" --windows format
This seems like it needs to be replaced with the *nix version of a named pipe.
I created a named pipe via "mkfifo mpvDupedAudio" and then used the path to that pipe like this:
local pipe = "/home/ubuntu/mpvDupedAudio" --windows format
I also tried a few variations of:
os.execute("mkfifo mpvDupedAudio")
Nothing works for me.

I don't understand how to make it work in Ubuntu (or any other *nix). I'd appreciate any help.
Making cycle-adevice.lua work was easy, all I had to do was replace "wasapi" with "alsa".

@bitingsock
Copy link
Author

bitingsock commented Aug 22, 2023

@HairyOtter
i haven't tested it on *nix since the first publish so I can't say for certain it actually still works but
(as of now) line 100: "--input-ipc-server=" .. pipe .. control,
is where pipes are actually spawned using mpv's built-in feature.
all I can say for now is maybe see if you need to escape some characters in your pipe names.
I may get around to testing this myself at some point but don't hold your breath.

@HairyOtter
Copy link

all I can say for now is maybe see if you need to escape some characters in your pipe names.

I don't think so. I've just tested it again and used:
local pipe = "/home/ubuntu/pipetest" --windows format
Then I opened up a movie and pressed Ctrl+Shift+a, Ctrl+Shift+Alt+a and Ctrl+Alt+a a few times (nothing happened in mpv), however afterwards there were a bunch of "pipetest1, pipetest2, pipetest3" etc. files in /home/ubuntu/. All containing "quit".
And while playback is running the contents change depending on which of the hotkeys I use.
"no-osd set speed 1.000000", "cycle aid" etc..

I may get around to testing this myself at some point but don't hold your breath.

If all else fails I'll just use windows instead of *nix on the hpc, the script works well on windows.

@bitingsock
Copy link
Author

@HairyOtter
I've updated it with configurable options and proper OS checking and did some rudimentary testing. Please give it a try.

@HairyOtter
Copy link

HairyOtter commented Aug 23, 2023

Please give it a try.

Done. Now it creates a subprocess on Ubuntu, however the subprocess does not react to play/pause or jumping around in the timeline. It also does not react to the other hotkeys ('Ctrl+Alt+a', 'Ctrl+Alt+A').
It just keeps on playing the same audio track from the same point in time, on the same device it was started on.

@bitingsock
Copy link
Author

Ya I experienced something similar when trying any combos with Alt. I think the DE might be hijacking it.

@HairyOtter
Copy link

Ya I experienced something similar when trying any combos with Alt. I think the DE might be hijacking it.

Should I try different hotkeys that do not use Alt?
That doesn't explain why the subprocess does not react to pausing the video (rightclick) or jumping around in the timeline (left click) tho, does it?

@bitingsock
Copy link
Author

bitingsock commented Aug 23, 2023

Just confirmed it working on debian(mate). Seeking and pausing is synced. Something in some DEs is probably eating any ALT combo.
I should make a note, but make sure you have socat installed. I would like to not need this but writing to the pipes directly proves challenging, if anyone knows a more native way to do it let me know. I saw netcat too but it is also not installed be default on my live debian 12 disc.

@HairyOtter
Copy link

Something in some DEs is probably eating any ALT combo.

The alt hotkeys work for me in Ubuntu Desktop. Sync also works after installing socat. Thanks a lot!

@JimPancakes
Copy link

@bitingsock thank you for this awesome script. I noticed you used the following code to change the volume of the child:

os.execute("echo add volume 5" .. piper .. o.pipe_template .. control)

Just wondering if you know of a more direct approach, one that doesn't use the command line? I'm on windows and this results in my screen constantly flickering in fullscreen whenever I change the volume.

@bitingsock
Copy link
Author

bitingsock commented Aug 27, 2023

I am on windows and do not exhibit that behavior. IPC is the most direct way to control other processes. there surely is a different windows specific way to modify wasapi apps volume but I would prefer to address the real problem. Do you use a third party terminal emulator or something? Can you show a -v log with these "flickers"? Arbitrary calls to the shell should not create a console window, normally.

@Raidfire-SDR
Copy link

Cant seem to get the child process to spawn, my hope was the main process would run the tv speakers via and the 2nd i could send to the virtual speakers in asiolinkpro which allow you to split off the individual dolby channels and send them to any speaker in your asio driver.

asiolinkprod river works in mpv splits the channels into 8 assigned to multiple soundcards with ease but mpvs bad handling of sound occasionally overloads the asio driver no matter how its normalised, requiring a quick driver reset several times per movie.

Potplayer does dual audio devices but the sync is not very tight between the 2

@bitingsock
Copy link
Author

@Raidfire-SDR can you verify the child process is not spawning? Try adding a --log-file=temp.log to the child arguments around line 100 and see if it dumps anything useful.

@Raidfire-SDR
Copy link

Hi, I got this working, my usage is a little different to the initial intention and I would like to modify the script so it will always run with a default config, I would like your input on how I would go about doing the following:
On runtime a child is spawned with both audio devices hardcoded.
I have managed successfully to run mpv on my default soundcard (10 channel pro studio rack) this gives me software control of the device so i can mute the front left, right and center and just get the sub and rear channels, spawn a child then using a profile swap this to an spdif stream i have everything sent to the tv and mute out the rear and sub, works beautifully, much better latency than voicemeeter, would just like to hard code it now so it all happens at runtime without the use of shortcuts.

@Raidfire-SDR
Copy link

Raidfire-SDR commented Apr 10, 2024

I tried to disable the need for CTRL+A to spawn a child but i can't get it working, another angle would be, is there anyway to echo the command via script messaging from the main conf file, tried several iterations and couldn't get that running either.
I did however manage to set a static sound card for the child, so at least I'm 1 keypress away from full dolby at runtime. As a side note how easy would it be to access the individual channel gains for dolby that are exposed in mpv , directly in the lua script?

@bitingsock
Copy link
Author

bitingsock commented Apr 10, 2024

@Raidfire-SDR
I would probably do something like this at the very end of the script:

local function onstart()
	if mp.get_property("vid") ~= "no" then
		start_child()
		mp.unregister_event(onstart)
	end
end
mp.register_event("file-loaded", onstart)

and change line 104 "--audio-device=" .. mp.get_property("audio-device"), to be the audio device that you want for the child.
The problem with starting the child at runtime is the info the child needs (media file path for example) is not populated yet, so we have to at least wait until playback actually starts.

@Raidfire-SDR
Copy link

Raidfire-SDR commented Apr 11, 2024

Thanks that gave me a good starting point, after a little playing so i could see what was doing what in your script i found a nice way to handle the latency, tunable per your own system but seem solid once done, the 0.4 value is equivalent to a delay of 400ms (lua uses seconds mpv uses ms, actual behavior is the child is playing 500ms up front of the parent to negate the latency). also I couldn't get anything but null from audio-pts so i removed it and went with just time-pos, i believe there is a macro request to update the resolution of audio-pts and time-pos in the mpv - lua hooks, so latency should become less of an issue when we can get sub 100ms sync resolution in mpv, currently the closest you can pull 2 streams with ipc is 100ms, this seemed like a viable workaround. Failure to reset local controlling to false yielded some weird results!

local function onstart()
if mp.get_property("vid") ~= "no" then
start_child()
mp.unregister_event(onstart)
delay()
end
end

function delay()
os.execute("echo add audio-delay 0.4" .. piper .. o.pipe_template .. control)
end
mp.register_event("file-loaded", onstart)

@Raidfire-SDR
Copy link

seems this is controling the parent not the child, what am i doing wrong? the goal is to hardcode an audio delay to the child process

@bitingsock
Copy link
Author

If you just need it hardcoded and not tunable you should just put it as a runtime option in the start_child function (line 93):

mp.command_native_async({
		name = "subprocess",
		args = {
			mpvexe,
			mp.get_property("path"),
			"--speed=" .. mp.get_property("speed"),
			"--pause=" .. mp.get_property("pause"),
			"--no-video",
			"--no-sub",
			"--osd-level=1",
			"--aid=" .. mp.get_property("aid"),
			"--audio-device=" .. mp.get_property("audio-device"),
			"--input-ipc-server=" .. o.pipe_template .. control,
			"--start=" .. (mp.get_property("audio-pts") or mp.get_property("time-pos") or "0"),
			"--audio-delay=0.4"
		},
		playback_only = false
	}, function() end)

@Raidfire-SDR
Copy link

Raidfire-SDR commented Apr 11, 2024

Works well need to fine tune the latency but almost there.
line 104 of lua hardcoded child process audio device
line 107 of lua latency tuning value
mpv conf - parent audio device and spdif passthru
https://github.com/Raidfire-SDR/V2-Videophile-MPV-Configs/blob/main/scripts/audio-dupe-dolby.lua

For Dolby to work well, use your device hosting the front speakers and center as the parent device set in mpv.conf, this ensures mpv's main a/v sync is locked to the voice tracks keeping the dubbing smooth set your back speaker device in lua and use a dolby test file to fine tune the latency value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment