tef-animation 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,9 +4,22 @@ require_relative 'ProgramID.rb'
4
4
 
5
5
  module TEF
6
6
  module ProgramSelection
7
+ # Sheet Sequence collection
8
+ #
9
+ # This class is meant as a convenient container for
10
+ # {Sequencing::Sheet}s. It automatically registers
11
+ # used {ID}s with the used {Selector}, and is also
12
+ # responsible for easily registering {ProgramSheet}s.
7
13
  class SequenceCollection
14
+ # @return [Hash<ID, Hash>] Options to pass to
15
+ # specific sheets when instantiating them.
16
+ # Useful when re-using a {Sequencing::Sheet} for
17
+ # different programs.
8
18
  attr_reader :sheet_opts
9
19
 
20
+ # @return [SequenceCollection] Last Collection that
21
+ # was instantiated. Used by {ProgramSelection} to
22
+ # register itself.
10
23
  def self.current_collection
11
24
  @current_collection
12
25
  end
@@ -14,6 +27,26 @@ module TEF
14
27
  @current_collection = n_collection
15
28
  end
16
29
 
30
+ # Initialize a collection.
31
+ #
32
+ # The passed {Selector} is used to register Sheet {ID}s, while
33
+ # the passed {Sequencing::Player} is used to start
34
+ # playing Sheets when using {#play}
35
+ #
36
+ # Will set {SequenceCollection#current_collection} to self
37
+ #
38
+ # @example
39
+ # sheet_collection = SequenceCollection.new(programs, player);
40
+ #
41
+ # ProgramSheet.new() do |s|
42
+ # s.add_key 'hello', ['portal', 'turret']
43
+ #
44
+ # # Write sheet contents here.
45
+ # # The ProgramSheet will self-register to the
46
+ # # created sheet collection.
47
+ # end
48
+ #
49
+ # sheet_collection.play(programs.fetch_string('hello'));
17
50
  def initialize(program_selector, sequence_runner)
18
51
  @program_selector = program_selector
19
52
  @sequence_runner = sequence_runner
@@ -24,14 +57,36 @@ module TEF
24
57
  self.class.current_collection = self
25
58
  end
26
59
 
60
+ # @return [nil, Sequencing::Sheet, ProgramSheet] Sheet matching
61
+ # the given {ID}, or nil.
27
62
  def [](key)
28
63
  @known_programs[key]
29
64
  end
65
+
66
+ # Register a given program.
67
+ #
68
+ # @param [ID] key {ID} to register the program under.
69
+ # Will be registered with the {Selector} passed to the
70
+ # constructor.
71
+ # @param [Sequencing::Sheet, ProgramSheet] n_program New program
72
+ # to register.
30
73
  def []=(key, n_program)
31
74
  key = @program_selector.register_ID key
32
75
  @known_programs[key] = n_program
33
76
  end
34
77
 
78
+ # Start playing the {Sequencing::Sheet} matching the given {ID}
79
+ #
80
+ # This function will instantiate a new {Sequencing::SheetSequence} if
81
+ # the registered program was a {ProgramSheet}. It will pass the
82
+ # {#sheet_opts} matching its ID to the SheetSequence, and will
83
+ # additionally set the option's :program_key value to the {ID} used
84
+ # here.
85
+ # This allows easy re-using of sheets by passing parameters via
86
+ # the sheet options.
87
+ #
88
+ # The created sheet is then passed to {Sequencing::Player#[]=}, using
89
+ # either {ProgramSheet#program_key} or 'default' as key.
35
90
  def play(key)
36
91
  prog = @known_programs[key]
37
92
  return unless prog
@@ -51,7 +106,14 @@ module TEF
51
106
  end
52
107
  end
53
108
 
109
+ # Convenience class.
110
+ # Mainly extends {Sequencing::Sheet} with an {#add_key} function, which
111
+ # self-registers this program under the last created {SequenceCollection}.
54
112
  class ProgramSheet < TEF::Sequencing::Sheet
113
+
114
+ # Optional key to use when passing to {Sequencing::Player#[]=}.
115
+ # Different keys are necessary to not overwrite the previous running
116
+ # program.
55
117
  attr_accessor :program_key
56
118
 
57
119
  def initialize()
@@ -59,16 +121,20 @@ module TEF
59
121
 
60
122
  yield(self) if block_given?
61
123
  end
62
-
63
- ## TODO Give option to add multiple keys with options-hash
64
-
65
- def add_key(title, groups = [], variation = '.mp3')
124
+
125
+ # Register this sheet under a given key.
126
+ # Syntax is the same as {Selector#register_ID}, with a default
127
+ # variant of '.mp3' to comply with the default variant set by
128
+ # {SoundCollection}.
129
+ def add_key(title, groups = [], variation = '.mp3', options = nil)
66
130
  prog_collection = SequenceCollection.current_collection
67
131
  raise "No program collection was instantiated yet!" unless prog_collection
68
132
 
69
133
  id = ID.new(title, groups, variation)
70
134
 
71
135
  prog_collection[id] = self
136
+
137
+ prog_collection.sheet_opts[id] = options if options.is_a? Hash
72
138
  end
73
139
  end
74
140
  end
@@ -5,11 +5,59 @@ require 'yaml'
5
5
 
6
6
  module TEF
7
7
  module ProgramSelection
8
+
9
+ # Sound-File collection.
10
+ #
11
+ # This class is meant as convenient container for sound-filenames.
12
+ # It will automatically scan the current directory for any file ending
13
+ # with '.mp3', '.ogg' or '.wav', and will construct a unique {ID} for
14
+ # it based on the filepath.
15
+ # Paths can then be retrieved by using {#soundmap}.
16
+ #
17
+ # Additionally, it keeps a list of silences, the {#silence_maps}. They
18
+ # are auto-generated lists of silences or loud sections of the file,
19
+ # useful for auto-generating programs.
20
+ # They can be retrieved via {#silence_maps}
21
+ #
22
+ # Note that to play a sound, this should be done via
23
+ # {Sequencing::SheetSequence#play}, to ensure that the sound is killed
24
+ # in synch with the program.
8
25
  class SoundCollection
26
+ # @return [Hash<String, Hash<Numeric, Numeric>>] List of noise levels
27
+ # for a given file-path. Is in the format { timestamp (in s) => 0 to 1 }
28
+ # It is ensured that a 0 is inserted at the beginning and end of the
29
+ # sound track, and it is ensured that the hash keys are sorted.
30
+ #
31
+ # @see #silences_for
9
32
  attr_reader :silence_maps
33
+
34
+ # @return [Hash<ID, String>] Map of {ID}s to matching file-paths
10
35
  attr_reader :soundmap
36
+
37
+ # @return [Hash] Config loaded from './sounds/soundconfig.yml'
11
38
  attr_reader :load_config
12
39
 
40
+ # Initialize a SoundCollection
41
+ #
42
+ # This will scan the current directory for any sound files. Found
43
+ # files will auto-generate a ID based on their path, and are
44
+ # registered with the passed {Selector}.
45
+ #
46
+ # Paths are deconstructed as follows:
47
+ # - The path is split along any '/' or '-'. Each element up to
48
+ # the last is taken as a group. The last element in the list
49
+ # is taken as title.
50
+ # - The variant is generated by taking the sequence (-\d+)?\.(mp3|ogg|wav)
51
+ # from the end. This means that variants can be specified by appending
52
+ # a '-1234' to the title.
53
+ #
54
+ # './sounds/portal/announcer-hello-4.mp3' is registered as:
55
+ # {Selector#register_ID}('hello', ['sounds', 'portal', 'announcer'], '-4.mp3');
56
+ #
57
+ # Also note that a custom config file, {#load_config}, is loaded from
58
+ # a YAML file './sounds/soundconfig.yml', if present.
59
+ # Additionally, the {#silence_maps} are loaded from, and saved to,
60
+ # './sounds/silence_maps.yml'
13
61
  def initialize(program_handler)
14
62
  @handler = program_handler
15
63
  @soundmap = {}
@@ -28,11 +76,11 @@ module TEF
28
76
  `find ./`.split("\n").each { |fn| add_file fn };
29
77
 
30
78
  File.write('./sounds/silence_maps.yml', YAML.dump(@silence_maps));
31
-
32
- @play_pids = {};
33
79
  end
34
80
 
35
- def generate_silences(fname)
81
+ # Internal function. Generates a list
82
+ # of silences/loud sections with ffmpeg.
83
+ private def generate_silences(fname)
36
84
  return if @silence_maps[fname]
37
85
 
38
86
  ffmpeg_str = `ffmpeg -i #{fname} -af silencedetect=n=0.1:d=0.1 -f null - 2>&1`
@@ -65,6 +113,10 @@ module TEF
65
113
  @silence_maps[fname] = out_event
66
114
  end
67
115
 
116
+ # Add a file to the collection of files.
117
+ #
118
+ # Will auto-generate silences and a matching {ID} as described
119
+ # in {#initialize}.
68
120
  def add_file(fname)
69
121
  rMatch = /^\.\/sounds\/(?<groups>(?:[a-z_]+[\/-])*)(?<title>[a-z_]+)(?<variant>(?:-\d+)?\.(?:ogg|mp3|wav))/.match fname;
70
122
  return unless rMatch;
@@ -81,28 +133,13 @@ module TEF
81
133
  generate_silences fname
82
134
  end
83
135
 
136
+ # @return [Hash<Numeric, Numeric>, nil] The silence map for
137
+ # the passed {ID}, or nil if none was found.
138
+ #
139
+ # @see #silence_maps
84
140
  def silences_for(key)
85
141
  @silence_maps[@soundmap[key]]
86
142
  end
87
-
88
- def play(id, collection_id = 'default')
89
- sound_name = @soundmap[id]
90
- return if sound_name.nil?
91
-
92
- collection_id ||= id;
93
-
94
- if old_pid = @play_pids[collection_id]
95
- Process.kill('QUIT', old_pid)
96
- end
97
-
98
- Thread.new do
99
- fork_pid = spawn(*%W{play -q --volume 0.3 #{sound_name}});
100
-
101
- @play_pids[collection_id] = fork_pid;
102
- Process.waitpid fork_pid;
103
- @play_pids.delete collection_id
104
- end
105
- end
106
143
  end
107
144
  end
108
145
  end
@@ -1,17 +1,50 @@
1
1
 
2
2
  require_relative 'EventCollector.rb'
3
3
 
4
+ # TheElectricFursuits module.
5
+ # @see https://github.com/TheElectricFursuits
4
6
  module TEF
5
7
  module Sequencing
8
+ # Base sequence class.
9
+ #
10
+ # It implements the minium necessary tools to make different Sequences
11
+ # work together nicely. It's main function is to provide {#append_events},
12
+ # which is used by a {Player} to fetch the next queued event for execution.
6
13
  class BaseSequence
14
+ # @return [Numeric] Start time of this sequence, in local time.
15
+ # Specifies when to call {#setup}
7
16
  attr_reader :start_time
17
+ # @return [Numeric, nil] End time of this sequence, in local time.
18
+ # Specifies when to call {#teardown}. Can be left as nil for no
19
+ # automatic teardown.
8
20
  attr_reader :end_time
9
21
 
22
+ # @return [Numeric, Time] The offset to apply to this Sequence,
23
+ # used when converting between local-time and parent-time.
24
+ #
25
+ # This MAY be modified during runtime, though this may cause some
26
+ # events to be skipped!
10
27
  attr_reader :offset
28
+ # @return [Numeric] Slope to apply to this sequence.
29
+ # @see #offset
11
30
  attr_reader :slope
12
31
 
32
+ # @return [Symbol] State of this Sequence. Mainly used for internal
33
+ # purposes. Can be:
34
+ # - :uninitialized (right after construction)
35
+ # - :running (after having called setup())
36
+ # - :torn_down (after teardown() was called)
13
37
  attr_reader :state
14
38
 
39
+ # Initialize a BaseSequence.
40
+ #
41
+ # @param [Numeric, Time] offset Provide a offset for time-conversion.
42
+ # @see #offset
43
+ # @param [Numeric] slope Provide a slope for time-conversion.
44
+ # @see #slope
45
+ #
46
+ # @param [Numeric] :start_time Local time to begin playing at.
47
+ # @param [Numeric] :end_time Local time to tear down at.
15
48
  def initialize(offset, slope, **options)
16
49
  @start_time ||= options[:start_time] || 0;
17
50
  @end_time ||= options[:end_time];
@@ -40,6 +73,15 @@ module TEF
40
73
  @opts_hash = nil;
41
74
  end
42
75
 
76
+ # Look for the next possible event that this sequence wants to
77
+ # execute.
78
+ # Will ensure that this sequence's {#setup} and {#teardown} blocks
79
+ # are called at the appropriate time.
80
+ #
81
+ # Should only be called by a {Player} or another sequence!
82
+ #
83
+ # @note When using BaseSequence as base class, the user
84
+ # shall overload {#overload_append_events} rather than this function!
43
85
  def append_events(collector)
44
86
  local_collector = collector.offset_collector(@offset, @slope);
45
87
 
@@ -49,7 +91,7 @@ module TEF
49
91
 
50
92
  if @state == :uninitialized
51
93
  local_collector.add_event({
52
- time: @start_time,
94
+ time: [@start_time, local_collector.start_time + 0.01].max,
53
95
  code: proc { self.setup() }
54
96
  });
55
97
  end
@@ -61,7 +103,7 @@ module TEF
61
103
  if !@end_time.nil?
62
104
  local_collector.add_event({
63
105
  time: @end_time,
64
- code: proc { self.teardown }
106
+ code: proc { self.teardown() }
65
107
  })
66
108
  end
67
109
  end
@@ -1,43 +1,83 @@
1
1
 
2
2
  require 'xasin_logger'
3
3
 
4
+ # TheElectricFursuits module.
5
+ # @see https://github.com/TheElectricFursuits
4
6
  module TEF
7
+ # Program Sequencing module.
8
+ #
9
+ # This module contains all components necessary to define how and when
10
+ # a animation or program will execute. It provides a base class
11
+ # to define how execution information is passed along between code, as well
12
+ # as a more user friendly interface to define a fixed sequence of events,
13
+ # a so called {Sheet}.
14
+ #
15
+ # {Sheet}s are designed to be as reuseable as possible,
16
+ # with dynamic creation to let the same sheet be adapted to different
17
+ # situations, as well as Sheet and Event nesting that makes it easy to
18
+ # re-use other {Sheet}s, for example to define segments of a song or
19
+ # generic beat-segments.
5
20
  module Sequencing
21
+ # Purely internal class
22
+ #
23
+ # Used by {BaseSequence} when fetching the next event from child
24
+ # sequences. It wraps {EventCollector} and automatically applies
25
+ # a sequence's offset and slope, converting between the timeframe
26
+ # of the parent and the child.
27
+ #
28
+ # This collector is created in the child, within {BaseSequence#append_events}
6
29
  class OffsetCollector
30
+ # @return [EventCollector] Top-Level collector
7
31
  attr_reader :parent
8
32
 
33
+ # @return [Time] Offset of the conversion. Used as follows:
34
+ # local_time = (Time.at(x) - total_offset) * total_slope
9
35
  attr_reader :total_offset
36
+ # @return [Numeric] Slope of the time conversion.
37
+ # @see #total_offset
10
38
  attr_reader :total_slope
11
39
 
40
+ # Initialize a new offset collector.
41
+ # This should only be done via {EventCollector#offset_collector}!
12
42
  def initialize(parent, total_offset, total_slope)
13
43
  @parent = parent
14
44
  @total_offset = total_offset
15
45
  @total_slope = total_slope
16
46
  end
17
47
 
48
+ # @param [Time, nil] global_time Time to convert
49
+ # @return [Numeric, nil] Converted time
18
50
  def convert_to_local(global_time)
19
51
  return nil if global_time.nil?
20
52
 
21
53
  (global_time - @total_offset) * @total_slope
22
54
  end
55
+
56
+ # @param [Numeric, nil] local_time Time (abstract) to convert back
57
+ # into the global frame.
58
+ # @return [Time, nil] Time (as Time object) of the event
23
59
  def convert_to_global(local_time)
24
60
  return nil if local_time.nil?
25
61
 
26
62
  @total_offset + (local_time.to_f / @total_slope)
27
63
  end
28
64
 
65
+ # (see EventCollector#start_time)
29
66
  def start_time
30
67
  convert_to_local @parent.start_time
31
68
  end
32
69
 
70
+ # (see EventCollector#event_time)
33
71
  def event_time
34
72
  convert_to_local @parent.event_time
35
73
  end
36
74
 
75
+ # (see EventCollector#has_events?)
37
76
  def has_events?
38
77
  return @parent.has_events?
39
78
  end
40
79
 
80
+ # (see EventCollector#add_event)
41
81
  def add_event(event)
42
82
  event = event.clone
43
83
 
@@ -46,21 +86,36 @@ module TEF
46
86
  @parent.add_event event
47
87
  end
48
88
 
89
+ # (see EventCollector#add_events)
49
90
  def add_events(list)
50
91
  list.each { |event| add_event event }
51
92
  end
52
93
 
94
+ # (see EventCollector#offset_collector)
53
95
  def offset_collector(offset, slope)
54
96
  OffsetCollector.new(@parent, convert_to_global(offset), @total_slope * slope)
55
97
  end
56
98
  end
57
99
 
100
+ # Event Collector class
101
+ #
102
+ # This class provides the means to efficiently fetch the next event
103
+ # from a list of {BaseSequence}s, and is mainly meant for
104
+ # internal purposes. It is created by {Player}
58
105
  class EventCollector
59
106
  include XasLogger::Mix
60
107
 
108
+ # @return [Time] The Time to start looking for an event.
109
+ # Any event earlier than this will be discarded!
61
110
  attr_accessor :start_time
111
+
112
+ # @return [nil, Time] The Time of the current event, or nil if
113
+ # there is no event. Any event later than this will be
114
+ # discarded. Any event equal to this time will be appended to
115
+ # {#current_events}
62
116
  attr_reader :event_time
63
117
 
118
+ # @return [Array<Hash>] List of current events to execute.
64
119
  attr_reader :current_events
65
120
 
66
121
  def initialize()
@@ -71,6 +126,12 @@ module TEF
71
126
  init_x_log("Sequence Player")
72
127
  end
73
128
 
129
+ # Internal function to add an event.
130
+ # The event will be discarded if it is earlier than or equal to
131
+ # start time, or later than the event time.
132
+ # It if it is earlier than event time it will set the new event time
133
+ # and set the event list to [event], else append the event
134
+ # to the event list.
74
135
  def add_event(event)
75
136
  return if event[:time] <= @start_time
76
137
  return if (!@event_time.nil?) && (event[:time] > @event_time)
@@ -83,10 +144,14 @@ module TEF
83
144
  end
84
145
  end
85
146
 
147
+ # @return [true, false] Were any events found?
86
148
  def has_events?
87
149
  !@current_events.empty?
88
150
  end
89
151
 
152
+ # This function will try to wait until {#event_time}.
153
+ # If no event was found it will return immediately, and Thread.run()
154
+ # can be used to prematurely end the wait.
90
155
  def wait_until_event
91
156
  return unless has_events?
92
157
 
@@ -101,6 +166,8 @@ module TEF
101
166
  sleep t_diff if t_diff > 0
102
167
  end
103
168
 
169
+ # Wait until the next event and then execute
170
+ # the code of each event.
104
171
  def execute!
105
172
  return unless has_events?
106
173
 
@@ -114,11 +181,18 @@ module TEF
114
181
  restart();
115
182
  end
116
183
 
184
+ # Restart this collector.
185
+ # Will clear {#current_events} and {#event_time}
117
186
  def restart()
118
187
  @current_events = []
119
188
  @event_time = nil;
120
189
  end
121
190
 
191
+ # Generate a {OffsetCollector}
192
+ # This is mainly an internal function used by {BaseSequence} to
193
+ # provide an {OffsetCollector}. It converts between the global time-frame
194
+ # used by this collector, and the local timeframes of each
195
+ # sub-sequence.
122
196
  def offset_collector(offset, slope)
123
197
  OffsetCollector.new(self, offset, slope);
124
198
  end