wavesync 1.0.0.alpha2 → 1.0.0.alpha4

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +95 -59
  3. data/config/devices.yml +20 -0
  4. data/lib/wavesync/acid_chunk.rb +45 -4
  5. data/lib/wavesync/analyzer.rb +17 -4
  6. data/lib/wavesync/audio.rb +153 -90
  7. data/lib/wavesync/audio_format.rb +9 -2
  8. data/lib/wavesync/bpm_detector.rb +14 -7
  9. data/lib/wavesync/cli.rb +3 -0
  10. data/lib/wavesync/commands/analyze.rb +2 -0
  11. data/lib/wavesync/commands/clear_cache.rb +75 -0
  12. data/lib/wavesync/commands/command.rb +4 -1
  13. data/lib/wavesync/commands/help.rb +6 -0
  14. data/lib/wavesync/commands/pull.rb +43 -0
  15. data/lib/wavesync/commands/setlist.rb +66 -0
  16. data/lib/wavesync/commands/sync.rb +19 -26
  17. data/lib/wavesync/commands.rb +52 -12
  18. data/lib/wavesync/config.rb +43 -3
  19. data/lib/wavesync/cue_chunk.rb +203 -0
  20. data/lib/wavesync/device.rb +32 -7
  21. data/lib/wavesync/essentia_bpm_detector.rb +38 -0
  22. data/lib/wavesync/ffmpeg/probe.rb +74 -0
  23. data/lib/wavesync/ffmpeg.rb +144 -0
  24. data/lib/wavesync/file_converter.rb +7 -2
  25. data/lib/wavesync/libmtp.rb +333 -0
  26. data/lib/wavesync/logger.rb +84 -0
  27. data/lib/wavesync/path_resolver.rb +32 -6
  28. data/lib/wavesync/percival_bpm_detector.rb +31 -0
  29. data/lib/wavesync/python_venv.rb +25 -0
  30. data/lib/wavesync/scanner.rb +143 -27
  31. data/lib/wavesync/{set.rb → setlist.rb} +28 -12
  32. data/lib/wavesync/setlist_editor.rb +556 -0
  33. data/lib/wavesync/track_padding.rb +15 -4
  34. data/lib/wavesync/transport/filesystem.rb +36 -0
  35. data/lib/wavesync/transport/mtp.rb +285 -0
  36. data/lib/wavesync/transport.rb +21 -0
  37. data/lib/wavesync/ui.rb +67 -12
  38. data/lib/wavesync/version.rb +2 -1
  39. data/lib/wavesync.rb +7 -2
  40. metadata +17 -32
  41. data/lib/wavesync/commands/set.rb +0 -63
  42. data/lib/wavesync/set_editor.rb +0 -245
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 166c4643be69e020ac6ac73611146502faa4891ad35f60bd30f3143702113c14
4
- data.tar.gz: f41b3be18c7f4014f892a19e27612a363502d1f70effe098e74d239b19d9d873
3
+ metadata.gz: 3b3c24cfdb7c1e1e6fe97157437f690995f59a9981b0771f42f1669af101a31a
4
+ data.tar.gz: 03c846e8a0c9e19d6a6b9f8a6cb72edc426a1db17dcd4cfdbcaa556c643a1eb1
5
5
  SHA512:
6
- metadata.gz: d1df257786183fa8253036ca6e59b7b559bab4d82fba2da101aa86c58e91f56af272215fd406785e05c8eec6cb473c22ea8d90ff9fd4367f6894ae9916aef54a
7
- data.tar.gz: b29f51c0a796aedddee42d219e042e02b150b89ec23775b40ca7c8d7e93b2b7c1dae438a240aa3e549e2e3f53318f3960d6305965d29a6199b29f62d99c1fa10
6
+ metadata.gz: 7f3ce9462f43a8c1fd453df2eb1919fdf6c21ef5f2efa95aec8effdb737309801f1c94b3ec31353a766e448f7c14148208628f4f0f80c86e0b19079fef6f6ac0
7
+ data.tar.gz: a51decb6dc314e43c134fa11b5f0332eddbde5616b2051bafa645861b386dc965f88f0d0725fc9a435d5d3206f1e8af8ad38d26b204eb49072b5e2a98daaa763
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), the [Elektron Octatrack MKII](https://www.elektron.se/explore/octatrack-mkii), and the [Playdate](https://kicooya.com/), 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,23 +19,43 @@ 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 libmtp (only required for syncing to TP-7)
48
+
49
+ ```bash
50
+ brew install libmtp
51
+ ```
52
+
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.
38
54
 
39
- https://teenage.engineering/guides/fieldkit
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.
40
59
 
41
60
  ## Configuration
42
61
 
@@ -49,102 +68,119 @@ library: ~/Music/Library
49
68
  devices:
50
69
  - name: TP-7
51
70
  model: TP-7
52
- path: ~/Library/Containers/engineering.teenage.fieldkit/Data/Documents/TP-7 MTP Device-F1ELN21A/library
71
+ transport: mtp
72
+ path: library
53
73
  - name: Octatrack
54
74
  model: Octatrack
55
75
  path: /Volumes/OCTATRACK/LIBRARY/AUDIO
76
+ - name: Playdate
77
+ model: Playdate
78
+ path: /Volumes/PLAYDATE/Data/com.abenokobo.kicooya/media/music
56
79
  ```
57
80
 
58
81
  - `library`: path to your source music library
59
82
  - `devices`: list of devices to sync to, each with:
60
83
  - `name`: a label for this device, used with the `-d` command-line option
61
- - `model`: device model (`TP-7` or `Octatrack`)
62
- - `path`: path to the device's library directory
84
+ - `model`: device model (`TP-7`, `Octatrack`, or `Playdate`)
85
+ - `path`: where to write files on the device
86
+ - For `transport: filesystem` (default): a path on your local filesystem (e.g. a mounted USB volume).
87
+ - For `transport: mtp`: a folder path inside the device (e.g. `library` for the TP-7).
88
+ - `transport` (optional): `filesystem` (default) or `mtp`. Use `mtp` for the TP-7.
89
+ - `mp3_bitrate` (optional): bitrate in kbps to use when encoding MP3 files for this device. Accepted values: `96`, `128`, `160`, `192`, `256`, `320`. Defaults to `192`. Source files that are already MP3 are copied as-is regardless of this setting.
90
+
91
+ When syncing over MTP, wavesync caches converted files in `~/.cache/wavesync/<device-name>/` so subsequent syncs only push files that aren't already on the device.
63
92
 
64
- Wavesync will exit with an error if a device model in the config is not supported.
93
+ ## Usage
65
94
 
66
- ## Help
95
+ ### Help
67
96
 
68
97
  ```bash
98
+ # List all available commands
69
99
  wavesync help
70
100
  ```
71
101
 
72
- ## Usage
102
+ ### Analyze
103
+
104
+ ```bash
105
+ # Analyze library files for BPM and write results to file metadata
106
+ # Files that already have BPM set are skipped
107
+ wavesync analyze
108
+
109
+ # Overwrite existing BPM values
110
+ wavesync analyze -f
111
+ ```
112
+
113
+ ### Sync
73
114
 
74
115
  ```bash
75
- # Sync library (uses default config at ~/wavesync.yml)
116
+ # Sync library
76
117
  # If multiple devices are configured, you will be prompted to select one
77
118
  wavesync sync
78
119
 
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)
120
+ # Sync to a specific device (by name as defined in config)
83
121
  wavesync sync -d Octatrack
84
122
 
85
123
  # Pad each track with silence so its total length aligns to a multiple of 64 bars
86
124
  # (Octatrack only — requires BPM metadata on each track)
87
125
  wavesync sync -p
126
+ ```
88
127
 
89
- # Analyze library files for BPM and write results to file metadata
90
- # Files that already have BPM set are skipped
91
- wavesync analyze
128
+ 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.
92
129
 
93
- # Overwrite existing BPM values
94
- wavesync analyze --force
130
+ ### Pull
95
131
 
96
- # Analyze with a specific config path
97
- wavesync analyze -c /path/to/wavesync.yml
132
+ ```bash
133
+ # Read cue points from the device's WAV files and write them back into the
134
+ # matching source files in your library. Useful when you've added or edited
135
+ # cue points on the device and want them reflected in the source library.
136
+ # If multiple devices are configured, you will be prompted to select one.
137
+ wavesync pull
138
+
139
+ # Pull from a specific device (by name as defined in config)
140
+ wavesync pull -d TP-7
98
141
  ```
99
142
 
100
- ## Sets
143
+ Only WAV source files with matching WAV target files on the device are updated. If the source already has the same cue points as the device, it is left untouched.
144
+
145
+ ### Setlists (experimental)
101
146
 
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.
147
+ A setlist is a named, ordered selection of tracks from your library. Setlists are stored as YAML files inside a `.setlists` folder within the library directory. Syncing setlists to devices is not yet implemented.
103
148
 
104
149
  ```bash
105
- # Create a new set and open the interactive editor
106
- wavesync set create <name>
150
+ # Create a new setlist and open the interactive editor
151
+ wavesync setlist create NAME
107
152
 
108
- # Edit an existing set
109
- wavesync set edit <name>
153
+ # Edit an existing setlist
154
+ wavesync setlist edit NAME
110
155
 
111
- # List all sets
112
- wavesync set list
156
+ # List all setlists
157
+ wavesync setlist list
113
158
  ```
114
159
 
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`.
160
+ ## Development
129
161
 
130
- ## Sample Rate Selection
162
+ ### Running Tests, RuboCop, and Type Checks
131
163
 
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.
164
+ ```bash
165
+ rake
166
+ ```
133
167
 
134
- Example: If a 96kHz file is synced to an Octatrack (which only supports 44.1kHz), it will be downsampled to 44.1kHz.
168
+ This runs RuboCop (with auto-fix), Steep type checks, and the test suite in sequence.
135
169
 
136
- ## Development
170
+ ### Running Integration Tests
137
171
 
138
- ### Running Tests
172
+ Integration tests sync against real connected devices and are not run as part of the default `rake` task.
139
173
 
140
174
  ```bash
141
- rake test
175
+ rake test:integration
142
176
  ```
143
177
 
144
- ### Running RuboCop
178
+ ### Regenerating Test Fixtures
179
+
180
+ Test fixtures are pre-generated audio files committed to the repository. To regenerate them:
145
181
 
146
182
  ```bash
147
- bundle exec rubocop
183
+ rake fixtures:generate
148
184
  ```
149
185
 
150
186
  ### Releasing
data/config/devices.yml CHANGED
@@ -1,22 +1,29 @@
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:
12
14
  - wav
13
15
  - mp3
14
16
  bpm_source: :acid_chunk
17
+ unsupported_characters:
18
+ - "™"
19
+ - '"'
20
+ uppercase_paths: true
15
21
 
16
22
  - name: Octatrack
17
23
  sample_rates:
18
24
  - 44100
19
25
  bit_depths:
26
+ - 8
20
27
  - 16
21
28
  - 24
22
29
  file_types:
@@ -25,3 +32,16 @@ devices:
25
32
  - aif
26
33
  bpm_source: :filename
27
34
  bar_multiple: 64
35
+ unsupported_characters:
36
+ - "™"
37
+ - '"'
38
+ - "’"
39
+ - ":"
40
+ - "?"
41
+
42
+ - name: Playdate
43
+ sample_rates:
44
+ - 44100
45
+ file_types:
46
+ - mp3
47
+ transliterate_metadata: true
@@ -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,43 @@ module Wavesync
47
49
  nil
48
50
  end
49
51
 
52
+ #: (String filepath, Integer | Float | String new_bpm) -> void
53
+ def self.write_bpm_in_place(filepath, new_bpm)
54
+ bpm_bytes = [new_bpm.to_f].pack(FLOAT32_LITTLE_ENDIAN)
55
+
56
+ File.open(filepath, 'r+b') do |file|
57
+ file.seek(RIFF_HEADER_SIZE)
58
+
59
+ acid_chunk_found = false
60
+
61
+ until file.eof?
62
+ chunk_id = file.read(CHUNK_ID_SIZE)
63
+ break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
64
+
65
+ chunk_size_bytes = file.read(CHUNK_SIZE_FIELD_SIZE) || ''
66
+ chunk_size = (chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
67
+
68
+ if chunk_id == ACID_CHUNK_ID
69
+ file.seek(ACID_TEMPO_OFFSET, IO::SEEK_CUR)
70
+ file.write(bpm_bytes)
71
+ acid_chunk_found = true
72
+ break
73
+ else
74
+ padding = chunk_size.odd? ? 1 : 0
75
+ file.seek(chunk_size + padding, IO::SEEK_CUR)
76
+ end
77
+ end
78
+
79
+ unless acid_chunk_found
80
+ file.seek(0, IO::SEEK_END)
81
+ create_acid_chunk(file, new_bpm.to_f)
82
+ end
83
+ end
84
+
85
+ update_riff_size(filepath)
86
+ end
87
+
88
+ #: (String source_filepath, String output_filepath, Integer | Float | String new_bpm) -> void
50
89
  def self.write_bpm(source_filepath, output_filepath, new_bpm)
51
90
  bpm_bytes = [new_bpm.to_f].pack(FLOAT32_LITTLE_ENDIAN)
52
91
  acid_chunk_found = false
@@ -60,8 +99,8 @@ module Wavesync
60
99
  chunk_id = input.read(CHUNK_ID_SIZE)
61
100
  break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE
62
101
 
63
- chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE)
64
- chunk_size = chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN)
102
+ chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE) || ''
103
+ chunk_size = (chunk_size_bytes.unpack1(UINT32_LITTLE_ENDIAN) || 0).to_i
65
104
 
66
105
  output.write(chunk_id)
67
106
  output.write(chunk_size_bytes)
@@ -94,6 +133,7 @@ module Wavesync
94
133
  update_riff_size(output_filepath)
95
134
  end
96
135
 
136
+ #: (untyped output, Float bpm) -> void
97
137
  def self.create_acid_chunk(output, bpm)
98
138
  # Write ACID chunk ID
99
139
  output.write(ACID_CHUNK_ID)
@@ -125,6 +165,7 @@ module Wavesync
125
165
  output.write([bpm].pack(FLOAT32_LITTLE_ENDIAN))
126
166
  end
127
167
 
168
+ #: (String filepath) -> void
128
169
  def self.update_riff_size(filepath)
129
170
  File.open(filepath, 'r+b') do |file|
130
171
  file.seek(0, IO::SEEK_END)
@@ -1,23 +1,32 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative 'logger'
2
5
 
3
6
  module Wavesync
4
7
  class Analyzer
5
8
  CONFIRM_MESSAGE = 'wavesync analyze will add bpm meta data to files in library. Continue? [y/N] '
9
+ SETUP_INSTRUCTIONS = 'brew install python@3.11 && python3.11 -m venv ~/.wavesync-venv && ~/.wavesync-venv/bin/pip install essentia'
6
10
 
11
+ #: (String library_path) -> void
7
12
  def initialize(library_path)
8
- @library_path = File.expand_path(library_path)
9
- @audio_files = find_audio_files
10
- @ui = UI.new
13
+ @library_path = File.expand_path(library_path) #: String
14
+ Logger.configure(@library_path)
15
+ @audio_files = find_audio_files #: Array[String]
16
+ @ui = UI.new #: UI
11
17
  end
12
18
 
19
+ #: (?overwrite: bool) -> void
13
20
  def analyze(overwrite: false)
14
21
  unless BpmDetector.available?
15
- puts 'Error: bpm-tools or ffmpeg is not installed. Install with: brew install bpm-tools ffmpeg'
22
+ puts "Error: essentia is not installed. Set up the Python venv with:\n #{SETUP_INSTRUCTIONS}"
16
23
  exit 1
17
24
  end
18
25
 
19
26
  return unless @ui.confirm(CONFIRM_MESSAGE)
20
27
 
28
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+
21
30
  @audio_files.each_with_index do |file, index|
22
31
  audio = Audio.new(file)
23
32
 
@@ -32,10 +41,14 @@ module Wavesync
32
41
  @ui.analyze_progress(index, @audio_files.size)
33
42
  @ui.analyze_result(file, bpm)
34
43
  end
44
+
45
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
46
+ Logger.log_run_time(elapsed)
35
47
  end
36
48
 
37
49
  private
38
50
 
51
+ #: () -> Array[String]
39
52
  def find_audio_files
40
53
  Audio.find_all(@library_path).sort
41
54
  end