wavesync 1.0.0.alpha1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/bin/wavesync +8 -0
- data/config/devices.yml +27 -0
- data/lib/wavesync/acid_chunk.rb +140 -0
- data/lib/wavesync/analyzer.rb +39 -0
- data/lib/wavesync/audio.rb +212 -0
- data/lib/wavesync/audio_format.rb +13 -0
- data/lib/wavesync/bpm_detector.rb +19 -0
- data/lib/wavesync/cli.rb +159 -0
- data/lib/wavesync/config.rb +70 -0
- data/lib/wavesync/device.rb +70 -0
- data/lib/wavesync/file_converter.rb +40 -0
- data/lib/wavesync/path_resolver.rb +55 -0
- data/lib/wavesync/scanner.rb +97 -0
- data/lib/wavesync/set.rb +82 -0
- data/lib/wavesync/set_editor.rb +245 -0
- data/lib/wavesync/track_padding.rb +27 -0
- data/lib/wavesync/ui.rb +139 -0
- data/lib/wavesync/version.rb +5 -0
- data/lib/wavesync.rb +21 -0
- metadata +144 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-prompt'
|
|
4
|
+
require 'io/console'
|
|
5
|
+
|
|
6
|
+
module Wavesync
|
|
7
|
+
class SetEditor
|
|
8
|
+
KEY_MAP = {
|
|
9
|
+
'a' => :add,
|
|
10
|
+
'u' => :move_up,
|
|
11
|
+
'd' => :move_down,
|
|
12
|
+
'r' => :remove,
|
|
13
|
+
's' => :save,
|
|
14
|
+
'c' => :quit,
|
|
15
|
+
' ' => :toggle_play,
|
|
16
|
+
"\e[A" => :cursor_up,
|
|
17
|
+
"\e[B" => :cursor_down
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(set, library_path)
|
|
21
|
+
@set = set
|
|
22
|
+
@library_path = library_path
|
|
23
|
+
@prompt = TTY::Prompt.new(interrupt: :exit, active_color: :red)
|
|
24
|
+
@ui = UI.new
|
|
25
|
+
@selected = @set.tracks.empty? ? nil : 0
|
|
26
|
+
@player_pid = nil
|
|
27
|
+
@player_track = nil
|
|
28
|
+
@player_state = :stopped
|
|
29
|
+
@player_offset = 0
|
|
30
|
+
@player_started_at = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
loop do
|
|
35
|
+
check_player
|
|
36
|
+
render
|
|
37
|
+
action = KEY_MAP[read_key]
|
|
38
|
+
next unless action
|
|
39
|
+
|
|
40
|
+
result = handle_action(action)
|
|
41
|
+
break if result == :quit
|
|
42
|
+
end
|
|
43
|
+
ensure
|
|
44
|
+
stop_playback
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def read_key
|
|
50
|
+
$stdin.raw do |io|
|
|
51
|
+
char = io.getc
|
|
52
|
+
if char == "\e"
|
|
53
|
+
rest = begin
|
|
54
|
+
io.read_nonblock(3)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
''
|
|
57
|
+
end
|
|
58
|
+
char + rest
|
|
59
|
+
else
|
|
60
|
+
char
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def relative_path(absolute)
|
|
66
|
+
absolute.sub("#{@library_path}/", '')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def display_name(relative)
|
|
70
|
+
File.basename(relative, '.*').sub(/\A\d+[\s.\-_]+/, '')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render(title = "wavesync set #{@set.name}")
|
|
74
|
+
@ui.clear
|
|
75
|
+
puts @ui.color(title, :primary)
|
|
76
|
+
puts
|
|
77
|
+
|
|
78
|
+
if @set.tracks.empty?
|
|
79
|
+
puts @ui.color(' (no tracks)', :secondary)
|
|
80
|
+
else
|
|
81
|
+
@set.tracks.each_with_index do |track, i|
|
|
82
|
+
render_track(i, relative_path(track), i == @selected, track == @player_track)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
puts
|
|
87
|
+
puts @ui.color('[↑↓] navigate [space] play/pause [a] add [u] up [d] down [r] remove [s] save [c] cancel',
|
|
88
|
+
:secondary)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_track(index, relative, selected, playing)
|
|
92
|
+
name = display_name(relative)
|
|
93
|
+
folder = File.dirname(relative)
|
|
94
|
+
icon = if playing
|
|
95
|
+
@player_state == :paused ? '⏸' : '▶'
|
|
96
|
+
else
|
|
97
|
+
' '
|
|
98
|
+
end
|
|
99
|
+
puts "#{@ui.color(icon, :surface)} #{@ui.color("#{index + 1}.", selected ? :highlight : :extra)} #{@ui.color(name, selected ? :highlight : :primary)}"
|
|
100
|
+
puts @ui.color(" #{folder}", selected ? :highlight : :tertiary) unless folder == '.'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_action(action)
|
|
104
|
+
case action
|
|
105
|
+
when :cursor_up
|
|
106
|
+
@selected = [@selected - 1, 0].max unless @set.tracks.empty?
|
|
107
|
+
when :cursor_down
|
|
108
|
+
@selected = [@selected + 1, @set.tracks.size - 1].min unless @set.tracks.empty?
|
|
109
|
+
when :toggle_play then toggle_playback
|
|
110
|
+
when :add then add_track
|
|
111
|
+
when :remove then remove_track
|
|
112
|
+
when :move_up then move_track(:up)
|
|
113
|
+
when :move_down then move_track(:down)
|
|
114
|
+
when :save
|
|
115
|
+
@set.save
|
|
116
|
+
path = Set.set_path(@library_path, @set.name)
|
|
117
|
+
puts @ui.color("Saved '#{@set.name}' to #{path}.", :primary)
|
|
118
|
+
:quit
|
|
119
|
+
when :quit
|
|
120
|
+
:quit
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def toggle_playback
|
|
125
|
+
return if @selected.nil?
|
|
126
|
+
|
|
127
|
+
track = @set.tracks[@selected]
|
|
128
|
+
|
|
129
|
+
if @player_track == track
|
|
130
|
+
case @player_state
|
|
131
|
+
when :playing
|
|
132
|
+
@player_offset += Time.now - @player_started_at
|
|
133
|
+
kill_player
|
|
134
|
+
@player_state = :paused
|
|
135
|
+
when :paused
|
|
136
|
+
start_player(track, @player_offset)
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
stop_playback
|
|
140
|
+
start_player(track)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def start_player(track, offset = 0)
|
|
145
|
+
ffplay = FFMPEG.ffmpeg_binary.sub('ffmpeg', 'ffplay')
|
|
146
|
+
args = [ffplay, '-nodisp', '-autoexit', '-loglevel', 'quiet', '-probesize', '32', '-analyzeduration', '0']
|
|
147
|
+
args += ['-ss', offset.to_s] if offset.positive?
|
|
148
|
+
args << track
|
|
149
|
+
@player_pid = spawn(*args, out: File::NULL, err: File::NULL)
|
|
150
|
+
@player_track = track
|
|
151
|
+
@player_offset = offset
|
|
152
|
+
@player_started_at = Time.now
|
|
153
|
+
@player_state = :playing
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def kill_player
|
|
157
|
+
return unless @player_pid
|
|
158
|
+
|
|
159
|
+
Process.kill('TERM', @player_pid)
|
|
160
|
+
@player_pid = nil
|
|
161
|
+
rescue Errno::ESRCH
|
|
162
|
+
@player_pid = nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def stop_playback
|
|
166
|
+
kill_player
|
|
167
|
+
@player_track = nil
|
|
168
|
+
@player_state = :stopped
|
|
169
|
+
@player_offset = 0
|
|
170
|
+
@player_started_at = nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def check_player
|
|
174
|
+
return unless @player_pid
|
|
175
|
+
|
|
176
|
+
result = Process.waitpid(@player_pid, Process::WNOHANG)
|
|
177
|
+
return unless result
|
|
178
|
+
|
|
179
|
+
@player_pid = nil
|
|
180
|
+
@player_track = nil
|
|
181
|
+
@player_state = :stopped
|
|
182
|
+
@player_offset = 0
|
|
183
|
+
advance_and_play
|
|
184
|
+
rescue Errno::ECHILD
|
|
185
|
+
@player_pid = nil
|
|
186
|
+
@player_track = nil
|
|
187
|
+
@player_state = :stopped
|
|
188
|
+
@player_offset = 0
|
|
189
|
+
advance_and_play
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def advance_and_play
|
|
193
|
+
return if @selected.nil? || @selected >= @set.tracks.size - 1
|
|
194
|
+
|
|
195
|
+
@selected += 1
|
|
196
|
+
start_player(@set.tracks[@selected])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def audio_files
|
|
200
|
+
@audio_files ||= Audio.find_all(@library_path)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def add_track
|
|
204
|
+
if audio_files.empty?
|
|
205
|
+
puts @ui.color('No audio files found in library.', :highlight)
|
|
206
|
+
sleep 1
|
|
207
|
+
return
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
choices = audio_files.map { |file| { name: relative_path(file), value: file } }
|
|
211
|
+
|
|
212
|
+
render("wavesync set #{@set.name} — add track")
|
|
213
|
+
puts
|
|
214
|
+
picked = @prompt.select('Select a track to add:', choices, cycle: true, filter: true, per_page: 20)
|
|
215
|
+
|
|
216
|
+
insert_at = @selected.nil? ? 0 : @selected + 1
|
|
217
|
+
@set.tracks.insert(insert_at, picked)
|
|
218
|
+
@selected = insert_at
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def remove_track
|
|
222
|
+
return if @set.tracks.empty? || @selected.nil?
|
|
223
|
+
|
|
224
|
+
stop_playback if @player_track == @set.tracks[@selected]
|
|
225
|
+
@set.remove_track(@selected)
|
|
226
|
+
@selected = if @set.tracks.empty?
|
|
227
|
+
nil
|
|
228
|
+
else
|
|
229
|
+
[@selected, @set.tracks.size - 1].min
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def move_track(direction)
|
|
234
|
+
return if @set.tracks.size < 2 || @selected.nil?
|
|
235
|
+
|
|
236
|
+
if direction == :up
|
|
237
|
+
@set.move_up(@selected)
|
|
238
|
+
@selected = [@selected - 1, 0].max
|
|
239
|
+
else
|
|
240
|
+
@set.move_down(@selected)
|
|
241
|
+
@selected = [@selected + 1, @set.tracks.size - 1].min
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wavesync
|
|
4
|
+
class TrackPadding
|
|
5
|
+
BEATS_PER_BAR = 4
|
|
6
|
+
|
|
7
|
+
def self.compute(duration_seconds, bpm, bar_multiple)
|
|
8
|
+
return 0 if bpm.nil? || bpm.to_f.zero? || duration_seconds.nil? || duration_seconds <= 0
|
|
9
|
+
|
|
10
|
+
seconds_per_bar = BEATS_PER_BAR * 60.0 / bpm.to_f
|
|
11
|
+
track_bars = (duration_seconds / seconds_per_bar).round(6)
|
|
12
|
+
target_bars = (track_bars / bar_multiple.to_f).ceil * bar_multiple
|
|
13
|
+
|
|
14
|
+
padding = (target_bars * seconds_per_bar) - duration_seconds
|
|
15
|
+
padding < 0.001 ? 0 : padding
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.bar_counts(duration_seconds, bpm, bar_multiple)
|
|
19
|
+
return [nil, nil] if bpm.nil? || bpm.to_f.zero? || duration_seconds.nil? || duration_seconds <= 0
|
|
20
|
+
|
|
21
|
+
seconds_per_bar = BEATS_PER_BAR * 60.0 / bpm.to_f
|
|
22
|
+
original_bars = (duration_seconds / seconds_per_bar).round
|
|
23
|
+
target_bars = ((original_bars.to_f / bar_multiple).ceil * bar_multiple).to_i
|
|
24
|
+
[original_bars, target_bars]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/wavesync/ui.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-cursor'
|
|
4
|
+
require 'rainbow'
|
|
5
|
+
|
|
6
|
+
module Wavesync
|
|
7
|
+
class UI
|
|
8
|
+
THEME = {
|
|
9
|
+
primary: :lightgray,
|
|
10
|
+
secondary: :darkgray,
|
|
11
|
+
tertiary: :dimgray,
|
|
12
|
+
highlight: :orangered,
|
|
13
|
+
surface: :hotpink,
|
|
14
|
+
extra: :deepskyblue
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@cursor = TTY::Cursor
|
|
19
|
+
@sticky_lines = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def file_progress(filename)
|
|
23
|
+
path = Pathname.new(filename)
|
|
24
|
+
file_stem = path.basename(path.extname).to_s
|
|
25
|
+
parent_name = path.parent.basename.to_s
|
|
26
|
+
sticky(in_color(parent_name, :secondary), 1)
|
|
27
|
+
sticky(in_color(file_stem, :tertiary), 2)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sync_progress(index, total_count, device)
|
|
31
|
+
parts = [
|
|
32
|
+
in_color("wavesync #{device.name}", :primary),
|
|
33
|
+
in_color("#{index + 1}/#{total_count}", :extra)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
sticky(parts.join(' '), 0)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def conversion_progress(source_format, target_format)
|
|
40
|
+
effective = source_format.merge(target_format)
|
|
41
|
+
|
|
42
|
+
source_info = audio_info(source_format.sample_rate, source_format.bit_depth)
|
|
43
|
+
target_info = audio_info(effective.sample_rate, effective.bit_depth)
|
|
44
|
+
|
|
45
|
+
formatted_line = in_color(
|
|
46
|
+
"Converting #{source_format.file_type} (#{source_info}) ⇢ #{effective.file_type} (#{target_info})", :highlight
|
|
47
|
+
)
|
|
48
|
+
sticky(formatted_line, 3)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def copy(source_format)
|
|
52
|
+
info = audio_info(source_format.sample_rate, source_format.bit_depth)
|
|
53
|
+
|
|
54
|
+
sticky(in_color("Copying #{source_format.file_type} (#{info})", :highlight), 3)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def skip
|
|
58
|
+
sticky(in_color('↷ Skipping, already synced', :highlight), 3)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def bpm(tbpm, original_bars: nil, target_bars: nil)
|
|
62
|
+
if tbpm.nil?
|
|
63
|
+
sticky('', 4)
|
|
64
|
+
elsif original_bars && target_bars
|
|
65
|
+
bar_info = original_bars == target_bars ? "#{original_bars} bars" : "#{original_bars} → #{target_bars} bars"
|
|
66
|
+
sticky("#{tbpm} bpm · #{bar_info}", 4)
|
|
67
|
+
else
|
|
68
|
+
sticky("#{tbpm} bpm", 4)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def analyze_progress(index, total_count)
|
|
73
|
+
parts = [
|
|
74
|
+
in_color('wavesync analyze', :primary),
|
|
75
|
+
in_color("#{index + 1}/#{total_count}", :extra)
|
|
76
|
+
]
|
|
77
|
+
sticky(parts.join(' '), 0)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def analyze_skip(file, bpm)
|
|
81
|
+
set_analyze_file_stickies(file, in_color("↷ #{bpm} BPM already set", :highlight))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def analyze_result(file, bpm)
|
|
85
|
+
label = bpm ? in_color("#{bpm} BPM", :highlight) : in_color('No BPM detected', :highlight)
|
|
86
|
+
set_analyze_file_stickies(file, label)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def color(text, key)
|
|
90
|
+
in_color(text, key)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def clear
|
|
94
|
+
print @cursor.clear_screen
|
|
95
|
+
print @cursor.move_to(0, 0)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def audio_info(sample_rate, bit_depth)
|
|
101
|
+
[
|
|
102
|
+
sample_rate_to_khz(sample_rate),
|
|
103
|
+
bit_depth
|
|
104
|
+
].compact.join('/')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def in_color(string, key)
|
|
108
|
+
Rainbow(string).color(THEME[key])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def sticky(text, index)
|
|
112
|
+
set_sticky(text, index)
|
|
113
|
+
redraw
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def set_sticky(text, index)
|
|
117
|
+
@sticky_lines[index] = text
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def set_analyze_file_stickies(file, label)
|
|
121
|
+
path = Pathname.new(file)
|
|
122
|
+
set_sticky(in_color(path.parent.basename.to_s, :secondary), 1)
|
|
123
|
+
set_sticky(in_color(path.basename(path.extname).to_s, :tertiary), 2)
|
|
124
|
+
set_sticky(label, 3)
|
|
125
|
+
redraw
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def redraw
|
|
129
|
+
print @cursor.clear_screen
|
|
130
|
+
print @cursor.move_to(0, 0)
|
|
131
|
+
puts @sticky_lines.join("\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def sample_rate_to_khz(rate)
|
|
135
|
+
khz = rate.to_f / 1000
|
|
136
|
+
(khz % 1).zero? ? khz.to_i.to_s : khz.round(1).to_s
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/wavesync.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wavesync
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
require 'wavesync/version'
|
|
7
|
+
require 'wavesync/acid_chunk'
|
|
8
|
+
require 'wavesync/audio_format'
|
|
9
|
+
require 'wavesync/audio'
|
|
10
|
+
require 'wavesync/track_padding'
|
|
11
|
+
require 'wavesync/config'
|
|
12
|
+
require 'wavesync/device'
|
|
13
|
+
require 'wavesync/ui'
|
|
14
|
+
require 'wavesync/path_resolver'
|
|
15
|
+
require 'wavesync/file_converter'
|
|
16
|
+
require 'wavesync/scanner'
|
|
17
|
+
require 'wavesync/bpm_detector'
|
|
18
|
+
require 'wavesync/analyzer'
|
|
19
|
+
require 'wavesync/set'
|
|
20
|
+
require 'wavesync/set_editor'
|
|
21
|
+
require 'wavesync/cli'
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wavesync
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0.alpha1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andreas Zecher
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rainbow
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: streamio-ffmpeg
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: taglib-ruby
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tty-cursor
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.7.1
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.7.1
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: tty-prompt
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.23'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.23'
|
|
96
|
+
executables:
|
|
97
|
+
- wavesync
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- LICENSE
|
|
102
|
+
- README.md
|
|
103
|
+
- bin/wavesync
|
|
104
|
+
- config/devices.yml
|
|
105
|
+
- lib/wavesync.rb
|
|
106
|
+
- lib/wavesync/acid_chunk.rb
|
|
107
|
+
- lib/wavesync/analyzer.rb
|
|
108
|
+
- lib/wavesync/audio.rb
|
|
109
|
+
- lib/wavesync/audio_format.rb
|
|
110
|
+
- lib/wavesync/bpm_detector.rb
|
|
111
|
+
- lib/wavesync/cli.rb
|
|
112
|
+
- lib/wavesync/config.rb
|
|
113
|
+
- lib/wavesync/device.rb
|
|
114
|
+
- lib/wavesync/file_converter.rb
|
|
115
|
+
- lib/wavesync/path_resolver.rb
|
|
116
|
+
- lib/wavesync/scanner.rb
|
|
117
|
+
- lib/wavesync/set.rb
|
|
118
|
+
- lib/wavesync/set_editor.rb
|
|
119
|
+
- lib/wavesync/track_padding.rb
|
|
120
|
+
- lib/wavesync/ui.rb
|
|
121
|
+
- lib/wavesync/version.rb
|
|
122
|
+
homepage: https://github.com/pixelate/wavesync
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata: {}
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '3.0'
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 4.0.3
|
|
141
|
+
specification_version: 4
|
|
142
|
+
summary: Sync your music library to hardware devices like the teenage engineering
|
|
143
|
+
TP-7 and Elektron Octatrack
|
|
144
|
+
test_files: []
|