duo_splitter 0.1.0 → 0.2.0
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 +4 -4
- data/README.ja.md +128 -0
- data/README.md +13 -11
- data/exe/duo_splitter +9 -3
- data/lib/duo_splitter.rb +39 -10
- data/lib/duo_splitter/batch_processor.rb +36 -0
- data/lib/duo_splitter/commands/base_command.rb +121 -0
- data/lib/duo_splitter/commands/overlapping_command.rb +37 -0
- data/lib/duo_splitter/commands/repeating_command.rb +38 -0
- data/lib/duo_splitter/commands/split_command.rb +31 -0
- data/lib/duo_splitter/context.rb +34 -0
- data/lib/duo_splitter/encoders/aac_encoder.rb +17 -0
- data/lib/duo_splitter/encoders/base_encoder.rb +57 -0
- data/lib/duo_splitter/encoders/mp3_encoder.rb +17 -0
- data/lib/duo_splitter/encoders/wav_encoder.rb +17 -0
- data/lib/duo_splitter/models/album.rb +23 -0
- data/lib/duo_splitter/models/section.rb +38 -0
- data/lib/duo_splitter/models/sentence.rb +35 -0
- data/lib/duo_splitter/runner.rb +43 -64
- data/lib/duo_splitter/services/create_overlapping_sentences.rb +19 -0
- data/lib/duo_splitter/services/create_repeating_sentences.rb +20 -0
- data/lib/duo_splitter/services/create_sentences.rb +18 -0
- data/lib/duo_splitter/services/support/base_concatenate.rb +46 -0
- data/lib/duo_splitter/services/support/create_blank_sentences.rb +40 -0
- data/lib/duo_splitter/services/support/encode.rb +23 -0
- data/lib/duo_splitter/services/support/overlapping_concatenate.rb +19 -0
- data/lib/duo_splitter/services/support/repeating_concatenate.rb +22 -0
- data/lib/duo_splitter/services/support/split_tracks.rb +42 -0
- data/lib/duo_splitter/temp_dir_wrapper.rb +20 -0
- data/lib/duo_splitter/version.rb +3 -1
- metadata +34 -15
- data/lib/duo_splitter/aac_encoder.rb +0 -13
- data/lib/duo_splitter/album.rb +0 -21
- data/lib/duo_splitter/encoder.rb +0 -71
- data/lib/duo_splitter/mp3_encoder.rb +0 -13
- data/lib/duo_splitter/section.rb +0 -30
- data/lib/duo_splitter/sentence.rb +0 -22
- data/lib/duo_splitter/wav_encoder.rb +0 -13
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Commands
|
5
|
+
class RepeatingCommand < BaseCommand
|
6
|
+
def run
|
7
|
+
Services::CreateRepeatingSentences.new(context: context).run
|
8
|
+
print_done
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_output_dir
|
14
|
+
Pathname.new('~/Desktop/DUO 3.0 (Repeating)').expand_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_options(argv)
|
18
|
+
parser.banner = %(Usage: #{parser.program_name} [options] repeating "01 Track 01.wav" ... "45 Track 45.wav")
|
19
|
+
|
20
|
+
on_output_dir
|
21
|
+
on_output_format
|
22
|
+
on_output_intro
|
23
|
+
on_prefix_section_number
|
24
|
+
on_show_progress
|
25
|
+
on_ffmpeg_path
|
26
|
+
on_sox_path
|
27
|
+
on_use_notification_sound
|
28
|
+
on_notification_sound_path
|
29
|
+
|
30
|
+
parse!(argv)
|
31
|
+
|
32
|
+
after_parse
|
33
|
+
end
|
34
|
+
|
35
|
+
Runner.register_command(:repeating, self)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Commands
|
5
|
+
class SplitCommand < BaseCommand
|
6
|
+
def run
|
7
|
+
Services::CreateSentences.new(context: context).run
|
8
|
+
print_done
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def parse_options(argv)
|
14
|
+
parser.banner = %(Usage: #{parser.program_name} [options] split "01 Track 01.wav" ... "45 Track 45.wav")
|
15
|
+
|
16
|
+
on_output_dir
|
17
|
+
on_output_format
|
18
|
+
on_output_intro
|
19
|
+
on_prefix_section_number
|
20
|
+
on_show_progress
|
21
|
+
on_ffmpeg_path
|
22
|
+
|
23
|
+
parse!(argv)
|
24
|
+
|
25
|
+
after_parse
|
26
|
+
end
|
27
|
+
|
28
|
+
Runner.register_command(:split, self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module DuoSplitter
|
6
|
+
class Context
|
7
|
+
attr_reader :album
|
8
|
+
|
9
|
+
attr_accessor :output_dir
|
10
|
+
attr_accessor :output_format
|
11
|
+
attr_accessor :output_intro
|
12
|
+
attr_accessor :prefix_section_number
|
13
|
+
attr_accessor :show_progress
|
14
|
+
attr_accessor :ffmpeg_path
|
15
|
+
attr_accessor :sox_path
|
16
|
+
attr_accessor :use_notification_sound
|
17
|
+
attr_accessor :notification_sound_path
|
18
|
+
attr_accessor :temp_dir
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@album = Models::Album.new
|
22
|
+
@output_dir = Pathname.new('~/Desktop/DUO 3.0').expand_path
|
23
|
+
@output_format = 'wav'
|
24
|
+
@output_intro = true
|
25
|
+
@prefix_section_number = true
|
26
|
+
@show_progress = true
|
27
|
+
@ffmpeg_path = 'ffmpeg'
|
28
|
+
@sox_path = 'sox'
|
29
|
+
@use_notification_sound = true
|
30
|
+
@notification_sound_path = ASSETS_DIR.join('C4.wav')
|
31
|
+
@temp_dir = nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Encoders
|
5
|
+
class AACEncoder < BaseEncoder
|
6
|
+
def encoding_options
|
7
|
+
%w[-codec:a aac -b:a 256k]
|
8
|
+
end
|
9
|
+
|
10
|
+
def ext
|
11
|
+
'm4a'
|
12
|
+
end
|
13
|
+
|
14
|
+
BaseEncoder.register_encoder(:aac, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Encoders
|
5
|
+
class BaseEncoder
|
6
|
+
class << self
|
7
|
+
def known_encoders
|
8
|
+
@known_encoders ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register_encoder(encoder_name, encoder_class)
|
12
|
+
known_encoders[encoder_name.to_sym] = encoder_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :commands
|
17
|
+
|
18
|
+
def initialize(context:)
|
19
|
+
@context = context
|
20
|
+
@commands = build_commands
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
return if @commands.empty?
|
25
|
+
|
26
|
+
@context.output_dir.mkpath
|
27
|
+
|
28
|
+
BatchProcessor.new(message: 'encoding...', show_progress: @context.show_progress).run(@commands)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_commands
|
34
|
+
sentences = @context.album.sentences(intro: @context.output_intro).select(&:source_audio_path)
|
35
|
+
|
36
|
+
sentences.map do |sentence|
|
37
|
+
output_basename = sentence.output_basename(prefix_section_number: @context.prefix_section_number, ext: ext)
|
38
|
+
|
39
|
+
command = [@context.ffmpeg_path, '-y']
|
40
|
+
command += ['-i', sentence.source_audio_path.to_s]
|
41
|
+
command += encoding_options
|
42
|
+
command += [@context.output_dir.join(output_basename).to_s]
|
43
|
+
|
44
|
+
command
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def encoding_options
|
49
|
+
raise Error, '#encoding_options must be defined in sub classes'
|
50
|
+
end
|
51
|
+
|
52
|
+
def ext
|
53
|
+
raise Error, '#ext must be defined in sub classes'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Encoders
|
5
|
+
class MP3Encoder < BaseEncoder
|
6
|
+
def encoding_options
|
7
|
+
%w[-codec:a libmp3lame -b:a 320k]
|
8
|
+
end
|
9
|
+
|
10
|
+
def ext
|
11
|
+
'mp3'
|
12
|
+
end
|
13
|
+
|
14
|
+
BaseEncoder.register_encoder(:mp3, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Encoders
|
5
|
+
class WAVEncoder < BaseEncoder
|
6
|
+
def encoding_options
|
7
|
+
%w[-codec:a pcm_s16le -ar 44100 -ac 2]
|
8
|
+
end
|
9
|
+
|
10
|
+
def ext
|
11
|
+
'wav'
|
12
|
+
end
|
13
|
+
|
14
|
+
BaseEncoder.register_encoder(:wav, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Models
|
5
|
+
class Album
|
6
|
+
attr_reader :sections
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@sections = load_sections
|
10
|
+
end
|
11
|
+
|
12
|
+
def sentences(intro: true)
|
13
|
+
sections.map {|section| section.sentences(intro: intro) }.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def load_sections
|
19
|
+
1.upto(NUMBER_OF_SECTIONS).map {|n| Section.new(number: n) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Models
|
5
|
+
class Section
|
6
|
+
attr_reader :number
|
7
|
+
|
8
|
+
attr_accessor :audio_path
|
9
|
+
|
10
|
+
def initialize(number:)
|
11
|
+
@number = number
|
12
|
+
@sentences = load_sentences
|
13
|
+
end
|
14
|
+
|
15
|
+
def sentences(intro: true)
|
16
|
+
@sentences.reject {|sentence| !intro && sentence.intro? }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def load_sentences
|
22
|
+
label_path = DATA_DIR.join(format('labels/%02<number>d.txt', number: number))
|
23
|
+
|
24
|
+
records = label_path.readlines.map {|line| line.chomp.split(/\t/) }
|
25
|
+
|
26
|
+
records.unshift(%w[0 0 0])
|
27
|
+
|
28
|
+
records.each_cons(2).map do |current_record, next_record|
|
29
|
+
sentence_number = current_record.last.to_i
|
30
|
+
start_pos = current_record.first
|
31
|
+
end_pos = next_record.first
|
32
|
+
|
33
|
+
Sentence.new(number: sentence_number, start_pos: start_pos, end_pos: end_pos, section: self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuoSplitter
|
4
|
+
module Models
|
5
|
+
class Sentence
|
6
|
+
attr_reader :number, :start_pos, :end_pos, :section
|
7
|
+
|
8
|
+
attr_accessor :audio_path, :blank_audio_path, :source_audio_path
|
9
|
+
|
10
|
+
def initialize(number:, start_pos:, end_pos:, section:)
|
11
|
+
@number = number
|
12
|
+
@start_pos = start_pos
|
13
|
+
@end_pos = end_pos
|
14
|
+
@section = section
|
15
|
+
end
|
16
|
+
|
17
|
+
def duration
|
18
|
+
Float(@end_pos) - Float(@start_pos)
|
19
|
+
end
|
20
|
+
|
21
|
+
def intro?
|
22
|
+
number.zero?
|
23
|
+
end
|
24
|
+
|
25
|
+
def output_basename(prefix_section_number: true, blank: false, concatenated: false, ext:)
|
26
|
+
basename = format('%03<number>d', number: number)
|
27
|
+
basename = "#{basename}_blank" if blank
|
28
|
+
basename = "#{basename}_concatenated" if concatenated
|
29
|
+
basename = "#{basename}.#{ext}"
|
30
|
+
basename = format('%02<section_number>d-', section_number: @section.number) + basename if prefix_section_number
|
31
|
+
basename
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/duo_splitter/runner.rb
CHANGED
@@ -1,93 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
|
-
require 'pathname'
|
3
4
|
|
4
5
|
module DuoSplitter
|
5
6
|
class Runner
|
6
|
-
|
7
|
+
class << self
|
8
|
+
def known_commands
|
9
|
+
@known_commands ||= {}
|
10
|
+
end
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@known_formats = Encoder.known_encoders.keys.sort
|
12
|
-
@output_intro = true
|
13
|
-
@add_section_prefix = true
|
14
|
-
@ffmpeg_path = 'ffmpeg'
|
15
|
-
@album = Album.new
|
16
|
-
|
17
|
-
parse_options(argv)
|
18
|
-
handle_source_paths(argv)
|
12
|
+
def register_command(command_name, command_class)
|
13
|
+
known_commands[command_name.to_sym] = command_class
|
14
|
+
end
|
19
15
|
end
|
20
16
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
encoder = encoder_class.new(
|
25
|
-
album: @album,
|
26
|
-
output_dir: @output_dir,
|
27
|
-
output_intro: @output_intro,
|
28
|
-
add_section_prefix: @add_section_prefix,
|
29
|
-
ffmpeg_path: @ffmpeg_path
|
30
|
-
)
|
17
|
+
def initialize(argv)
|
18
|
+
@argv = argv.dup
|
19
|
+
@parser = OptionParser.new
|
31
20
|
|
32
|
-
|
21
|
+
parse_options
|
33
22
|
end
|
34
23
|
|
35
|
-
|
24
|
+
def run
|
25
|
+
command_name = @argv.shift
|
36
26
|
|
37
|
-
|
38
|
-
|
27
|
+
if command_name
|
28
|
+
command_class = self.class.known_commands[command_name.to_sym]
|
39
29
|
|
40
|
-
|
30
|
+
raise Error, "unknown command: #{command_name}" unless command_class
|
41
31
|
|
42
|
-
|
43
|
-
|
32
|
+
command_class.new(@argv).run
|
33
|
+
else
|
34
|
+
show_help
|
44
35
|
end
|
36
|
+
end
|
45
37
|
|
46
|
-
|
47
|
-
@format = value.to_sym
|
48
|
-
end
|
38
|
+
private
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
40
|
+
def parse_options
|
41
|
+
@parser.banner = "Usage: #{@parser.program_name} [options] command"
|
53
42
|
|
54
|
-
parser.on('
|
55
|
-
|
43
|
+
@parser.on('-h', '--help', 'Print this message') do
|
44
|
+
show_help
|
45
|
+
exit
|
56
46
|
end
|
57
47
|
|
58
|
-
parser.on('
|
59
|
-
|
48
|
+
@parser.on('-v', '--version', 'Print version number') do
|
49
|
+
puts DuoSplitter::VERSION
|
50
|
+
exit
|
60
51
|
end
|
61
52
|
|
62
53
|
begin
|
63
|
-
parser.
|
64
|
-
rescue OptionParser::ParseError =>
|
65
|
-
raise Error,
|
66
|
-
end
|
67
|
-
|
68
|
-
unless @add_section_prefix
|
69
|
-
@output_intro = false
|
54
|
+
@parser.order!(@argv)
|
55
|
+
rescue OptionParser::ParseError => e
|
56
|
+
raise Error, e.message
|
70
57
|
end
|
71
58
|
end
|
72
59
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
section_number = $1.to_i
|
60
|
+
def show_help
|
61
|
+
puts @parser
|
62
|
+
puts
|
63
|
+
puts <<~MESSAGE
|
64
|
+
Available commands are:
|
79
65
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
else
|
85
|
-
raise Error, "invalid section number: #{section_number}"
|
86
|
-
end
|
87
|
-
else
|
88
|
-
raise Error, "input file name doesn't start with a section number: #{source_path}"
|
89
|
-
end
|
90
|
-
end
|
66
|
+
- split
|
67
|
+
- repeating
|
68
|
+
- overlapping
|
69
|
+
MESSAGE
|
91
70
|
end
|
92
71
|
end
|
93
72
|
end
|