# Category = Debug

# This is a user-code file for Misterhouse that supports importance for
# speech events as well as the ability to send speech to one or more
# rooms through a whole-house audio system.  

# You can get the most current version of this file and other files related 
# whole-house music/speech setup here:
#   http://www.linux.kaybee.org:81/tabs/whole_house_audio/

# This file assumes you have one or more properly installed and configured
# sound cards with Linux ALSA drivers.  See my sample asound.conf for how
# I have four stereo outputs with a Delta M-Audio 410.

# This file also assumes you are using my VirtualAudio.pm for the management
# of your whole-house audio system.

# Also, this file assumes you are using my AlsaPlayer.pm to handle any MP3
# players sharing the audio system, but it could be modified easily to support
# other MP3 players.

# This code also assumes you are using a Netstreams Musica whole-house audio
# system and my Musica.pm module to control it.  But it also could be modified
# fairly easily to support another whole-house audio system.

# Finally, this provides optional integration with occupancy/presence tracking
# using Jason Sharpee's Occupancy_Monitor.pm and Presence_Monitor.pm.

# Importance (only applies to specified rooms if rooms are specified)
#    debug: turns on zones in occupied rooms if necessary
#    notice: turns on zones in occupied rooms if necessary, reduces volume
#       of (or pauses) MP3s and talks over them if necessary.
#    important: Turns on (and changes sources for) all necessary zones,
#       talks over MP3s after volume is decreased or player is paused
#    urgent: Turns on (and changes sources for) all zones, talks
#       over MP3s after volume is decreased or player is paused
#       * ignores any muting restrictions

#####################################################################
# noloop=start

#####################################################################
# Begin Configuration section
#####################################################################

# I find it best if the festival output is converted to 44100 since
# my asound.conf is set up for that bitrate.  Set to 0 to disable
# bitrate modification with sox (reduces speech delay when disabled)
#my $bitrate = 44100;
my $bitrate = 0;

# Same thing here -- I converted this to a PCM WAV file at 44100
# Set to a blank string if you want to disable a pre-speak wav file
my $pre_speak_wav = '/mh/data/trek.wav';

# If you want certain zones to turn on only if certain rooms are occupied,
# you can list zone names and then associated presence objects here
%presence_by_room = ( 
   'kitchen' =>    [ $om_presence_kitchen, $om_presence_family_room ],
   'master_bed' => [ $om_presence_master_bath, $om_presence_mbath_toilet, 
                     $om_presence_master_closet, $om_presence_master_bedroom ],
   'outside' =>    [ $om_presence_back_patio ],
   'guest_room' => [ $om_presence_guest_room ],
);

# Define your alsa channels here (leave the undef there to fill the space
# of the 0 array index).  These should be listed in the same order that
# they are connected to your whole-house audio system
my @alsa_channels = ( undef, 'channel12', 'channel34', 'channel56', 'channel78' );

# Provide list of rooms to be used when speech goes to 'all' rooms
# Eventually, I'll have all of these:
#    kitchen, master_bed, outside, office, guest_room, nursery
my @all_rooms = ( 'kitchen', 'master_bed', 'outside', 'guest_room', 'nursery' );

# These rooms are never spoken to unless occupied, regardless of the priority
my @always_only_if_occupied = ( 'outside' );

# Provide the Musica zone object for each room
%zones_by_room = ( 'kitchen' => $music_kitchen,
                   'master_bed' => $music_mb,
                   'outside' => $music_patio,
                   'guest_room' => $music_guest_room,
                   'nursery' => $music_nursery
                 );

# Provide the volume and bass level to be used when zone is turned on 
# for speech for each Musica zone object.  Each zone object can be 
# listed in one, both, or neither of these lists.
my %speech_volume = ( $music_kitchen => 20, 
                      $music_mb => 20, 
                      $music_patio => 15,
                      $music_guest_room => 15,
                      $music_nursery => 15
                    );
my %speech_bass = ( $music_kitchen => -7, 
                    $music_mb => -7, 
                    $music_patio => -4,
                    $music_guest_room => -2,
                    $music_nursery => -2
                  );

# Set to 0 to reduce volume of AlsaPlayer MP3s only, or 1 to pause
my $pause_mp3s = 1;

# When a zone was turned on by this user code to allow for voice output,
# this is how long the zone will remain on, with any additional speech
# resetting the timer.
$speech_delayoff = 120; #seconds

# Define this if you want to display text to your tivo using Tivo_Control.pm
my $tivo = undef;
#my $tivo = new Tivo_Control;

# Any number of entries may be listed here.  Currently, you are required
# to have an 'object' field in each entry, and then either an 'allow' or
# 'disallow' field.  If the state of the object equals the 'disallow' field,
# speech will not occur.  Alternatively, if an 'allow' field is specified,
# speech will only occur if the state matches that value.  The rule applies
# to speech at or below the specified importance level
my @speech_restrictions = (
   {'importance' => 'urgent', 'object' => $mode_occupied, 'disallow' => 'away'},
   {'importance' => 'urgent', 'object' => $mode_vacation, 'disallow' => 'all'},
   {'importance' => 'notice', 'object' => $party_mode, 'disallow' => 'enabled'},
);

# How long to wait at the most for everything to turn on before proceeding
# with speech
my $max_speak_wait = 15; #seconds

# Define a Process_Item here for each source available for speech,
# being sure to leave the 'undef' in the first position.
my @wav_players = ( undef, new Process_Item(), new Process_Item(),
   new Process_Item(), new Process_Item() );

# Importance in order of priority
my %importance_levels = (
   'debug' => 0,
   'notice' => 1,
   'important' => 2,
   'urgent' => 3
   );

#####################################################################
# End Configuration section
#####################################################################

my @pending_speech;
my @zone_recovery;
my @resume_mp3s;
my $SpeechCount = 0;

sub play_audio {
   my ($output, $file, $only_if_done) = @_;
   my $cmd;
   if ($bitrate and $file =~ /speak_festival/) {
      $cmd = "sox '$file' -r $bitrate '$file.new.wav'; /usr/bin/aplay -D '$alsa_channels[$output]' '$file.new.wav'";
   } else {
      $cmd = "/usr/bin/aplay -D '$alsa_channels[$output]' '$file'";
   }
   $cmd .= ' ; sleep 1';
   if ($wav_players[$output]->done()) {
      print_log "Speech: running command '$cmd'";
      $wav_players[$output]->set($cmd);
      $wav_players[$output]->start();
   } else {
      if ($only_if_done) {
         print_log "Speech: dropping command '$cmd' because wav_player is busy";
      } else {
         print_log "Speech: queueing command '$cmd'";
         $wav_players[$output]->add($cmd);
      }
   }
}

sub pre_play_hook {
   my ($parms) = @_;
   if ($$parms{'use_source'}) {
      #$$parms{'sound_program'} = "aplay -D '$alsa_channels[$$parms{'use_source'}]'";
   } else {
      $$parms{'sound_program'} = "aplay -D 'channelALL'";
   }
}

sub is_room_occupied {
   my ($room) = @_;
   return 0 unless ($presence_by_room{$room});
   foreach (@{$presence_by_room{$room}}) {
      if ($_->state() eq 'occupied') {
         return 1;
      }
   }
   return 0;
}

sub check_for_pause {
   my ($zone_num) = @_;
   my $source = $audio_router->get_real_source_number_for_zone($zone_num);
   print_log "Speech: check_for_pause($zone_num), source=$source";
   return unless ($source > 0);
   unless ($players[$source]->is_paused()) {
      if ($pause_mp3s) {
         print_log "Speech: Pausing $source MP3 player...";
         $players[$source]->pause();
      } else {
         print_log "Speech: Reducing volume of $source MP3 player...";
         $players[$source]->volume('0.2');
      }
      print_log "Speech: resume_mp3s[$source]=$resume_mp3s[$source]";
      $resume_mp3s[$source]++;
   }
   return $source;
}

sub set_zone_for_speech {
   my ($zone_obj, $source) = @_;
   my $zone_num = $zone_obj->get_zone_num();
   if ($speech_bass{$zone_obj}) {
      unless ($zone_obj->get_bass_level() == $speech_bass{$zone_obj}) {
         $zone_recovery[$zone_num]->{'bass'} = $zone_obj->get_bass_level();
         $zone_recovery[$zone_num]->{'source'} = $source;
         $zone_recovery[$zone_num]->{'zone_obj'} = $zone_obj;
         print_log "Speech: Zone $zone_num: Changing to speech bass level of $speech_bass{$zone_obj} (current=$zone_recovery[$zone_num]->{'bass'})";
         $zone_obj->set_bass($speech_bass{$zone_obj});
      }
   }
   if ($speech_volume{$zone_obj}) {
      if (($zone_obj->get_volume() < $speech_volume{$zone_obj}) or ($zone_obj->get_source() eq '0')) {
         print_log "Speech: Zone $zone_num: Changing to speech volume of $speech_volume{$zone_obj}";
         if ($zone_obj->get_source()) {
            $zone_recovery[$zone_num]->{'volume'} = $zone_obj->get_volume();
            $zone_recovery[$zone_num]->{'source'} = $source;
            $zone_recovery[$zone_num]->{'zone_obj'} = $zone_obj;
         }
         $zone_obj->set_volume($speech_volume{$zone_obj});
      }
   }
}

# Handles setting up speech to one particular room
sub pre_handle_room {
   my ($room, $importance, $only_if_occupied) = @_;
   my $source = 0;
   my $zone_ret = 0;
   return (0, 0) unless ($zones_by_room{$room});
   my $zone_num = $zones_by_room{$room}->get_zone_num();
   if ($zone_recovery[$zone_num]) {
      # Must still be switched to voice mode from a recent speak event
      print_log "Speech: Room $room(zone=$zone_num, source=$zone_recovery[$zone_num]->{'source'}): zone_recovery already pending -- skipping setup";
      return ($zone_recovery[$zone_num]->{'source'}, $zone_num);
   }
   print_log "Speech: Checking room $room(zone=$zone_num): importance=$importance, only_if_occupied=$only_if_occupied";
   print_log "Speech:    name of current virtual source: " . $audio_router->get_virtual_source_name_for_zone($zone_num);
   print_log "Speech:    current source: " . $zones_by_room{$room}->get_source();
   if (($audio_router->get_virtual_source_name_for_zone($zone_num) eq 'v_voice') and ($zones_by_room{$room}->get_source() ne '0') and (not $Save{"mute_$room"})) {
      # Always play if room is already tuned in...
      $source = $audio_router->get_real_source_number_for_zone($zone_num);
      print_log "Speech:    Room $room(zone=$zone_num): already on voice (source=$source)";
      if ($players[$source]->is_paused() or ($importance ne 'debug')) {
         # Check for pause in case I'm playing MP3s through the Misterhouse web-based jukebox
         &check_for_pause($zone_num);
         &set_zone_for_speech($zones_by_room{$room}, $source);
         $zone_ret = $zone_num;
         if ($zones_by_room{$room}->delay_off()) {
            # Reset delay off if it was set
            print_log "Speech: Resetting delay_off for $room";
            $zones_by_room{$room}->delay_off($speech_delayoff);
         }
      } else {
         $source = 0;
      }
   } elsif ($zones_by_room{$room}->get_source() ne '0') {
      if ($importance ne 'debug') {
         # Zone is already on another virtual source... 
         print_log "Speech:    mute=" . $Save{"mute_$room"};
         if (($importance eq 'urgent') or (not $Save{"mute_$room"})) {
            my $vsource = $audio_router->get_virtual_source_obj_for_zone($zone_num);
            if ($vsource and $vsource->get_data('playlist')) {
               # Source is playing MP3s... 
               print_log "Speech:    vsource=$$vsource{name}";
               $source = &check_for_pause($zone_num);
               $zone_ret = $zone_num;
               &set_zone_for_speech($zones_by_room{$room}, $source);
               print_log "Speech:    Room $room(zone=$zone_num): listening to MP3s. (source=$source)";
            } elsif (($importance eq 'urgent') or ($importance eq 'important')) {
               # Not tuned to voice or MP3s
               if (not $only_if_occupied or &is_room_occupied($room)) {
                  if ($zones_by_room{$room}->get_source() eq 'E') {
                     $zone_recovery[$zone_num]->{'vsource'} = 'E';
                  } elsif ($zones_by_room{$room}->get_source() eq 'F') {
                     $zone_recovery[$zone_num]->{'vsource'} = 'F';
                  } else {
                     $zone_recovery[$zone_num]->{'vsource'} = $audio_router->get_virtual_source_name_for_zone($zone_num);
                  }
                  print_log "Speech:    vsource=$zone_recovery[$zone_num]->{'vsource'}";
                  $source = $audio_router->select_virtual_source($zone_num, 'v_voice');
                  $zone_ret = $zone_num;
                  $zones_by_room{$room}->set_source($source) if ($source > 0);
                  print_log "Speech:    Room $room(zone=$zone_num): listening to something else, switching to voice source. (source=$source";
                  $zone_recovery[$zone_num]->{'source'} = $source;
                  $zone_recovery[$zone_num]->{'new_source'} = $source;
                  $zone_recovery[$zone_num]->{'zone_obj'} = $zones_by_room{$room};
                  &set_zone_for_speech($zones_by_room{$room}, $source);
               }
            }
         } else {
            # Make sure zone is not on to speech for any reason
            if ($audio_router->get_virtual_source_name_for_zone($zone_num) eq 'v_voice') {
               $zones_by_room{$room}->turn_off();
            }
         }
      }
   } else {
      # Zone is off... turn on
      if (not $only_if_occupied or &is_room_occupied($room)) {
         if (($importance eq 'urgent') or (not $Save{"mute_$room"})) {
            $source = $audio_router->select_virtual_source($zone_num, 'v_voice');
            if ($source > 0) {
               $zone_ret = $zone_num;
               print_log "Speech:    Room $room(zone=$zone_num): turning on to source $source";
               $zones_by_room{$room}->set_source($source);
               $zone_recovery[$zone_num]->{'source'} = $source;
               $zone_recovery[$zone_num]->{'new_source'} = $source;
               $zone_recovery[$zone_num]->{'zone_obj'} = $zones_by_room{$room};
               &set_zone_for_speech($zones_by_room{$room}, $source);
               print_log "Speech: Setting delay_off for $room";
               $zones_by_room{$room}->delay_off($speech_delayoff);
            }
         }
      }
   }
   print_log "Speech:    returning source $source for room $room(zone=$zone_ret)";
   return ($source, $zone_ret);
}

sub pre_speak_hook {
   my ($parms) = @_;
   print_log "Speech: pre_speak_hook importance=$$parms{importance}, rooms=[$$parms{rooms}], text=[$$parms{text}]";
   my $only_if_occupied = 1;
   my $importance = $$parms{'importance'};
   unless ($importance) {
      $importance = 'debug';
   }
   unless ($$parms{'restrictions'} and $$parms{'restrictions'} eq 'no') {
      foreach (@speech_restrictions) {
         if ($_->{'importance'}) {
            next unless ($importance_levels{$importance} <= $importance_levels{$_->{'importance'}});
         }
         if (($_->{'disallow'}) and ($_->{'object'}->state() eq $_->{'disallow'})) {
            print_log "Speech: not speaking because $_->{'object'}->{'object_name'} equals $_->{'disallow'}";
            $parms->{'no_speak'} = 1;
            return;
         }
         if (($_->{'allow'}) and ($_->{'object'}->state() ne $_->{'allow'})) {
            print_log "Speech: not speaking because $_->{'object'}->{'object_name'} does not equal $_->{'allow'}";
            $parms->{'no_speak'} = 1;
            return;
         }
      }
   }
   # Determine in which rooms we want to speak
   my @rooms = @all_rooms;
   if ($$parms{'rooms'} and ($$parms{'rooms'} ne 'all')) {
      $only_if_occupied = 0;
      @rooms = split /,/, $$parms{'rooms'};
   }
   if ($$parms{'zone_objs'} and ref $$parms{'zone_objs'} eq 'ARRAY') {
      @rooms = ();
      OUTER: foreach my $obj (@{$$parms{'zone_objs'}}) {
         foreach (keys %zones_by_room) {
            if ($zones_by_room{$_} eq $obj) {
               push @rooms, $_;
               next OUTER;
            }
         }
      }
   }
   if ($importance eq 'urgent') {
      $only_if_occupied = 0;
   }
   # Now, check each room and store the whole-house audio system
   # source to be used for each room
   my ($source, $zone);
   my %use_players;
   my %use_zones;
   foreach my $room (@rooms) {
      foreach (@always_only_if_occupied) {
         if ($room eq $_) {
            $only_if_occupied = 1;
         }
      }
      ($source, $zone) = &pre_handle_room($room, $importance, $only_if_occupied);
      if ($source > 0) {
         print_log "Speech: got source $source, zone $zone for room $room";
         $use_players{$source}++;
         $use_zones{$zone}++;
      }
   }
   # Now generate string of zones that need to listen
   my $zone_str = '';
   foreach (keys %use_zones) {
      if ($use_zones{$_}) {
         $zone_str .= ",$_";
      }
   }
   $zone_str =~ s/^,//;
   print_log "Speech: zone string is: $zone_str";
   # Now generate string of sources that need to be spoken to
   my $player_str = '';
   foreach (keys %use_players) {
      if ($use_players{$_}) {
         $player_str .= ",$_";
      }
   }
   $player_str =~ s/^,//;
   print_log "Speech: player string is: $player_str";
   if ($player_str and $zone_str) {
      $SpeechCount++;
      $parms->{'speech_count'} = $SpeechCount;
      $parms->{'to_file'} = "$config_parms{data_dir}/cache/speak_festival.${Hour}.${Minute}.${Second}.${SpeechCount}.wav";
      $parms->{'use_players'} = $player_str;
      $parms->{'use_zones'} = $zone_str;
      foreach (keys %use_zones) {
         if ($zone_recovery[$_]) {
            print_log "Speech: adding speech event $SpeechCount to list for zone $_";
            push @{$zone_recovery[$_]->{'events'}}, $SpeechCount;
         }
      }
   } else {
      $parms->{'no_speak'} = 1;
   }
}

sub post_speak_hook {
   my(%parms) = @_;
   my $ptr;
   unless (-f $parms{'to_file'}) {
      print_log "Speech: event $parms{'speech_count'} going to wait for the file to be there";
      $ptr->{'pending'}->{'file'} = $parms{'to_file'};
   }
   foreach (split /,/, $parms{'use_zones'}) {
      if ($zone_recovery[$_] and $zone_recovery[$_]->{'zone_obj'}) {
         my $zone_obj = $zone_recovery[$_]->{'zone_obj'};
         if ($zone_recovery[$_]->{'volume'}) {
            # volume was changed, see if that is done yet
            unless ($zone_obj->get_volume() == $speech_volume{$zone_obj}) {
               print_log "Speech: event $parms{'speech_count'} going to wait for volume to be set";
               $ptr->{'pending'}->{"zone$_"}->{'zone_obj'} = $zone_obj;
               $ptr->{'pending'}->{"zone$_"}->{'volume'} = $speech_volume{$zone_obj};
            }
         }
         if ($zone_recovery[$_]->{'new_source'}) {
            # source was changed, see if that is done yet
            unless ($zone_obj->get_source() == $zone_recovery[$_]->{'new_source'}) {
               print_log "Speech: event $parms{'speech_count'} going to wait for source to be set";
               $ptr->{'pending'}->{"zone$_"}->{'zone_obj'} = $zone_obj;
               $ptr->{'pending'}->{"zone$_"}->{'source'} = $zone_recovery[$_]->{'new_source'};
            }
         }
      }
   }
   if ($ptr) {
      $ptr->{'parms'} = \%parms;
      $ptr->{'time'} = $::Time;
      push @pending_speech, $ptr;
   } else {
      # Nothing pending -- do speech now
      &do_actual_speech(\%parms);
   }
}

sub do_actual_speech {
   my($parms) = @_;
   print_log "Speech: $parms->{'speech_count'} actually speaking";
   foreach (split /,/, $parms->{'use_zones'}) {
      if ($zone_recovery[$_] and $zone_recovery[$_]->{'events'}) {
         for (my $i = 0; $i <= $#{@{$zone_recovery[$_]->{'events'}}}; $i++) {
            if ($zone_recovery[$_]->{'events'}->[$i] eq $parms->{'speech_count'}) {
               splice @{$zone_recovery[$_]->{'events'}}, $i, 1;
               last;
            }
         }
      }
   }
   if ($tivo) {
      $tivo->display($parms->{'text'});
   }
   foreach (split /,/, $parms->{'use_players'}) {
      if ($pre_speak_wav) {
         &play_audio($_, $pre_speak_wav, 1);
      }
      &play_audio($_, $parms->{'to_file'});
   }
}

# noloop=stop
#####################################################################

for (my $i = 0; $i <= $#{@pending_speech}; $i++) {
   if ($pending_speech[$i]->{'pending'}->{'file'}) {
      if (-f $pending_speech[$i]->{'pending'}->{'file'}) {
         print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'} found file";
         delete $pending_speech[$i]->{'pending'}->{'file'};
      }
   }
   foreach my $zone (keys %{$pending_speech[$i]->{'pending'}}) {
      if ($zone =~ /^zone/) {
         if ($pending_speech[$i]->{'pending'}->{$zone}->{'bass'}) {
            if ($pending_speech[$i]->{'pending'}->{$zone}->{'zone_obj'}->get_bass_level() == $pending_speech[$i]->{'pending'}->{$zone}->{'bass'}) {
               print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'} bass is set";
               delete $pending_speech[$i]->{'pending'}->{$zone}->{'bass'};
            }
         }
         if ($pending_speech[$i]->{'pending'}->{$zone}->{'volume'}) {
            if ($pending_speech[$i]->{'pending'}->{$zone}->{'zone_obj'}->get_volume() == $pending_speech[$i]->{'pending'}->{$zone}->{'volume'}) {
               print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'} volume is set";
               delete $pending_speech[$i]->{'pending'}->{$zone}->{'volume'};
            }
         }
         if ($pending_speech[$i]->{'pending'}->{$zone}->{'source'}) {
            if ($pending_speech[$i]->{'pending'}->{$zone}->{'zone_obj'}->get_source() == $pending_speech[$i]->{'pending'}->{$zone}->{'source'}) {
               print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'} source is set";
               delete $pending_speech[$i]->{'pending'}->{$zone}->{'source'};
            }
         }
         my @left = keys %{$pending_speech[$i]->{'pending'}->{$zone}};
         if ($#left == 0) {
            print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'}: $zone preparation is complete";
            delete $pending_speech[$i]->{'pending'}->{$zone};
         } else {
            print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'}: $#left keys left for $zone: @left";
         }
      }
   }
   if (($pending_speech[$i]->{'time'} + $max_speak_wait) < $::Time) {
      print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'}: time has expired -- speaking";
      &do_actual_speech($pending_speech[$i]->{'parms'});
      splice @pending_speech, $i, 1;
   } elsif (keys %{$pending_speech[$i]->{'pending'}}) {
      my @left = keys %{$pending_speech[$i]->{'pending'}};
      print_log "Speech: event $pending_speech[$i]->{'parms'}->{'speech_count'}: $#left keys left: @left";
   } else {
      &do_actual_speech($pending_speech[$i]->{'parms'});
      splice @pending_speech, $i, 1;
   }
}

if ($New_Hour) {
   system('find', "$config_parms{data_dir}/cache", '-mmin', '+5', '-exec', 'rm', '-f', '{}', ';');
}

if ($Reload) {
   &Speak_parms_add_hook(\&pre_speak_hook);
   &Speak_post_add_hook(\&post_speak_hook);
   &Play_parms_add_hook(\&pre_play_hook);
}

# Watch for completion...
for (my $source = 1; $source <= $#wav_players; $source++) {
   if ($wav_players[$source]->done_now()) {
      print_log "Speech:    speech to source $source is complete (resume_mp3s=$resume_mp3s[$source]).";
      for (my $zone_num = 1; $zone_num <= $#zone_recovery; $zone_num++) {
         my $recover = 1;
         next unless $zone_recovery[$zone_num];
         if ($zone_recovery[$zone_num] and $zone_recovery[$zone_num]->{'events'}) {
            if (@{$zone_recovery[$zone_num]->{'events'}}) {
               # Still pending speech events, don't recover yet
               print_log "Speech:    Not recovering zone $zone_num because of pending speech events: @{$zone_recovery[$zone_num]->{'events'}}).";
               $recover = 0;
            }
         }
         if ($recover) {
            if ($zone_recovery[$zone_num]->{'source'} == $source) {
               print_log "Speech: Zone $zone_num: Zone recovery in progress";
               if ($zone_recovery[$zone_num]->{'volume'}) {
                  print_log "Speech:    returning zone $zone_num to volume $zone_recovery[$zone_num]->{'volume'}";
                  $zone_recovery[$zone_num]->{'zone_obj'}->set_volume($zone_recovery[$zone_num]->{'volume'});
               }
               if ($zone_recovery[$zone_num]->{'bass'}) {
                  print_log "Speech:    returning zone $zone_num to bass level $zone_recovery[$zone_num]->{'bass'}";
                  $zone_recovery[$zone_num]->{'zone_obj'}->set_bass($zone_recovery[$zone_num]->{'bass'});
               }
               if ($zone_recovery[$zone_num]->{'vsource'}) {
                  print_log "Speech:    returning zone $zone_num to virtual source $zone_recovery[$zone_num]->{'vsource'}";
                  if ($zone_recovery[$zone_num]->{'vsource'} eq 'E') {
                     $zone_recovery[$zone_num]->{'zone_obj'}->set_source('E');
                  } elsif ($zone_recovery[$zone_num]->{'vsource'} eq 'F') {
                     $zone_recovery[$zone_num]->{'zone_obj'}->set_source('F');
                  } else {
                     my $newsource = $audio_router->select_virtual_source($zone_num, $zone_recovery[$zone_num]->{'vsource'});
                     $zone_recovery[$zone_num]->{'zone_obj'}->set_source($newsource) if ($newsource > 0);
                  }
               }
               $zone_recovery[$zone_num] = undef;
            }
         }
      }
      if ($resume_mp3s[$source]) {
         my $resume = 1;
         foreach my $pending (@pending_speech) {
            foreach my $zone (keys %{$pending->{'pending'}}) {
               if ($pending->{'pending'}->{$zone}->{'source'} == $source) {
                  $resume = 0;
               }
            }
         }
         if ($resume) {
            if ($pause_mp3s) {
               print_log "Speech:    Unpausing MP3s on source $source";
               $players[$source]->unpause();
            } else {
               print_log "Speech:    Increasing volume of MP3s on source $source";
               $players[$source]->volume('1.0');
            }
            $resume_mp3s[$source] = 0;
         }
      }
   }
}

if ($state = state_changed $mode_guest) {
   if ($state eq 'disabled') {
      $Save{'mute_guest_room'} = 0;
   } else {
      $Save{'mute_guest_room'} = 1;
   }
}

$say_urgent = new Voice_Cmd 'Say Something Urgent';
if (said $say_urgent) {
   print_log "Speech: Saying something urgent";
   speak(rooms => 'all', importance => 'urgent', text => 'This is an urgent message');
}

$say_important = new Voice_Cmd 'Say Something Important';
if (said $say_important) {
   print_log "Speech: Saying something important";
   speak(rooms => 'all', importance => 'important', text => 'This is an important message');
}

$say_notice = new Voice_Cmd 'Say Something Possibly Worth Mentioning';
if (said $say_notice) {
   print_log "Speech: Saying something at 'notice' level";
   speak(rooms => 'all', importance => 'notice', text => 'This may be an interesting message');
}

$say_debug = new Voice_Cmd 'Say Something Not Worth Mentioning';
if (said $say_debug) {
   print_log "Speech: Saying something at 'debug' level";
   speak(rooms => 'all', importance => 'debug', text => 'This is probably not an interesting message');
}

$say_several = new Voice_Cmd 'Say Several Things';
if (said $say_several) {
   speak(rooms => 'all', importance => 'important', text => 'this is the first message');
   speak(rooms => 'all', importance => 'important', text => 'this is the second message');
   speak(rooms => 'all', importance => 'important', text => 'this is the third message');
   speak(rooms => 'all', importance => 'important', text => 'this is the fourth message');
}

$say_short = new Voice_Cmd 'Say Something Short';
if (said $say_short) {
   print_log "Speech: Saying something short at 'debug' level";
   speak(rooms => 'all', importance => 'debug', text => 'Short');
}

$say_long = new Voice_Cmd 'Say Something Long';
if (said $say_long) {
   print_log "Speech: Saying something long at 'debug' level";
   speak(rooms => 'all', importance => 'debug', text => 'This is a particularly long message that should take quite a long time to fully synthesize and speak.');
}

$v_enable_tivo_skip = new Voice_Cmd 'Enable Tivo Skip';
if (said $v_enable_tivo_skip) {
   $tivo->_send_cmd("SKIP");
}

$reset_musica = new Voice_Cmd 'Reset Musica';
if (said $reset_musica) {
   &reset_musica();
}

$turn_musica_off = new Voice_Cmd 'Musica Off';
if (said $turn_musica_off) {
   set $musica_switch OFF;
}

$turn_musica_on = new Voice_Cmd 'Musica On';
if (said $turn_musica_on) {
   set $musica_switch ON;
}
