Group
Extension

Audio-Nama/lib/Audio/Nama/Persistence.pm

# ---------- Persistent State Support -------------


package Audio::Nama;
use File::Copy;
use v5.36; no warnings 'uninitialized';
use vars '$VERSION';


sub save_state {
	logsub((caller(0))[3]);
	my $filename = shift;
	my $path = $filename || $file->state_store();

		# remove extension if present
		
		$filename =~ s/\.json//;

		# append filename if warranted
		
		$filename = 
				$filename =~ m{/} 	
									? $filename	# as-is if input contains slashes
									: join_path(project_dir(),$filename) ;
	$project->{nama_version} = $VERSION;

	# store playback position, if possible
	$project->{playback_position} = ecasound_iam("getpos") if $this_engine->valid_setup();

	# some stuff get saved independently of our state file
	
	logpkg(__FILE__,__LINE__,'debug', "saving palette");
	$ui->save_palette;

	logpkg(__FILE__,__LINE__,'debug',"Saving state as ", $path);
	save_system_state($path);
	save_global_effect_chains();

	save_midish();

	# store alsa settings

	if ( $config->{opts}->{a} ) {
		my $filename = $filename;
		$filename =~ s/\.yml$//;
		pager("storing ALSA settings\n");
		pager(qx(alsactl -f $filename.alsa store))
	}
}
sub initialize_marshalling_arrays {
	@tracks_data = (); # zero based, iterate over these to restore
	@bus_data = (); # 
	@marks_data = ();
	@fade_data = ();
	@inserts_data = ();
	@effects_data = ();
	@edit_data = ();
	@project_effect_chain_data = ();
	@global_effect_chain_data = ();

}

sub save_system_state {

	my $path = shift;
	my $output_format = shift;

	sync_effect_parameters(); # in case a controller has made a change
	# we sync read-only parameters, too, but I think that is
	# harmless

	initialize_marshalling_arrays();
	
	# prepare tracks for storage
	
	$this_track_name = $this_track->name if $this_track;

	logpkg(__FILE__,__LINE__,'debug', "copying tracks data");

	map { push @tracks_data, $_->as_hash } all_tracks();

	# print "found ", scalar @tracks_data, "tracks\n";

	# delete obsolete fields
	map { my $t = $_;
				map{ delete $t->{$_} } 
					qw(ch_r ch_m source_select send_select jack_source jack_send);
	} @tracks_data;

	logpkg(__FILE__,__LINE__,'debug', "copying bus data");

	@bus_data = map{ $_->as_hash } sort { $a->name cmp $b->name} Audio::Nama::Bus::all();


	my $by_n = sub { $a->{n} <=> $b->{n} };

	# prepare inserts data for storage
	
	logpkg(__FILE__,__LINE__,'debug', "copying inserts data");
	
	@inserts_data = sort $by_n map{ $_->as_hash } values %Audio::Nama::Insert::by_index;

	# prepare marks data for storage (new Mark objects)

	logpkg(__FILE__,__LINE__,'debug', "copying marks data");

	@marks_data = sort {$a->{time} <=> $b->{time} } map{ $_->as_hash } Audio::Nama::Mark::all();
	@effects_data = sort { $a->{id} cmp $b->{id} } map{ $_->as_hash } values %Audio::Nama::Effect::by_id;
	
	@fade_data = sort $by_n map{ $_->as_hash } values %Audio::Nama::Fade::by_index;

	@edit_data = sort $by_n map{ $_->as_hash } values %Audio::Nama::Edit::by_index;

	@project_effect_chain_data = sort $by_n map { $_->as_hash } 
		Audio::Nama::EffectChain::find(project => 1);

	# save history -- 50 entries, maximum

	my @history;
# 	@history = $text->{term}->GetHistory if $text->{term};
# 	my %seen;
# 	$text->{command_history} = [];
# 	map { push @{$text->{command_history}}, $_ 
# 			unless $seen{$_}; $seen{$_}++ } @history;
	my $max = scalar @{$text->{command_history}};
	$max = 50 if $max > 50;
	my $hist = $text->{command_history}; 
	@$hist = @$hist[-$max..-1];
	$text->{command_index} = $max;
	logpkg(__FILE__,__LINE__,'debug', "serializing");

	my @formats = $output_format || $config->serialize_formats;

	map{ 	my $format = $_ ;
			serialize(
				file => $path,
				format => $format,
				vars => [ (grep {!  /save_file_version_number/ } @tracked_vars) ],
				class => 'Audio::Nama',
				);

	} @formats;

	serialize(
		file => $file->untracked_state_store,
		format => 'json',
		vars => [ (grep {!  /save_file_version_number/ } @persistent_vars) ],
		class => 'Audio::Nama',
	);	

	"$path.json";
}

{ my %decode = 
	(
		json => \&json_in,
		yaml => sub 
		{ 
			my $yaml = shift;

			# remove empty key hash lines to satisfy YAML::Tiny
			$yaml = join $/, grep{ ! /^\s*:/ } split $/, $yaml;

			$yaml = quote_yaml_scalars( $yaml );

			yaml_in($yaml);
		},
		perl => sub {my $perl_source = shift; eval $perl_source},
		storable => sub { my $bin = shift; thaw( $bin) },
	);
	
	# allow dispatch by either file format or suffix 
	@decode{qw(yml pl bin)} = @decode{qw(yaml perl storable)};

sub decode {

	my ($source, $suffix) = @_;
	$decode{$suffix} 
		or die qq(key $suffix: expecting one of).join q(,),keys %decode;
	$decode{$suffix}->($source);
}
}

sub restore_state_from_file {
	logsub((caller(0))[3]);
	my $filename = shift;
	$filename //= $file->state_store();

	initialize_marshalling_arrays();

	my $suffix = 'json';	
	my $path = $file->untracked_state_store;
	if (-r $path)
	{
		my $source = read_file($path);

		my $ref = decode($source, $suffix);
		assign(
				data	=> $ref,	
				vars   	=> \@persistent_vars,
				class 	=> 'Audio::Nama');
		assign_singletons( { data => $ref });
	}
	
	$path = $filename;
	if (-r $path)
	{
		my $source = read_file($path);
		my $ref = decode($source, $suffix);

		assign(
					data => $ref,
					vars   => \@tracked_vars,
					class => 'Audio::Nama');
		

		# perform assignments for singleton
		# hash entries (such as $fx->{ applied});
		# that that assign() misses
		
		assign_singletons({ data => $ref });

	}
	
	restore_global_effect_chains();

	####### Backward Compatibility ########

	$project->{nama_version} //= delete $project->{save_file_version_number};

	if ( $project->{nama_version} < 1.100){ 
		map{ Audio::Nama::EffectChain::move_attributes($_) } 
			(@project_effect_chain_data, @global_effect_chain_data)
	}
	if ( $project->{nama_version} < 1.105){ 
		map{ $_->{class} = 'Audio::Nama::BoostTrack' } 
		grep{ $_->{name} eq 'Boost' } @tracks_data;
	}
	if ( $project->{nama_version} < 1.109){ 
		map
		{ 	if ($_->{class} eq 'Audio::Nama::MixTrack') { 
				$_->{is_mix_track}++;
				$_->{class} = $_->{was_class};
				$_->{class} = 'Audio::Nama::Track';
		  	}
		  	delete $_->{was_class};
		} @tracks_data;
		map
		{    if($_->{class} eq 'Audio::Nama::MasterBus') {
				$_->{class} = 'Audio::Nama::SubBus';
			 }
		} @bus_data;

	}
	if ( $project->{nama_version} < 1.111){ 
		map
		{
			convert_rw($_);
			delete $_->{effect_chain_stack} ;
            delete $_->{rec_defeat};
            delete $_->{was_class};
			delete $_->{is_mix_track};
			$_->{rw} = MON if $_->{name} eq 'Master';
		} @tracks_data;
		map
		{
			$_->{rw} = MON if $_->{rw} eq 'REC'
		} @bus_data;
	}

	# convert effect object format
	
	if ( $project->{nama_version} < 1.200 )
	{
		@effects_data = 
			map{ my $hashref = $fx->{applied}->{$_}; 
					$hashref->{params} = $fx->{params}->{$_}; 
					$hashref->{class} = 'Audio::Nama::Effect';
					$hashref->{owns} ||= [];
					$hashref }
			grep { defined $_ } 
			keys %{$fx->{applied}};
		#say "effects data: ", json_out \@effects_data;
		delete $fx->{applied};
		delete $fx->{params};
	}
	if ( $project->{nama_version} <= 1.201 )
	{
		map{ $_->{owns} ||= [] } @effects_data;
	}
	if ( $project->{nama_version} <= 1.208 )
	{
		map
		{ 
			$_->{midi_versions} ||= [];
			$_->{name} =~ s/^Master$/Main/;
			$_->{group} =~ s/^Master$/Null/;
			$_->{group}	  =~ s/^Open$/Null/;
		} 
		@tracks_data;
		map
		{
			$_->{send_id} =~ s/^Master$/Main/;
			$_->{name}	  =~ s/^null$/Aux/;
			$_->{name}	  =~ s/^Open$/Null/;

		}
		@bus_data;
	}
	if ( $project->{nama_version} <= 1.208 )
	{
		# older projects did not store this
		$project->{sample_rate} //= $config->{sample_rate} 
	}
	if ( $project->{nama_version} <= 1.211){ 
		map { $_->{source_id} = 'Main', $_->{source_type} = 'bus' }
		grep { $_->{name} eq 'Main'	} @tracks_data;
		map { $_->{source_id} = 'Main'; 
			  $_->{source_type} = 'track';
			  $_->{send_type} = 'soundcard';
			  $_->{send_id} = 1;
			}
		grep { $_->{name} eq 'Mixdown'	} @tracks_data;
	}
	if ( $project->{nama_version} <= 1.212 )
	{
		my($boost) = grep{$_->{name} eq 'Boost'} @tracks_data; 
		delete $boost->{target}
	}
	if ( $project->{nama_version} <= 1.213 )
	{
		map { 
			$project->{track_comments}->{         $_->{name} } = delete $_->{comment}         if $_->{comment};
			$project->{track_version_comments}->{ $_->{name} } = delete $_->{version_comment} if $_->{version_comment};

		} @tracks_data; 
	}
	if ( $project->{nama_version} <= 1.214 )
	{
		map 
		{
			$_->{class} =~ s/^Nama::/Audio::Nama::/ if $_->{class} 
		}	@tracks_data,
			@bus_data,
			@marks_data,
			@fade_data,
			@edit_data,
			@inserts_data,
			@effects_data,
			@global_effect_chain_data,
			@project_effect_chain_data;
	}
	if ( $project->{nama_version} <= 1.216)
	{
		map { delete $_->{active} } @marks_data
	}


	# restore effects, no change to track objects needed
	
	map
	{ my %args = %$_;
		my $class = delete $args{class};
		my $FX = $class->new(%args, restore => 1);
	} @effects_data;
	
	# restore user buses
		
	Audio::Nama::Bus::initialize();	
	map{ my $class = $_->{class}; $class->new( %$_ ) } @bus_data;
	create_system_buses();

	# temporary turn on mastering mode to enable
	# recreating mastering tracksk

	#my $current_master_mode = $mode->{mastering};
	#$mode->{mastering} = 1;

	# convert field "latency" to "latency_op"
	map{ $_->{latency_op} = delete $_->{latency} if $_->{latency} } @tracks_data;

	# restore tracks
	map{ 
		my %args = %$_; 
		my $class = $args{class} || "Audio::Nama::Track";
		my $track = $class->new( %args, restore => 1 );
	} @tracks_data;


	# restore inserts
	
	Audio::Nama::Insert::initialize();
	
	map{ 
		bless $_, $_->{class}; # bless directly, bypassing constructor
		$Audio::Nama::Insert::by_index{$_->{n}} = $_;
	} @inserts_data;

	# Restore GUI for user tracks
	map{ 
		my $n = $_->{n};

		# create gui
		$ui->track_gui($n) unless $n <= 2;

	} @tracks_data;

	$this_track = $tn{$this_track_name}, set_current_bus() if $this_track_name;
	
	#print "\n---\n", $main->dump;  
	#print "\n---\n", map{$_->dump} Audio::Nama::audio_tracks();# exit; 
	$ui->manifest;
	logpkg(__FILE__,__LINE__,'debug', sub{ join " ", map{ ref $_, $/ } all_tracks() });


	# restore Alsa mixer settings
	if ( $config->{opts}->{a} ) {
		my $filename = $filename; 
		$filename =~ s/\.yml$//;
		pager("restoring ALSA settings\n");
		pager(qx(alsactl -f $filename.alsa restore));
	}

	# text mode marks 

 	map
    {
		my %h = %$_;
		my $mark = Audio::Nama::Mark->new( %h ) ;
    } 
    grep { (ref $_) =~ /HASH/ } @marks_data;

	$ui->restore_time_marks();
	$ui->paint_mute_buttons;

	# track fades
	
	map{ 
		my %h = %$_; 
		my $fade = Audio::Nama::Fade->new( %h ) ;
	} @fade_data;

	# edits 
	
	map{ 
		my %h = %$_; 
		my $edit = Audio::Nama::Edit->new( %h ) ;
	} @edit_data;

	# restore command history
	
	#$text->{term}->SetHistory(@{$text->{command_history}})
	#	if (ref $text->{command_history}) =~ /ARRAY/;

;
	# restore effect chains and profiles
	
	%Audio::Nama::EffectChain::by_index = ();
	#say "Project Effect Chain Data\n", json_out( \@project_effect_chain_data);
 	map { my $fx_chain = Audio::Nama::EffectChain->new(%$_) } 
		(@project_effect_chain_data, @global_effect_chain_data);

	my $fname = $file->midi_store;
	midish_cmd(qq<load "$fname">);
	
} 
sub convert_rw {
	my $h = shift;
	$h->{rw} = MON, return if $h->{rw} eq 'REC' and ($h->{rec_defeat} or $h->{is_mix_track});
	$h->{rw} = PLAY, return if $h->{rw} eq 'MON';
}
sub is_nonempty_hash {
	my $ref = shift;
	return if (ref $ref) !~ /HASH/;
	return (keys %$ref);
}
	 

sub save_global_effect_chains {

	@global_effect_chain_data  = map{ $_->as_hash } Audio::Nama::EffectChain::find(global => 1);

	# always save global effect chain data because it contains
	# incrementing counter

	map{ 	my $format = $_ ;
			serialize(
				file => $file->global_effect_chains,
				format => $format,
				vars => \@global_effect_chain_vars, 
				class => 'Audio::Nama',
			);
	} $config->serialize_formats;

}
sub restore_global_effect_chains {

	logsub((caller(0))[3]);
		my $path =  $file->global_effect_chains;
		-r $path or return;
		my $source = read_file($path);
		throw("$path: empty file"), return unless $source;
		my $suffix = 'json';
		my $ref = decode($source, $suffix);
		assign(
				data => $ref,
				vars   => \@global_effect_chain_vars, 
				class => 'Audio::Nama');
		assign_singletons({ data => $ref });
}
1;

__END__

Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.