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