Module: MusicMaster::Utils
- Included in:
- Launcher
- Defined in:
- lib/MusicMaster/Utils.rb
Constant Summary collapse
- OPTIM_GROUPS =
The groups of processes that can be optimized, and their corresponding optimization methods They are sorted by importance: first ones will have greater priority Here are the symbols used for each group:
-
: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.
- Parameters
-
iLstProcesses (list<map<Symbol,Object>>): List of processes to optimize
- Return
-
list<map<Symbol,Object>>: List of optimized processes. Can be empty to delete them, or nil to not optimize them.
-
[ [ [ 'VolCorrection' ], { :OptimizeProc => Proc.new do |iLstProcesses| rOptimizedProcesses = [] lRatio = 0.0 iLstProcesses.each do |iProcessInfo| lRatio += readStrRatio(iProcessInfo[:Factor]) end if (lRatio != 0) # Replace the serie with just 1 volume correction rOptimizedProcesses = [ { :Name => 'VolCorrection', :Factor => "#{lRatio}db" } ] end next rOptimizedProcesses end } ], [ [ 'DCShifter' ], { :OptimizeProc => Proc.new do |iLstProcesses| rOptimizedProcesses = [] lDCOffset = 0 iLstProcesses.each do |iProcessInfo| lDCOffset += iProcessInfo[:Offset] end if (lDCOffset != 0) # Replace the serie with just 1 DC offset rOptimizedProcesses = [ { :Name => 'DCShifter', :Offset => lDCOffset } ] end next rOptimizedProcesses end } ] ]
- OPTIM_DEBUG =
Activate debug log for this method only
false
Class Method Summary collapse
-
.readStrRatio(iStrValue) ⇒ Object
Read a ratio or db, and get back the corresponding ratio in db.
Instance Method Summary collapse
-
#analyzeFile(iWaveFile, iAnalysisFile) ⇒ Object
Analyze a given wav file, and store the result in the given file name.
-
#fftProfileFile(iWaveFile, iFFTProfileFile) ⇒ Object
Make an FFT profile of a given wav file, and store the result in the given file name.
-
#getAnalysis(iAnalysisFileName) ⇒ Object
Get analysis result.
-
#getDCOffsets(iAnalyzeRecordedFileName) ⇒ Object
Get DC offsets out of an analysis file.
-
#getRMSValue(iAnalysisFileName) ⇒ Object
Get average RMS value from an analysis file.
-
#getThresholds(iAnalysisFileName, iOptions = {}) ⇒ Object
Get signal thresholds, without DC offsets, from an analysis file.
-
#initialize_Utils ⇒ Object
Initialize variables used by utils.
-
#optimizeProcesses(iLstProcesses) ⇒ Object
Optimize a list of processes.
-
#optimizeProcessesByGroups(iLstProcesses, iLstGroups) ⇒ Object
Optimize (or choose not to) a list of processes based on a potential list of optimization groups Prerequisites: * The list of processes has a size > 1 * The list of groups has a size > 0 * Each optimization group has at least 1 process in each of the processes’ list’s elements.
-
#record(iFileName, iAlreadyPrepared = false) ⇒ Object
Record into a given file.
-
#shiftThresholdsByDCOffset(iThresholds, iDCOffsets) ⇒ Object
Shift thresholds by a given DC offset.
-
#wsk(iInputFile, iOutputFile, iAction, iParams = '') ⇒ Object
Call WSK.
Class Method Details
.readStrRatio(iStrValue) ⇒ Object
Read a ratio or db, and get back the corresponding ratio in db
- Parameters
-
iStrValue (String): The value to read
- Return
-
Float: The corresponding ratio in db
426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/MusicMaster/Utils.rb', line 426 def self.readStrRatio(iStrValue) rRatio = nil lMatch = iStrValue.match(/^(.*)db$/) if (lMatch == nil) # The argument is a ratio rRatio = val2db(iStrValue.to_f) else # The argument is already in db rRatio = iStrValue.to_f end return rRatio end |
Instance Method Details
#analyzeFile(iWaveFile, iAnalysisFile) ⇒ Object
Analyze a given wav file, and store the result in the given file name.
- Parameters
-
iWaveFile (String): The wav file to analyze
-
iAnalysisFile (String): The analysis file to store into
85 86 87 88 89 90 91 92 |
# File 'lib/MusicMaster/Utils.rb', line 85 def analyzeFile(iWaveFile, iAnalysisFile) lDummyFile = "#{Dir.tmpdir}/MusicMaster/Dummy.wav" FileUtils::mkdir_p(File.dirname(lDummyFile)) wsk(iWaveFile, lDummyFile, 'Analyze') File.unlink(lDummyFile) FileUtils::mkdir_p(File.dirname(iAnalysisFile)) FileUtils::mv('analyze.result', iAnalysisFile) end |
#fftProfileFile(iWaveFile, iFFTProfileFile) ⇒ Object
Make an FFT profile of a given wav file, and store the result in the given file name.
- Parameters
-
iWaveFile (String): The wav file to analyze
-
iFFTProfileFile (String): The analysis file to store into
71 72 73 74 75 76 77 78 |
# File 'lib/MusicMaster/Utils.rb', line 71 def fftProfileFile(iWaveFile, iFFTProfileFile) lDummyFile = "#{Dir.tmpdir}/MusicMaster/Dummy.wav" FileUtils::mkdir_p(File.dirname(lDummyFile)) wsk(iWaveFile, lDummyFile, 'FFT') File.unlink(lDummyFile) FileUtils::mkdir_p(File.dirname(iFFTProfileFile)) FileUtils::mv('fft.result', iFFTProfileFile) end |
#getAnalysis(iAnalysisFileName) ⇒ Object
Get analysis result
- Parameters
-
iAnalysisFileName (String): The name of the analysis file
- Return
-
map<Symbol,Object>: The analyze result
100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/MusicMaster/Utils.rb', line 100 def getAnalysis(iAnalysisFileName) rResult = nil if (@Cache[:Analysis][iAnalysisFileName] == nil) File.open(iAnalysisFileName, 'rb') do |iFile| rResult = Marshal.load(iFile.read) end @Cache[:Analysis][iAnalysisFileName] = rResult else rResult = @Cache[:Analysis][iAnalysisFileName] end return rResult end |
#getDCOffsets(iAnalyzeRecordedFileName) ⇒ Object
Get DC offsets out of an analysis file
- Parameters
-
iAnalyzeRecordedFileName (String): Name of the file containing analysis
- Return
-
Boolean: Is there an offset ?
-
list<Float>: The DC offsets, per channel
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/MusicMaster/Utils.rb', line 122 def getDCOffsets(iAnalyzeRecordedFileName) rOffset = false rDCOffsets = [] if (@Cache[:DCOffsets][iAnalyzeRecordedFileName] == nil) lAnalyze = getAnalysis(iAnalyzeRecordedFileName) lAnalyze[:MoyValues].each do |iMoyValue| lDCOffset = iMoyValue.round rDCOffsets << lDCOffset if (lDCOffset != 0) rOffset = true end end @Cache[:DCOffsets][iAnalyzeRecordedFileName] = [ rOffset, rDCOffsets ] else rOffset, rDCOffsets = @Cache[:DCOffsets][iAnalyzeRecordedFileName] end return rOffset, rDCOffsets end |
#getRMSValue(iAnalysisFileName) ⇒ Object
Get average RMS value from an analysis file
- Parameters
-
iAnalysisFileName (String): Name of the analysis file
- Return
-
Float: The average RMS value
149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/MusicMaster/Utils.rb', line 149 def getRMSValue(iAnalysisFileName) rRMSValue = nil if (@Cache[:RMSValues][iAnalysisFileName] == nil) lAnalysis = getAnalysis(iAnalysisFileName) rRMSValue = lAnalysis[:RMSValues].inject{ |iSum, iValue| next (iSum + iValue) } / lAnalysis[:RMSValues].size @Cache[:RMSValues][iAnalysisFileName] = rRMSValue else rRMSValue = @Cache[:RMSValues][iAnalysisFileName] end return rRMSValue end |
#getThresholds(iAnalysisFileName, iOptions = {}) ⇒ Object
Get signal thresholds, without DC offsets, from an analysis file
- Parameters
-
iAnalysisFileName (String): Name of the file containing analysis
-
iOptions (map<Symbol,Object>): Additional options [optional = {}]
-
:margin (Float): The margin to be added, in terms of fraction of the maximal signal value [optional = 0.0]
-
- Return
-
list< [Integer,Integer] >: The [min,max] values, per channel
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/MusicMaster/Utils.rb', line 171 def getThresholds(iAnalysisFileName, iOptions = {}) rThresholds = [] if (@Cache[:Thresholds][iAnalysisFileName] == nil) # Get silence thresholds from the silence file lSilenceAnalyze = getAnalysis(iAnalysisFileName) # Compute the DC offsets lSilenceDCOffsets = lSilenceAnalyze[:MoyValues].map { |iValue| iValue.round } lMargin = iOptions[:margin] || 0.0 lSilenceAnalyze[:MaxValues].each_with_index do |iMaxValue, iIdxChannel| # Remove silence DC Offset lCorrectedMinValue = lSilenceAnalyze[:MinValues][iIdxChannel] - lSilenceDCOffsets[iIdxChannel] lCorrectedMaxValue = iMaxValue - lSilenceDCOffsets[iIdxChannel] # Compute the silence threshold by adding the margin rThresholds << [(lCorrectedMinValue-lCorrectedMinValue.abs*lMargin).to_i, (lCorrectedMaxValue+lCorrectedMaxValue.abs*lMargin).to_i] end @Cache[:Thresholds][iAnalysisFileName] = rThresholds else rThresholds = @Cache[:Thresholds][iAnalysisFileName] end return rThresholds end |
#initialize_Utils ⇒ Object
Initialize variables used by utils
14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/MusicMaster/Utils.rb', line 14 def initialize_Utils # A little cache # map< Symbol, Object > # * *:Analysis* (<em>map<String,Object></em>): Analysis object, per analysis file name # * *:DCOffsets* (<em>map<String,list<Float>></em>): Channels DC offsets, per analysis file name # * *:RMSValues* (<em>map<String,Float></em>): The average RMS values, per analysis file name # * *:Thresholds* (<em>map<String,list< [Integer,Integer] >></em>): List of [min,max] thresholds per channel, per analysis file name @Cache = { :Analysis => {}, :DCOffsets => {}, :RMSValues => {}, :Thresholds => {} } end |
#optimizeProcesses(iLstProcesses) ⇒ Object
Optimize a list of processes. Delete useless ones or ones that cancel themselves.
- Parameters
-
iLstProcesses (list<map<Symbol,Object>>): List of processes
- Return
-
list<map<Symbol,Object>>: The optimized list of processes
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/MusicMaster/Utils.rb', line 275 def optimizeProcesses(iLstProcesses) rNewLstProcesses = [] lModified = true rNewLstProcesses = iLstProcesses while (lModified) # rNewLstProcesses contains the current list log_debug "[Optimize]: ========== Launch optimization for processes list: #{rNewLstProcesses.inspect}" if OPTIM_DEBUG lLstCurrentProcesses = rNewLstProcesses rNewLstProcesses = [] lModified = false # The list of all possible group keys that can be used for optimizations # list< [ list<String>, map<Symbol,Object> ] > lCurrentMatchingGroups = nil lIdxGroupBegin = nil lIdxProcess = 0 while (lIdxProcess < lLstCurrentProcesses.size) lProcessInfo = lLstCurrentProcesses[lIdxProcess] 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 if (lIdxGroupBegin == nil) # We can begin grouping lCurrentMatchingGroups = [] OPTIM_GROUPS.each do |iGroupInfo| if (iGroupInfo[0].include?(lProcessInfo[:Name])) # This group key can begin a new group lCurrentMatchingGroups << iGroupInfo end end if (lCurrentMatchingGroups.empty?) # We can't do anything with this process rNewLstProcesses << lProcessInfo else # We can begin a group lIdxGroupBegin = lIdxProcess end log_debug "[Optimize]: Set process group begin to #{lIdxGroupBegin.inspect}" if OPTIM_DEBUG lIdxProcess += 1 else # We already have some group candidates # Now we remove the groups that do not fit with our current process lNewGroups = lCurrentMatchingGroups.clone.delete_if { |iGroupInfo| !iGroupInfo[0].include?(lProcessInfo[:Name]) } if (lNewGroups.empty?) log_debug '[Optimize]: Closing current matching groups.' if OPTIM_DEBUG # We are closing the group(s) we got lIdxGroupEnd = lIdxProcess - 1 if (lIdxGroupBegin == lIdxGroupEnd) # This is a group of 1 element. log_debug '[Optimize]: Just 1 element to close.' if OPTIM_DEBUG # Just ignore it rNewLstProcesses << lLstCurrentProcesses[lIdxGroupBegin] else log_debug "[Optimize]: #{lIdxGroupEnd-lIdxGroupBegin+1} elements to close." if OPTIM_DEBUG lOptimizedProcesses = optimizeProcessesByGroups(lLstCurrentProcesses[lIdxGroupBegin..lIdxGroupEnd], lCurrentMatchingGroups) if (lOptimizedProcesses == nil) # No optimization log_debug '[Optimize]: Optimizer decided to not optimize.' if OPTIM_DEBUG rNewLstProcesses.concat(lLstCurrentProcesses[lIdxGroupBegin..lIdxGroupEnd]) else # Optimization log_debug "[Optimize]: Optimizer decided to optimize from #{lIdxGroupEnd-lIdxGroupBegin+1} to #{lOptimizedProcesses.size} elements." if OPTIM_DEBUG rNewLstProcesses.concat(lOptimizedProcesses) lModified = true end end lIdxGroupBegin = nil # Process again this element else log_debug "[Optimize]: Matching groups reduced from #{lCurrentMatchingGroups.size} to #{lNewGroups.size} elements." if OPTIM_DEBUG # We just remove groups that are out due to the current process lCurrentMatchingGroups = lNewGroups # Go on to the next element lIdxProcess += 1 end end end # Last elements could have been part of a group 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 if (lIdxGroupBegin != nil) if (lIdxGroupBegin < lLstCurrentProcesses.size - 1) # Indeed lOptimizedProcesses = optimizeProcessesByGroups(lLstCurrentProcesses[lIdxGroupBegin..-1], lCurrentMatchingGroups) if (lOptimizedProcesses == nil) # No optimization log_debug '[Optimize]: Optimizer decided to not optimize last group.' if OPTIM_DEBUG rNewLstProcesses.concat(lLstCurrentProcesses[lIdxGroupBegin..-1]) else # Optimization log_debug "[Optimize]: Optimizer decided to optimize from #{lLstCurrentProcesses.size-lIdxGroupBegin} to #{lOptimizedProcesses.size} elements." if OPTIM_DEBUG rNewLstProcesses.concat(lOptimizedProcesses) lModified = true end else # Just the last element is remaining in the group log_debug '[Optimize]: Just 1 element to close at the end.' if OPTIM_DEBUG rNewLstProcesses << lLstCurrentProcesses[-1] end end end return rNewLstProcesses end |
#optimizeProcessesByGroups(iLstProcesses, iLstGroups) ⇒ Object
Optimize (or choose not to) a list of processes based on a potential list of optimization groups Prerequisites:
-
The list of processes has a size > 1
-
The list of groups has a size > 0
-
Each optimization group has at least 1 process in each of the processes’ list’s elements
- Parameters
-
iLstProcesses (list<map<Symbol,Object>>): The list of processes to optimize
-
iLstGroups (list< [list<String>,map<Symbol,Object>] >): The list of potential optimization groups
- Return
-
list<map<Symbol,Object>>: The corresponding list of processes optimized. Can be empty to delete them, or nil to not optimize them
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/MusicMaster/Utils.rb', line 389 def optimizeProcessesByGroups(iLstProcesses, iLstGroups) rOptimizedProcesses = nil # Now we remove the groups needing several processes and that do not have all their processes among the selected group lLstProcessesNames = iLstProcesses.map { |iProcessInfo| iProcessInfo[:Name] }.uniq lLstMatchingGroups = iLstGroups.clone.delete_if do |iGroupInfo| # All processes from iGroupKey must be present among the current processes group next !(iGroupInfo[0] - lLstProcessesNames).empty? end # lLstMatchingGroups contain all the groups that can offer optimizations log_debug "[Optimize]: #{lLstMatchingGroups.size} groups can offer optimization." if OPTIM_DEBUG if (!lLstMatchingGroups.empty?) # Here we can optimize for real while ((rOptimizedProcesses == nil) and (!lLstMatchingGroups.empty?)) # Choose the biggest priority group first lGroupInfo = lLstMatchingGroups.first # Call the relevant grouping function from the selected group on our list of processes log_debug "[Optimize]: Apply optimization from group #{lGroupInfo.inspect} to processes: #{iLstProcesses.inspect}" if OPTIM_DEBUG rOptimizedProcesses = lGroupInfo[1][:OptimizeProc].call(iLstProcesses) if (rOptimizedProcesses == nil) log_debug '[Optimize]: Group optimizer decided to not optimize.' lLstMatchingGroups = lLstMatchingGroups[1..-1] end end end log_debug "Processes optimized: from\n#{iLstProcesses.pretty_inspect}\nto\n#{rOptimizedProcesses.pretty_inspect}" if (rOptimizedProcesses != nil) return rOptimizedProcesses end |
#record(iFileName, iAlreadyPrepared = false) ⇒ Object
Record into a given file
- Parameters
-
iFileName (String): File name to record into
-
iAlreadyPrepared (Boolean): Is the file to be recorded already prepared ? [optional = false]
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/MusicMaster/Utils.rb', line 34 def record(iFileName, iAlreadyPrepared = false) lTryAgain = true if (File.exists?(iFileName)) puts "File \"#{iFileName}\" already exists. Overwrite ? ['y' = yes]" lTryAgain = ($stdin.gets.chomp == 'y') end while (lTryAgain) puts "Record file \"#{iFileName}\"" lSkip = nil if (iAlreadyPrepared) lSkip = false else puts 'Press Enter to continue once done. Type \'s\' to skip it.' lSkip = ($stdin.gets.chomp == 's') end if (lSkip) lTryAgain = false else # Get the recorded file name lFileName = @MusicMasterConf[:Record][:RecordedFileGetter].call if (!File.exists?(lFileName)) log_err "File #{lFileName} does not exist. Could not get recorded file." else log_info "Getting recorded file: #{lFileName} => #{iFileName}" FileUtils::mkdir_p(File.dirname(iFileName)) FileUtils::mv(lFileName, iFileName) lTryAgain = false end end end end |
#shiftThresholdsByDCOffset(iThresholds, iDCOffsets) ⇒ Object
Shift thresholds by a given DC offset.
- Parameters
-
iThresholds (list< [Integer,Integer] >): The thresholds to shift
-
iDCOffsets (list<Integer>): The DC offsets
- Return
-
list< [Integer,Integer] >: The shifted thresholds
202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/MusicMaster/Utils.rb', line 202 def shiftThresholdsByDCOffset(iThresholds, iDCOffsets) rCorrectedThresholds = [] # Compute the silence thresholds with DC offset applied iThresholds.each_with_index do |iThresholdInfo, iIdxChannel| lChannelDCOffset = iDCOffsets[iIdxChannel] rCorrectedThresholds << iThresholdInfo.map { |iValue| iValue + lChannelDCOffset } end return rCorrectedThresholds end |
#wsk(iInputFile, iOutputFile, iAction, iParams = '') ⇒ Object
Call WSK
- Parameters
-
iInputFile (String): The input file
-
iOutputFile (String): The output file
-
iAction (String): The action
-
iParams (String): Action parameters [optional = ”]
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 |
# File 'lib/MusicMaster/Utils.rb', line 448 def wsk(iInputFile, iOutputFile, iAction, iParams = '') log_info '' log_info "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ..." FileUtils::mkdir_p(File.dirname(iOutputFile)) lCmd = "#{@MusicMasterConf[:WSKCmdLine]} --input \"#{iInputFile}\" --output \"#{iOutputFile}\" --action #{iAction} -- #{iParams}" log_debug "#{Dir.getwd}> #{lCmd}" system(lCmd) lErrorCode = $?.exitstatus if (lErrorCode == 0) log_info "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... OK" else log_err "========== Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... ERROR #{lErrorCode}" raise RuntimeError, "Processing #{iInputFile} ==#{iAction}==> #{iOutputFile} | #{iParams} ... ERROR #{lErrorCode}" end log_info '' end |