class TEF::ProgramSelection::SoundCollection

Sound-File collection.

This class is meant as convenient container for sound-filenames. It will automatically scan the current directory for any file ending with '.mp3', '.ogg' or '.wav', and will construct a unique {ID} for it based on the filepath. Paths can then be retrieved by using {#soundmap}.

Additionally, it keeps a list of silences, the {#silence_maps}. They are auto-generated lists of silences or loud sections of the file, useful for auto-generating programs. They can be retrieved via {#silence_maps}

Note that to play a sound, this should be done via {Sequencing::SheetSequence#play}, to ensure that the sound is killed in synch with the program.

Attributes

load_config[R]

@return [Hash] Config loaded from './sounds/soundconfig.yml'

silence_maps[R]

@return [Hash<String, Hash<Numeric, Numeric>>] List of noise levels

for a given file-path. Is in the format { timestamp (in s) => 0 to 1 }
It is ensured that a 0 is inserted at the beginning and end of the
sound track, and it is ensured that the hash keys are sorted.

@see silences_for

soundmap[R]

@return [Hash<ID, String>] Map of {ID}s to matching file-paths

Public Class Methods

new(program_handler) click to toggle source

Initialize a SoundCollection

This will scan the current directory for any sound files. Found files will auto-generate a ID based on their path, and are registered with the passed {Selector}.

Paths are deconstructed as follows:

  • The path is split along any '/' or '-'. Each element up to the last is taken as a group. The last element in the list is taken as title.

  • The variant is generated by taking the sequence (-d+)?.(mp3|ogg|wav) from the end. This means that variants can be specified by appending a '-1234' to the title.

'./sounds/portal/announcer-hello-4.mp3' is registered as:

{Selector#register_ID}('hello', ['sounds', 'portal', 'announcer'], '-4.mp3');

Also note that a custom config file, {#load_config}, is loaded from

a YAML file './sounds/soundconfig.yml', if present.

Additionally, the {#silence_maps} are loaded from, and saved to,

'./sounds/silence_maps.yml'
# File lib/tef/ProgramSelection/SoundCollection.rb, line 61
def initialize(program_handler)
        @handler = program_handler
        @soundmap = {}

        @load_config  = {};
        @silence_maps = {}

        if File.file? './sounds/soundconfig.yml'
                @load_config = YAML.load File.read './sounds/soundconfig.yml'
        end

        if File.file? './sounds/silence_maps.yml'
                @silence_maps = YAML.load File.read './sounds/silence_maps.yml'
        end

        `find ./`.split("\n").each { |fn| add_file fn };

        File.write('./sounds/silence_maps.yml', YAML.dump(@silence_maps));
end

Public Instance Methods

add_file(fname) click to toggle source

Add a file to the collection of files.

Will auto-generate silences and a matching {ID} as described in {#initialize}.

# File lib/tef/ProgramSelection/SoundCollection.rb, line 120
def add_file(fname)
        rMatch = /^\.\/sounds\/(?<groups>(?:[a-z_]+[\/-])*)(?<title>[a-z_]+)(?<variant>(?:-\d+)?\.(?:ogg|mp3|wav))/.match fname;
        return unless rMatch;

        title = rMatch[:title].gsub('_', ' ');
        groups = rMatch[:groups].gsub('_', ' ').gsub('-','/').split('/');

        groups = ["default"] if groups.empty?

        id = @handler.register_ID(title, groups, rMatch[:variant])

        @soundmap[id] = fname

        generate_silences fname
end
silences_for(key) click to toggle source

@return [Hash<Numeric, Numeric>, nil] The silence map for

the passed {ID}, or nil if none was found.

@see silence_maps

# File lib/tef/ProgramSelection/SoundCollection.rb, line 140
def silences_for(key)
        @silence_maps[@soundmap[key]]
end

Private Instance Methods

generate_silences(fname) click to toggle source

Internal function. Generates a list of silences/loud sections with ffmpeg.

# File lib/tef/ProgramSelection/SoundCollection.rb, line 83
        def generate_silences(fname)
        return if @silence_maps[fname]

        ffmpeg_str = `ffmpeg -i #{fname} -af silencedetect=n=0.1:d=0.1 -f null - 2>&1`

        out_event = {}

        ffmpeg_str.split("\n").each do |line|
                if line =~ /silence_start: ([\d\.-]*)/
                        out_event[$1.to_f] = 0
                elsif line =~ /silence_end: ([\d\.-]*)/
                        out_event[$1.to_f] = 1
                elsif line =~ /Duration: (\d+):(\d+):([\d\.]+)/
                        out_event[$1.to_f * 3600 + $2.to_f * 60 + $3.to_f] = 0
                end
        end

        out_event = out_event.sort.to_h

        if(out_event.empty?)
                out_event[0.01] = 1
        elsif (k = out_event.keys[0]) < 0
                out_event.delete k
                out_event[0.01] = 0
        else
                out_event[0.01] = 1
        end

        out_event = out_event.sort.to_h

        @silence_maps[fname] = out_event
end