duo_splitter 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|