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

Instance Method Summary collapse

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_UtilsObject

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