wavesync 1.0.0.alpha2 → 1.0.0.alpha3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 166c4643be69e020ac6ac73611146502faa4891ad35f60bd30f3143702113c14
4
- data.tar.gz: f41b3be18c7f4014f892a19e27612a363502d1f70effe098e74d239b19d9d873
3
+ metadata.gz: 460ae33ce4f3fb42e31cb8505f3ce403f558080d4f1a41f9ca84466c41d3cf27
4
+ data.tar.gz: '084dfb18be270a6d702295326eb70538bd38106ba6ddd853af834d46acf404e5'
5
5
  SHA512:
6
- metadata.gz: d1df257786183fa8253036ca6e59b7b559bab4d82fba2da101aa86c58e91f56af272215fd406785e05c8eec6cb473c22ea8d90ff9fd4367f6894ae9916aef54a
7
- data.tar.gz: b29f51c0a796aedddee42d219e042e02b150b89ec23775b40ca7c8d7e93b2b7c1dae438a240aa3e549e2e3f53318f3960d6305965d29a6199b29f62d99c1fa10
6
+ metadata.gz: e20ff7f979e58e2602fe5b02452e2b6aa1a619cfbb27b303665fa986377942154f5f976f20da97ce23d7e9b3982d5b01eb08fbdcc84532704d2a287b6ed8de05
7
+ data.tar.gz: eb710a3fcc8b9e729bbd86f3e2f506fdfadcad64ce0c112f4c6480677e09bee9f61c1de69bf54f942429cb9c6708c5652b9b083893ab9b05e6795c304996aad9
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Wavesync
2
2
 
3
- Wavesync is a Ruby-based CLI tool that scans your music library and automatically converts audio files to match the specifications of specific hardware music devices like the teenage engineering TP-7 and Elektron Octatrack, adjusting sample rate, bit depth and file format as needed while preserving your original library structure and only converting files that don't already meet the device requirements. It also reads BPM information from the original file and converts it so that the target device can read it.
3
+ Wavesync is a Ruby-based CLI tool that scans your music library and automatically converts audio files to match the specifications of the [teenage engineering TP-7](https://teenage.engineering/products/tp-7) and the [Elektron Octatrack MKII](https://www.elektron.se/explore/octatrack-mkii), adjusting sample rate, bit depth and file format as needed while preserving your original library structure and only converting files that don't already meet the device requirements.
4
4
 
5
- ## Supported devices
5
+ It can also analyse the BPM of the tracks in your library and store the information as metadata in your files, as well as convert the metadata so that the target device can read the BPM. When syncing to the Octatrack, you can choose to add padding to each track, so that it's dead-simple to auto-slice your tracks to a multiple of full bars which allows precise looping, and seamlessly jumping to different parts of a track with Octatrack's sequencer.
6
6
 
7
- - teenage engineering TP-7
8
- - Elektron Octatrack MKII
7
+ ![Wavesync syncing to Octatrack](assets/screenshot.png)
9
8
 
10
9
  ## Supported file types
11
10
 
@@ -20,21 +19,32 @@ Unsupported file types will be ignored when syncing.
20
19
 
21
20
  ## Installation
22
21
 
23
- 1. Install dependencies
22
+ 1. Install Ruby
23
+
24
+ https://www.ruby-lang.org/en/documentation/installation/
25
+
26
+ 2. Install dependencies
24
27
 
25
28
  ```bash
26
- brew install ffmpeg # required for all commands
27
- brew install taglib # required for all commands
28
- brew install bpm-tools # required for analyze command
29
+ brew install ffmpeg
30
+ brew install taglib
29
31
  ```
30
32
 
31
- 2. Install Wavesync
33
+ 3. Install Wavesync
32
34
 
33
35
  ```bash
34
36
  gem install wavesync --pre
35
37
  ```
36
38
 
37
- 3. Install field kit (only required for syncing to TP-7)
39
+ 4. Set up the Python environment for BPM analysis
40
+
41
+ ```bash
42
+ brew install python@3.11
43
+ python3.11 -m venv ~/.wavesync-venv
44
+ ~/.wavesync-venv/bin/pip install essentia
45
+ ```
46
+
47
+ 5. Install field kit (only required for syncing to TP-7)
38
48
 
39
49
  https://teenage.engineering/guides/fieldkit
40
50
 
@@ -61,91 +71,67 @@ devices:
61
71
  - `model`: device model (`TP-7` or `Octatrack`)
62
72
  - `path`: path to the device's library directory
63
73
 
64
- Wavesync will exit with an error if a device model in the config is not supported.
74
+ ## Usage
65
75
 
66
- ## Help
76
+ ### Help
67
77
 
68
78
  ```bash
79
+ # List all available commands
69
80
  wavesync help
70
81
  ```
71
82
 
72
- ## Usage
83
+ ### Analyze
84
+
85
+ ```bash
86
+ # Analyze library files for BPM and write results to file metadata
87
+ # Files that already have BPM set are skipped
88
+ wavesync analyze
89
+
90
+ # Overwrite existing BPM values
91
+ wavesync analyze -f
92
+ ```
93
+
94
+ ### Sync
73
95
 
74
96
  ```bash
75
- # Sync library (uses default config at ~/wavesync.yml)
97
+ # Sync library
76
98
  # If multiple devices are configured, you will be prompted to select one
77
99
  wavesync sync
78
100
 
79
- # Use a config at a specific path
80
- wavesync sync -c /path/to/wavesync.yml
81
-
82
- # Sync to a specific device only (by name as defined in config)
101
+ # Sync to a specific device (by name as defined in config)
83
102
  wavesync sync -d Octatrack
84
103
 
85
104
  # Pad each track with silence so its total length aligns to a multiple of 64 bars
86
105
  # (Octatrack only — requires BPM metadata on each track)
87
106
  wavesync sync -p
88
-
89
- # Analyze library files for BPM and write results to file metadata
90
- # Files that already have BPM set are skipped
91
- wavesync analyze
92
-
93
- # Overwrite existing BPM values
94
- wavesync analyze --force
95
-
96
- # Analyze with a specific config path
97
- wavesync analyze -c /path/to/wavesync.yml
98
107
  ```
99
108
 
100
- ## Sets
109
+ 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.
101
110
 
102
- A set is a named, ordered selection of tracks from your library. Sets are stored as YAML files inside a `.sets` folder within the library directory.
111
+ ### Sets (experimental)
112
+
113
+ A set is a named, ordered selection of tracks from your library. Sets are stored as YAML files inside a `.sets` folder within the library directory. Syncing sets to devices is not yet implemented.
103
114
 
104
115
  ```bash
105
116
  # Create a new set and open the interactive editor
106
- wavesync set create <name>
117
+ wavesync set create NAME
107
118
 
108
119
  # Edit an existing set
109
- wavesync set edit <name>
120
+ wavesync set edit NAME
110
121
 
111
122
  # List all sets
112
123
  wavesync set list
113
124
  ```
114
125
 
115
- The set editor is a keyboard-driven interactive UI:
116
-
117
- | Key | Action |
118
- |-----|--------|
119
- | `↑` / `↓` | Navigate tracks |
120
- | `space` | Play / pause selected track |
121
- | `a` | Add track after selection |
122
- | `u` | Move selected track up |
123
- | `d` | Move selected track down |
124
- | `r` | Remove selected track |
125
- | `s` | Save and exit |
126
- | `c` | Cancel without saving |
127
-
128
- The playing track is indicated by `▶` (playing) or `⏸` (paused) before the track number. When a track finishes, the next track plays automatically. Changes are only written to disk when you press `s`.
129
-
130
- ## Sample Rate Selection
131
-
132
- When a source file's sample rate isn't supported by the target device, Wavesync selects the closest supported rate. For files with equal distance to two rates, it chooses the higher rate to minimize quality loss.
133
-
134
- Example: If a 96kHz file is synced to an Octatrack (which only supports 44.1kHz), it will be downsampled to 44.1kHz.
135
-
136
126
  ## Development
137
127
 
138
- ### Running Tests
128
+ ### Running Tests, RuboCop, and Type Checks
139
129
 
140
130
  ```bash
141
- rake test
131
+ rake
142
132
  ```
143
133
 
144
- ### Running RuboCop
145
-
146
- ```bash
147
- bundle exec rubocop
148
- ```
134
+ This runs RuboCop (with auto-fix), Steep type checks, and the test suite in sequence.
149
135
 
150
136
  ### Releasing
151
137
 
data/config/devices.yml CHANGED
@@ -1,11 +1,13 @@
1
1
  devices:
2
2
  - name: TP-7
3
3
  sample_rates:
4
+ - 22050
4
5
  - 44100
5
6
  - 48000
6
7
  - 88200
7
8
  - 96000
8
9
  bit_depths:
10
+ - 8
9
11
  - 16
10
12
  - 24
11
13
  file_types:
@@ -17,6 +19,7 @@ devices:
17
19
  sample_rates:
18
20
  - 44100
19
21
  bit_depths:
22
+ - 8
20
23
  - 16
21
24
  - 24
22
25
  file_types:
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  module Wavesync
4
5
  class AcidChunk
@@ -27,6 +28,7 @@ module Wavesync
27
28
  UINT32_LITTLE_ENDIAN = 'V'
28
29
  FLOAT32_LITTLE_ENDIAN = 'e'
29
30
 
31
+ #: (String filepath) -> Float?
30
32
  def self.read_bpm(filepath)
31
33
  File.open(filepath, 'rb') do |file|
32
34
  file.seek(RIFF_HEADER_SIZE)
@@ -34,10 +36,10 @@ module Wavesync
34
36
  chunk_id = file.read(CHUNK_ID_SIZE)
35
37
  break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
36
38
 
37
- chunk_size = file.read(CHUNK_SIZE_FIELD_SIZE).unpack1(UINT32_LITTLE_ENDIAN)
39
+ chunk_size = ((file.read(CHUNK_SIZE_FIELD_SIZE) || '').unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
38
40
  if chunk_id == ACID_CHUNK_ID
39
41
  file.seek(ACID_TEMPO_OFFSET, IO::SEEK_CUR)
40
- return file.read(ACID_TEMPO_SIZE).unpack1(FLOAT32_LITTLE_ENDIAN)
42
+ return ((file.read(ACID_TEMPO_SIZE) || '').unpack1(FLOAT32_LITTLE_ENDIAN) || 0).to_f
41
43
  else
42
44
  padding = chunk_size.odd? ? 1 : 0
43
45
  file.seek(chunk_size + padding, IO::SEEK_CUR)
@@ -47,6 +49,7 @@ module Wavesync
47
49
  nil
48
50
  end
49
51
 
52
+ #: (String source_filepath, String output_filepath, Integer | Float | String new_bpm) -> void
50
53
  def self.write_bpm(source_filepath, output_filepath, new_bpm)
51
54
  bpm_bytes = [new_bpm.to_f].pack(FLOAT32_LITTLE_ENDIAN)
52
55
  acid_chunk_found = false
@@ -60,8 +63,8 @@ module Wavesync
60
63
  chunk_id = input.read(CHUNK_ID_SIZE)
61
64
  break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
62
65
 
63
- chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE)
64
- chunk_size = chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN)
66
+ chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE) || ''
67
+ chunk_size = (chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
65
68
 
66
69
  output.write(chunk_id)
67
70
  output.write(chunk_size_bytes)
@@ -94,6 +97,7 @@ module Wavesync
94
97
  update_riff_size(output_filepath)
95
98
  end
96
99
 
100
+ #: (untyped output, Float bpm) -> void
97
101
  def self.create_acid_chunk(output, bpm)
98
102
  # Write ACID chunk ID
99
103
  output.write(ACID_CHUNK_ID)
@@ -125,6 +129,7 @@ module Wavesync
125
129
  output.write([bpm].pack(FLOAT32_LITTLE_ENDIAN))
126
130
  end
127
131
 
132
+ #: (String filepath) -> void
128
133
  def self.update_riff_size(filepath)
129
134
  File.open(filepath, 'r+b') do |file|
130
135
  file.seek(0, IO::SEEK_END)
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  module Wavesync
4
5
  class Analyzer
5
6
  CONFIRM_MESSAGE = 'wavesync analyze will add bpm meta data to files in library. Continue? [y/N] '
7
+ SETUP_INSTRUCTIONS = 'brew install python@3.11 && python3.11 -m venv ~/.wavesync-venv && ~/.wavesync-venv/bin/pip install essentia'
6
8
 
9
+ #: (String library_path) -> void
7
10
  def initialize(library_path)
8
- @library_path = File.expand_path(library_path)
9
- @audio_files = find_audio_files
10
- @ui = UI.new
11
+ @library_path = File.expand_path(library_path) #: String
12
+ @audio_files = find_audio_files #: Array[String]
13
+ @ui = UI.new #: UI
11
14
  end
12
15
 
16
+ #: (?overwrite: bool) -> void
13
17
  def analyze(overwrite: false)
14
18
  unless BpmDetector.available?
15
- puts 'Error: bpm-tools or ffmpeg is not installed. Install with: brew install bpm-tools ffmpeg'
19
+ puts "Error: essentia is not installed. Set up the Python venv with:\n #{SETUP_INSTRUCTIONS}"
16
20
  exit 1
17
21
  end
18
22
 
@@ -36,6 +40,7 @@ module Wavesync
36
40
 
37
41
  private
38
42
 
43
+ #: () -> Array[String]
39
44
  def find_audio_files
40
45
  Audio.find_all(@library_path).sort
41
46
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require 'streamio-ffmpeg'
4
5
  require 'securerandom'
@@ -10,35 +11,42 @@ module Wavesync
10
11
  class Audio
11
12
  SUPPORTED_FORMATS = %w[.m4a .mp3 .wav .aif .aiff].freeze
12
13
 
14
+ #: (String library_path) -> Array[String]
13
15
  def self.find_all(library_path)
14
16
  Dir.glob(File.join(library_path, '**', '*'))
15
17
  .select { |f| SUPPORTED_FORMATS.include?(File.extname(f).downcase) }
16
18
  end
17
19
 
20
+ #: (String file_path) -> void
18
21
  def initialize(file_path)
19
- @file_path = file_path
20
- @file_ext = File.extname(@file_path).downcase
21
- @audio = FFMPEG::Movie.new(file_path)
22
+ @file_path = file_path #: String
23
+ @file_ext = File.extname(@file_path).downcase #: String
24
+ @audio = FFMPEG::Movie.new(file_path) #: untyped
22
25
  end
23
26
 
27
+ #: () -> Float
24
28
  def duration
25
29
  @audio.duration
26
30
  end
27
31
 
32
+ #: () -> Integer?
28
33
  def sample_rate
29
34
  @sample_rate ||= @audio.audio_sample_rate
30
35
  end
31
36
 
37
+ #: () -> Integer?
32
38
  def bit_depth
33
39
  @bit_depth ||= calculate_bit_depth
34
40
  end
35
41
 
42
+ #: () -> (String | Integer)?
36
43
  def bpm
37
44
  return @bpm if defined?(@bpm)
38
45
 
39
46
  @bpm = bpm_from_file
40
47
  end
41
48
 
49
+ #: () -> AudioFormat
42
50
  def format
43
51
  AudioFormat.new(
44
52
  file_type: @file_ext.delete_prefix('.'),
@@ -47,6 +55,21 @@ module Wavesync
47
55
  )
48
56
  end
49
57
 
58
+ #: () -> Array[{identifier: Integer, sample_offset: Integer, label: String?}]
59
+ def cue_points
60
+ return [] unless @file_ext == '.wav'
61
+
62
+ CueChunk.read(@file_path)
63
+ end
64
+
65
+ #: (Array[{identifier: Integer, sample_offset: Integer, label: String?}] cue_points) -> void
66
+ def write_cue_points(cue_points)
67
+ temp_path = "#{@file_path}.tmp"
68
+ CueChunk.write(@file_path, temp_path, cue_points)
69
+ FileUtils.mv(temp_path, @file_path)
70
+ end
71
+
72
+ #: (String | Integer | Float bpm) -> void
50
73
  def write_bpm(bpm)
51
74
  case @file_ext
52
75
  when '.m4a'
@@ -61,6 +84,7 @@ module Wavesync
61
84
  @bpm = bpm
62
85
  end
63
86
 
87
+ #: (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?) -> bool
64
88
  def transcode(target_path, target_sample_rate: nil, target_file_type: nil, target_bit_depth: nil, padding_seconds: nil)
65
89
  options = build_transcode_options(target_sample_rate, target_bit_depth, padding_seconds)
66
90
  ext = target_file_type || @file_ext.delete_prefix('.')
@@ -83,6 +107,7 @@ module Wavesync
83
107
 
84
108
  private
85
109
 
110
+ #: () -> Integer?
86
111
  def calculate_bit_depth
87
112
  data = @audio.metadata
88
113
  return nil unless data && data[:streams]
@@ -96,8 +121,9 @@ module Wavesync
96
121
  nil
97
122
  end
98
123
 
124
+ #: (Integer? target_sample_rate, Integer? target_bit_depth, ?Numeric? padding_seconds) -> Hash[Symbol, untyped]
99
125
  def build_transcode_options(target_sample_rate, target_bit_depth, padding_seconds = nil)
100
- options = { custom: %w[-loglevel warning -nostats -hide_banner] }
126
+ options = { custom: %w[-loglevel warning -nostats -hide_banner] } #: Hash[Symbol, untyped]
101
127
 
102
128
  if target_bit_depth == 24
103
129
  options[:audio_codec] = 'pcm_s24le'
@@ -116,6 +142,7 @@ module Wavesync
116
142
  options
117
143
  end
118
144
 
145
+ #: () -> (String | Integer)?
119
146
  def bpm_from_file
120
147
  case @file_ext
121
148
  when '.m4a'
@@ -129,6 +156,7 @@ module Wavesync
129
156
  end
130
157
  end
131
158
 
159
+ #: () -> Integer?
132
160
  def bpm_from_m4a
133
161
  TagLib::MP4::File.open(@file_path) do |file|
134
162
  tag = file.tag
@@ -136,6 +164,7 @@ module Wavesync
136
164
  end
137
165
  end
138
166
 
167
+ #: () -> String?
139
168
  def bpm_from_mp3
140
169
  TagLib::MPEG::File.open(@file_path) do |file|
141
170
  tag = file.id3v2_tag
@@ -143,6 +172,7 @@ module Wavesync
143
172
  end
144
173
  end
145
174
 
175
+ #: () -> (String | Integer)?
146
176
  def bpm_from_wav
147
177
  TagLib::RIFF::WAV::File.open(@file_path) do |file|
148
178
  tag = file.id3v2_tag
@@ -153,6 +183,7 @@ module Wavesync
153
183
  bpm_from_acid_chunk
154
184
  end
155
185
 
186
+ #: () -> String?
156
187
  def bpm_from_aiff
157
188
  TagLib::RIFF::AIFF::File.open(@file_path) do |file|
158
189
  tag = file.tag
@@ -160,26 +191,31 @@ module Wavesync
160
191
  end
161
192
  end
162
193
 
194
+ #: (untyped tag) -> Integer?
163
195
  def bpm_from_item_map(tag)
164
196
  tmpo = tag.item_map['tmpo']&.to_int
165
197
  tmpo&.zero? ? nil : tmpo
166
198
  end
167
199
 
200
+ #: (untyped tag) -> String?
168
201
  def bpm_from_frame_list(tag)
169
202
  tag.frame_list('TBPM').first&.to_s
170
203
  end
171
204
 
205
+ #: () -> Integer?
172
206
  def bpm_from_acid_chunk
173
207
  tmpo = Wavesync::AcidChunk.read_bpm(@file_path).to_i
174
208
  tmpo&.zero? ? nil : tmpo
175
209
  end
176
210
 
211
+ #: (String | Integer | Float bpm) -> void
177
212
  def write_bpm_to_wav(bpm)
178
213
  temp_path = "#{@file_path}.tmp"
179
214
  AcidChunk.write_bpm(@file_path, temp_path, bpm)
180
215
  FileUtils.mv(temp_path, @file_path)
181
216
  end
182
217
 
218
+ #: (String | Integer | Float bpm) -> void
183
219
  def write_bpm_to_mp3(bpm)
184
220
  TagLib::MPEG::File.open(@file_path) do |file|
185
221
  write_id3v2_bpm(file.id3v2_tag(true), bpm)
@@ -187,6 +223,7 @@ module Wavesync
187
223
  end
188
224
  end
189
225
 
226
+ #: (String | Integer | Float bpm) -> void
190
227
  def write_bpm_to_m4a(bpm)
191
228
  TagLib::MP4::File.open(@file_path) do |file|
192
229
  tag = file.tag
@@ -195,6 +232,7 @@ module Wavesync
195
232
  end
196
233
  end
197
234
 
235
+ #: (String | Integer | Float bpm) -> void
198
236
  def write_bpm_to_aiff(bpm)
199
237
  TagLib::RIFF::AIFF::File.open(@file_path) do |file|
200
238
  write_id3v2_bpm(file.tag, bpm)
@@ -202,6 +240,7 @@ module Wavesync
202
240
  end
203
241
  end
204
242
 
243
+ #: (untyped tag, String | Integer | Float bpm) -> void
205
244
  def write_id3v2_bpm(tag, bpm)
206
245
  tag.remove_frames('TBPM')
207
246
  frame = TagLib::ID3v2::TextIdentificationFrame.new('TBPM', TagLib::String::UTF8)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Wavesync
4
4
  AudioFormat = Data.define(:file_type, :sample_rate, :bit_depth) do
5
+ #: (AudioFormat other) -> AudioFormat
5
6
  def merge(other)
6
7
  with(
7
8
  file_type: other.file_type || file_type,
@@ -1,19 +1,26 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
- require 'shellwords'
4
+ require_relative 'essentia_bpm_detector'
5
+ require_relative 'percival_bpm_detector'
4
6
 
5
7
  module Wavesync
6
8
  class BpmDetector
9
+ CONFIDENCE_THRESHOLD = 2.0
10
+
11
+ #: () -> bool?
7
12
  def self.available?
8
- system('which bpm > /dev/null 2>&1') && system('which ffmpeg > /dev/null 2>&1')
13
+ EssentiaBpmDetector.available? || PercivalBpmDetector.available?
9
14
  end
10
15
 
16
+ #: (String file_path) -> Integer?
11
17
  def self.detect(file_path)
12
- output = `ffmpeg -i #{Shellwords.escape(file_path)} -ac 1 -ar 44100 -f f32le - 2>/dev/null | bpm`
13
- bpm = output.strip.to_f
14
- bpm.positive? ? bpm.round : nil
15
- rescue StandardError
16
- nil
18
+ if EssentiaBpmDetector.available?
19
+ essentia_result = EssentiaBpmDetector.detect(file_path)
20
+ return essentia_result[:bpm] if essentia_result && essentia_result[:confidence] > CONFIDENCE_THRESHOLD
21
+ end
22
+
23
+ PercivalBpmDetector.detect(file_path) if PercivalBpmDetector.available?
17
24
  end
18
25
  end
19
26
  end
data/lib/wavesync/cli.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require_relative 'commands'
4
5
 
5
6
  module Wavesync
6
7
  class CLI
8
+ #: () -> void
7
9
  def self.start
8
10
  command_name = ARGV.first && !ARGV.first.start_with?('-') ? ARGV.shift : 'sync'
9
11
  command_class = Commands::ALL.find { |cmd| command_name == cmd.name }
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require 'optparse'
4
5
 
@@ -11,6 +12,7 @@ module Wavesync
11
12
  self.description = 'Detect and write BPM metadata to library tracks'
12
13
  self.options = [FORCE_OPTION].freeze
13
14
 
15
+ #: () -> void
14
16
  def run
15
17
  options, config = parse_options(banner: 'Usage: wavesync analyze [options]') do |opts, opts_hash|
16
18
  opts.on(*FORCE_OPTION.to_a) { opts_hash[:overwrite] = true }
@@ -13,8 +13,9 @@ module Wavesync
13
13
  def subcommands = @subcommands || []
14
14
  end
15
15
 
16
+ #: (banner: String) ?{ (OptionParser, Hash[Symbol, untyped]) -> void } -> [Hash[Symbol, untyped], Config]
16
17
  def parse_options(banner:)
17
- options = {}
18
+ options = {} #: Hash[Symbol, untyped]
18
19
  OptionParser.new do |opts|
19
20
  opts.banner = banner
20
21
  opts.on(*CONFIG_OPTION.to_a) { |value| options[:config] = value }
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require 'optparse'
4
5
  require 'rainbow'
@@ -11,6 +12,7 @@ module Wavesync
11
12
 
12
13
  DESCRIPTION_COLUMN = 23
13
14
 
15
+ #: () -> void
14
16
  def run
15
17
  subcommand_name = ARGV.shift
16
18
 
@@ -30,6 +32,7 @@ module Wavesync
30
32
 
31
33
  private
32
34
 
35
+ #: () -> void
33
36
  def show_general_help
34
37
  puts 'Usage: wavesync [command] [options]'
35
38
  puts ''
@@ -47,6 +50,7 @@ module Wavesync
47
50
  GLOBAL_OPTIONS.each { |option| puts format_option_line(option, indent: 2) }
48
51
  end
49
52
 
53
+ #: (untyped command) -> void
50
54
  def show_command_help(command)
51
55
  if command.subcommands.any?
52
56
  puts "Usage: wavesync #{command.name} <subcommand> [options]"
@@ -68,10 +72,12 @@ module Wavesync
68
72
  end
69
73
  end
70
74
 
75
+ #: (String name, String description) -> String
71
76
  def format_command_line(name, description)
72
77
  " #{name.ljust(DESCRIPTION_COLUMN - 2)}#{description}"
73
78
  end
74
79
 
80
+ #: (untyped option, indent: Integer) -> String
75
81
  def format_option_line(option, indent:)
76
82
  key = "#{option.short}, #{option.long}"
77
83
  Rainbow("#{' ' * indent}#{key.ljust(DESCRIPTION_COLUMN - indent)}#{option.description}").darkgray
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require 'optparse'
4
5
 
@@ -12,6 +13,7 @@ module Wavesync
12
13
  Subcommand.new(usage: 'set list', description: 'List all track sets')
13
14
  ].freeze
14
15
 
16
+ #: () -> void
15
17
  def run
16
18
  subcommand = ARGV.shift
17
19
 
@@ -50,6 +52,7 @@ module Wavesync
50
52
 
51
53
  private
52
54
 
55
+ #: (String subcommand) -> String
53
56
  def require_name(subcommand)
54
57
  name = ARGV.shift
55
58
  unless name
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require 'optparse'
4
5
 
@@ -12,6 +13,7 @@ module Wavesync
12
13
  self.description = 'Sync music library to a device'
13
14
  self.options = [DEVICE_OPTION, PAD_OPTION].freeze
14
15
 
16
+ #: () -> void
15
17
  def run
16
18
  options, config = parse_options(banner: 'Usage: wavesync sync [options]') do |opts, opts_hash|
17
19
  opts.on(*DEVICE_OPTION.to_a) { |value| opts_hash[:device] = value }
@@ -40,11 +42,13 @@ module Wavesync
40
42
  exit 1
41
43
  end
42
44
  [device_config, device]
43
- end
45
+ end #: Array[untyped]
44
46
 
45
47
  scanner = Wavesync::Scanner.new(config.library)
46
48
 
47
- device_pairs.each do |device_config, device|
49
+ device_pairs.each do |pair|
50
+ device_config = pair[0] #: { name: String, model: String, path: String }
51
+ device = pair[1] #: Wavesync::Device
48
52
  scanner.sync(device_config[:path], device, pad: options[:pad] || false)
49
53
  end
50
54
  end