tef-animation 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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