Skip to content

Instantly share code, notes, and snippets.

@lugia-kun
Last active December 16, 2015 23:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lugia-kun/5517620 to your computer and use it in GitHub Desktop.
Save lugia-kun/5517620 to your computer and use it in GitHub Desktop.
jack-aloop-daemon
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'jack'
raise "You need jack-ffi, not jack gem." unless JACK
require 'yaml'
include JACK
CONFIG_FILE = File.expand_path("~/.jack-aloop-daemon.yaml")
begin
CONFIG = YAML.load(File.open(CONFIG_FILE, "r").read)
rescue Errno::ENOENT
CONFIG = {
"daemonize" => false,
"channels" => 2,
"sample_rate" => 44100,
"buffer_size" => 1024,
"launch_info" => {
"in" => {
"type" => "capture",
# nickname, device
"devices" => {
"cloop" => "cloop",
},
"command" => "alsa_in -j %1$s -d %2$s -c %3$d -r %4$d -p %5$d",
},
"out" => {
"type" => "playback",
"devices" => {
"ploop" => "ploop",
},
"command" => "alsa_out -j %1$s -d %2$s -c %3$d -r %4$d -p %5$d",
},
},
"connect" => {
"cloop" => ["system"],
"system" => ["ploop"],
},
}
File.open(CONFIG_FILE, "w") do |fp|
fp.print CONFIG.to_yaml
end
end
Client.new("jack-aloop-daemon") do |jack|
CONFIG["launch_info"].each do |key, val|
devs = val["devices"]
fail "no devices in launch_info #{key}" unless devs.respond_to?(:keys)
fail "no devices in launch_info #{key}" if devs.size == 0
nicks = devs.keys
type = val["type"]
fail "type must be \"capture\" or \"playback\"." unless
%w[capture playback].any? { |x| x == type }
nicks.each do |nick|
port_name = "%s:%s_1" % [nick, type]
begin
jack.port_by_name(port_name)
fail "Jack Server already has a port for #{nick}."
rescue Errors::NoSuchPortError
# NOP
end
end
end
end
AloopDaemon = Struct.new(:type, :nick, :channels, :conns,
:cmd, :pid, :thread) do
def reconnect
if self.type == "capture"
from = [self.nick]
to = self.conns
else
from = self.conns
to = [self.nick]
end
Client.new("jack-aloo-daemon_connector_#{self.nick}") do |jack|
from.each do |fr|
to.each do |tt|
(1..self.channels).each do |i|
tries = 0
begin
jack.connect("#{fr}:capture_#{i}", "#{tt}:playback_#{i}")
rescue Errors::NoSuchPortError
sleep 1
tries += 1
retry if tries < 5
raise
end
end
end
end
end
end
end
$aloopdaemons = Array.new
Process.daemon if CONFIG["daemonize"] == true
channels = CONFIG["channels"]
sample_rate = CONFIG["sample_rate"]
buffer_size = CONFIG["buffer_size"]
begin
channels = Integer(channels)
sample_rate = Integer(sample_rate)
buffer_size = Integer(buffer_size)
rescue ArgumentError
fail "channels, sameple_rate and buffer_size must be integers."
end
CONFIG["launch_info"].each do |key, val|
devices = val["devices"]
cmd = val["command"]
type = val["type"]
fail "no command to #{key} launch_info" unless cmd
devices.each do |nick, dev|
cmdline = cmd % [nick, dev, channels, sample_rate, buffer_size]
conns = CONFIG["connect"].select do |key, val|
if type == "capture"
key == nick
else
val.any? { |x| x == nick }
end
end
if type == "capture"
conns = conns.values.flatten
else
conns = conns.keys
end
add = AloopDaemon.new(type, nick, channels, conns, cmdline)
thread = Thread.new(add) do |add|
first = true
loop do
add.pid = Process.spawn(add.cmd, :in => "/dev/null", :out => "/dev/null")
add.reconnect unless first
first = false
Process.wait(add.pid)
end
end
add.thread = thread
$aloopdaemons << add
end
end
Client.new("jack-aloop-daemon") do |jack|
(1..channels).each do |i|
CONFIG["connect"].each do |fr, arr_to|
arr_to.each do |to|
tries = 0
begin
jack.connect("#{fr}:capture_#{i}", "#{to}:playback_#{i}")
rescue Errors::NoSuchPortError
sleep 1
tries += 1
retry if tries < 5
raise
end
end
end
end
end
Signal.trap("INT") do
Process.kill("TERM", Process.pid)
end
Signal.trap("TERM") do
$aloopdaemons.each do |add|
Thread.kill(add.thread)
Process.kill("TERM", add.pid)
end
Process.waitall
exit
end
sleep
@lugia-kun
Copy link
Author

Ruby version of http://pastebin.com/uyyVzEhM and some improvements. Please see details in my comment.

  • You needs jack-ffi gem (jack gem must not be installed; they are conflicted).
  • Configuration for command string will be parsed in simple strategy: Quotes will act as same as other regular characters.
  • To kill this daemon, send INT or TERM signal; KILL or others will not cleanup on exit.
  • You may also use secondary ALSA physical cards with this script and send playback sounds to main card, network, firewire etc.
  • I'm not sure whether the alsa_in and alsa_out commands will terminate safely by sending TERM signal.

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