MusicMaster 0.0.1.20101110 → 1.0.0.20120307

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 (49) hide show
  1. data/AUTHORS +4 -1
  2. data/ChangeLog +28 -0
  3. data/LICENSE +1 -1
  4. data/README +2 -5
  5. data/ReleaseInfo +8 -8
  6. data/bin/Calibrate.rb +55 -0
  7. data/bin/Clean.rb +55 -0
  8. data/bin/DBConvert.rb +3 -1
  9. data/bin/Deliver.rb +73 -42
  10. data/bin/Mix.rb +39 -78
  11. data/bin/Process.rb +55 -0
  12. data/bin/Record.rb +63 -116
  13. data/bin/{Album.rb → old/Album.rb} +18 -18
  14. data/bin/{AnalyzeAlbum.rb → old/AnalyzeAlbum.rb} +11 -11
  15. data/bin/{DeliverAlbum.rb → old/DeliverAlbum.rb} +12 -12
  16. data/bin/{Fct2Wave.rb → old/Fct2Wave.rb} +11 -11
  17. data/{album.conf.rb.example → bin/old/album.conf.rb.example} +0 -0
  18. data/lib/MusicMaster/FilesNamer.rb +253 -0
  19. data/lib/MusicMaster/Formats/MP3.rb +50 -0
  20. data/lib/MusicMaster/Formats/Test.rb +44 -0
  21. data/lib/MusicMaster/Formats/Wave.rb +80 -0
  22. data/lib/MusicMaster/Hash.rb +20 -0
  23. data/lib/MusicMaster/Launcher.rb +241 -0
  24. data/lib/MusicMaster/Processes/ApplyVolumeFct.rb +4 -8
  25. data/lib/MusicMaster/Processes/Compressor.rb +50 -47
  26. data/lib/MusicMaster/Processes/Custom.rb +3 -3
  27. data/lib/MusicMaster/Processes/{AddSilence.rb → Cut.rb} +8 -4
  28. data/lib/MusicMaster/Processes/CutFirstSignal.rb +6 -3
  29. data/lib/MusicMaster/Processes/DCShifter.rb +30 -0
  30. data/lib/MusicMaster/Processes/GVerb.rb +3 -2
  31. data/lib/MusicMaster/Processes/Normalize.rb +7 -6
  32. data/lib/MusicMaster/Processes/SilenceInserter.rb +31 -0
  33. data/lib/MusicMaster/Processes/Test.rb +39 -0
  34. data/lib/MusicMaster/Processes/VolCorrection.rb +3 -3
  35. data/lib/MusicMaster/RakeProcesses.rb +1014 -0
  36. data/lib/MusicMaster/Symbol.rb +12 -0
  37. data/lib/MusicMaster/Task.rb +29 -0
  38. data/lib/MusicMaster/Utils.rb +467 -0
  39. data/lib/MusicMaster/old/Common.rb +101 -0
  40. data/musicmaster.conf.rb.example +42 -59
  41. data/record.conf.rb.example +374 -30
  42. metadata +91 -41
  43. data/TODO +0 -3
  44. data/bin/Master.rb +0 -60
  45. data/bin/PrepareMix.rb +0 -422
  46. data/lib/MusicMaster/Common.rb +0 -197
  47. data/lib/MusicMaster/ConfLoader.rb +0 -56
  48. data/lib/MusicMaster/musicmaster.conf.rb +0 -96
  49. data/master.conf.rb.example +0 -13
@@ -0,0 +1,12 @@
1
+ #--
2
+ # Copyright (c) 2012 Muriel Salvan (muriel@x-aeon.com)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ if (RUBY_VERSION < '1.9')
7
+ class Symbol
8
+ def <=>(iOther)
9
+ return self.to_s <=> iOther.to_s
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ #--
2
+ # Copyright (c) 2012 Muriel Salvan (muriel@x-aeon.com)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ # Make prerequisites re-evaluated when changed
7
+ module Rake
8
+ class Task
9
+
10
+ # Data stored in the task itself, built during its invocation.
11
+ # Useful to represent targets having data not stored in a file.
12
+ attr_accessor :data
13
+
14
+ # Keep original method
15
+ alias :invoke_prerequisites_ORG :invoke_prerequisites
16
+ # Rewrite it
17
+ def invoke_prerequisites(task_args, invocation_chain)
18
+ prerequisites_changed = true
19
+ while (prerequisites_changed)
20
+ # Keep original prerequisites list
21
+ original_prerequisites = prerequisite_tasks.clone
22
+ # Call original method (this call might change the prerequisites list)
23
+ invoke_prerequisites_ORG(task_args, invocation_chain)
24
+ prerequisites_changed = (prerequisite_tasks != original_prerequisites)
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,467 @@
1
+ #--
2
+ # Copyright (c) 2011 - 2012 Muriel Salvan (muriel@x-aeon.com)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ require 'pp'
7
+ require 'tmpdir'
8
+
9
+ module MusicMaster
10
+
11
+ module Utils
12
+
13
+ # Initialize variables used by utils
14
+ def initialize_Utils
15
+ # A little cache
16
+ # map< Symbol, Object >
17
+ # * *:Analysis* (<em>map<String,Object></em>): Analysis object, per analysis file name
18
+ # * *:DCOffsets* (<em>map<String,list<Float>></em>): Channels DC offsets, per analysis file name
19
+ # * *:RMSValues* (<em>map<String,Float></em>): The average RMS values, per analysis file name
20
+ # * *:Thresholds* (<em>map<String,list< [Integer,Integer] >></em>): List of [min,max] thresholds per channel, per analysis file name
21
+ @Cache = {
22
+ :Analysis => {},
23
+ :DCOffsets => {},
24
+ :RMSValues => {},
25
+ :Thresholds => {}
26
+ }
27
+ end
28
+
29
+ # Record into a given file
30
+ #
31
+ # Parameters::
32
+ # * *iFileName* (_String_): File name to record into
33
+ # * *iAlreadyPrepared* (_Boolean_): Is the file to be recorded already prepared ? [optional = false]
34
+ def record(iFileName, iAlreadyPrepared = false)
35
+ lTryAgain = true
36
+ if (File.exists?(iFileName))
37
+ puts "File \"#{iFileName}\" already exists. Overwrite ? ['y' = yes]"
38
+ lTryAgain = ($stdin.gets.chomp == 'y')
39
+ end
40
+ while (lTryAgain)
41
+ puts "Record file \"#{iFileName}\""
42
+ lSkip = nil
43
+ if (iAlreadyPrepared)
44
+ lSkip = false
45
+ else
46
+ puts 'Press Enter to continue once done. Type \'s\' to skip it.'
47
+ lSkip = ($stdin.gets.chomp == 's')
48
+ end
49
+ if (lSkip)
50
+ lTryAgain = false
51
+ else
52
+ # Get the recorded file name
53
+ lFileName = @MusicMasterConf[:Record][:RecordedFileGetter].call
54
+ if (!File.exists?(lFileName))
55
+ log_err "File #{lFileName} does not exist. Could not get recorded file."
56
+ else
57
+ log_info "Getting recorded file: #{lFileName} => #{iFileName}"
58
+ FileUtils::mkdir_p(File.dirname(iFileName))
59
+ FileUtils::mv(lFileName, iFileName)
60
+ lTryAgain = false
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Make an FFT profile of a given wav file, and store the result in the given file name.
67
+ #
68
+ # Parameters::
69
+ # * *iWaveFile* (_String_): The wav file to analyze
70
+ # * *iFFTProfileFile* (_String_): The analysis file to store into
71
+ def fftProfileFile(iWaveFile, iFFTProfileFile)
72
+ lDummyFile = "#{Dir.tmpdir}/MusicMaster/Dummy.wav"
73
+ FileUtils::mkdir_p(File.dirname(lDummyFile))
74
+ wsk(iWaveFile, lDummyFile, 'FFT')
75
+ File.unlink(lDummyFile)
76
+ FileUtils::mkdir_p(File.dirname(iFFTProfileFile))
77
+ FileUtils::mv('fft.result', iFFTProfileFile)
78
+ end
79
+
80
+ # Analyze a given wav file, and store the result in the given file name.
81
+ #
82
+ # Parameters::
83
+ # * *iWaveFile* (_String_): The wav file to analyze
84
+ # * *iAnalysisFile* (_String_): The analysis file to store into
85
+ def analyzeFile(iWaveFile, iAnalysisFile)
86
+ lDummyFile = "#{Dir.tmpdir}/MusicMaster/Dummy.wav"
87
+ FileUtils::mkdir_p(File.dirname(lDummyFile))
88
+ wsk(iWaveFile, lDummyFile, 'Analyze')
89
+ File.unlink(lDummyFile)
90
+ FileUtils::mkdir_p(File.dirname(iAnalysisFile))
91
+ FileUtils::mv('analyze.result', iAnalysisFile)
92
+ end
93
+
94
+ # Get analysis result
95
+ #
96
+ # Parameters::
97
+ # * *iAnalysisFileName* (_String_): The name of the analysis file
98
+ # Return::
99
+ # * <em>map<Symbol,Object></em>: The analyze result
100
+ def getAnalysis(iAnalysisFileName)
101
+ rResult = nil
102
+
103
+ if (@Cache[:Analysis][iAnalysisFileName] == nil)
104
+ File.open(iAnalysisFileName, 'rb') do |iFile|
105
+ rResult = Marshal.load(iFile.read)
106
+ end
107
+ @Cache[:Analysis][iAnalysisFileName] = rResult
108
+ else
109
+ rResult = @Cache[:Analysis][iAnalysisFileName]
110
+ end
111
+
112
+ return rResult
113
+ end
114
+
115
+ # Get DC offsets out of an analysis file
116
+ #
117
+ # Parameters::
118
+ # * *iAnalyzeRecordedFileName* (_String_): Name of the file containing analysis
119
+ # Return::
120
+ # * _Boolean_: Is there an offset ?
121
+ # * <em>list<Float></em>: The DC offsets, per channel
122
+ def getDCOffsets(iAnalyzeRecordedFileName)
123
+ rOffset = false
124
+ rDCOffsets = []
125
+
126
+ if (@Cache[:DCOffsets][iAnalyzeRecordedFileName] == nil)
127
+ lAnalyze = getAnalysis(iAnalyzeRecordedFileName)
128
+ lAnalyze[:MoyValues].each do |iMoyValue|
129
+ lDCOffset = iMoyValue.round
130
+ rDCOffsets << lDCOffset
131
+ if (lDCOffset != 0)
132
+ rOffset = true
133
+ end
134
+ end
135
+ @Cache[:DCOffsets][iAnalyzeRecordedFileName] = [ rOffset, rDCOffsets ]
136
+ else
137
+ rOffset, rDCOffsets = @Cache[:DCOffsets][iAnalyzeRecordedFileName]
138
+ end
139
+
140
+ return rOffset, rDCOffsets
141
+ end
142
+
143
+ # Get average RMS value from an analysis file
144
+ #
145
+ # Parameters::
146
+ # * *iAnalysisFileName* (_String_): Name of the analysis file
147
+ # Return::
148
+ # * _Float_: The average RMS value
149
+ def getRMSValue(iAnalysisFileName)
150
+ rRMSValue = nil
151
+
152
+ if (@Cache[:RMSValues][iAnalysisFileName] == nil)
153
+ lAnalysis = getAnalysis(iAnalysisFileName)
154
+ rRMSValue = lAnalysis[:RMSValues].inject{ |iSum, iValue| next (iSum + iValue) } / lAnalysis[:RMSValues].size
155
+ @Cache[:RMSValues][iAnalysisFileName] = rRMSValue
156
+ else
157
+ rRMSValue = @Cache[:RMSValues][iAnalysisFileName]
158
+ end
159
+
160
+ return rRMSValue
161
+ end
162
+
163
+ # Get signal thresholds, without DC offsets, from an analysis file
164
+ #
165
+ # Parameters::
166
+ # * *iAnalysisFileName* (_String_): Name of the file containing analysis
167
+ # * *iOptions* (<em>map<Symbol,Object></em>): Additional options [optional = {}]
168
+ # * *:margin* (_Float_): The margin to be added, in terms of fraction of the maximal signal value [optional = 0.0]
169
+ # Return::
170
+ # * <em>list< [Integer,Integer] ></em>: The [min,max] values, per channel
171
+ def getThresholds(iAnalysisFileName, iOptions = {})
172
+ rThresholds = []
173
+
174
+ if (@Cache[:Thresholds][iAnalysisFileName] == nil)
175
+ # Get silence thresholds from the silence file
176
+ lSilenceAnalyze = getAnalysis(iAnalysisFileName)
177
+ # Compute the DC offsets
178
+ lSilenceDCOffsets = lSilenceAnalyze[:MoyValues].map { |iValue| iValue.round }
179
+ lMargin = iOptions[:margin] || 0.0
180
+ lSilenceAnalyze[:MaxValues].each_with_index do |iMaxValue, iIdxChannel|
181
+ # Remove silence DC Offset
182
+ lCorrectedMinValue = lSilenceAnalyze[:MinValues][iIdxChannel] - lSilenceDCOffsets[iIdxChannel]
183
+ lCorrectedMaxValue = iMaxValue - lSilenceDCOffsets[iIdxChannel]
184
+ # Compute the silence threshold by adding the margin
185
+ rThresholds << [(lCorrectedMinValue-lCorrectedMinValue.abs*lMargin).to_i, (lCorrectedMaxValue+lCorrectedMaxValue.abs*lMargin).to_i]
186
+ end
187
+ @Cache[:Thresholds][iAnalysisFileName] = rThresholds
188
+ else
189
+ rThresholds = @Cache[:Thresholds][iAnalysisFileName]
190
+ end
191
+
192
+ return rThresholds
193
+ end
194
+
195
+ # Shift thresholds by a given DC offset.
196
+ #
197
+ # Parameters::
198
+ # * *iThresholds* (<em>list< [Integer,Integer] ></em>): The thresholds to shift
199
+ # * *iDCOffsets* (<em>list<Integer></em>): The DC offsets
200
+ # Return::
201
+ # * <em>list< [Integer,Integer] ></em>: The shifted thresholds
202
+ def shiftThresholdsByDCOffset(iThresholds, iDCOffsets)
203
+ rCorrectedThresholds = []
204
+
205
+ # Compute the silence thresholds with DC offset applied
206
+ iThresholds.each_with_index do |iThresholdInfo, iIdxChannel|
207
+ lChannelDCOffset = iDCOffsets[iIdxChannel]
208
+ rCorrectedThresholds << iThresholdInfo.map { |iValue| iValue + lChannelDCOffset }
209
+ end
210
+
211
+ return rCorrectedThresholds
212
+ end
213
+
214
+ # The groups of processes that can be optimized, and their corresponding optimization methods
215
+ # They are sorted by importance: first ones will have greater priority
216
+ # Here are the symbols used for each group:
217
+ # * *:OptimizeProc* (_Proc_): The code called to optimize a group. It is called only for groups containing all processes from the group key, and including no other processes. Only for groups strictly larger than 1 element.
218
+ # Parameters::
219
+ # * *iLstProcesses* (<em>list<map<Symbol,Object>></em>): List of processes to optimize
220
+ # Return::
221
+ # * <em>list<map<Symbol,Object>></em>: List of optimized processes. Can be empty to delete them, or nil to not optimize them.
222
+ OPTIM_GROUPS = [
223
+ [ [ 'VolCorrection' ],
224
+ {
225
+ :OptimizeProc => Proc.new do |iLstProcesses|
226
+ rOptimizedProcesses = []
227
+
228
+ lRatio = 0.0
229
+ iLstProcesses.each do |iProcessInfo|
230
+ lRatio += readStrRatio(iProcessInfo[:Factor])
231
+ end
232
+ if (lRatio != 0)
233
+ # Replace the serie with just 1 volume correction
234
+ rOptimizedProcesses = [ {
235
+ :Name => 'VolCorrection',
236
+ :Factor => "#{lRatio}db"
237
+ } ]
238
+ end
239
+
240
+ next rOptimizedProcesses
241
+ end
242
+ }
243
+ ],
244
+ [ [ 'DCShifter' ],
245
+ {
246
+ :OptimizeProc => Proc.new do |iLstProcesses|
247
+ rOptimizedProcesses = []
248
+
249
+ lDCOffset = 0
250
+ iLstProcesses.each do |iProcessInfo|
251
+ lDCOffset += iProcessInfo[:Offset]
252
+ end
253
+ if (lDCOffset != 0)
254
+ # Replace the serie with just 1 DC offset
255
+ rOptimizedProcesses = [ {
256
+ :Name => 'DCShifter',
257
+ :Offset => lDCOffset
258
+ } ]
259
+ end
260
+
261
+ next rOptimizedProcesses
262
+ end
263
+ }
264
+ ]
265
+ ]
266
+ # Activate debug log for this method only
267
+ OPTIM_DEBUG = false
268
+ # Optimize a list of processes.
269
+ # Delete useless ones or ones that cancel themselves.
270
+ #
271
+ # Parameters::
272
+ # * *iLstProcesses* (<em>list<map<Symbol,Object>></em>): List of processes
273
+ # Return::
274
+ # * <em>list<map<Symbol,Object>></em>: The optimized list of processes
275
+ def optimizeProcesses(iLstProcesses)
276
+ rNewLstProcesses = []
277
+
278
+ lModified = true
279
+ rNewLstProcesses = iLstProcesses
280
+ while (lModified)
281
+ # rNewLstProcesses contains the current list
282
+ log_debug "[Optimize]: ========== Launch optimization for processes list: #{rNewLstProcesses.inspect}" if OPTIM_DEBUG
283
+ lLstCurrentProcesses = rNewLstProcesses
284
+ rNewLstProcesses = []
285
+ lModified = false
286
+
287
+ # The list of all possible group keys that can be used for optimizations
288
+ # list< [ list<String>, map<Symbol,Object> ] >
289
+ lCurrentMatchingGroups = nil
290
+ lIdxGroupBegin = nil
291
+ lIdxProcess = 0
292
+ while (lIdxProcess < lLstCurrentProcesses.size)
293
+ lProcessInfo = lLstCurrentProcesses[lIdxProcess]
294
+ log_debug "[Optimize]: ===== Process Index: #{lIdxProcess} - Process: #{lProcessInfo.inspect} - Process group begin: #{lIdxGroupBegin.inspect} - Current matching groups: #{lCurrentMatchingGroups.inspect} - New processes list: #{rNewLstProcesses.inspect}" if OPTIM_DEBUG
295
+ if (lIdxGroupBegin == nil)
296
+ # We can begin grouping
297
+ lCurrentMatchingGroups = []
298
+ OPTIM_GROUPS.each do |iGroupInfo|
299
+ if (iGroupInfo[0].include?(lProcessInfo[:Name]))
300
+ # This group key can begin a new group
301
+ lCurrentMatchingGroups << iGroupInfo
302
+ end
303
+ end
304
+ if (lCurrentMatchingGroups.empty?)
305
+ # We can't do anything with this process
306
+ rNewLstProcesses << lProcessInfo
307
+ else
308
+ # We can begin a group
309
+ lIdxGroupBegin = lIdxProcess
310
+ end
311
+ log_debug "[Optimize]: Set process group begin to #{lIdxGroupBegin.inspect}" if OPTIM_DEBUG
312
+ lIdxProcess += 1
313
+ else
314
+ # We already have some group candidates
315
+ # Now we remove the groups that do not fit with our current process
316
+ lNewGroups = lCurrentMatchingGroups.clone.delete_if { |iGroupInfo| !iGroupInfo[0].include?(lProcessInfo[:Name]) }
317
+ if (lNewGroups.empty?)
318
+ log_debug '[Optimize]: Closing current matching groups.' if OPTIM_DEBUG
319
+ # We are closing the group(s) we got
320
+ lIdxGroupEnd = lIdxProcess - 1
321
+ if (lIdxGroupBegin == lIdxGroupEnd)
322
+ # This is a group of 1 element.
323
+ log_debug '[Optimize]: Just 1 element to close.' if OPTIM_DEBUG
324
+ # Just ignore it
325
+ rNewLstProcesses << lLstCurrentProcesses[lIdxGroupBegin]
326
+ else
327
+ log_debug "[Optimize]: #{lIdxGroupEnd-lIdxGroupBegin+1} elements to close." if OPTIM_DEBUG
328
+ lOptimizedProcesses = optimizeProcessesByGroups(lLstCurrentProcesses[lIdxGroupBegin..lIdxGroupEnd], lCurrentMatchingGroups)
329
+ if (lOptimizedProcesses == nil)
330
+ # No optimization
331
+ log_debug '[Optimize]: Optimizer decided to not optimize.' if OPTIM_DEBUG
332
+ rNewLstProcesses.concat(lLstCurrentProcesses[lIdxGroupBegin..lIdxGroupEnd])
333
+ else
334
+ # Optimization
335
+ log_debug "[Optimize]: Optimizer decided to optimize from #{lIdxGroupEnd-lIdxGroupBegin+1} to #{lOptimizedProcesses.size} elements." if OPTIM_DEBUG
336
+ rNewLstProcesses.concat(lOptimizedProcesses)
337
+ lModified = true
338
+ end
339
+ end
340
+ lIdxGroupBegin = nil
341
+ # Process again this element
342
+ else
343
+ log_debug "[Optimize]: Matching groups reduced from #{lCurrentMatchingGroups.size} to #{lNewGroups.size} elements." if OPTIM_DEBUG
344
+ # We just remove groups that are out due to the current process
345
+ lCurrentMatchingGroups = lNewGroups
346
+ # Go on to the next element
347
+ lIdxProcess += 1
348
+ end
349
+ end
350
+ end
351
+ # Last elements could have been part of a group
352
+ log_debug "[Optimize]: ===== Process Index: #{lIdxProcess} - End of processes list - Process group begin: #{lIdxGroupBegin.inspect} - Current matching groups: #{lCurrentMatchingGroups.inspect} - New processes list: #{rNewLstProcesses.inspect}" if OPTIM_DEBUG
353
+ if (lIdxGroupBegin != nil)
354
+ if (lIdxGroupBegin < lLstCurrentProcesses.size - 1)
355
+ # Indeed
356
+ lOptimizedProcesses = optimizeProcessesByGroups(lLstCurrentProcesses[lIdxGroupBegin..-1], lCurrentMatchingGroups)
357
+ if (lOptimizedProcesses == nil)
358
+ # No optimization
359
+ log_debug '[Optimize]: Optimizer decided to not optimize last group.' if OPTIM_DEBUG
360
+ rNewLstProcesses.concat(lLstCurrentProcesses[lIdxGroupBegin..-1])
361
+ else
362
+ # Optimization
363
+ log_debug "[Optimize]: Optimizer decided to optimize from #{lLstCurrentProcesses.size-lIdxGroupBegin} to #{lOptimizedProcesses.size} elements." if OPTIM_DEBUG
364
+ rNewLstProcesses.concat(lOptimizedProcesses)
365
+ lModified = true
366
+ end
367
+ else
368
+ # Just the last element is remaining in the group
369
+ log_debug '[Optimize]: Just 1 element to close at the end.' if OPTIM_DEBUG
370
+ rNewLstProcesses << lLstCurrentProcesses[-1]
371
+ end
372
+ end
373
+ end
374
+
375
+ return rNewLstProcesses
376
+ end
377
+
378
+ # Optimize (or choose not to) a list of processes based on a potential list of optimization groups
379
+ # Prerequisites:
380
+ # * The list of processes has a size > 1
381
+ # * The list of groups has a size > 0
382
+ # * Each optimization group has at least 1 process in each of the processes' list's elements
383
+ #
384
+ # Parameters::
385
+ # * *iLstProcesses* (<em>list<map<Symbol,Object>></em>): The list of processes to optimize
386
+ # * *iLstGroups* (<em>list< [list<String>,map<Symbol,Object>] ></em>): The list of potential optimization groups
387
+ # Return::
388
+ # * <em>list<map<Symbol,Object>></em>: The corresponding list of processes optimized. Can be empty to delete them, or nil to not optimize them
389
+ def optimizeProcessesByGroups(iLstProcesses, iLstGroups)
390
+ rOptimizedProcesses = nil
391
+
392
+ # Now we remove the groups needing several processes and that do not have all their processes among the selected group
393
+ lLstProcessesNames = iLstProcesses.map { |iProcessInfo| iProcessInfo[:Name] }.uniq
394
+ lLstMatchingGroups = iLstGroups.clone.delete_if do |iGroupInfo|
395
+ # All processes from iGroupKey must be present among the current processes group
396
+ next !(iGroupInfo[0] - lLstProcessesNames).empty?
397
+ end
398
+ # lLstMatchingGroups contain all the groups that can offer optimizations
399
+ log_debug "[Optimize]: #{lLstMatchingGroups.size} groups can offer optimization." if OPTIM_DEBUG
400
+ if (!lLstMatchingGroups.empty?)
401
+ # Here we can optimize for real
402
+ while ((rOptimizedProcesses == nil) and
403
+ (!lLstMatchingGroups.empty?))
404
+ # Choose the biggest priority group first
405
+ lGroupInfo = lLstMatchingGroups.first
406
+ # Call the relevant grouping function from the selected group on our list of processes
407
+ log_debug "[Optimize]: Apply optimization from group #{lGroupInfo.inspect} to processes: #{iLstProcesses.inspect}" if OPTIM_DEBUG
408
+ rOptimizedProcesses = lGroupInfo[1][:OptimizeProc].call(iLstProcesses)
409
+ if (rOptimizedProcesses == nil)
410
+ log_debug '[Optimize]: Group optimizer decided to not optimize.'
411
+ lLstMatchingGroups = lLstMatchingGroups[1..-1]
412
+ end
413
+ end
414
+ end
415
+ log_debug "Processes optimized: from\n#{iLstProcesses.pretty_inspect}\nto\n#{rOptimizedProcesses.pretty_inspect}" if (rOptimizedProcesses != nil)
416
+
417
+ return rOptimizedProcesses
418
+ end
419
+
420
+ # Read a ratio or db, and get back the corresponding ratio in db
421
+ #
422
+ # Parameters::
423
+ # * *iStrValue* (_String_): The value to read
424
+ # Return::
425
+ # * _Float_: The corresponding ratio in db
426
+ def self.readStrRatio(iStrValue)
427
+ rRatio = nil
428
+
429
+ lMatch = iStrValue.match(/^(.*)db$/)
430
+ if (lMatch == nil)
431
+ # The argument is a ratio
432
+ rRatio = val2db(iStrValue.to_f)
433
+ else
434
+ # The argument is already in db
435
+ rRatio = iStrValue.to_f
436
+ end
437
+
438
+ return rRatio
439
+ end
440
+
441
+ # Call WSK
442
+ #
443
+ # Parameters::
444
+ # * *iInputFile* (_String_): The input file
445
+ # * *iOutputFile* (_String_): The output file
446
+ # * *iAction* (_String_): The action
447
+ # * *iParams* (_String_): Action parameters [optional = '']
448
+ def wsk(iInputFile, iOutputFile, iAction, iParams = '')
449
+ log_info ''
450
+ log_info "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ..."
451
+ FileUtils::mkdir_p(File.dirname(iOutputFile))
452
+ lCmd = "#{@MusicMasterConf[:WSKCmdLine]} --input \"#{iInputFile}\" --output \"#{iOutputFile}\" --action #{iAction} -- #{iParams}"
453
+ log_debug "#{Dir.getwd}> #{lCmd}"
454
+ system(lCmd)
455
+ lErrorCode = $?.exitstatus
456
+ if (lErrorCode == 0)
457
+ log_info "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... OK"
458
+ else
459
+ log_err "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... ERROR #{lErrorCode}"
460
+ raise RuntimeError, "Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... ERROR #{lErrorCode}"
461
+ end
462
+ log_info ''
463
+ end
464
+
465
+ end
466
+
467
+ end
@@ -0,0 +1,101 @@
1
+ #--
2
+ # Copyright (c) 2009 - 2012 Muriel Salvan (muriel@x-aeon.com)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ module MusicMaster
7
+
8
+ # Apply given record effects on a Wave file.
9
+ # It modifies the given Wave file.
10
+ # It saves original and intermediate Wave files before modifications.
11
+ #
12
+ # Parameters::
13
+ # * *iEffects* (<em>list<map<Symbol,Object>></em>): List of effects to apply
14
+ # * *iFileName* (_String_): File name to apply effects to
15
+ # * *iDir* (_String_): The directory where temporary files are stored
16
+ def self.applyProcesses(iEffects, iFileName, iDir)
17
+ lFileNameNoExt = File.basename(iFileName[0..-5])
18
+ iEffects.each_with_index do |iEffectInfo, iIdxEffect|
19
+ begin
20
+ access_plugin('Processes', iEffectInfo[:Name]) do |ioActionPlugin|
21
+ # Save the file before using the plugin
22
+ lSave = true
23
+ lSaveFileName = "#{iDir}/#{lFileNameNoExt}.Before_#{iIdxEffect}_#{iEffectInfo[:Name]}.wav"
24
+ if (File.exists?(lSaveFileName))
25
+ puts "!!! File #{lSaveFileName} already exists. Overwrite and apply effect ? [y='yes']"
26
+ lSave = ($stdin.gets.chomp == 'y')
27
+ end
28
+ if (lSave)
29
+ log_info "Saving file #{iFileName} to #{lSaveFileName} ..."
30
+ FileUtils::mv(iFileName, lSaveFileName)
31
+ log_info "===== Apply Effect #{iEffectInfo[:Name]} to #{iFileName} ====="
32
+ ioActionPlugin.execute(lSaveFileName, iFileName, iDir, iEffectInfo.clone.delete_if{|iKey, iValue| next (iKey == :Name)})
33
+ end
34
+ end
35
+ rescue Exception
36
+ log_err "An error occurred while processing #{iFileName} with process #{iEffectInfo[:Name]}: #{$!}."
37
+ raise
38
+ end
39
+ end
40
+ end
41
+
42
+ # Convert a Wave file to another music file
43
+ #
44
+ # Parameters::
45
+ # * *iSrcFile* (_String_): Source WAVE file
46
+ # * *iDstFile* (_String_): Destination file
47
+ # * *iParams* (<em>map<Symbol,Object></em>): The parameters:
48
+ # * *:SampleRate* (_Integer_): The new sample rate in Hz
49
+ # * *:BitDepth* (_Integer_): The new bit depth (only for Wave) [optional = nil]
50
+ # * *:Dither* (_Boolean_): Do we apply dither (only for Wave) ? [optional = false]
51
+ # * *:BitRate* (_Integer_): Bit rate in kbps (only for MP3) [optional = 320]
52
+ # * *:FileFormat* (_Symbol_): File format. Here are the possible values: [optional = :Wave]
53
+ # * *:Wave*: Uncompressed PCM Wave file
54
+ # * *:MP3*: MP3 file
55
+ def self.src(iSrcFile, iDstFile, iParams)
56
+ if ((iParams[:FileFormat] != nil) and
57
+ (iParams[:FileFormat] == :MP3))
58
+ # MP3 conversion
59
+ lTranslatedParams = []
60
+ iParams.each do |iParam, iValue|
61
+ case iParam
62
+ when :SampleRate
63
+ lTranslatedParams << "Sample rate: #{iValue} Hz"
64
+ when :BitRate
65
+ lTranslatedParams << "Bit rate: #{iValue} kbps"
66
+ when :FileFormat
67
+ # Nothing to do
68
+ else
69
+ log_err "Unknown MP3 parameter: #{iParam} (value #{iValue.inspect}). Ignoring it."
70
+ end
71
+ end
72
+ puts "Convert file #{iSrcFile} into file #{iDstFile} in MP3 format with following parameters: #{lTranslatedParams.join(', ')}"
73
+ puts 'Press Enter when done.'
74
+ $stdin.gets
75
+ else
76
+ # Wave conversion
77
+ lTranslatedParams = [ '--profile standard', '--twopass' ]
78
+ iParams.each do |iParam, iValue|
79
+ case iParam
80
+ when :SampleRate
81
+ lTranslatedParams << "--rate #{iValue}"
82
+ when :BitDepth
83
+ lTranslatedParams << "--bits #{iValue}"
84
+ when :Dither
85
+ if (iValue == true)
86
+ lTranslatedParams << '--dither 4'
87
+ end
88
+ when :FileFormat
89
+ # Nothing to do
90
+ else
91
+ log_err "Unknown Wave parameter: #{iParam} (value #{iValue.inspect}). Ignoring it."
92
+ end
93
+ end
94
+ FileUtils::mkdir_p(File.dirname(iDstFile))
95
+ lCmd = "#{@MusicMasterConf[:SRCCmdLine]} #{lTranslatedParams.join(' ')} \"#{iSrcFile}\" \"#{iDstFile}\""
96
+ log_info "=> #{lCmd}"
97
+ system(lCmd)
98
+ end
99
+ end
100
+
101
+ end