MusicMaster 0.0.1.20101110 → 1.0.0.20120307

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