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 config/devices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ devices:
- 44100
file_types:
- mp3
transliterate_metadata: true
47 changes: 47 additions & 0 deletions lib/wavesync/audio.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,46 @@ def write_cue_points(cue_points)
FileUtils.mv(temp_path, @file_path)
end

ID3V2_FRAME_TITLE = 'TIT2'
ID3V2_FRAME_ARTIST = 'TPE1'
ID3V2_FRAME_ALBUM = 'TALB'
ID3V2_FRAME_ALBUM_ARTIST = 'TPE2'
ID3V2_FRAME_GENRE = 'TCON'
ID3V2_FRAME_COMPOSER = 'TCOM'
ID3V2_FRAME_ENCODED_BY = 'TENC'
ID3V2_FRAME_COMPILATION = 'TCMP'

COMBINING_MARKS = /\p{Mn}/

TRANSLITERATE_FRAME_IDS = [
ID3V2_FRAME_TITLE,
ID3V2_FRAME_ARTIST,
ID3V2_FRAME_ALBUM,
ID3V2_FRAME_ALBUM_ARTIST,
ID3V2_FRAME_GENRE,
ID3V2_FRAME_COMPOSER,
ID3V2_FRAME_ENCODED_BY,
ID3V2_FRAME_COMPILATION
].freeze

#: () -> void
def transliterate_tags
return unless @file_ext == '.mp3'

TagLib::MPEG::File.open(@file_path) do |file|
tag = file.id3v2_tag
next if tag.nil?

TRANSLITERATE_FRAME_IDS.each do |frame_id|
tag.frame_list(frame_id).each do |frame|
frame.text = transliterate(frame.to_string)
end
end

file.save
end
end

#: (String | Integer | Float bpm) -> void
def write_bpm(bpm)
case @file_ext
Expand Down Expand Up @@ -222,5 +262,12 @@ def write_id3v2_bpm(tag, bpm)
frame.text = bpm.to_s
tag.add_frame(frame)
end

#: (String string) -> String
def transliterate(string)
string
.unicode_normalize(:nfd)
.gsub(COMBINING_MARKS, '')
end
end
end
9 changes: 6 additions & 3 deletions lib/wavesync/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ class Device
attr_reader :bpm_source #: Symbol?
attr_reader :bar_multiple #: Integer?
attr_reader :unsupported_characters #: Array[String]
attr_reader :transliterate_metadata #: bool

#: (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String]) -> void
def initialize(name:, sample_rates:, file_types:, bit_depths: [], bpm_source: nil, bar_multiple: nil, unsupported_characters: [])
#: (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String], ?transliterate_metadata: bool) -> void
def initialize(name:, sample_rates:, file_types:, bit_depths: [], bpm_source: nil, bar_multiple: nil, unsupported_characters: [], transliterate_metadata: false)
@name = name
@sample_rates = sample_rates
@bit_depths = bit_depths
@file_types = file_types
@bpm_source = bpm_source
@bar_multiple = bar_multiple
@unsupported_characters = unsupported_characters
@transliterate_metadata = transliterate_metadata
end

#: () -> String
Expand Down Expand Up @@ -49,7 +51,8 @@ def self.load_from_yaml
file_types: attrs['file_types'],
bpm_source: attrs['bpm_source']&.to_sym,
bar_multiple: attrs['bar_multiple'],
unsupported_characters: attrs['unsupported_characters'] || []
unsupported_characters: attrs['unsupported_characters'] || [],
transliterate_metadata: attrs['transliterate_metadata'] || false
)
end
end
Expand Down
11 changes: 10 additions & 1 deletion lib/wavesync/scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def sync(target_library_path, device, pad: false)
before_transcode: -> { @ui.conversion_progress(source_format, target_format) }) do |local_temp_path|
inject_acid_bpm(local_temp_path, bpm, device)
inject_cue_points(local_temp_path, audio, source_format, target_format)
inject_transliterated_metadata(local_temp_path, device)
end
path_resolver.resolve(file, audio, target_file_type: target_format.file_type)
else
Expand All @@ -76,7 +77,8 @@ def sync(target_library_path, device, pad: false)
end
else
copied = copy_file(audio, file, path_resolver)
path_resolver.resolve(file, audio)
target_path = path_resolver.resolve(file, audio)
inject_transliterated_metadata(target_path.to_s, device) if copied
end
@ui.copy(source_format)
end
Expand Down Expand Up @@ -116,6 +118,13 @@ def copy_file(audio, source_file_path, path_resolver)
end
end

#: (String path, Device device) -> void
def inject_transliterated_metadata(path, device)
return unless device.transliterate_metadata

Audio.new(path).transliterate_tags
end

#: (String local_temp_path, (Integer | Float | String)? bpm, Device device) -> void
def inject_acid_bpm(local_temp_path, bpm, device)
return unless device.bpm_source == :acid_chunk && bpm && File.extname(local_temp_path).downcase == '.wav'
Expand Down
30 changes: 28 additions & 2 deletions sig/generated/wavesync/audio.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ module Wavesync
# : (Array[{identifier: Integer, sample_offset: Integer, label: String?}] cue_points) -> void
def write_cue_points: (Array[{ identifier: Integer, sample_offset: Integer, label: String? }] cue_points) -> void

ID3V2_FRAME_TITLE: ::String

ID3V2_FRAME_ARTIST: ::String

ID3V2_FRAME_ALBUM: ::String

ID3V2_FRAME_ALBUM_ARTIST: ::String

ID3V2_FRAME_GENRE: ::String

ID3V2_FRAME_COMPOSER: ::String

ID3V2_FRAME_ENCODED_BY: ::String

ID3V2_FRAME_COMPILATION: ::String

COMBINING_MARKS: ::Regexp

TRANSLITERATE_FRAME_IDS: untyped

# : () -> void
def transliterate_tags: () -> void

# : (String | Integer | Float bpm) -> void
def write_bpm: (String | Integer | Float bpm) -> void

Expand All @@ -39,8 +62,8 @@ module Wavesync

private

# : (Integer? target_sample_rate, Integer? target_bit_depth, ?Numeric? padding_seconds) -> Hash[Symbol, untyped]
def build_transcode_options: (Integer? target_sample_rate, Integer? target_bit_depth, ?Numeric? padding_seconds) -> Hash[Symbol, untyped]
# : (String target_file_type, Integer? target_bit_depth) -> String
def transcode_codec: (String target_file_type, Integer? target_bit_depth) -> String

# : () -> (String | Integer)?
def bpm_from_file: () -> (String | Integer)?
Expand Down Expand Up @@ -80,5 +103,8 @@ module Wavesync

# : (untyped tag, String | Integer | Float bpm) -> void
def write_id3v2_bpm: (untyped tag, String | Integer | Float bpm) -> void

# : (String string) -> String
def transliterate: (String string) -> String
end
end
6 changes: 4 additions & 2 deletions sig/generated/wavesync/device.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ module Wavesync

attr_reader unsupported_characters: Array[String]

# : (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String]) -> void
def initialize: (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String]) -> void
attr_reader transliterate_metadata: bool

# : (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String], ?transliterate_metadata: bool) -> void
def initialize: (name: String, sample_rates: Array[Integer], file_types: Array[String], ?bit_depths: Array[Integer], ?bpm_source: Symbol?, ?bar_multiple: Integer?, ?unsupported_characters: Array[String], ?transliterate_metadata: bool) -> void

# : () -> String
def self.config_path: () -> String
Expand Down
3 changes: 3 additions & 0 deletions sig/generated/wavesync/scanner.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ module Wavesync
# : (Audio audio, String source_file_path, PathResolver path_resolver) -> bool
def copy_file: (Audio audio, String source_file_path, PathResolver path_resolver) -> bool

# : (String path, Device device) -> void
def inject_transliterated_metadata: (String path, Device device) -> void

# : (String local_temp_path, (Integer | Float | String)? bpm, Device device) -> void
def inject_acid_bpm: (String local_temp_path, (Integer | Float | String)? bpm, Device device) -> void

Expand Down
12 changes: 6 additions & 6 deletions sig/generated/wavesync/set_editor.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ module Wavesync
# : (Float semitones) -> String
def format_pitch_shift: (Float semitones) -> String

# : () -> void
def kill_player: () -> void

# : () -> void
def check_player: () -> void

private

# : () -> void
Expand Down Expand Up @@ -105,9 +99,15 @@ module Wavesync
# : () -> void
def jump_to_next_cue: () -> void

# : () -> void
def kill_player: () -> void

# : () -> void
def stop_playback: () -> void

# : () -> void
def check_player: () -> void

# : () -> void
def advance_and_play: () -> void

Expand Down
1 change: 1 addition & 0 deletions sig/taglib.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module TagLib
class TextIdentificationFrame
def initialize: (::String id, untyped encoding) -> void
def text=: (::String text) -> ::String
def to_string: () -> ::String
end

class Tag
Expand Down
70 changes: 70 additions & 0 deletions test/wavesync/audio_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,52 @@ class AudioTest < Wavesync::TestCase
assert_nil audio('44100_16.aiff').bpm
end

test 'transliterate_tags replaces umlauts across all supported id3v2 frames for mp3' do
with_temp_copy('44100.mp3') do |path|
write_id3v2_frames(
path,
'TIT2' => 'Jóga',
'TPE1' => 'Björk',
'TALB' => 'Åström Remixes',
'TPE2' => 'Sigur Rós',
'TCON' => 'Électronique',
'TCOM' => 'Jean-Michel Jarré',
'TENC' => 'Encöder',
'TCMP' => '1'
)

Audio.new(path).transliterate_tags

frames = read_id3v2_frames(path)
assert_equal 'Joga', frames['TIT2']
assert_equal 'Bjork', frames['TPE1']
assert_equal 'Astrom Remixes', frames['TALB']
assert_equal 'Sigur Ros', frames['TPE2']
assert_equal 'Electronique', frames['TCON']
assert_equal 'Jean-Michel Jarre', frames['TCOM']
assert_equal 'Encoder', frames['TENC']
assert_equal '1', frames['TCMP']
end
end

test 'transliterate_tags leaves ascii-only tags unchanged' do
with_temp_copy('44100.mp3') do |path|
write_id3v2_frames(path, 'TPE1' => 'Aphex Twin', 'TIT2' => 'Windowlicker')

Audio.new(path).transliterate_tags

frames = read_id3v2_frames(path)
assert_equal 'Aphex Twin', frames['TPE1']
assert_equal 'Windowlicker', frames['TIT2']
end
end

test 'transliterate_tags is a no-op for non-mp3 files' do
with_temp_copy('44100_16.wav') do |path|
Audio.new(path).transliterate_tags
end
end

test 'write_bpm round-trips for wav via acid chunk' do
with_temp_copy('44100_16.wav') do |path|
Audio.new(path).write_bpm(128)
Expand Down Expand Up @@ -155,6 +201,30 @@ def audio(name)
Audio.new(File.join(FIXTURES_PATH, name))
end

def write_id3v2_frames(path, frames)
TagLib::MPEG::File.open(path) do |file|
tag = file.id3v2_tag(true)
frames.each do |frame_id, text|
tag.remove_frames(frame_id)
frame = TagLib::ID3v2::TextIdentificationFrame.new(frame_id, TagLib::String::UTF8)
frame.text = text
tag.add_frame(frame)
end
file.save
end
end

def read_id3v2_frames(path)
result = {}
TagLib::MPEG::File.open(path) do |file|
tag = file.id3v2_tag
tag.frame_list.each do |frame|
result[frame.frame_id] = frame.to_string
end
end
result
end

def with_temp_copy(name)
ext = File.extname(name)
tmp = Tempfile.new(['audio_test', ext])
Expand Down
14 changes: 14 additions & 0 deletions test/wavesync/device_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ class DeviceTest < Wavesync::TestCase
octatrack = Device.find_by(name: 'Octatrack')
assert_includes octatrack.unsupported_characters, '"'
end
test 'Playdate has transliterate_metadata enabled' do
playdate = Device.find_by(name: 'Playdate')
assert_equal true, playdate.transliterate_metadata
end

test 'transliterate_metadata defaults to false' do
device = Device.new(
name: 'Test',
sample_rates: [44_100],
file_types: ['wav']
)
assert_equal false, device.transliterate_metadata
end

test 'unsupported_characters defaults to empty array' do
device = Device.new(
name: 'Test',
Expand Down
Loading