Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/es-419/hub/tutorials/spice.ipynb
25118 views
Kernel: Python 3

Licensed under the Apache License, Version 2.0 (the "License");

#@title Copyright 2020 The TensorFlow Hub Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ==============================================================================

Detección de tono con SPICE

En este Colab, le mostraremos cómo usar el modelo SPICE que se descarga de TensorFlow Hub.

!sudo apt-get install -q -y timidity libsndfile1
# All the imports to deal with sound data !pip install pydub librosa music21
import tensorflow as tf import tensorflow_hub as hub import numpy as np import matplotlib.pyplot as plt import librosa from librosa import display as librosadisplay import logging import math import statistics import sys from IPython.display import Audio, Javascript from scipy.io import wavfile from base64 import b64decode import music21 from pydub import AudioSegment logger = logging.getLogger() logger.setLevel(logging.ERROR) print("tensorflow: %s" % tf.__version__) #print("librosa: %s" % librosa.__version__)

El archivo de entrada de audio

Ahora la parte más difícil: ¡Graba tu canto! 😃

Proporcionamos cuatro métodos para obtener un archivo de audio:

  1. Grabar el audio directamente en Colab

  2. Cargar desde su computadora

  3. Usar un archivo guardado en Google Drive

  4. Descargar el archivo desde Internet

Elija uno de los cuatro métodos siguientes.

#@title [Run this] Definition of the JS code to record audio straight from the browser RECORD = """ const sleep = time => new Promise(resolve => setTimeout(resolve, time)) const b2text = blob => new Promise(resolve => { const reader = new FileReader() reader.onloadend = e => resolve(e.srcElement.result) reader.readAsDataURL(blob) }) var record = time => new Promise(async resolve => { stream = await navigator.mediaDevices.getUserMedia({ audio: true }) recorder = new MediaRecorder(stream) chunks = [] recorder.ondataavailable = e => chunks.push(e.data) recorder.start() await sleep(time) recorder.onstop = async ()=>{ blob = new Blob(chunks) text = await b2text(blob) resolve(text) } recorder.stop() }) """ def record(sec=5): try: from google.colab import output except ImportError: print('No possible to import output from google.colab') return '' else: print('Recording') display(Javascript(RECORD)) s = output.eval_js('record(%d)' % (sec*1000)) fname = 'recorded_audio.wav' print('Saving to', fname) b = b64decode(s.split(',')[1]) with open(fname, 'wb') as f: f.write(b) return fname
#@title Select how to input your audio { run: "auto" } INPUT_SOURCE = 'https://storage.googleapis.com/download.tensorflow.org/data/c-scale-metronome.wav' #@param ["https://storage.googleapis.com/download.tensorflow.org/data/c-scale-metronome.wav", "RECORD", "UPLOAD", "./drive/My Drive/YOUR_MUSIC_FILE.wav"] {allow-input: true} print('You selected', INPUT_SOURCE) if INPUT_SOURCE == 'RECORD': uploaded_file_name = record(5) elif INPUT_SOURCE == 'UPLOAD': try: from google.colab import files except ImportError: print("ImportError: files from google.colab seems to not be available") else: uploaded = files.upload() for fn in uploaded.keys(): print('User uploaded file "{name}" with length {length} bytes'.format( name=fn, length=len(uploaded[fn]))) uploaded_file_name = next(iter(uploaded)) print('Uploaded file: ' + uploaded_file_name) elif INPUT_SOURCE.startswith('./drive/'): try: from google.colab import drive except ImportError: print("ImportError: files from google.colab seems to not be available") else: drive.mount('/content/drive') # don't forget to change the name of the file you # will you here! gdrive_audio_file = 'YOUR_MUSIC_FILE.wav' uploaded_file_name = INPUT_SOURCE elif INPUT_SOURCE.startswith('http'): !wget --no-check-certificate 'https://storage.googleapis.com/download.tensorflow.org/data/c-scale-metronome.wav' -O c-scale.wav uploaded_file_name = 'c-scale.wav' else: print('Unrecognized input format!') print('Please select "RECORD", "UPLOAD", or specify a file hosted on Google Drive or a file from the web to download file to download')

Preparar los datos de audio

Ahora que tenemos el audio, lo convertiremos en el formato requerido y luego los escucharemos.

La entrada del modelo SPICE debe ser un archivo de audio a una frecuencia de muestreo de 16 kHz y con un solo canal (mono).

Para ayudarle con esta parte, creamos una función (convert_audio_for_model) para convertir cualquier archivo wav que tenga al formato que requiere el modelo:

# Function that converts the user-created audio to the format that the model # expects: bitrate 16kHz and only one channel (mono). EXPECTED_SAMPLE_RATE = 16000 def convert_audio_for_model(user_file, output_file='converted_audio_file.wav'): audio = AudioSegment.from_file(user_file) audio = audio.set_frame_rate(EXPECTED_SAMPLE_RATE).set_channels(1) audio.export(output_file, format="wav") return output_file
# Converting to the expected format for the model # in all the input 4 input method before, the uploaded file name is at # the variable uploaded_file_name converted_audio_file = convert_audio_for_model(uploaded_file_name)
# Loading audio samples from the wav file: sample_rate, audio_samples = wavfile.read(converted_audio_file, 'rb') # Show some basic information about the audio. duration = len(audio_samples)/sample_rate print(f'Sample rate: {sample_rate} Hz') print(f'Total duration: {duration:.2f}s') print(f'Size of the input: {len(audio_samples)}') # Let's listen to the wav file. Audio(audio_samples, rate=sample_rate)

Primero lo primero, veamos la forma de onda de nuestro canto.

# We can visualize the audio as a waveform. _ = plt.plot(audio_samples)

Una visualización más informativa es el espectrograma, que muestra las frecuencias presentes a lo largo del tiempo.

Aquí usamos una escala de frecuencia logarítmica para que el canto sea más claramente visible.

MAX_ABS_INT16 = 32768.0 def plot_stft(x, sample_rate, show_black_and_white=False): x_stft = np.abs(librosa.stft(x, n_fft=2048)) fig, ax = plt.subplots() fig.set_size_inches(20, 10) x_stft_db = librosa.amplitude_to_db(x_stft, ref=np.max) if(show_black_and_white): librosadisplay.specshow(data=x_stft_db, y_axis='log', sr=sample_rate, cmap='gray_r') else: librosadisplay.specshow(data=x_stft_db, y_axis='log', sr=sample_rate) plt.colorbar(format='%+2.0f dB') plot_stft(audio_samples / MAX_ABS_INT16 , sample_rate=EXPECTED_SAMPLE_RATE) plt.show()

Ahora se necesita una última conversión. Las muestras de audio están en formato int16. y debemos normalizarlas en floats de entre -1 y 1.

audio_samples = audio_samples / float(MAX_ABS_INT16)

Ejecutar el modelo

Ahora la parte fácil, carguemos el modelo con TensorFlow Hub e ingresemos el audio. SPICE nos dará dos salidas: tono e incertidumbre.

TensorFlow Hub es una biblioteca para la publicación, descubrimiento y consumo de partes reutilizables de modelos de aprendizaje automático. Facilita el uso del aprendizaje automático para resolver sus desafíos.

Para cargar el modelo solo se necesita el módulo Hub y la URL que apunta al modelo:

# Loading the SPICE model is easy: model = hub.load("https://tfhub.dev/google/spice/2")

Nota: Un detalle interesante a destacar es que todas las URL del modelo de Hub se pueden usar para descargar y también para leer la documentación, por lo que si apunta su navegador a ese enlace, puede leer la documentación sobre cómo usar el modelo y aprender más sobre cómo fue entrenado.

Una vez que se carga el modelo y se preparan los datos, necesitamos 3 líneas para obtener el resultado:

# We now feed the audio to the SPICE tf.hub model to obtain pitch and uncertainty outputs as tensors. model_output = model.signatures["serving_default"](tf.constant(audio_samples, tf.float32)) pitch_outputs = model_output["pitch"] uncertainty_outputs = model_output["uncertainty"] # 'Uncertainty' basically means the inverse of confidence. confidence_outputs = 1.0 - uncertainty_outputs fig, ax = plt.subplots() fig.set_size_inches(20, 10) plt.plot(pitch_outputs, label='pitch') plt.plot(confidence_outputs, label='confidence') plt.legend(loc="lower right") plt.show()

Para que los resultados sean más fáciles de entender, eliminaremos todas las estimaciones de tono con confianza baja (confianza < 0,9) y trazaremos las restantes.

confidence_outputs = list(confidence_outputs) pitch_outputs = [ float(x) for x in pitch_outputs] indices = range(len (pitch_outputs)) confident_pitch_outputs = [ (i,p) for i, p, c in zip(indices, pitch_outputs, confidence_outputs) if c >= 0.9 ] confident_pitch_outputs_x, confident_pitch_outputs_y = zip(*confident_pitch_outputs) fig, ax = plt.subplots() fig.set_size_inches(20, 10) ax.set_ylim([0, 1]) plt.scatter(confident_pitch_outputs_x, confident_pitch_outputs_y, ) plt.scatter(confident_pitch_outputs_x, confident_pitch_outputs_y, c="r") plt.show()

Los valores de tono que devuelve SPICE están en el rango de 0 a 1. Los convertiremos en valores de tono absoluto en Hz.

def output2hz(pitch_output): # Constants taken from https://tfhub.dev/google/spice/2 PT_OFFSET = 25.58 PT_SLOPE = 63.07 FMIN = 10.0; BINS_PER_OCTAVE = 12.0; cqt_bin = pitch_output * PT_SLOPE + PT_OFFSET; return FMIN * 2.0 ** (1.0 * cqt_bin / BINS_PER_OCTAVE) confident_pitch_values_hz = [ output2hz(p) for p in confident_pitch_outputs_y ]

Ahora, veamos qué tan buena es la predicción: vamos a superponer los tonos predichos sobre el espectrograma original. Para que las predicciones de tono sean más visibles, cambiamos el espectrograma a blanco y negro.

plot_stft(audio_samples / MAX_ABS_INT16 , sample_rate=EXPECTED_SAMPLE_RATE, show_black_and_white=True) # Note: conveniently, since the plot is in log scale, the pitch outputs # also get converted to the log scale automatically by matplotlib. plt.scatter(confident_pitch_outputs_x, confident_pitch_values_hz, c="r") plt.show()

Convertir a notas musicales

Ahora que tenemos los valores de tono, ¡vamos a convertirlos en notas! Esta parte sí que es un desafío. Tenemos que tener en cuenta dos cosas:

  1. las pausas (cuando no hay canto)

  2. el tamaño de cada nota (desplazamientos)

1: Agregar ceros a la salida para indicar los momentos sin canto

pitch_outputs_and_rests = [ output2hz(p) if c >= 0.9 else 0 for i, p, c in zip(indices, pitch_outputs, confidence_outputs) ]

2: Agregar desplazamientos de notas

Cuando una persona canta libremente, la melodía puede tener un desplazamiento de los valores de tono absoluto que las notas pueden representar. Por lo tanto, para convertir predicciones en notas, es necesario corregir este posible desplazamiento. Esto es lo que calcula el siguiente código.

A4 = 440 C0 = A4 * pow(2, -4.75) note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] def hz2offset(freq): # This measures the quantization error for a single note. if freq == 0: # Rests always have zero error. return None # Quantized note. h = round(12 * math.log2(freq / C0)) return 12 * math.log2(freq / C0) - h # The ideal offset is the mean quantization error for all the notes # (excluding rests): offsets = [hz2offset(p) for p in pitch_outputs_and_rests if p != 0] print("offsets: ", offsets) ideal_offset = statistics.mean(offsets) print("ideal offset: ", ideal_offset)

Ahora podemos usar algunas heurísticas para intentar estimar la secuencia más probable de notas cantadas. El desplazamiento ideal calculado anteriormente es un ingrediente, pero también necesitamos conocer la velocidad (¿cuántas predicciones forman, por ejemplo, un octavo?) y el desplazamiento de tiempo para comenzar a cuantificar. Para simplificar, probaremos diferentes velocidades y desplazamientos de tiempo y mediremos el error de cuantificación. Finalmente, usaremos los valores que minimicen este error.

def quantize_predictions(group, ideal_offset): # Group values are either 0, or a pitch in Hz. non_zero_values = [v for v in group if v != 0] zero_values_count = len(group) - len(non_zero_values) # Create a rest if 80% is silent, otherwise create a note. if zero_values_count > 0.8 * len(group): # Interpret as a rest. Count each dropped note as an error, weighted a bit # worse than a badly sung note (which would 'cost' 0.5). return 0.51 * len(non_zero_values), "Rest" else: # Interpret as note, estimating as mean of non-rest predictions. h = round( statistics.mean([ 12 * math.log2(freq / C0) - ideal_offset for freq in non_zero_values ])) octave = h // 12 n = h % 12 note = note_names[n] + str(octave) # Quantization error is the total difference from the quantized note. error = sum([ abs(12 * math.log2(freq / C0) - ideal_offset - h) for freq in non_zero_values ]) return error, note def get_quantization_and_error(pitch_outputs_and_rests, predictions_per_eighth, prediction_start_offset, ideal_offset): # Apply the start offset - we can just add the offset as rests. pitch_outputs_and_rests = [0] * prediction_start_offset + \ pitch_outputs_and_rests # Collect the predictions for each note (or rest). groups = [ pitch_outputs_and_rests[i:i + predictions_per_eighth] for i in range(0, len(pitch_outputs_and_rests), predictions_per_eighth) ] quantization_error = 0 notes_and_rests = [] for group in groups: error, note_or_rest = quantize_predictions(group, ideal_offset) quantization_error += error notes_and_rests.append(note_or_rest) return quantization_error, notes_and_rests best_error = float("inf") best_notes_and_rests = None best_predictions_per_note = None for predictions_per_note in range(20, 65, 1): for prediction_start_offset in range(predictions_per_note): error, notes_and_rests = get_quantization_and_error( pitch_outputs_and_rests, predictions_per_note, prediction_start_offset, ideal_offset) if error < best_error: best_error = error best_notes_and_rests = notes_and_rests best_predictions_per_note = predictions_per_note # At this point, best_notes_and_rests contains the best quantization. # Since we don't need to have rests at the beginning, let's remove these: while best_notes_and_rests[0] == 'Rest': best_notes_and_rests = best_notes_and_rests[1:] # Also remove silence at the end. while best_notes_and_rests[-1] == 'Rest': best_notes_and_rests = best_notes_and_rests[:-1]

¡Ahora escribamos las notas cuantificadas como partituras!

Para eso usaremos dos bibliotecas: music21 y Open Sheet Music Display

Nota: para simplificar, asumimos aquí que todas las notas tienen la misma duración (una blanca).

# Creating the sheet music score. sc = music21.stream.Score() # Adjust the speed to match the actual singing. bpm = 60 * 60 / best_predictions_per_note print ('bpm: ', bpm) a = music21.tempo.MetronomeMark(number=bpm) sc.insert(0,a) for snote in best_notes_and_rests: d = 'half' if snote == 'Rest': sc.append(music21.note.Rest(type=d)) else: sc.append(music21.note.Note(snote, type=d))
#@title [Run this] Helper function to use Open Sheet Music Display (JS code) to show a music score from IPython.core.display import display, HTML, Javascript import json, random def showScore(score): xml = open(score.write('musicxml')).read() showMusicXML(xml) def showMusicXML(xml): DIV_ID = "OSMD_div" display(HTML('<div id="'+DIV_ID+'">loading OpenSheetMusicDisplay</div>')) script = """ var div_id = %%DIV_ID%%; function loadOSMD() { return new Promise(function(resolve, reject){ if (window.opensheetmusicdisplay) { return resolve(window.opensheetmusicdisplay) } // OSMD script has a 'define' call which conflicts with requirejs var _define = window.define // save the define object window.define = undefined // now the loaded script will ignore requirejs var s = document.createElement( 'script' ); s.setAttribute( 'src', "https://cdn.jsdelivr.net/npm/[email protected]/build/opensheetmusicdisplay.min.js" ); //s.setAttribute( 'src', "/custom/opensheetmusicdisplay.js" ); s.onload=function(){ window.define = _define resolve(opensheetmusicdisplay); }; document.body.appendChild( s ); // browser will try to load the new script tag }) } loadOSMD().then((OSMD)=>{ window.openSheetMusicDisplay = new OSMD.OpenSheetMusicDisplay(div_id, { drawingParameters: "compacttight" }); openSheetMusicDisplay .load(%%data%%) .then( function() { openSheetMusicDisplay.render(); } ); }) """.replace('%%DIV_ID%%',DIV_ID).replace('%%data%%',json.dumps(xml)) display(Javascript(script)) return
# rendering the music score showScore(sc) print(best_notes_and_rests)

Convirtamos las notas musicales a un archivo MIDI y escuchémoslo.

Para crear este archivo, podemos usar la secuencia que creamos antes.

# Saving the recognized musical notes as a MIDI file converted_audio_file_as_midi = converted_audio_file[:-4] + '.mid' fp = sc.write('midi', fp=converted_audio_file_as_midi)
wav_from_created_midi = converted_audio_file_as_midi.replace(' ', '_') + "_midioutput.wav" print(wav_from_created_midi)

Para escucharlo en Colab, necesitamos volver a convertirlo a wav. Una forma sencilla de hacerlo es con Timidity.

!timidity $converted_audio_file_as_midi -Ow -o $wav_from_created_midi

Y finalmente, escuche el audio; creado a partir de notas, creado vía MIDI a partir de los tonos predichos, inferidos por el modelo.

Audio(wav_from_created_midi)