tef-animation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +6 -0
- data/lib/tef/Animation/Animatable.rb +213 -0
- data/lib/tef/Animation/Animation_Handler.rb +180 -0
- data/lib/tef/Animation/Color.rb +112 -0
- data/lib/tef/Animation/Coordinate.rb +49 -0
- data/lib/tef/Animation/Eyes.rb +21 -0
- data/lib/tef/Animation/Value.rb +133 -0
- data/lib/tef/ParameterStack/Override.rb +66 -0
- data/lib/tef/ParameterStack/Stack.rb +149 -0
- data/lib/tef/ProgramSelection/ProgramID.rb +56 -0
- data/lib/tef/ProgramSelection/ProgramSelector.rb +76 -0
- data/lib/tef/ProgramSelection/SequenceCollection.rb +75 -0
- data/lib/tef/ProgramSelection/SoundCollection.rb +108 -0
- data/lib/tef/Sequencing/BaseSequence.rb +72 -0
- data/lib/tef/Sequencing/EventCollector.rb +127 -0
- data/lib/tef/Sequencing/SequencePlayer.rb +91 -0
- data/lib/tef/Sequencing/Sheet.rb +33 -0
- data/lib/tef/Sequencing/SheetSequence.rb +138 -0
- data/lib/tef/animation.rb +11 -0
- metadata +105 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
require_relative 'ProgramID.rb'
|
3
|
+
|
4
|
+
module TEF
|
5
|
+
module ProgramSelection
|
6
|
+
class Selector
|
7
|
+
attr_accessor :group_weights
|
8
|
+
|
9
|
+
def initialize()
|
10
|
+
@known_programs = {} # Hash based on titles
|
11
|
+
@known_groups = {}
|
12
|
+
|
13
|
+
@group_weights = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def register_ID(program, pgroups = [], pvariant = nil)
|
17
|
+
if program.is_a? String
|
18
|
+
program = ID.new(program, pgroups, pvariant)
|
19
|
+
end
|
20
|
+
|
21
|
+
proglist = (@known_programs[program.title] ||= [])
|
22
|
+
|
23
|
+
if found_prog = proglist.find { |prg| prg == program }
|
24
|
+
return found_prog
|
25
|
+
end
|
26
|
+
|
27
|
+
proglist << program
|
28
|
+
|
29
|
+
program.groups.each { |group| @known_groups[group] = true }
|
30
|
+
|
31
|
+
program
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_ID(title, group_weights = {})
|
35
|
+
return nil if @known_programs[title].nil?
|
36
|
+
|
37
|
+
weights = @group_weights.merge(group_weights)
|
38
|
+
|
39
|
+
current_best = nil;
|
40
|
+
current_list = [];
|
41
|
+
|
42
|
+
@known_programs[title].each do |prg|
|
43
|
+
p_score = prg.get_scoring(weights);
|
44
|
+
current_best ||= p_score
|
45
|
+
|
46
|
+
next if current_best > p_score
|
47
|
+
|
48
|
+
if current_best == p_score
|
49
|
+
current_list << prg
|
50
|
+
else
|
51
|
+
current_best = p_score
|
52
|
+
current_list = [prg]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
current_list.sample
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_string(str)
|
60
|
+
title, groups = str.split(" from ");
|
61
|
+
groups ||= "";
|
62
|
+
groups = groups.split(" and ").map { |g| [g, 2] }.to_h
|
63
|
+
|
64
|
+
fetch_ID(title, groups)
|
65
|
+
end
|
66
|
+
|
67
|
+
def all_titles()
|
68
|
+
@known_programs.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
def all_groups()
|
72
|
+
@known_groups.keys
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
require_relative '../Sequencing/SheetSequence.rb'
|
3
|
+
require_relative 'ProgramID.rb'
|
4
|
+
|
5
|
+
module TEF
|
6
|
+
module ProgramSelection
|
7
|
+
class SequenceCollection
|
8
|
+
attr_reader :sheet_opts
|
9
|
+
|
10
|
+
def self.current_collection
|
11
|
+
@current_collection
|
12
|
+
end
|
13
|
+
def self.current_collection=(n_collection)
|
14
|
+
@current_collection = n_collection
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(program_selector, sequence_runner)
|
18
|
+
@program_selector = program_selector
|
19
|
+
@sequence_runner = sequence_runner
|
20
|
+
|
21
|
+
@known_programs = {}
|
22
|
+
@sheet_opts = {}
|
23
|
+
|
24
|
+
self.class.current_collection = self
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
@known_programs[key]
|
29
|
+
end
|
30
|
+
def []=(key, n_program)
|
31
|
+
key = @program_selector.register_ID key
|
32
|
+
@known_programs[key] = n_program
|
33
|
+
end
|
34
|
+
|
35
|
+
def play(key)
|
36
|
+
prog = @known_programs[key]
|
37
|
+
return unless prog
|
38
|
+
|
39
|
+
prog_key = prog.program_key if prog.is_a? ProgramSheet
|
40
|
+
|
41
|
+
if prog.is_a? Sequencing::Sheet
|
42
|
+
opts = @sheet_opts[key] || {}
|
43
|
+
opts[:sheet] = prog
|
44
|
+
opts[:program_key] = key
|
45
|
+
|
46
|
+
prog = Sequencing::SheetSequence.new(Time.now(), 1, **opts)
|
47
|
+
end
|
48
|
+
prog_key ||= 'default'
|
49
|
+
|
50
|
+
@sequence_runner[prog_key] = prog
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ProgramSheet < TEF::Sequencing::Sheet
|
55
|
+
attr_accessor :program_key
|
56
|
+
|
57
|
+
def initialize()
|
58
|
+
super()
|
59
|
+
|
60
|
+
yield(self) if block_given?
|
61
|
+
end
|
62
|
+
|
63
|
+
## TODO Give option to add multiple keys with options-hash
|
64
|
+
|
65
|
+
def add_key(title, groups = [], variation = '.mp3')
|
66
|
+
prog_collection = SequenceCollection.current_collection
|
67
|
+
raise "No program collection was instantiated yet!" unless prog_collection
|
68
|
+
|
69
|
+
id = ID.new(title, groups, variation)
|
70
|
+
|
71
|
+
prog_collection[id] = self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
require_relative 'ProgramSelector.rb'
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module TEF
|
7
|
+
module ProgramSelection
|
8
|
+
class SoundCollection
|
9
|
+
attr_reader :silence_maps
|
10
|
+
attr_reader :soundmap
|
11
|
+
attr_reader :load_config
|
12
|
+
|
13
|
+
def initialize(program_handler)
|
14
|
+
@handler = program_handler
|
15
|
+
@soundmap = {}
|
16
|
+
|
17
|
+
@load_config = {};
|
18
|
+
@silence_maps = {}
|
19
|
+
|
20
|
+
if File.file? './sounds/soundconfig.yml'
|
21
|
+
@load_config = YAML.load File.read './sounds/soundconfig.yml'
|
22
|
+
end
|
23
|
+
|
24
|
+
if File.file? './sounds/silence_maps.yml'
|
25
|
+
@silence_maps = YAML.load File.read './sounds/silence_maps.yml'
|
26
|
+
end
|
27
|
+
|
28
|
+
`find ./`.split("\n").each { |fn| add_file fn };
|
29
|
+
|
30
|
+
File.write('./sounds/silence_maps.yml', YAML.dump(@silence_maps));
|
31
|
+
|
32
|
+
@play_pids = {};
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_silences(fname)
|
36
|
+
return if @silence_maps[fname]
|
37
|
+
|
38
|
+
ffmpeg_str = `ffmpeg -i #{fname} -af silencedetect=n=0.1:d=0.1 -f null - 2>&1`
|
39
|
+
|
40
|
+
out_event = {}
|
41
|
+
|
42
|
+
ffmpeg_str.split("\n").each do |line|
|
43
|
+
if line =~ /silence_start: ([\d\.-]*)/
|
44
|
+
out_event[$1.to_f] = 0
|
45
|
+
elsif line =~ /silence_end: ([\d\.-]*)/
|
46
|
+
out_event[$1.to_f] = 1
|
47
|
+
elsif line =~ /Duration: (\d+):(\d+):([\d\.]+)/
|
48
|
+
out_event[$1.to_f * 3600 + $2.to_f * 60 + $3.to_f] = 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
out_event = out_event.sort.to_h
|
53
|
+
|
54
|
+
if(out_event.empty?)
|
55
|
+
out_event[0.01] = 1
|
56
|
+
elsif (k = out_event.keys[0]) < 0
|
57
|
+
out_event.delete k
|
58
|
+
out_event[0.01] = 0
|
59
|
+
else
|
60
|
+
out_event[0.01] = 1
|
61
|
+
end
|
62
|
+
|
63
|
+
out_event = out_event.sort.to_h
|
64
|
+
|
65
|
+
@silence_maps[fname] = out_event
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_file(fname)
|
69
|
+
rMatch = /^\.\/sounds\/(?<groups>(?:[a-z_]+[\/-])*)(?<title>[a-z_]+)(?<variant>(?:-\d+)?\.(?:ogg|mp3|wav))/.match fname;
|
70
|
+
return unless rMatch;
|
71
|
+
|
72
|
+
title = rMatch[:title].gsub('_', ' ');
|
73
|
+
groups = rMatch[:groups].gsub('_', ' ').gsub('-','/').split('/');
|
74
|
+
|
75
|
+
groups = ["default"] if groups.empty?
|
76
|
+
|
77
|
+
id = @handler.register_ID(title, groups, rMatch[:variant])
|
78
|
+
|
79
|
+
@soundmap[id] = fname
|
80
|
+
|
81
|
+
generate_silences fname
|
82
|
+
end
|
83
|
+
|
84
|
+
def silences_for(key)
|
85
|
+
@silence_maps[@soundmap[key]]
|
86
|
+
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
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
require_relative 'EventCollector.rb'
|
3
|
+
|
4
|
+
module TEF
|
5
|
+
module Sequencing
|
6
|
+
class BaseSequence
|
7
|
+
attr_reader :start_time
|
8
|
+
attr_reader :end_time
|
9
|
+
|
10
|
+
attr_reader :offset
|
11
|
+
attr_reader :slope
|
12
|
+
|
13
|
+
attr_reader :state
|
14
|
+
|
15
|
+
def initialize(offset, slope, **options)
|
16
|
+
@start_time ||= options[:start_time] || 0;
|
17
|
+
@end_time ||= options[:end_time];
|
18
|
+
|
19
|
+
@offset = offset;
|
20
|
+
@slope = slope;
|
21
|
+
|
22
|
+
@state = :uninitialized
|
23
|
+
|
24
|
+
@opts_hash = options;
|
25
|
+
end
|
26
|
+
|
27
|
+
def parent_start_time
|
28
|
+
@offset + @start_time / @slope
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup()
|
32
|
+
raise 'Program had to be uninitialized!' unless @state == :uninitialized
|
33
|
+
@state = :running
|
34
|
+
end
|
35
|
+
|
36
|
+
def teardown()
|
37
|
+
return unless @state == :running
|
38
|
+
@state = :torn_down
|
39
|
+
|
40
|
+
@opts_hash = nil;
|
41
|
+
end
|
42
|
+
|
43
|
+
def append_events(collector)
|
44
|
+
local_collector = collector.offset_collector(@offset, @slope);
|
45
|
+
|
46
|
+
return if local_collector.has_events? &&
|
47
|
+
local_collector.event_time < @start_time
|
48
|
+
return if @state == :torn_down
|
49
|
+
|
50
|
+
if @state == :uninitialized
|
51
|
+
local_collector.add_event({
|
52
|
+
time: @start_time,
|
53
|
+
code: proc { self.setup() }
|
54
|
+
});
|
55
|
+
end
|
56
|
+
|
57
|
+
if @state == :running
|
58
|
+
overload_append_events(local_collector)
|
59
|
+
end
|
60
|
+
|
61
|
+
if !@end_time.nil?
|
62
|
+
local_collector.add_event({
|
63
|
+
time: @end_time,
|
64
|
+
code: proc { self.teardown }
|
65
|
+
})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def overload_append_events(_collector) end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
require 'xasin_logger'
|
3
|
+
|
4
|
+
module TEF
|
5
|
+
module Sequencing
|
6
|
+
class OffsetCollector
|
7
|
+
attr_reader :parent
|
8
|
+
|
9
|
+
attr_reader :total_offset
|
10
|
+
attr_reader :total_slope
|
11
|
+
|
12
|
+
def initialize(parent, total_offset, total_slope)
|
13
|
+
@parent = parent
|
14
|
+
@total_offset = total_offset
|
15
|
+
@total_slope = total_slope
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert_to_local(global_time)
|
19
|
+
return nil if global_time.nil?
|
20
|
+
|
21
|
+
(global_time - @total_offset) * @total_slope
|
22
|
+
end
|
23
|
+
def convert_to_global(local_time)
|
24
|
+
return nil if local_time.nil?
|
25
|
+
|
26
|
+
@total_offset + (local_time.to_f / @total_slope)
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_time
|
30
|
+
convert_to_local @parent.start_time
|
31
|
+
end
|
32
|
+
|
33
|
+
def event_time
|
34
|
+
convert_to_local @parent.event_time
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_events?
|
38
|
+
return @parent.has_events?
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_event(event)
|
42
|
+
event = event.clone
|
43
|
+
|
44
|
+
event[:time] = convert_to_global event[:time]
|
45
|
+
|
46
|
+
@parent.add_event event
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_events(list)
|
50
|
+
list.each { |event| add_event event }
|
51
|
+
end
|
52
|
+
|
53
|
+
def offset_collector(offset, slope)
|
54
|
+
OffsetCollector.new(@parent, convert_to_global(offset), @total_slope * slope)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class EventCollector
|
59
|
+
include XasLogger::Mix
|
60
|
+
|
61
|
+
attr_accessor :start_time
|
62
|
+
attr_reader :event_time
|
63
|
+
|
64
|
+
attr_reader :current_events
|
65
|
+
|
66
|
+
def initialize()
|
67
|
+
@current_events = []
|
68
|
+
@start_time = Time.at(0);
|
69
|
+
@event_time = nil;
|
70
|
+
|
71
|
+
init_x_log("Sequence Player")
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_event(event)
|
75
|
+
return if event[:time] <= @start_time
|
76
|
+
return if (!@event_time.nil?) && (event[:time] > @event_time)
|
77
|
+
|
78
|
+
if (!@event_time.nil?) && (event[:time] == @event_time)
|
79
|
+
@current_events << event
|
80
|
+
else
|
81
|
+
@current_events = [event]
|
82
|
+
@event_time = event[:time]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def has_events?
|
87
|
+
!@current_events.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def wait_until_event
|
91
|
+
return unless has_events?
|
92
|
+
|
93
|
+
t_diff = @event_time - Time.now();
|
94
|
+
|
95
|
+
if t_diff < -0.5
|
96
|
+
x_logf('Sequence long overdue!')
|
97
|
+
elsif t_diff < -0.1
|
98
|
+
x_logw('Sequencing overdue')
|
99
|
+
end
|
100
|
+
|
101
|
+
sleep t_diff if t_diff > 0
|
102
|
+
end
|
103
|
+
|
104
|
+
def execute!
|
105
|
+
return unless has_events?
|
106
|
+
|
107
|
+
wait_until_event
|
108
|
+
|
109
|
+
@current_events.each do |event|
|
110
|
+
event[:code].call()
|
111
|
+
end
|
112
|
+
|
113
|
+
@start_time = @event_time
|
114
|
+
restart();
|
115
|
+
end
|
116
|
+
|
117
|
+
def restart()
|
118
|
+
@current_events = []
|
119
|
+
@event_time = nil;
|
120
|
+
end
|
121
|
+
|
122
|
+
def offset_collector(offset, slope)
|
123
|
+
OffsetCollector.new(self, offset, slope);
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|