# ----------- Initialize --------
#
#
# These routines are executed once on program startup
#
#
package Audio::Nama;
use Modern::Perl; use Carp;
use Socket qw(getnameinfo NI_NUMERICHOST) ;
sub is_test_script { $config->{opts}->{J} }
# if we are using fake JACK client data,
# probably a test script is running
sub apply_test_args {
push @ARGV, qw(-f /dev/null), # force to use internal namarc
qw(-t), # set text mode
qw(-d), $Audio::Nama::test_dir,
q(-E), # suppress loading Ecasound
q(-J), # fake jack client data
q(-T), # don't initialize terminal
# load fake effects cache
# q(-c), 'test-project',
#qw(-L SUB); # logging
$jack->{periodsize} = 1024;
}
sub apply_ecasound_test_args {
apply_test_args();
@ARGV = grep { $_ ne q(-E) } @ARGV
}
sub definitions {
$| = 1; # flush STDOUT buffer on every write
$ui eq 'bullwinkle' or die "no \$ui, bullwinkle";
@global_effect_chain_vars = qw(
@global_effect_chain_data
$Audio::Nama::EffectChain::n
$fx->{alias}
);
@tracked_vars = qw(
@tracks_data
@bus_data
@groups_data
@marks_data
@fade_data
@edit_data
@inserts_data
@effects_data
$project->{save_file_version_number}
$fx->{applied}
$fx->{params}
$fx->{params_log}
);
@persistent_vars = qw(
$project->{save_file_version_number}
$project->{timebase}
$project->{command_buffer}
$project->{track_version_comments}
$project->{track_comments}
$project->{bunch}
$project->{current_op}
$project->{current_param}
$project->{current_stepsize}
$project->{playback_position}
@project_effect_chain_data
$fx->{id_counter}
$setup->{loop_endpoints}
$mode->{loop_enable}
$mode->{mastering}
$mode->{preview}
$mode->{midish_terminal}
$mode->{midish_transport_sync}
$gui->{_seek_unit}
$text->{command_history}
$this_track_name
$this_op
);
$text->{wrap} = new Text::Format {
columns => 75,
firstIndent => 0,
bodyIndent => 0,
tabstop => 4,
};
####### Initialize singletons #######
# Some of these "singletons" (imported by 'use Globals')
# are just hashes, some have object behavior as
# the sole instance of their class.
$project = bless {}, 'Audio::Nama::Project';
$mode = bless {}, 'Audio::Nama::Mode';
{ package Audio::Nama::Mode;
sub mastering { $Audio::Nama::tn{Eq} and ! $Audio::Nama::tn{Eq}->{hide} }
no warnings 'uninitialized';
sub eager { $Audio::Nama::mode->{eager} }
sub doodle {
#my $set = shift;
#if (defined $set){ $Audio::Nama::mode->{preview} = $set ? 'doodle' : 0 }
$Audio::Nama::mode->{preview} eq 'doodle' }
sub preview { $Audio::Nama::mode->{preview} eq 'preview' }
sub song { $Audio::Nama::mode->eager and $Audio::Nama::mode->preview }
sub live { $Audio::Nama::mode->eager and $Audio::Nama::mode->doodle }
}
# for example, $file belongs to class Audio::Nama::File, and uses
# AUTOLOAD to generate methods to provide full path
# to various system files, such as $file->state_store
{
package Audio::Nama::File;
use Carp;
sub logfile {
my $self = shift;
$ENV{NAMA_LOGFILE} || $self->_logfile
}
sub AUTOLOAD {
my ($self, $filename) = @_;
# get tail of method call
my ($method) = $Audio::Nama::File::AUTOLOAD =~ /([^:]+)$/;
croak "$method: illegal method call" unless $self->{$method};
my $dir_sub = $self->{$method}->[1];
$filename ||= $self->{$method}->[0];
my $path = Audio::Nama::join_path($dir_sub->(), $filename);
$path;
}
sub DESTROY {}
1;
}
$file = bless
{
effects_cache => ['.effects_cache.json', \&project_root],
gui_palette => ['palette', \&project_root],
state_store => ['State', \&project_dir ],
git_state_store => ['State.json', \&project_dir ],
untracked_state_store => ['Aux', \&project_dir ],
effect_profile => ['effect_profiles', \&project_root],
chain_setup => ['Setup.ecs', \&project_dir ],
user_customization => ['customize.pl', \&project_root],
project_effect_chains => ['project_effect_chains',\&project_dir ],
global_effect_chains => ['global_effect_chains', \&project_root],
old_effect_chains => ['effect_chains', \&project_root],
_logfile => ['nama.log', \&project_root],
}, 'Audio::Nama::File';
$gui->{_save_id} = "State";
$gui->{_seek_unit} = 1;
$gui->{marks} = {};
#
# use this section to specify
# defaults for config variables
#
# These are initial, lowest priority defaults
# defaults for Nama config. Some variables
# may be overwritten during subsequent read_config's
#
# config variable sources are prioritized as follows
#
# + command line argument -f /path/to/namarc
# + project specific namarc # currently disabled
# + user namarc (usually ~/.namarc)
# + internal namarc
# + internal initialization
$config = bless {
root_dir => join_path( $ENV{HOME}, "nama"),
soundcard_channels => 10,
memoize => 1,
use_pager => 1,
use_placeholders => 1,
use_git => 1,
autosave => 'undo',
volume_control_operator => 'ea', # default to linear scale
sync_mixdown_and_monitor_version_numbers => 1, # not implemented yet
engine_tcp_port => 2868, # 'default' engine
engine_fade_length_on_start_stop => 0.18,# when starting/stopping transport
engine_fade_default_length => 0.5, # for fade-in, fade-out
engine_base_jack_seek_delay => 0.1, # seconds
engine_command_output_buffer_size => 2**22, # 4 MB
edit_playback_end_margin => 3,
edit_crossfade_time => 0.03,
fade_down_fraction => 0.75,
fade_time1_fraction => 0.9,
fade_time2_fraction => 0.1,
fader_op => 'ea',
mute_level => {ea => 0, eadb => -96},
fade_out_level => {ea => 0, eadb => -40},
unity_level => {ea => 100, eadb => 0},
fade_resolution => 100, # steps per second
engine_muting_time => 0.03,
enforce_channel_bounds => 1,
serialize_formats => 'json', # for save_system_state()
latency_op => 'el:delay_n',
latency_op_init => [0,0],
latency_op_set => sub
{
my $id = shift;
my $delay = shift();
modify_effect($id,2,undef,$delay)
},
hotkey_beep => 'beep -f 250 -l 200',
# this causes beeping during make test
# beep_command => 'beep -f 350 -l 700',
}, 'Audio::Nama::Config';
{ package Audio::Nama::Config;
use Carp;
use Audio::Nama::Globals qw(:singletons);
use Modern::Perl;
our @ISA = 'Audio::Nama::Object'; # for ->dump and ->as_hash methods
sub serialize_formats { split " ", $_[0]->{serialize_formats} }
sub hardware_latency {
no warnings 'uninitialized';
$config->{devices}->{$config->{alsa_capture_device}}{hardware_latency} || 0
}
sub buffersize {
package Audio::Nama;
Audio::Nama::ChainSetup::setup_requires_realtime()
? $config->{engine_buffersize}->{realtime}->{default}
: $config->{engine_buffersize}->{nonrealtime}->{default}
}
sub globals_realtime {
Audio::Nama::ChainSetup::setup_requires_realtime()
? $config->{engine_globals}->{realtime}
: $config->{engine_globals}->{nonrealtime}
}
} # end Audio::Nama::Config package
$prompt = "nama ('h' for help)> ";
$this_bus = 'Main';
$setup->{_old_snapshot} = {};
$setup->{_last_rec_tracks} = [];
$mastering->{track_names} = [ qw(Eq Low Mid High Boost) ];
init_wav_memoize() if $config->{memoize};
}
sub initialize_interfaces {
logsub("&intialize_interfaces");
if ( ! $config->{opts}->{t} and Audio::Nama::Graphical::initialize_tk() ){
$ui = Audio::Nama::Graphical->new();
} else {
pager_newline( "Unable to load perl Tk module. Starting in console mode.") if $config->{opts}->{g};
$ui = Audio::Nama::Text->new();
can_load( modules =>{ Event => undef})
or die "Perl Module 'Event' not found. Please install it and try again. Stopping.";
;
import Event qw(loop unloop unloop_all);
}
can_load( modules => {AnyEvent => undef})
or die "Perl Module 'AnyEvent' not found. Please install it and try again. Stopping.";
use AnyEvent::TermKey qw( FORMAT_VIM KEYMOD_CTRL );
can_load( modules => {jacks => undef})
and $jack->{use_jacks}++;
choose_sleep_routine();
initialize_terminal() unless $config->{opts}->{T};
$config->{want_logging} = initialize_logger($config->{opts}->{L});
$project->{name} = shift @ARGV;
{no warnings 'uninitialized';
logpkg(__FILE__,__LINE__,'debug',"project name: $project->{name}");
}
logpkg(__FILE__,__LINE__,'debug', sub{"Command line options\n". json_out($config->{opts})});
read_config(global_config()); # from .namarc if we have one
# overwrite default hotkey bindings by those in .namarc
$config->{hotkeys} = {
%{json_in(get_data_section 'hotkey_bindings') },
%{$config->{hotkeys} }
};
logpkg(__FILE__,__LINE__,'debug',sub{"Config data\n".Dumper $config});
select_ecasound_interface();
start_osc_listener($config->{osc_listener_port})
if $config->{osc_listener_port}
and can_load(modules => {'Protocol::OSC' => undef});
start_remote_listener($config->{remote_control_port}) if $config->{remote_control_port};
logpkg(__FILE__,__LINE__,'debug',"reading config file");
if ($config->{opts}->{d}){
pager("project_root $config->{opts}->{d} specified on command line\n");
$config->{root_dir} = $config->{opts}->{d};
}
if ($config->{opts}->{p}){
$config->{root_dir} = getcwd();
pager("placing all files in current working directory ($config->{root_dir})\n");
}
# skip initializations if user (test) supplies project
# directory
first_run() unless $config->{opts}->{d};
prepare_static_effects_data() unless $config->{opts}->{S};
setup_user_customization(); # depends on effect_index() in above
get_ecasound_iam_keywords();
load_keywords(); # for autocompletion
chdir $config->{root_dir} # for filename autocompletion
or warn "$config->{root_dir}: chdir failed: $!\n";
$ui->init_gui;
$ui->transport_gui;
$ui->time_gui;
# fake JACK for testing environment
if( $config->{opts}->{J}){
parse_ports_list(get_data_section("fake_jack_lsp"));
parse_port_latency(get_data_section("fake_jack_latency"));
$jack->{jackd_running} = 1;
}
# periodically check if JACK is running, and get client/port/latency list
poll_jack() unless $config->{opts}->{J} or $config->{opts}->{A};
sleeper(0.2); # allow time for first polling
# we will start jack.plumbing only when we need it
if( $config->{use_jack_plumbing}
and $jack->{jackd_running}
and process_is_running('jack.plumbing')
){
pager_newline(<<PLUMB);
Jack.plumbing daemon detected!
Attempting to stop it...
(This may break other software that depends in jack.plumbing.)
Nama will restart it as needed for Nama's use only.
PLUMB
kill_jack_plumbing();
sleeper(0.2);
if( process_is_running('jack.plumbing') )
{
throw(q(Unable to stop jack.plumbing daemon.
Please do one of the following, then restart Nama:
- kill the jack.plumbing daemon ("killall jack.plumbing")
- set "use_jack_plumbing: 0" in .namarc
....Exiting.) );
exit;
}
else { pager_newline("Stopped.") }
}
start_midish() if $config->{use_midish};
# set default project to "untitled"
#convert_project_format(); # mark with .conversion_completed file in ~/nama
if (! $project->{name} ){
$project->{name} = "untitled";
$config->{opts}->{c}++;
}
pager("\nproject_name: $project->{name}\n");
load_project( name => $project->{name}, create => $config->{opts}->{c}) ;
1;
}
{ my $is_connected_remote;
sub start_remote_listener {
my $port = shift;
pager_newline("Starting remote control listener on port $port");
$project->{remote_control_socket} = IO::Socket::INET->new(
LocalAddr => 'localhost',
LocalPort => $port,
Proto => 'tcp',
Type => SOCK_STREAM,
Listen => 1,
Reuse => 1) || die $!;
start_remote_watcher();
}
sub start_remote_watcher {
$project->{events}->{remote_control} = AE::io(
$project->{remote_control_socket}, 0, \&process_remote_command )
}
sub remove_remote_watcher {
undef $project->{events}->{remote_control};
}
sub process_remote_command {
if ( ! $is_connected_remote++ ){
pager_newline("making connection");
$project->{remote_control_socket} =
$project->{remote_control_socket}->accept();
remove_remote_watcher();
$project->{events}->{remote_control} = AE::io(
$project->{remote_control_socket}, 0, \&process_remote_command );
}
my $input;
eval {
$project->{remote_control_socket}->recv($input, $project->{remote_control_socket}->sockopt(SO_RCVBUF));
};
$@ and throw("caught error: $@, resetting..."), reset_remote_control_socket(), revise_prompt(), return;
logpkg(__FILE__,__LINE__,'debug',"Got remote control socketput: $input");
process_command($input);
my $out;
{ no warnings 'uninitialized';
$out = $text->{eval_result} . "\n";
}
eval {
$project->{remote_control_socket}->send($out);
};
$@ and throw("caught error: $@, resetting..."), reset_remote_control_socket(), revise_prompt(), return;
revise_prompt();
}
sub reset_remote_control_socket {
undef $is_connected_remote;
undef $@;
$project->{remote_control_socket}->shutdown(2);
undef $project->{remote_control_socket};
remove_remote_watcher();
start_remote_listener($config->{remote_control_port});
}
}
sub start_osc_listener {
my $port = shift;
say("Starting OSC listener on port $port");
my $osc_in = $project->{osc_socket} = IO::Socket::INET->new(
LocalAddr => 'localhost',
LocalPort => $port,
Proto => 'udp',
Type => SOCK_DGRAM) || die $!;
$project->{events}->{osc} = AE::io( $osc_in, 0, \&process_osc_command );
$project->{osc} = Protocol::OSC->new;
}
sub process_osc_command {
my $in = $project->{osc_socket};
my $osc = $project->{osc};
my $source_ip = $in->recv(my $packet, $in->sockopt(SO_RCVBUF));
my($err, $hostname, $servicename) = getnameinfo($source_ip, NI_NUMERICHOST);
my $p = $osc->parse($packet);
my @args = @$p;
my ($path, $template, $command, @vals) = @args;
$path =~ s(^/)();
$path =~ s(/$)();
my ($trackname, $fx, $param) = split '/', $path;
process_command($trackname);
process_command("$command @vals") if $command;
process_command("show_effect $fx") if $fx; # select
process_command("show_track") if $trackname and not $fx;
process_command("show_tracks") if ! $trackname;
say "got OSC: ", Dumper $p;
say "got args: @args";
my $osc_out = IO::Socket::INET->new(
PeerAddr => $hostname,
PeerPort => $config->{osc_reply_port},
Proto => 'udp',
Type => SOCK_DGRAM) || die $!;
$osc_out->send(join "",@{$text->{output_buffer}});
delete $text->{output_buffer};
}
sub sanitize_remote_input {
my $input = shift;
my $error_msg;
do{ $input = "" ; $error_msg = "error: perl/shell code is not allowed"}
if $input =~ /(^|;)\s*(!|eval\b)/;
throw($error_msg) if $error_msg;
$input
}
sub select_ecasound_interface {
my %args;
my $class;
if ($config->{opts}->{A} or $config->{opts}->{E})
{
pager_newline("Starting dummy engine only");
%args = (
name => 'Nama',
jack_transport_mode => 'send',
);
$class = 'Audio::Nama::Engine';
}
elsif (
$config->{opts}->{l}
and can_load( modules => { 'Audio::Ecasound' => undef })
and say("loaded Audio::Ecasound")
){
%args = (
name => 'Nama',
jack_transport_mode => 'send',
);
$class = 'Audio::Nama::LibEngine';
}
else {
%args = (
name => 'Nama',
port => $config->{engine_tcp_port},
jack_transport_mode => 'send',
);
$class = 'Audio::Nama::NetEngine';
}
$class->new(%args);
}
sub choose_sleep_routine {
if ( can_load(modules => {'Time::HiRes'=> undef} ) )
{ *sleeper = *finesleep;
$config->{hires_timer}++; }
else { *sleeper = *select_sleep }
}
sub finesleep {
my $sec = shift;
Time::HiRes::usleep($sec * 1e6);
}
sub select_sleep {
my $seconds = shift;
select( undef, undef, undef, $seconds );
}
sub munge_category {
my $cat = shift;
# override undefined category by magical global setting
# default to 'ECI_OTHER'
$cat ||= ($config->{category} || 'ECI_OTHER');
# force all categories to 'ECI' if 'ECI' is selected for logging
# (exception: ECI_WAVINFO, which is too noisy)
no warnings 'uninitialized';
return 'ECI' if $config->{want_logging}->{ECI} and not $cat eq 'ECI_WAVINFO';
$cat
}
sub start_logging {
$config->{want_logging} = initialize_logger($config->{opts}->{L})
}
sub eval_iam { $this_engine and $this_engine->eval_iam(@_) }
1;
__END__