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.
- data/AUTHORS +4 -1
- data/ChangeLog +28 -0
- data/LICENSE +1 -1
- data/README +2 -5
- data/ReleaseInfo +8 -8
- data/bin/Calibrate.rb +55 -0
- data/bin/Clean.rb +55 -0
- data/bin/DBConvert.rb +3 -1
- data/bin/Deliver.rb +73 -42
- data/bin/Mix.rb +39 -78
- data/bin/Process.rb +55 -0
- data/bin/Record.rb +63 -116
- data/bin/{Album.rb → old/Album.rb} +18 -18
- data/bin/{AnalyzeAlbum.rb → old/AnalyzeAlbum.rb} +11 -11
- data/bin/{DeliverAlbum.rb → old/DeliverAlbum.rb} +12 -12
- data/bin/{Fct2Wave.rb → old/Fct2Wave.rb} +11 -11
- data/{album.conf.rb.example → bin/old/album.conf.rb.example} +0 -0
- data/lib/MusicMaster/FilesNamer.rb +253 -0
- data/lib/MusicMaster/Formats/MP3.rb +50 -0
- data/lib/MusicMaster/Formats/Test.rb +44 -0
- data/lib/MusicMaster/Formats/Wave.rb +80 -0
- data/lib/MusicMaster/Hash.rb +20 -0
- data/lib/MusicMaster/Launcher.rb +241 -0
- data/lib/MusicMaster/Processes/ApplyVolumeFct.rb +4 -8
- data/lib/MusicMaster/Processes/Compressor.rb +50 -47
- data/lib/MusicMaster/Processes/Custom.rb +3 -3
- data/lib/MusicMaster/Processes/{AddSilence.rb → Cut.rb} +8 -4
- data/lib/MusicMaster/Processes/CutFirstSignal.rb +6 -3
- data/lib/MusicMaster/Processes/DCShifter.rb +30 -0
- data/lib/MusicMaster/Processes/GVerb.rb +3 -2
- data/lib/MusicMaster/Processes/Normalize.rb +7 -6
- data/lib/MusicMaster/Processes/SilenceInserter.rb +31 -0
- data/lib/MusicMaster/Processes/Test.rb +39 -0
- data/lib/MusicMaster/Processes/VolCorrection.rb +3 -3
- data/lib/MusicMaster/RakeProcesses.rb +1014 -0
- data/lib/MusicMaster/Symbol.rb +12 -0
- data/lib/MusicMaster/Task.rb +29 -0
- data/lib/MusicMaster/Utils.rb +467 -0
- data/lib/MusicMaster/old/Common.rb +101 -0
- data/musicmaster.conf.rb.example +42 -59
- data/record.conf.rb.example +374 -30
- metadata +91 -41
- data/TODO +0 -3
- data/bin/Master.rb +0 -60
- data/bin/PrepareMix.rb +0 -422
- data/lib/MusicMaster/Common.rb +0 -197
- data/lib/MusicMaster/ConfLoader.rb +0 -56
- data/lib/MusicMaster/musicmaster.conf.rb +0 -96
- 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
|