Skip to content

Commit

Permalink
Distinguish between specified and audio duration
Browse files Browse the repository at this point in the history
  • Loading branch information
codez committed Apr 20, 2020
1 parent 2371d52 commit 66eb4c7
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 50 deletions.
3 changes: 3 additions & 0 deletions TODO.md
@@ -1,5 +1,8 @@
# TODOs

* Adjust Composer to new algorithm
* Get clear about when to use specified and audio duration (consider troubleshooting possibilities)

## Code

* create script to manually import partial broadcasts
Expand Down
13 changes: 1 addition & 12 deletions app/services/import/broadcast_mapping/builder/base.rb
Expand Up @@ -14,8 +14,7 @@ class Base
attr_reader :recordings

def initialize(recordings)
check_intervals(recordings)
@recordings = recordings.sort_by(&:started_at)
@recordings = recordings.sort_by { |r| [r.started_at, -r.specified_duration] }
end

def run
Expand All @@ -39,16 +38,6 @@ def build_mappings
raise(NotImplementedError)
end

def check_intervals(recordings)
recordings.group_by(&:started_at).each do |time, variants|
durations = variants.collect(&:duration).uniq
unless durations.size == 1
raise(ArgumentError,
"Recordings at #{time} must all have the same durations: #{durations.inspect}.")
end
end
end

def add_corresponding_recordings(mapping)
recordings.each do |r|
mapping.add_recording_if_overlapping(r)
Expand Down
2 changes: 1 addition & 1 deletion app/services/import/recording/chooser.rb
Expand Up @@ -23,7 +23,7 @@ def by_audio_length
# using with_index and a sort_by array results in a stable sort,
# i.e. positions remain the same if the duration is equal.
variants.sort_by.with_index do |v, i|
duration = v.audio_duration > v.duration ? v.duration : v.audio_duration
duration = [v.specified_duration, v.audio_duration].min
[-(duration / DURATION_TOLERANCE.to_f).round, i]
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/services/import/recording/composer.rb
Expand Up @@ -118,7 +118,7 @@ def trim_overlapped(current)

def trim_to_maximum_duration(current)
@previous_finished_at = current.started_at +
[current.duration, current.audio_duration].min.seconds
[current.specified_duration, current.audio_duration].min.seconds
file_with_maximum_duration(current)
end

Expand All @@ -129,7 +129,7 @@ def trim_end

def file_with_maximum_duration(recording)
if recording.audio_duration_too_long?
trim_available(recording, 0, recording.duration)
trim_available(recording, 0, recording.specified_duration)
else
recording
end
Expand Down
10 changes: 5 additions & 5 deletions app/services/import/recording/file/base.rb
Expand Up @@ -4,7 +4,7 @@ module Import
module Recording
module File
# A single recorded audio file. Recordings may come from different
# recorders/import directories, but must have the same intervals (start
# recorders/import directories, but should have the same intervals (start
# time and duration).
class Base

Expand All @@ -16,15 +16,15 @@ class Base
self.imported_glob = '%%%' # try hard to produce no matches
self.lossy = false

attr_reader :started_at, :duration, :path, :broadcasts_mappings
attr_reader :started_at, :specified_duration, :path, :broadcasts_mappings

def initialize(path)
@path = path
@broadcasts_mappings = []
end

def finished_at
started_at + duration.seconds
started_at + specified_duration.seconds
end

def audio_finished_at
Expand All @@ -37,11 +37,11 @@ def audio_duration
end

def audio_duration_too_short?
audio_duration < duration - DURATION_TOLERANCE
audio_duration < specified_duration - DURATION_TOLERANCE
end

def audio_duration_too_long?
audio_duration > duration + DURATION_TOLERANCE
audio_duration > specified_duration + DURATION_TOLERANCE
end

def mark_imported
Expand Down
4 changes: 2 additions & 2 deletions app/services/import/recording/file/iso8601.rb
Expand Up @@ -25,8 +25,8 @@ def started_at
end

# in seconds
def duration
@duration ||= parse_duration(filename_parts[2])
def specified_duration
@specified_duration ||= parse_duration(filename_parts[2])
end

def mark_imported
Expand Down
4 changes: 2 additions & 2 deletions app/services/import/recording/file/mp3_rec.rb
Expand Up @@ -14,8 +14,8 @@ def started_at
@started_at ||= Time.zone.parse(filename_parts[1].tr('-', ' '))
end

def duration
@duration ||= filename_parts[2].to_i.seconds
def specified_duration
@specified_duration ||= filename_parts[2].to_i.seconds
end

def show_name
Expand Down
2 changes: 1 addition & 1 deletion app/services/import/recording/too_short_error.rb
Expand Up @@ -8,7 +8,7 @@ class TooShortError < StandardError

def initialize(recording)
super("Recording #{recording.path} has an audio duration of " \
"#{recording.audio_duration}s, where #{recording.duration}s were expected.")
"#{recording.audio_duration}s, where #{recording.specified_duration}s were expected.")
@recording = recording
end

Expand Down
68 changes: 61 additions & 7 deletions test/services/import/broadcast_mapping/builder/airtime_db_test.rb
Expand Up @@ -16,13 +16,6 @@ class Import::BroadcastMapping::Builder::AirtimeDbTest < ActiveSupport::TestCase
assert_equal [], builder.run
end

test 'recordings are checked for equal intervals' do
recordings = build_recordings('2016-01-01T235959+0100_120.mp3',
'2016-01-01T235959+0100_110.mp3',
'2016-01-02T000000+0100_119.mp3')
assert_raise(ArgumentError) { new_builder(recordings) }
end

test 'recordings are mapped to overlapping broadcasts' do
recordings = build_recordings('2016-01-01T100000+0100_060.mp3',
'2016-01-01T110000+0100_060.mp3',
Expand Down Expand Up @@ -81,6 +74,67 @@ class Import::BroadcastMapping::Builder::AirtimeDbTest < ActiveSupport::TestCase
becken_map.recordings.collect(&:path)
end

test 'multiple recordings are mapped to overlapping broadcasts' do
recordings = build_recordings('2016-01-01T090000+0100_060.mp3',
'2016-01-01T090000+0100_030.mp3',
'2016-01-01T093000+0100_060.mp3',
'2016-01-01T100000+0100_060.mp3',
'2016-01-01T103000+0100_030.mp3',
'2016-01-01T110000+0100_060.mp3',
'2016-01-01T110000+0100_090.mp3')
morgen = Airtime::Show.create!(name: 'Morgen', description: 'La mañana')
info = Airtime::Show.create!(name: 'Info', description: 'Rabe Info')
becken = Airtime::Show.create!(name: 'Klangbecken', description: 'Only Hits')
morgen.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 8),
ends: Time.zone.local(2016, 1, 1, 11),
created: Time.zone.now)
info.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 11),
ends: Time.zone.local(2016, 1, 1, 11, 30),
created: Time.zone.now)
becken.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 11, 30),
ends: Time.zone.local(2016, 1, 1, 13),
created: Time.zone.now)

builder = new_builder(recordings)
mappings = builder.run

assert_equal 3, mappings.size

morgen_map = mappings.first
assert_equal morgen.name, morgen_map.show.name
assert_equal morgen.description, morgen_map.show.details
assert morgen_map.show.persisted?
assert_equal morgen.name, morgen_map.broadcast.label
assert_equal morgen.description, morgen_map.broadcast.details
assert_equal Time.zone.local(2016, 1, 1, 8), morgen_map.broadcast.started_at
assert_equal Time.zone.local(2016, 1, 1, 11), morgen_map.broadcast.finished_at
assert morgen_map.broadcast.new_record?
assert_not morgen_map.complete?
assert_equal [file('2016-01-01T090000+0100_060.mp3'),
file('2016-01-01T090000+0100_030.mp3'),
file('2016-01-01T093000+0100_060.mp3'),
file('2016-01-01T100000+0100_060.mp3'),
file('2016-01-01T103000+0100_030.mp3')],
morgen_map.recordings.collect(&:path)

info_map = mappings.second
assert_equal info.name, info_map.show.name
assert_equal Time.zone.local(2016, 1, 1, 11), info_map.broadcast.started_at
assert info_map.complete?
assert_equal [file('2016-01-01T110000+0100_090.mp3'),
file('2016-01-01T110000+0100_060.mp3')],
info_map.recordings.collect(&:path)

becken_map = mappings.third
assert_equal becken.name, becken_map.show.name
assert_equal Time.zone.local(2016, 1, 1, 11, 30), becken_map.broadcast.started_at
assert_equal Time.zone.local(2016, 1, 1, 13), becken_map.broadcast.finished_at
assert_not becken_map.complete?
assert_equal [file('2016-01-01T110000+0100_090.mp3'),
file('2016-01-01T110000+0100_060.mp3')],
becken_map.recordings.collect(&:path)
end

test 'recordings are mapped to adjacent broadcasts' do
recordings = build_recordings('2016-01-01T090000+0100_060.mp3',
'2016-01-01T100000+0100_060.mp3',
Expand Down
18 changes: 10 additions & 8 deletions test/services/import/recording/composer_test.rb
Expand Up @@ -5,23 +5,25 @@
class Import::Recording::ComposerTest < ActiveSupport::TestCase

test 'returns single recording if times correspond to mapping' do
composer = build_composer('2013-06-12T200000+0200_120.mp3')
mock_duration(mapping.recordings.first.path, 120)
assert_equal '2013-06-12T200000+0200_120.mp3', composer.compose.path
file = '2013-06-12T200000+0200_120.mp3'
composer = build_composer(file)
mock_duration(file, 120)
assert_equal file, composer.compose.path
end

test 'returns trimmed single recording if it is longer' do
composer = build_composer('2013-06-12T200000+0200_120.mp3')
file = mapping.recordings.first.path
file = '2013-06-12T200000+0200_120.mp3'
composer = build_composer(file)
expect_trim(file, 0, 120)
mock_duration(file, 125)
assert_not_equal '2013-06-12T200000+0200_120.mp3', composer.compose
assert_not_equal file, composer.compose.path
end

test 'returns trimmed recording if it is longer than mapping' do
composer = build_composer('2013-06-12T200000+0200_140.mp3')
file = '2013-06-12T200000+0200_140.mp3'
composer = build_composer(file)
expect_trim(:first, 0, 120)
mock_duration(mapping.recordings.first.path, 140)
mock_duration(file, 140)
composer.compose
end

Expand Down
20 changes: 10 additions & 10 deletions test/services/import/recording_test.rb
Expand Up @@ -18,29 +18,29 @@ class Import::RecordingTest < ActiveSupport::TestCase
assert_equal time, recording.started_at
end

test '#duration returns seconds from filename' do
test '#specified_duration returns seconds from filename' do
recording = Import::Recording::File.new(file('2016-01-01T235959+0200_120.mp3'))
assert_equal 120.minutes.to_i, recording.duration
assert_equal 120.minutes.to_i, recording.specified_duration
end

test '#duration returns seconds from filename period in minutes' do
test '#specified_duration returns seconds from filename period in minutes' do
recording = Import::Recording::File.new(file('2016-01-01T235959+0200_PT120M.mp3'))
assert_equal 120.minutes.to_i, recording.duration
assert_equal 120.minutes.to_i, recording.specified_duration
end

test '#duration returns seconds from filename period in hours' do
test '#specified_duration returns seconds from filename period in hours' do
recording = Import::Recording::File.new(file('2016-01-01T235959+0200_PT2.5H.mp3'))
assert_equal 2.5.hours.to_i, recording.duration
assert_equal 2.5.hours.to_i, recording.specified_duration
end

test '#duration returns seconds from filename mixed period' do
test '#specified_duration returns seconds from filename mixed period' do
recording = Import::Recording::File.new(file('2016-01-01T235959+0200_PT1H30M20S.mp3'))
assert_equal 1.hour.to_i + 30.minutes.to_i + 20.seconds.to_i, recording.duration
assert_equal 1.hour.to_i + 30.minutes.to_i + 20.seconds.to_i, recording.specified_duration
end

test '#duration returns seconds from filename for imported files' do
test '#specified_duration returns seconds from filename for imported files' do
recording = Import::Recording::File.new(file('2015-12-31T000000-1200_030_imported.mp3'))
assert_equal 30.minutes.to_i, recording.duration
assert_equal 30.minutes.to_i, recording.specified_duration
end

test '#finished_at returns correct time' do
Expand Down

0 comments on commit 66eb4c7

Please sign in to comment.