Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ devices:
- For `transport: filesystem` (default): a path on your local filesystem (e.g. a mounted USB volume).
- For `transport: mtp`: a folder path inside the device (e.g. `library` for the TP-7).
- `transport` (optional): `filesystem` (default) or `mtp`. Use `mtp` for the TP-7.
- `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.

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.

Expand Down
8 changes: 4 additions & 4 deletions lib/wavesync/audio.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,14 @@ def write_bpm(bpm)
@bpm = bpm
end

#: (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
def transcode(target_path, target_sample_rate: nil, target_file_type: nil, target_bit_depth: nil, padding_seconds: nil, metadata: {})
#: (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String], ?target_bitrate: Integer) ?{ (String) -> void } -> bool
def transcode(target_path, target_sample_rate: nil, target_file_type: nil, target_bit_depth: nil, padding_seconds: nil, metadata: {}, target_bitrate: 192)
ext = target_file_type || @file_ext.delete_prefix('.')
temp_path = File.join(Dir.tmpdir, "wavesync_transcode_#{SecureRandom.hex}.#{ext}")

begin
command = Wavesync::FFMPEG.new.input(@file_path).audio_codec(transcode_codec(ext, target_bit_depth))
command.audio_bitrate('192k') if ext == 'mp3'
command.audio_bitrate("#{target_bitrate}k") if ext == 'mp3'
command.sample_rate(target_sample_rate) if target_sample_rate
if padding_seconds&.positive?
total_duration = @audio.duration + padding_seconds
Expand All @@ -159,7 +159,7 @@ def transcode(target_path, target_sample_rate: nil, target_file_type: nil, targe
FileUtils.install(temp_path, target_path)
true
rescue Errno::ENOENT => e
Logger.log_error(e, call_site: 'Audio#transcode', arguments: { target_path:, target_sample_rate:, target_file_type:, target_bit_depth:, padding_seconds: })
Logger.log_error(e, call_site: 'Audio#transcode', arguments: { target_path:, target_sample_rate:, target_file_type:, target_bit_depth:, padding_seconds:, target_bitrate: })
false
ensure
FileUtils.rm_f(temp_path)
Expand Down
4 changes: 2 additions & 2 deletions lib/wavesync/commands/sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def run
pull_cue_points = options[:pull_cue_points] || false

device_pairs.each do |pair|
device_config = pair[0] #: { name: String, model: String, path: String, transport: String }
device_config = pair[0] #: { name: String, model: String, path: String, transport: String, mp3_bitrate: Integer }
device = pair[1] #: Wavesync::Device
transport = Wavesync::Transport.for(device_config)
with_mtp_retry(transport, device_config[:name]) do
Expand All @@ -61,7 +61,7 @@ def run
transport.begin_push!
end
begin
scanner.sync(transport.working_directory, device, pad: options[:pad] || false, pull_cue_points: pull_cue_points, staged: transport.is_a?(Wavesync::Transport::Mtp)) do |relative_path|
scanner.sync(transport.working_directory, device, pad: options[:pad] || false, pull_cue_points: pull_cue_points, staged: transport.is_a?(Wavesync::Transport::Mtp), mp3_bitrate: device_config[:mp3_bitrate]) do |relative_path|
transport.push_file!(relative_path)
end
ensure
Expand Down
30 changes: 27 additions & 3 deletions lib/wavesync/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ class Config
DEFAULT_PATH = File.join(Dir.home, 'wavesync.yml')

SUPPORTED_KEYS = %w[library devices].freeze
DEVICE_SUPPORTED_KEYS = %w[name model path transport].freeze
DEVICE_SUPPORTED_KEYS = %w[name model path transport mp3_bitrate].freeze
DEVICE_REQUIRED_KEYS = %w[name model path].freeze
SUPPORTED_TRANSPORTS = %w[filesystem mtp].freeze
SUPPORTED_MP3_BITRATES = [96, 128, 160, 192, 256, 320].freeze
DEFAULT_MP3_BITRATE = 192

attr_reader :library #: String
attr_reader :device_configs #: Array[{ name: String, model: String, path: String, transport: String }]
attr_reader :device_configs #: Array[{ name: String, model: String, path: String, transport: String, mp3_bitrate: Integer }]

#: (?String path) -> Config
def self.load(path = DEFAULT_PATH)
Expand All @@ -38,7 +40,13 @@ def initialize(data)
validate_device!(device, i)
transport = device['transport'] || 'filesystem'
path = transport == 'filesystem' ? File.expand_path(device['path']) : device['path']
{ name: device['name'], model: device['model'], path: path, transport: transport }
{
name: device['name'],
model: device['model'],
path: path,
transport: transport,
mp3_bitrate: device['mp3_bitrate'] || DEFAULT_MP3_BITRATE
}
end
end

Expand Down Expand Up @@ -75,12 +83,28 @@ def validate_device!(device, index)
raise ConfigError, "Device #{index + 1} '#{key}' must be a string" unless device[key].is_a?(String)
end

validate_device_transport!(device, index)
validate_device_mp3_bitrate!(device, index)
end

#: (Hash[String, untyped] device, Integer index) -> void
def validate_device_transport!(device, index)
return unless device.key?('transport')

raise ConfigError, "Device #{index + 1} 'transport' must be a string" unless device['transport'].is_a?(String)
return if SUPPORTED_TRANSPORTS.include?(device['transport'])

raise ConfigError, "Device #{index + 1} 'transport' must be one of: #{SUPPORTED_TRANSPORTS.join(', ')}"
end

#: (Hash[String, untyped] device, Integer index) -> void
def validate_device_mp3_bitrate!(device, index)
return unless device.key?('mp3_bitrate')

raise ConfigError, "Device #{index + 1} 'mp3_bitrate' must be an integer" unless device['mp3_bitrate'].is_a?(Integer)
return if SUPPORTED_MP3_BITRATES.include?(device['mp3_bitrate'])

raise ConfigError, "Device #{index + 1} 'mp3_bitrate' must be one of: #{SUPPORTED_MP3_BITRATES.join(', ')}"
end
end
end
5 changes: 3 additions & 2 deletions lib/wavesync/file_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module Wavesync
class FileConverter
DURATION_TOLERANCE_SECONDS = 0.5

#: (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
def convert(audio, source_file_path, path_resolver, source_format, target_format, padding_seconds: nil, before_transcode: nil, metadata: {}, &post_transcode)
#: (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String], ?mp3_bitrate: Integer) ?{ (String) -> void } -> bool
def convert(audio, source_file_path, path_resolver, source_format, target_format, padding_seconds: nil, before_transcode: nil, metadata: {}, mp3_bitrate: 192, &post_transcode)
needs_format_conversion = target_format.file_type || target_format.sample_rate || target_format.bit_depth
return false unless needs_format_conversion || padding_seconds&.positive?

Expand Down Expand Up @@ -36,6 +36,7 @@ def convert(audio, source_file_path, path_resolver, source_format, target_format
target_bit_depth: target_format.bit_depth || source_format.bit_depth,
padding_seconds: padding_seconds,
metadata: metadata,
target_bitrate: mp3_bitrate,
&post_transcode)

true
Expand Down
8 changes: 5 additions & 3 deletions lib/wavesync/scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def initialize(source_library_path)
@converter = FileConverter.new #: FileConverter
end

#: (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool) ?{ (String) -> void } -> void
def sync(target_library_path, device, pad: false, pull_cue_points: false, staged: false, &on_file_synced)
#: (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool, ?mp3_bitrate: Integer) ?{ (String) -> void } -> void
def sync(target_library_path, device, pad: false, pull_cue_points: false, staged: false, mp3_bitrate: 192, &on_file_synced)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
target_library_pathname = Pathname.new(target_library_path)
path_resolver = PathResolver.new(@source_library_path, target_library_path, device)
Expand All @@ -33,6 +33,7 @@ def sync(target_library_path, device, pad: false, pull_cue_points: false, staged

source_format = audio.format
target_format = device.target_format(source_format, file)
target_format = target_format.with(sample_rate: nil, bit_depth: nil) if source_format.file_type == 'mp3' && target_format.file_type.nil?

padding_seconds = nil #: Numeric?
original_bars = nil #: Integer?
Expand Down Expand Up @@ -64,7 +65,8 @@ def sync(target_library_path, device, pad: false, pull_cue_points: false, staged
converted = @converter.convert(audio, file, path_resolver, source_format, target_format,
padding_seconds: padding_seconds,
metadata: transliterated_metadata,
before_transcode: -> { @ui.conversion_progress(source_format, target_format) }) do |local_temp_path|
mp3_bitrate: mp3_bitrate,
before_transcode: -> { @ui.conversion_progress(source_format, target_format, mp3_bitrate) }) do |local_temp_path|
inject_acid_bpm(local_temp_path, bpm, device)
inject_cue_points(local_temp_path, audio, source_format, target_format)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/wavesync/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Wavesync
module Transport
SUPPORTED_KINDS = %w[filesystem mtp].freeze

#: ({ name: String, model: String, path: String, transport: String? } device_config) -> (Filesystem | Mtp)
#: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> (Filesystem | Mtp)
def self.for(device_config)
kind = device_config[:transport] || 'filesystem'
case kind
Expand Down
2 changes: 1 addition & 1 deletion lib/wavesync/transport/filesystem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Transport
class Filesystem
attr_reader :working_directory #: String

#: ({ name: String, model: String, path: String, transport: String? } device_config) -> void
#: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> void
def initialize(device_config)
@working_directory = device_config[:path]
end
Expand Down
2 changes: 1 addition & 1 deletion lib/wavesync/transport/mtp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def self.sanitize_dir_name(name)
sanitized.empty? ? 'device' : sanitized
end

#: ({ name: String, model: String, path: String, transport: String? } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
#: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
def initialize(device_config, libmtp: Libmtp.new, cache_root: DEFAULT_CACHE_ROOT)
@name = device_config[:name]
@device_path = device_config[:path]
Expand Down
14 changes: 6 additions & 8 deletions lib/wavesync/ui.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ def sync_progress(index, total_count, device)
sticky(parts.join(' '), 0)
end

#: (AudioFormat source_format, AudioFormat target_format) -> void
def conversion_progress(source_format, target_format)
#: (AudioFormat source_format, AudioFormat target_format, Integer mp3_bitrate) -> void
def conversion_progress(source_format, target_format, mp3_bitrate)
effective = source_format.merge(target_format)

source_info = audio_info(source_format)
target_info = target_audio_info(effective)
target_info = target_audio_info(effective, mp3_bitrate)

formatted_line = in_color(
"Converting #{source_format.file_type} (#{source_info}) ⇢ #{effective.file_type} (#{target_info})", :highlight
Expand Down Expand Up @@ -123,8 +123,6 @@ def clear
print @cursor.move_to(0, 0)
end

MP3_BITRATE_KBPS = 192

private

#: (AudioFormat format) -> String
Expand All @@ -136,9 +134,9 @@ def audio_info(format)
].compact.join('/')
end

#: (AudioFormat format) -> String
def target_audio_info(format)
quality = format.file_type == 'mp3' ? MP3_BITRATE_KBPS.to_s : format.bit_depth&.to_s
#: (AudioFormat format, Integer mp3_bitrate) -> String
def target_audio_info(format, mp3_bitrate)
quality = format.file_type == 'mp3' ? mp3_bitrate.to_s : format.bit_depth&.to_s
[
sample_rate_to_khz(format.sample_rate),
quality
Expand Down
4 changes: 2 additions & 2 deletions sig/generated/wavesync/audio.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ module Wavesync
# : (String | Integer | Float bpm) -> void
def write_bpm: (String | Integer | Float bpm) -> void

# : (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
def transcode: (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
# : (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String], ?target_bitrate: Integer) ?{ (String) -> void } -> bool
def transcode: (String target_path, ?target_sample_rate: Integer?, ?target_file_type: String?, ?target_bit_depth: Integer?, ?padding_seconds: Numeric?, ?metadata: Hash[String, String], ?target_bitrate: Integer) ?{ (String) -> void } -> bool

private

Expand Down
12 changes: 11 additions & 1 deletion sig/generated/wavesync/config.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ module Wavesync

SUPPORTED_TRANSPORTS: untyped

SUPPORTED_MP3_BITRATES: untyped

DEFAULT_MP3_BITRATE: ::Integer

attr_reader library: String

attr_reader device_configs: Array[{ name: String, model: String, path: String, transport: String }]
attr_reader device_configs: Array[{ name: String, model: String, path: String, transport: String, mp3_bitrate: Integer }]

# : (?String path) -> Config
def self.load: (?String path) -> Config
Expand All @@ -32,5 +36,11 @@ module Wavesync

# : (untyped device, Integer index) -> void
def validate_device!: (untyped device, Integer index) -> void

# : (Hash[String, untyped] device, Integer index) -> void
def validate_device_transport!: (Hash[String, untyped] device, Integer index) -> void

# : (Hash[String, untyped] device, Integer index) -> void
def validate_device_mp3_bitrate!: (Hash[String, untyped] device, Integer index) -> void
end
end
4 changes: 2 additions & 2 deletions sig/generated/wavesync/file_converter.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Wavesync
class FileConverter
DURATION_TOLERANCE_SECONDS: ::Float

# : (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
def convert: (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String]) ?{ (String) -> void } -> bool
# : (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String], ?mp3_bitrate: Integer) ?{ (String) -> void } -> bool
def convert: (Audio audio, String source_file_path, PathResolver path_resolver, AudioFormat source_format, AudioFormat target_format, ?padding_seconds: Numeric?, ?before_transcode: (^() -> void)?, ?metadata: Hash[String, String], ?mp3_bitrate: Integer) ?{ (String) -> void } -> bool
end
end
4 changes: 2 additions & 2 deletions sig/generated/wavesync/scanner.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module Wavesync
# : (String source_library_path) -> void
def initialize: (String source_library_path) -> void

# : (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool) ?{ (String) -> void } -> void
def sync: (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool) ?{ (String) -> void } -> void
# : (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool, ?mp3_bitrate: Integer) ?{ (String) -> void } -> void
def sync: (String target_library_path, Device device, ?pad: bool, ?pull_cue_points: bool, ?staged: bool, ?mp3_bitrate: Integer) ?{ (String) -> void } -> void

private

Expand Down
4 changes: 2 additions & 2 deletions sig/generated/wavesync/transport.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Wavesync
module Transport
SUPPORTED_KINDS: untyped

# : ({ name: String, model: String, path: String, transport: String? } device_config) -> (Filesystem | Mtp)
def self.for: ({ name: String, model: String, path: String, transport: String? } device_config) -> (Filesystem | Mtp)
# : ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> (Filesystem | Mtp)
def self.for: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> (Filesystem | Mtp)
end
end
4 changes: 2 additions & 2 deletions sig/generated/wavesync/transport/filesystem.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module Wavesync
class Filesystem
attr_reader working_directory: String

# : ({ name: String, model: String, path: String, transport: String? } device_config) -> void
def initialize: ({ name: String, model: String, path: String, transport: String? } device_config) -> void
# : ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> void
def initialize: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> void

# : () ?{ (Integer, Integer, String) -> void } -> void
def prepare!: () ?{ (Integer, Integer, String) -> void } -> void
Expand Down
4 changes: 2 additions & 2 deletions sig/generated/wavesync/transport/mtp.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ module Wavesync
# : (String name) -> String
def self.sanitize_dir_name: (String name) -> String

# : ({ name: String, model: String, path: String, transport: String? } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
def initialize: ({ name: String, model: String, path: String, transport: String? } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
# : ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
def initialize: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void

# : () ?{ (Integer, Integer, String) -> void } -> void
def prepare!: () ?{ (Integer, Integer, String) -> void } -> void
Expand Down
Loading
Loading