Extracting random fixed sized feature windows from variable input sizes in Audio with a Tensorflow dataset pipeline

Viewed 32

I have list of strong labeled audio files of variable length, i.e., with sound events with known start and end, with which I am training a Sound Event Detection model.

For this purpose I previously used a Data Generator in which I

  1. extracted random fixed-size feature windows from the audio files,
  2. transformed the audio slides to melspectrograms,
  3. and then created a multi hot vector from the list of sound events, so that the model received as input (batch_size, time_steps, freq_bins, n_channels) and (batch_size, time_steps, n_classes) as label.

For performance reasons, I am now trying to create a tf.data pipeline in which I am fist loading the variable sized audio files and labels:

def paths_and_labels_to_dataset(file_path):
    """Constructs a dataset of audios and labels."""
    # 1) Read X (audio wave) from file_path
    path_ds  = tf.data.Dataset.from_tensor_slices(file_path)
    audio_ds = path_ds.map(lambda x: path_to_audio(x), num_parallel_calls=AUTO)

    # 2) Convert sound event labels of shape (-1, 3) to ragged tensors
    labels = [[[0,     1.5, 0], # onset, offset, class_id
               [9.85,  10,  1]]]

    rt        = tf.ragged.constant(labels)
    label_ds  = tf.data.Dataset.from_tensor_slices(rt)
    
    return tf.data.Dataset.zip((audio_ds, label_ds))

def path_to_audio(path):
    """Reads and decodes an audio file."""
    audio    = tf.io.read_file(path)
    audio, _ = tf.audio.decode_wav(audio, 1)    
    return audio

and then cache them. For there on, I am extracting random audio windows of a fixed size with:

def limit_seconds_audio(x_audio, y):
    len_audio = len(x_audio)
    effective_length = SAMPLE_RATE * SECONDS
    
    # Shorter audio: Pad the end with zeros
    if len_audio <= effective_length:
        x_audio = tf.concat([x_audio, tf.expand_dims(tf.zeros([effective_length - len_audio]), 1)], axis=0)
        
    # Longer audio: Random crop a window size of SECONDS * SAMPLE_RATE
    else:
        start   = tf.random.uniform([1], maxval = len_audio - effective_length, dtype = tf.int32)[0]
        x_audio = x_audio[start : start+effective_length]
    
    return x_audio, y

After this, x_audio is transformed to melspectograms.

Now my issue is, that I need to transform the sound event label list of the shape:

labels = [[[0,     1.5, 0], # onset seconds, offset seconds, class_id
           [9.85,  10,  1]]]

to (time_steps, n_classes) for the given audio window from limit_seconds_audio.

In my DataGenerator, I simply would slice the sound event label list for a given audio with get_event_slice with the start and stop seconds:

def get_event_slice(signal, events_for_audiofile, start, stop, sample_rate = 4_000):
    signal     = signal[int(start):int(stop)]
    signal_len = len(signal) / sample_rate # in seconds
    events     = [item for item in events_for_audiofile if item[0] > start and item[0] <= stop]
    new_events = []
    
    for ev in events:
        new_events.append([
            (ev[0] - start),
            (ev[1] - start),
            ev[2]
        ])
    
    # truncate to the signal length
    if new_events[-1][1] / sample_rate > signal_len:
        new_events[-1][1] = len(signal)
 
    return signal, new_events

and then compute the multi hot vector by convert_labels_to_timesteps, where timesteps corresponds to the width dimension of the melspectogram:

def out_frames(sec, timesteps = 622, max_seconds = 10):
    return sec * (timesteps / max_seconds)

def convert_labels_to_timesteps(x_audio, y, timesteps = 622, num_classes = 8):
    # y is of shape (-1, 3), see above
    
    label       = np.zeros((timesteps, num_classes))
    
    # seconds to frames
    frame_start = np.floor(out_frames(y[:, 0])).astype(int)
    frame_end   = np.ceil(out_frames(y[:, 1])).astype(int)
    
    # iterate each class
    se_class    = y[:, 2].astype(int)

    for ind, val in enumerate(se_class):
        label[frame_start[ind]:frame_end[ind], val] = 1 # 1 for active

    return x_audio, label

resulting in (time_steps, n_classes), which I have not been able to do within a tf.data pipeline.

Any help or hint to 1) slice the ragged tensors for the respective feature window and then 2) transform then into the shape (time_steps, n_classes) is greatly appreciated.

1 Answers

For anyone dealing with the same issue of i) varying audio lengths and ii) variable label sizes, I've come up with a different solution that still extracts random feature windows:

  1. Zero pad all audios to either the longest one or to a fixed size
  2. Compute strong targets according to the new audio length
  3. Compute spectograms in your pipeline and concat them with the respective strong target of shape (time_steps, n_classes)
  4. Make use of tf.image.random_crop on the combined spectrogram-label Tensor and set the size to [TARGET_WINDOW, MEL BINS, n] to only crop in the time (or width) dimension

I've put everything together in a gist for reference.

Related