wavesync 1.0.0.alpha4 → 1.0.0.beta2
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 +4 -4
- data/README.md +14 -21
- data/config/devices.yml +1 -0
- data/lib/wavesync/acid_chunk.rb +78 -68
- data/lib/wavesync/analyzer.rb +29 -7
- data/lib/wavesync/audio.rb +17 -18
- data/lib/wavesync/commands/analyze.rb +4 -2
- data/lib/wavesync/commands/command.rb +2 -1
- data/lib/wavesync/commands/help.rb +3 -1
- data/lib/wavesync/commands/pull.rb +28 -11
- data/lib/wavesync/cue_chunk.rb +131 -86
- data/lib/wavesync/device.rb +5 -2
- data/lib/wavesync/essentia_bpm_detector.rb +10 -0
- data/lib/wavesync/ffmpeg/probe.rb +8 -4
- data/lib/wavesync/logger.rb +16 -2
- data/lib/wavesync/mp4_tmpo.rb +287 -0
- data/lib/wavesync/path_resolver.rb +11 -0
- data/lib/wavesync/scanner.rb +7 -6
- data/lib/wavesync/setlist_editor.rb +69 -30
- data/lib/wavesync/timing.rb +36 -0
- data/lib/wavesync/transliterator.rb +15 -0
- data/lib/wavesync/transport/mtp.rb +14 -8
- data/lib/wavesync/version.rb +1 -1
- data/lib/wavesync.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a763e53a386112a5a2311cf50108a3a237ae7442d1d9c53b98fb02696315a12
|
|
4
|
+
data.tar.gz: c9972d69bad12cfd4111067bcc34e9ec646f26e80b97df44a1c5a7762daf8abf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4964e829b8b3488f16e8a39857bfd178c624b94bb9739195d987d17cb20c57fb811e6d0a442dc6bf104ff7f18df658b40c038decbcc813a2d0f332e9d1384315
|
|
7
|
+
data.tar.gz: e96d2143e3667162200af22ffc97738cf36737cc0cd1a7f5ed2f3316b401544986d9a2a44841beb37e1e4059cf351986432a0783cbdfa1e29c9e0f3777b8e401
|
data/README.md
CHANGED
|
@@ -27,36 +27,18 @@ Unsupported file types will be ignored when syncing.
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
brew install ffmpeg
|
|
30
|
-
brew install
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
3. Install Wavesync
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
gem install wavesync --pre
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
4. Set up the Python environment for BPM analysis
|
|
40
|
-
|
|
41
|
-
```bash
|
|
30
|
+
brew install libmtp
|
|
42
31
|
brew install python@3.11
|
|
43
32
|
python3.11 -m venv ~/.wavesync-venv
|
|
44
33
|
~/.wavesync-venv/bin/pip install essentia
|
|
45
34
|
```
|
|
46
35
|
|
|
47
|
-
|
|
36
|
+
3. Install Wavesync
|
|
48
37
|
|
|
49
38
|
```bash
|
|
50
|
-
|
|
39
|
+
gem install wavesync --pre
|
|
51
40
|
```
|
|
52
41
|
|
|
53
|
-
Recent versions of field kit no longer expose the TP-7 as a filesystem path, so wavesync talks to the device over MTP directly.
|
|
54
|
-
|
|
55
|
-
Before syncing the TP-7:
|
|
56
|
-
|
|
57
|
-
- Quit field kit and any other app that may claim the TP-7. They cannot run at the same time as wavesync — only one process can hold the MTP session.
|
|
58
|
-
- Put the TP-7 into MTP mode: hold down the Stop button while turning the TP-7 on, and keep holding until MTP mode is engaged.
|
|
59
|
-
|
|
60
42
|
## Configuration
|
|
61
43
|
|
|
62
44
|
Wavesync is configured via a YAML file. By default it looks for `~/wavesync.yml`. You can also pass a path explicitly with the `-c` flag.
|
|
@@ -108,6 +90,12 @@ wavesync analyze
|
|
|
108
90
|
|
|
109
91
|
# Overwrite existing BPM values
|
|
110
92
|
wavesync analyze -f
|
|
93
|
+
|
|
94
|
+
# Analyze a single file instead of the whole library
|
|
95
|
+
wavesync analyze "/path/to/track.m4a"
|
|
96
|
+
|
|
97
|
+
# Analyze all files in a folder instead of the whole library
|
|
98
|
+
wavesync analyze "/path/to/folder"
|
|
111
99
|
```
|
|
112
100
|
|
|
113
101
|
### Sync
|
|
@@ -127,6 +115,11 @@ wavesync sync -p
|
|
|
127
115
|
|
|
128
116
|
When a source file's sample rate isn't supported by the target device, Wavesync selects the closest supported rate. Example: If a 96kHz file is synced to an Octatrack (which only supports 44.1kHz), it will be downsampled to 44.1kHz.
|
|
129
117
|
|
|
118
|
+
Before syncing the TP-7:
|
|
119
|
+
|
|
120
|
+
- Quit field kit and any other app that may claim the TP-7. They cannot run at the same time as wavesync — only one process can hold the MTP session.
|
|
121
|
+
- Put the TP-7 into MTP mode: hold down the Stop button while turning the TP-7 on, and keep holding until MTP mode is engaged.
|
|
122
|
+
|
|
130
123
|
### Pull
|
|
131
124
|
|
|
132
125
|
```bash
|
data/config/devices.yml
CHANGED
data/lib/wavesync/acid_chunk.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
+
require_relative 'timing'
|
|
5
|
+
|
|
4
6
|
module Wavesync
|
|
5
7
|
class AcidChunk
|
|
6
8
|
RIFF_HEADER_SIZE = 12 # 'RIFF' (4) + size (4) + 'WAVE' (4)
|
|
@@ -30,107 +32,115 @@ module Wavesync
|
|
|
30
32
|
|
|
31
33
|
#: (String filepath) -> Float?
|
|
32
34
|
def self.read_bpm(filepath)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
file.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
Timing.current.measure(:wav_chunks) do
|
|
36
|
+
result = nil #: Float?
|
|
37
|
+
File.open(filepath, 'rb') do |file|
|
|
38
|
+
file.seek(RIFF_HEADER_SIZE)
|
|
39
|
+
until file.eof?
|
|
40
|
+
chunk_id = file.read(CHUNK_ID_SIZE)
|
|
41
|
+
break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
|
|
42
|
+
|
|
43
|
+
chunk_size = ((file.read(CHUNK_SIZE_FIELD_SIZE) || '').unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
|
|
44
|
+
if chunk_id == ACID_CHUNK_ID
|
|
45
|
+
file.seek(ACID_TEMPO_OFFSET, IO::SEEK_CUR)
|
|
46
|
+
result = ((file.read(ACID_TEMPO_SIZE) || '').unpack1(FLOAT32_LITTLE_ENDIAN) || 0).to_f
|
|
47
|
+
break
|
|
48
|
+
else
|
|
49
|
+
padding = chunk_size.odd? ? 1 : 0
|
|
50
|
+
file.seek(chunk_size + padding, IO::SEEK_CUR)
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
end
|
|
54
|
+
result
|
|
48
55
|
end
|
|
49
|
-
nil
|
|
50
56
|
end
|
|
51
57
|
|
|
52
58
|
#: (String filepath, Integer | Float | String new_bpm) -> void
|
|
53
59
|
def self.write_bpm_in_place(filepath, new_bpm)
|
|
54
|
-
|
|
60
|
+
Timing.current.measure(:wav_chunks) do
|
|
61
|
+
bpm_bytes = [new_bpm.to_f].pack(FLOAT32_LITTLE_ENDIAN)
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
File.open(filepath, 'r+b') do |file|
|
|
64
|
+
file.seek(RIFF_HEADER_SIZE)
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
acid_chunk_found = false
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
until file.eof?
|
|
69
|
+
chunk_id = file.read(CHUNK_ID_SIZE)
|
|
70
|
+
break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
|
|
71
|
+
|
|
72
|
+
chunk_size_bytes = file.read(CHUNK_SIZE_FIELD_SIZE) || ''
|
|
73
|
+
chunk_size = (chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
|
|
74
|
+
|
|
75
|
+
if chunk_id == ACID_CHUNK_ID
|
|
76
|
+
file.seek(ACID_TEMPO_OFFSET, IO::SEEK_CUR)
|
|
77
|
+
file.write(bpm_bytes)
|
|
78
|
+
acid_chunk_found = true
|
|
79
|
+
break
|
|
80
|
+
else
|
|
81
|
+
padding = chunk_size.odd? ? 1 : 0
|
|
82
|
+
file.seek(chunk_size + padding, IO::SEEK_CUR)
|
|
83
|
+
end
|
|
76
84
|
end
|
|
77
|
-
end
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
unless acid_chunk_found
|
|
87
|
+
file.seek(0, IO::SEEK_END)
|
|
88
|
+
create_acid_chunk(file, new_bpm.to_f)
|
|
89
|
+
end
|
|
82
90
|
end
|
|
83
|
-
end
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
update_riff_size(filepath)
|
|
93
|
+
end
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
#: (String source_filepath, String output_filepath, Integer | Float | String new_bpm) -> void
|
|
89
97
|
def self.write_bpm(source_filepath, output_filepath, new_bpm)
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
Timing.current.measure(:wav_chunks) do
|
|
99
|
+
bpm_bytes = [new_bpm.to_f].pack(FLOAT32_LITTLE_ENDIAN)
|
|
100
|
+
acid_chunk_found = false
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
File.open(source_filepath, 'rb') do |input|
|
|
103
|
+
File.open(output_filepath, 'wb') do |output|
|
|
104
|
+
riff_header = input.read(RIFF_HEADER_SIZE)
|
|
105
|
+
output.write(riff_header)
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
until input.eof?
|
|
108
|
+
chunk_id = input.read(CHUNK_ID_SIZE)
|
|
109
|
+
break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE) || ''
|
|
112
|
+
chunk_size = (chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
output.write(chunk_id)
|
|
115
|
+
output.write(chunk_size_bytes)
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
if chunk_id == ACID_CHUNK_ID
|
|
118
|
+
acid_chunk_found = true
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
output.write(input.read(ACID_TEMPO_OFFSET))
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
input.read(ACID_TEMPO_SIZE)
|
|
114
123
|
|
|
115
|
-
|
|
124
|
+
output.write(bpm_bytes)
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
remaining = chunk_size - ACID_TEMPO_OFFSET - ACID_TEMPO_SIZE
|
|
127
|
+
output.write(input.read(remaining)) if remaining.positive?
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
output.write(input.read(1)) if chunk_size.odd?
|
|
130
|
+
else
|
|
131
|
+
padding = chunk_size.odd? ? 1 : 0
|
|
132
|
+
output.write(input.read(chunk_size + padding))
|
|
133
|
+
end
|
|
124
134
|
end
|
|
125
|
-
end
|
|
126
135
|
|
|
127
|
-
|
|
136
|
+
create_acid_chunk(output, new_bpm.to_f) unless acid_chunk_found
|
|
137
|
+
end
|
|
128
138
|
end
|
|
129
|
-
end
|
|
130
139
|
|
|
131
|
-
|
|
140
|
+
next if acid_chunk_found
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
update_riff_size(output_filepath)
|
|
143
|
+
end
|
|
134
144
|
end
|
|
135
145
|
|
|
136
146
|
#: (untyped output, Float bpm) -> void
|
data/lib/wavesync/analyzer.rb
CHANGED
|
@@ -5,7 +5,6 @@ require_relative 'logger'
|
|
|
5
5
|
|
|
6
6
|
module Wavesync
|
|
7
7
|
class Analyzer
|
|
8
|
-
CONFIRM_MESSAGE = 'wavesync analyze will add bpm meta data to files in library. Continue? [y/N] '
|
|
9
8
|
SETUP_INSTRUCTIONS = 'brew install python@3.11 && python3.11 -m venv ~/.wavesync-venv && ~/.wavesync-venv/bin/pip install essentia'
|
|
10
9
|
|
|
11
10
|
#: (String library_path) -> void
|
|
@@ -16,29 +15,30 @@ module Wavesync
|
|
|
16
15
|
@ui = UI.new #: UI
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
#: (?overwrite: bool) -> void
|
|
20
|
-
def analyze(overwrite: false)
|
|
18
|
+
#: (?overwrite: bool, ?path: String?) -> void
|
|
19
|
+
def analyze(overwrite: false, path: nil)
|
|
21
20
|
unless BpmDetector.available?
|
|
22
21
|
puts "Error: essentia is not installed. Set up the Python venv with:\n #{SETUP_INSTRUCTIONS}"
|
|
23
22
|
exit 1
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
files = files_for(path)
|
|
26
|
+
return unless @ui.confirm(confirm_message(path))
|
|
27
27
|
|
|
28
28
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
files.each_with_index do |file, index|
|
|
31
31
|
audio = Audio.new(file)
|
|
32
32
|
|
|
33
33
|
if audio.bpm && !overwrite
|
|
34
|
-
@ui.analyze_progress(index,
|
|
34
|
+
@ui.analyze_progress(index, files.size)
|
|
35
35
|
@ui.analyze_skip(file, audio.bpm)
|
|
36
36
|
next
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
bpm = BpmDetector.detect(file)
|
|
40
40
|
audio.write_bpm(bpm) if bpm
|
|
41
|
-
@ui.analyze_progress(index,
|
|
41
|
+
@ui.analyze_progress(index, files.size)
|
|
42
42
|
@ui.analyze_result(file, bpm)
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -48,6 +48,28 @@ module Wavesync
|
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
|
+
#: (String? path) -> String
|
|
52
|
+
def confirm_message(path)
|
|
53
|
+
target =
|
|
54
|
+
if path.nil?
|
|
55
|
+
'files in library'
|
|
56
|
+
elsif File.directory?(File.expand_path(path))
|
|
57
|
+
'files in folder'
|
|
58
|
+
else
|
|
59
|
+
'file'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
"wavesync analyze will add bpm meta data to #{target}. Continue? [y/N] "
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: (String? path) -> Array[String]
|
|
66
|
+
def files_for(path)
|
|
67
|
+
return @audio_files unless path
|
|
68
|
+
|
|
69
|
+
expanded_path = File.expand_path(path)
|
|
70
|
+
File.directory?(expanded_path) ? Audio.find_all(expanded_path) : [expanded_path]
|
|
71
|
+
end
|
|
72
|
+
|
|
51
73
|
#: () -> Array[String]
|
|
52
74
|
def find_audio_files
|
|
53
75
|
Audio.find_all(@library_path).sort
|
data/lib/wavesync/audio.rb
CHANGED
|
@@ -5,6 +5,9 @@ require 'securerandom'
|
|
|
5
5
|
require 'tmpdir'
|
|
6
6
|
require 'fileutils'
|
|
7
7
|
require_relative 'logger'
|
|
8
|
+
require_relative 'timing'
|
|
9
|
+
require_relative 'transliterator'
|
|
10
|
+
require_relative 'mp4_tmpo'
|
|
8
11
|
|
|
9
12
|
module Wavesync
|
|
10
13
|
class Audio
|
|
@@ -61,14 +64,19 @@ module Wavesync
|
|
|
61
64
|
)
|
|
62
65
|
end
|
|
63
66
|
|
|
64
|
-
#: () -> Array[{identifier: Integer, sample_offset: Integer, label: String?}]
|
|
67
|
+
#: () -> Array[{identifier: Integer, sample_offset: Integer, label: String?, note: String?}]
|
|
65
68
|
def cue_points
|
|
66
69
|
return [] unless @file_ext == '.wav'
|
|
67
70
|
|
|
68
71
|
CueChunk.read(@file_path)
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
#: (Array[{
|
|
74
|
+
#: () -> Array[{start_sample: Integer, end_sample: Integer}]
|
|
75
|
+
def loops
|
|
76
|
+
CueChunk.loops(cue_points)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#: (Array[{identifier: Integer, sample_offset: Integer, label: String?, note: String?}] cue_points) -> void
|
|
72
80
|
def write_cue_points(cue_points)
|
|
73
81
|
temp_path = "#{@file_path}.tmp"
|
|
74
82
|
CueChunk.write(@file_path, temp_path, cue_points)
|
|
@@ -97,8 +105,6 @@ module Wavesync
|
|
|
97
105
|
|
|
98
106
|
FFMPEG_KEY_TO_FRAME_ID = FRAME_ID_TO_FFMPEG_KEY.invert.freeze
|
|
99
107
|
|
|
100
|
-
COMBINING_MARKS = /\p{Mn}/
|
|
101
|
-
|
|
102
108
|
#: () -> Hash[String, String]
|
|
103
109
|
def transliterated_tag_changes
|
|
104
110
|
current_tags = @audio.tags
|
|
@@ -154,11 +160,11 @@ module Wavesync
|
|
|
154
160
|
command.audio_filter("apad=whole_dur=#{total_duration.round(6)}")
|
|
155
161
|
end
|
|
156
162
|
metadata.each { |key, value| command.metadata(key, value) }
|
|
157
|
-
command.run(temp_path)
|
|
163
|
+
Timing.current.measure(:transcode) { command.run(temp_path) }
|
|
158
164
|
yield temp_path if block_given?
|
|
159
|
-
FileUtils.install(temp_path, target_path)
|
|
165
|
+
Timing.current.measure(:copy) { FileUtils.install(temp_path, target_path) }
|
|
160
166
|
true
|
|
161
|
-
rescue
|
|
167
|
+
rescue SystemCallError => e
|
|
162
168
|
Logger.log_error(e, call_site: 'Audio#transcode', arguments: { target_path:, target_sample_rate:, target_file_type:, target_bit_depth:, padding_seconds:, target_bitrate: })
|
|
163
169
|
false
|
|
164
170
|
ensure
|
|
@@ -191,11 +197,7 @@ module Wavesync
|
|
|
191
197
|
|
|
192
198
|
#: () -> Integer?
|
|
193
199
|
def bpm_from_m4a
|
|
194
|
-
|
|
195
|
-
return nil if value.nil?
|
|
196
|
-
|
|
197
|
-
int_value = value.to_i
|
|
198
|
-
int_value.zero? ? nil : int_value
|
|
200
|
+
Mp4Tmpo.read_bpm(@file_path)
|
|
199
201
|
end
|
|
200
202
|
|
|
201
203
|
#: () -> String?
|
|
@@ -243,7 +245,7 @@ module Wavesync
|
|
|
243
245
|
|
|
244
246
|
#: (String | Integer | Float bpm) -> void
|
|
245
247
|
def write_bpm_to_m4a(bpm)
|
|
246
|
-
|
|
248
|
+
Mp4Tmpo.write_bpm(@file_path, bpm)
|
|
247
249
|
end
|
|
248
250
|
|
|
249
251
|
#: (String | Integer | Float bpm) -> void
|
|
@@ -256,10 +258,9 @@ module Wavesync
|
|
|
256
258
|
ext = File.extname(@file_path)
|
|
257
259
|
temp_path = File.join(Dir.tmpdir, "wavesync_meta_#{SecureRandom.hex}#{ext}")
|
|
258
260
|
command = FFMPEG.new.input(@file_path).copy_streams.map_metadata(0)
|
|
259
|
-
command.movflags('+use_metadata_tags') if ext == '.m4a'
|
|
260
261
|
command.write_id3v2(1) if %w[.aif .aiff].include?(ext)
|
|
261
262
|
metadata_hash.each { |key, value| command.metadata(key, value) }
|
|
262
|
-
command.run(temp_path)
|
|
263
|
+
Timing.current.measure(:ffmpeg_metadata) { command.run(temp_path) }
|
|
263
264
|
FileUtils.mv(temp_path, @file_path)
|
|
264
265
|
ensure
|
|
265
266
|
FileUtils.rm_f(temp_path)
|
|
@@ -267,9 +268,7 @@ module Wavesync
|
|
|
267
268
|
|
|
268
269
|
#: (String string) -> String
|
|
269
270
|
def transliterate(string)
|
|
270
|
-
string
|
|
271
|
-
.unicode_normalize(:nfd)
|
|
272
|
-
.gsub(COMBINING_MARKS, '')
|
|
271
|
+
Transliterator.transliterate(string)
|
|
273
272
|
end
|
|
274
273
|
end
|
|
275
274
|
end
|
|
@@ -11,14 +11,16 @@ module Wavesync
|
|
|
11
11
|
self.name = 'analyze'
|
|
12
12
|
self.description = 'Detect and write BPM metadata to library tracks'
|
|
13
13
|
self.options = [FORCE_OPTION].freeze
|
|
14
|
+
self.positional_args = '[file_or_folder]'
|
|
14
15
|
|
|
15
16
|
#: () -> void
|
|
16
17
|
def run
|
|
17
|
-
options, config = parse_options(banner: 'Usage: wavesync analyze [options]') do |opts, opts_hash|
|
|
18
|
+
options, config = parse_options(banner: 'Usage: wavesync analyze [options] [file_or_folder]') do |opts, opts_hash|
|
|
18
19
|
opts.on(*FORCE_OPTION.to_a) { opts_hash[:overwrite] = true }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
path = ARGV.shift
|
|
23
|
+
Wavesync::Analyzer.new(config.library).analyze(overwrite: options[:overwrite] || false, path: path)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
|
@@ -7,10 +7,11 @@ module Wavesync
|
|
|
7
7
|
class Command
|
|
8
8
|
class << self
|
|
9
9
|
attr_accessor :name, :description
|
|
10
|
-
attr_writer :options, :subcommands
|
|
10
|
+
attr_writer :options, :subcommands, :positional_args
|
|
11
11
|
|
|
12
12
|
def options = @options || []
|
|
13
13
|
def subcommands = @subcommands || []
|
|
14
|
+
def positional_args = @positional_args || ''
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
#: (banner: String) ?{ (OptionParser, Hash[Symbol, untyped]) -> void } -> [Hash[Symbol, untyped], Config]
|
|
@@ -65,7 +65,9 @@ module Wavesync
|
|
|
65
65
|
GLOBAL_OPTIONS.each { |option| puts " #{option.short}, #{option.long} #{option.description}" }
|
|
66
66
|
else
|
|
67
67
|
OptionParser.new do |opts|
|
|
68
|
-
|
|
68
|
+
banner = "Usage: wavesync #{command.name} [options]"
|
|
69
|
+
banner += " #{command.positional_args}" unless command.positional_args.empty?
|
|
70
|
+
opts.banner = banner
|
|
69
71
|
(command.options + GLOBAL_OPTIONS).each { |option| opts.on(*option.to_a) }
|
|
70
72
|
puts opts
|
|
71
73
|
end
|
|
@@ -23,19 +23,36 @@ module Wavesync
|
|
|
23
23
|
scanner = Wavesync::Scanner.new(config.library)
|
|
24
24
|
ui = Wavesync::UI.new
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
stop_requested = false
|
|
27
|
+
original_handler = Signal.trap('INT') do
|
|
28
|
+
if stop_requested
|
|
29
|
+
Signal.trap('INT', 'DEFAULT')
|
|
30
|
+
warn "\nAborting."
|
|
31
|
+
Process.kill('INT', Process.pid)
|
|
32
|
+
else
|
|
33
|
+
stop_requested = true
|
|
34
|
+
warn "\nFinishing current file then stopping. Press Ctrl+C again to abort immediately."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
device_pairs.each do |pair|
|
|
40
|
+
device_config = pair[0] #: { name: String, model: String, path: String, transport: String, mp3_bitrate: Integer }
|
|
41
|
+
device = pair[1] #: Wavesync::Device
|
|
42
|
+
transport = Wavesync::Transport.for(device_config)
|
|
43
|
+
Commands.with_mtp_retry(transport, device_config[:name]) do
|
|
44
|
+
next unless transport.is_a?(Wavesync::Transport::Mtp)
|
|
45
|
+
|
|
46
|
+
transport.prepare!(stop_when: -> { stop_requested }) do |index, total, relative_path|
|
|
47
|
+
ui.pull_staging_progress(index, total, device)
|
|
48
|
+
ui.file_progress(relative_path)
|
|
49
|
+
end
|
|
36
50
|
end
|
|
51
|
+
scanner.pull_cue_points(transport.working_directory, device)
|
|
52
|
+
break if stop_requested
|
|
37
53
|
end
|
|
38
|
-
|
|
54
|
+
ensure
|
|
55
|
+
Signal.trap('INT', original_handler)
|
|
39
56
|
end
|
|
40
57
|
end
|
|
41
58
|
end
|