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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.ja.md +128 -0
  3. data/README.md +13 -11
  4. data/exe/duo_splitter +9 -3
  5. data/lib/duo_splitter.rb +39 -10
  6. data/lib/duo_splitter/batch_processor.rb +36 -0
  7. data/lib/duo_splitter/commands/base_command.rb +121 -0
  8. data/lib/duo_splitter/commands/overlapping_command.rb +37 -0
  9. data/lib/duo_splitter/commands/repeating_command.rb +38 -0
  10. data/lib/duo_splitter/commands/split_command.rb +31 -0
  11. data/lib/duo_splitter/context.rb +34 -0
  12. data/lib/duo_splitter/encoders/aac_encoder.rb +17 -0
  13. data/lib/duo_splitter/encoders/base_encoder.rb +57 -0
  14. data/lib/duo_splitter/encoders/mp3_encoder.rb +17 -0
  15. data/lib/duo_splitter/encoders/wav_encoder.rb +17 -0
  16. data/lib/duo_splitter/models/album.rb +23 -0
  17. data/lib/duo_splitter/models/section.rb +38 -0
  18. data/lib/duo_splitter/models/sentence.rb +35 -0
  19. data/lib/duo_splitter/runner.rb +43 -64
  20. data/lib/duo_splitter/services/create_overlapping_sentences.rb +19 -0
  21. data/lib/duo_splitter/services/create_repeating_sentences.rb +20 -0
  22. data/lib/duo_splitter/services/create_sentences.rb +18 -0
  23. data/lib/duo_splitter/services/support/base_concatenate.rb +46 -0
  24. data/lib/duo_splitter/services/support/create_blank_sentences.rb +40 -0
  25. data/lib/duo_splitter/services/support/encode.rb +23 -0
  26. data/lib/duo_splitter/services/support/overlapping_concatenate.rb +19 -0
  27. data/lib/duo_splitter/services/support/repeating_concatenate.rb +22 -0
  28. data/lib/duo_splitter/services/support/split_tracks.rb +42 -0
  29. data/lib/duo_splitter/temp_dir_wrapper.rb +20 -0
  30. data/lib/duo_splitter/version.rb +3 -1
  31. metadata +34 -15
  32. data/lib/duo_splitter/aac_encoder.rb +0 -13
  33. data/lib/duo_splitter/album.rb +0 -21
  34. data/lib/duo_splitter/encoder.rb +0 -71
  35. data/lib/duo_splitter/mp3_encoder.rb +0 -13
  36. data/lib/duo_splitter/section.rb +0 -30
  37. data/lib/duo_splitter/sentence.rb +0 -22
  38. 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
@@ -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
- attr_reader :output_dir, :format, :output_intro, :add_section_prefix, :ffmpeg_path, :album
7
+ class << self
8
+ def known_commands
9
+ @known_commands ||= {}
10
+ end
7
11
 
8
- def initialize(argv)
9
- @output_dir = Pathname.new('~/Desktop/DUO 3.0').expand_path
10
- @format = :wav
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 run
22
- encoder_class = Encoder.known_encoders[@format]
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
- encoder.run
21
+ parse_options
33
22
  end
34
23
 
35
- private
24
+ def run
25
+ command_name = @argv.shift
36
26
 
37
- def parse_options(argv)
38
- parser = OptionParser.new
27
+ if command_name
28
+ command_class = self.class.known_commands[command_name.to_sym]
39
29
 
40
- parser.banner = %(Usage: #{File.basename($PROGRAM_NAME)} [options] "01 Track 01.wav" "02 Track 02.wav" ... "45 Track 45.wav")
30
+ raise Error, "unknown command: #{command_name}" unless command_class
41
31
 
42
- parser.on('-d', '--output-dir=PATH', "Output directory (default: #{@output_dir})") do |value|
43
- @output_dir = Pathname.new(value)
32
+ command_class.new(@argv).run
33
+ else
34
+ show_help
44
35
  end
36
+ end
45
37
 
46
- parser.on('-f', '--format=FORMAT', @known_formats, "Output format (default: #{@format}; one of #{@known_formats.join(', ')})") do |value|
47
- @format = value.to_sym
48
- end
38
+ private
49
39
 
50
- parser.on('--[no-]output-intro', "Output intro track (default: #{@output_intro})") do |flag|
51
- @output_intro = flag
52
- end
40
+ def parse_options
41
+ @parser.banner = "Usage: #{@parser.program_name} [options] command"
53
42
 
54
- parser.on('--[no-]section-number', "Add section number to output name (default: #{@add_section_prefix})") do |flag|
55
- @add_section_prefix = flag
43
+ @parser.on('-h', '--help', 'Print this message') do
44
+ show_help
45
+ exit
56
46
  end
57
47
 
58
- parser.on('--ffmpeg-path=PATH', "Path to ffmpeg command (default: #{@ffmpeg_path})") do |value|
59
- @ffmpeg_path = value
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.parse!(argv)
64
- rescue OptionParser::ParseError => error
65
- raise Error, error.message
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 handle_source_paths(argv)
74
- argv.each do |source_path|
75
- source_path = Pathname.new(source_path)
76
-
77
- if /\A(\d+)/ =~ source_path.basename.to_s
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
- section = @album.sections.find {|section| section.number == section_number }
81
-
82
- if section
83
- section.source_path = source_path
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