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.
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