improve script berikut ini agar dapat mendownload audio dari link spotify yang di berikan lalu menjalan kan music transcription nya lalu dapat di download hasil file midi nya, dan pastikan script dapat dijalankan dengan benar pada google collab, buang code-code yang tidak diperlukan pastikan script bersih dan lengkap : import os
import subprocess
import shutil
import tempfile
import librosa
import soundfile as sf
from basic_pitch.inference import predict_and_save
from basic_pitch import ICASSP_2022_MODEL_PATH
import warnings
import pretty_midi  # For advanced MIDI processing
import noisereduce as nr  # For advanced noise reduction
import fluidsynth  # For MIDI synthesis to audio

# Copyright 2021 Google LLC. 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.
# ==============================================================================

#@title Setup Environment
#@markdown Install MT3 and its dependencies (may take a few minutes).

!apt-get update -qq && apt-get install -qq libfluidsynth3 build-essential libasound2-dev libjack-dev

# install mt3
!git clone --branch=main https://github.com/magenta/mt3
!mv mt3 mt3_tmp; mv mt3_tmp/* .; rm -r mt3_tmp
!python3 -m pip install jax[cuda12] nest-asyncio pyfluidsynth==1.3.0 -e . -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html

# copy checkpoints
!gsutil -q -m cp -r gs://mt3/checkpoints .

# copy soundfont (originally from https://sites.google.com/site/soundfonts4u)
!gsutil -q -m cp gs://magentadata/soundfonts/SGM-v2.01-Sal-Guit-Bass-V1.3.sf2 .

import json
import IPython

# The below functions (load_gtag and log_event) handle Google Analytics event
# logging. The logging is anonymous and stores only very basic statistics of the
# audio and transcription e.g. length of audio, number of transcribed notes.

def load_gtag():
  """Loads gtag.js."""
  # Note: gtag.js MUST be loaded in the same cell execution as the one doing
  # synthesis. It does NOT persist across cell executions!
  html_code = '''
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4P250YRJ08"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-4P250YRJ08',
       {'referrer': document.referrer.split('?')[0],
        'anonymize_ip': true,
        'page_title': '',
        'page_referrer': '',
        'cookie_prefix': 'magenta',
        'cookie_domain': 'auto',
        'cookie_expires': 0,
        'cookie_flags': 'SameSite=None;Secure'});
</script>
'''
  IPython.display.display(IPython.display.HTML(html_code))

def log_event(event_name, event_details):
  """Log event with name and details dictionary."""
  details_json = json.dumps(event_details)
  js_string = "gtag('event', '%s', %s);" % (event_name, details_json)
  IPython.display.display(IPython.display.Javascript(js_string))

load_gtag()
log_event('setupComplete', {})



#@title Imports and Definitions

import functools
import os

import numpy as np
import tensorflow.compat.v2 as tf

import functools
import gin
import jax
import librosa
import note_seq
import seqio
import t5
import t5x

from mt3 import metrics_utils
from mt3 import models
from mt3 import network
from mt3 import note_sequences
from mt3 import preprocessors
from mt3 import spectrograms
from mt3 import vocabularies

from google.colab import files

import nest_asyncio
nest_asyncio.apply()

SAMPLE_RATE = 16000
SF2_PATH = 'SGM-v2.01-Sal-Guit-Bass-V1.3.sf2'

def upload_audio(sample_rate):
  data = list(files.upload().values())
  if len(data) > 1:
    print('Multiple files uploaded; using only one.')
  return note_seq.audio_io.wav_data_to_samples_librosa(
    data[0], sample_rate=sample_rate)



class InferenceModel(object):
  """Wrapper of T5X model for music transcription."""

  def __init__(self, checkpoint_path, model_type='mt3'):

    # Model Constants.
    if model_type == 'ismir2021':
      num_velocity_bins = 127
      self.encoding_spec = note_sequences.NoteEncodingSpec
      self.inputs_length = 512
    elif model_type == 'mt3':
      num_velocity_bins = 1
      self.encoding_spec = note_sequences.NoteEncodingWithTiesSpec
      self.inputs_length = 256
    else:
      raise ValueError('unknown model_type: %s' % model_type)

    gin_files = ['/content/mt3/gin/model.gin',
                 f'/content/mt3/gin/{model_type}.gin']

    self.batch_size = 8
    self.outputs_length = 1024
    self.sequence_length = {'inputs': self.inputs_length,
                            'targets': self.outputs_length}

    self.partitioner = t5x.partitioning.PjitPartitioner(
        num_partitions=1)

    # Build Codecs and Vocabularies.
    self.spectrogram_config = spectrograms.SpectrogramConfig()
    self.codec = vocabularies.build_codec(
        vocab_config=vocabularies.VocabularyConfig(
            num_velocity_bins=num_velocity_bins))
    self.vocabulary = vocabularies.vocabulary_from_codec(self.codec)
    self.output_features = {
        'inputs': seqio.ContinuousFeature(dtype=tf.float32, rank=2),
        'targets': seqio.Feature(vocabulary=self.vocabulary),
    }

    # Create a T5X model.
    self._parse_gin(gin_files)
    self.model = self._load_model()

    # Restore from checkpoint.
    self.restore_from_checkpoint(checkpoint_path)

  @property
  def input_shapes(self):
    return {
          'encoder_input_tokens': (self.batch_size, self.inputs_length),
          'decoder_input_tokens': (self.batch_size, self.outputs_length)
    }

  def _parse_gin(self, gin_files):
    """Parse gin files used to train the model."""
    gin_bindings = [
        'from __gin__ import dynamic_registration',
        'from mt3 import vocabularies',
        'VOCAB_CONFIG=@vocabularies.VocabularyConfig()',
        'vocabularies.VocabularyConfig.num_velocity_bins=%NUM_VELOCITY_BINS'
    ]
    with gin.unlock_config():
      gin.parse_config_files_and_bindings(
          gin_files, gin_bindings, finalize_config=False)

  def _load_model(self):
    """Load up a T5X `Model` after parsing training gin config."""
    model_config = gin.get_configurable(network.T5Config)()
    module = network.Transformer(config=model_config)
    return models.ContinuousInputsEncoderDecoderModel(
        module=module,
        input_vocabulary=self.output_features['inputs'].vocabulary,
        output_vocabulary=self.output_features['targets'].vocabulary,
        optimizer_def=t5x.adafactor.Adafactor(decay_rate=0.8, step_offset=0),
        input_depth=spectrograms.input_depth(self.spectrogram_config))


  def restore_from_checkpoint(self, checkpoint_path):
    """Restore training state from checkpoint, resets self._predict_fn()."""
    train_state_initializer = t5x.utils.TrainStateInitializer(
      optimizer_def=self.model.optimizer_def,
      init_fn=self.model.get_initial_variables,
      input_shapes=self.input_shapes,
      partitioner=self.partitioner)

    restore_checkpoint_cfg = t5x.utils.RestoreCheckpointConfig(
        path=checkpoint_path, mode='specific', dtype='float32')

    train_state_axes = train_state_initializer.train_state_axes
    self._predict_fn = self._get_predict_fn(train_state_axes)
    self._train_state = train_state_initializer.from_checkpoint_or_scratch(
        [restore_checkpoint_cfg], init_rng=jax.random.PRNGKey(0))

  @functools.lru_cache()
  def _get_predict_fn(self, train_state_axes):
    """Generate a partitioned prediction function for decoding."""
    def partial_predict_fn(params, batch, decode_rng):
      return self.model.predict_batch_with_aux(
          params, batch, decoder_params={'decode_rng': None})
    return self.partitioner.partition(
        partial_predict_fn,
        in_axis_resources=(
            train_state_axes.params,
            t5x.partitioning.PartitionSpec('data',), None),
        out_axis_resources=t5x.partitioning.PartitionSpec('data',)
    )

  def predict_tokens(self, batch, seed=0):
    """Predict tokens from preprocessed dataset batch."""
    prediction, _ = self._predict_fn(
        self._train_state.params, batch, jax.random.PRNGKey(seed))
    return self.vocabulary.decode_tf(prediction).numpy()

  def __call__(self, audio):
    """Infer note sequence from audio samples.

    Args:
      audio: 1-d numpy array of audio samples (16kHz) for a single example.

    Returns:
      A note_sequence of the transcribed audio.
    """
    ds = self.audio_to_dataset(audio)
    ds = self.preprocess(ds)

    model_ds = self.model.FEATURE_CONVERTER_CLS(pack=False)(
        ds, task_feature_lengths=self.sequence_length)
    model_ds = model_ds.batch(self.batch_size)

    inferences = (tokens for batch in model_ds.as_numpy_iterator()
                  for tokens in self.predict_tokens(batch))

    predictions = []
    for example, tokens in zip(ds.as_numpy_iterator(), inferences):
      predictions.append(self.postprocess(tokens, example))

    result = metrics_utils.event_predictions_to_ns(
        predictions, codec=self.codec, encoding_spec=self.encoding_spec)
    return result['est_ns']

  def audio_to_dataset(self, audio):
    """Create a TF Dataset of spectrograms from input audio."""
    frames, frame_times = self._audio_to_frames(audio)
    return tf.data.Dataset.from_tensors({
        'inputs': frames,
        'input_times': frame_times,
    })

  def _audio_to_frames(self, audio):
    """Compute spectrogram frames from audio."""
    frame_size = self.spectrogram_config.hop_width
    padding = [0, frame_size - len(audio) % frame_size]
    audio = np.pad(audio, padding, mode='constant')
    frames = spectrograms.split_audio(audio, self.spectrogram_config)
    num_frames = len(audio) // frame_size
    times = np.arange(num_frames) / self.spectrogram_config.frames_per_second
    return frames, times

  def preprocess(self, ds):
    pp_chain = [
        functools.partial(
            t5.data.preprocessors.split_tokens_to_inputs_length,
            sequence_length=self.sequence_length,
            output_features=self.output_features,
            feature_key='inputs',
            additional_feature_keys=['input_times']),
        # Cache occurs here during training.
        preprocessors.add_dummy_targets,
        functools.partial(
            preprocessors.compute_spectrograms,
            spectrogram_config=self.spectrogram_config)
    ]
    for pp in pp_chain:
      ds = pp(ds)
    return ds

  def postprocess(self, tokens, example):
    tokens = self._trim_eos(tokens)
    start_time = example['input_times'][0]
    # Round down to nearest symbolic token step.
    start_time -= start_time % (1 / self.codec.steps_per_second)
    return {
        'est_tokens': tokens,
        'start_time': start_time,
        # Internal MT3 code expects raw inputs, not used here.
        'raw_inputs': []
    }

  @staticmethod
  def _trim_eos(tokens):
    tokens = np.array(tokens, np.int32)
    if vocabularies.DECODED_EOS_ID in tokens:
      tokens = tokens[:np.argmax(tokens == vocabularies.DECODED_EOS_ID)]
    return tokens


#@title Load Model
#@markdown The `ismir2021` model transcribes piano only, with note velocities.
#@markdown The `mt3` model transcribes multiple simultaneous instruments,
#@markdown but without velocities.

MODEL = "mt3" #@param["ismir2021", "mt3"]

checkpoint_path = f'/content/checkpoints/{MODEL}/'

load_gtag()

log_event('loadModelStart', {'event_category': MODEL})
inference_model = InferenceModel(checkpoint_path, MODEL)
log_event('loadModelComplete', {'event_category': MODEL})

#@title Upload Audio

load_gtag()

log_event('uploadAudioStart', {})
audio = upload_audio(sample_rate=SAMPLE_RATE)
log_event('uploadAudioComplete', {'value': round(len(audio) / SAMPLE_RATE)})

note_seq.notebook_utils.colab_play(audio, sample_rate=SAMPLE_RATE)

#@title Transcribe Audio
#@markdown This may take a few minutes depending on the length of the audio file
#@markdown you uploaded.

load_gtag()

log_event('transcribeStart', {
    'event_category': MODEL,
    'value': round(len(audio) / SAMPLE_RATE)
})

est_ns = inference_model(audio)

log_event('transcribeComplete', {
    'event_category': MODEL,
    'value': round(len(audio) / SAMPLE_RATE),
    'numNotes': sum(1 for note in est_ns.notes if not note.is_drum),
    'numDrumNotes': sum(1 for note in est_ns.notes if note.is_drum),
    'numPrograms': len(set(note.program for note in est_ns.notes
                           if not note.is_drum))
})

note_seq.play_sequence(est_ns, synth=note_seq.fluidsynth,
                       sample_rate=SAMPLE_RATE, sf2_path=SF2_PATH)
note_seq.plot_sequence(est_ns)

#@title Download MIDI Transcription

load_gtag()
log_event('downloadTranscription', {
    'event_category': MODEL,
    'value': round(len(audio) / SAMPLE_RATE),
    'numNotes': sum(1 for note in est_ns.notes if not note.is_drum),
    'numDrumNotes': sum(1 for note in est_ns.notes if note.is_drum),
    'numPrograms': len(set(note.program for note in est_ns.notes
                           if not note.is_drum))
})

note_seq.sequence_proto_to_midi_file(est_ns, '/tmp/transcribed.mid')
files.download('/tmp/transcribed.mid')
warnings.filterwarnings("ignore")  # Suppress warnings for cleaner output

def download_song_from_spotify(song_query):
    """Download song from Spotify using spotdl."""
    tmpdirname = tempfile.mkdtemp()
    try:
        print("Starting song download...")
        # Update the spotdl command to match the latest syntax
        subprocess.run([
            'spotdl', song_query,
            '--output', os.path.join(tmpdirname, '{artist} - {title}.{output-ext}'),
            '--format', 'mp3'  # Ensure the output format is mp3
        ], check=True)
        
        # Fetch the downloaded song
        downloaded_files = os.listdir(tmpdirname)
        if downloaded_files:
            song_file = os.path.join(tmpdirname, downloaded_files[0])
            print(f"Song downloaded successfully: {downloaded_files[0]}")
            return song_file, downloaded_files[0], tmpdirname  # Return song path, song file name, and temp directory
        else:
            print("Failed to download song.")
            return None, None, tmpdirname
    except subprocess.CalledProcessError as e:
        print(f"An error occurred while downloading the song: {e}")
        return None, None, tmpdirname

def convert_audio_to_wav(input_audio_file):
    """Convert audio file to WAV format with standard settings."""
    try:
        print("Converting audio to WAV format...")
        y, sr = librosa.load(input_audio_file, sr=44100, mono=True)
        wav_file = os.path.splitext(input_audio_file)[0] + '.wav'
        sf.write(wav_file, y, sr, subtype='PCM_16')
        print(f"Audio converted to WAV: {wav_file}")
        return wav_file, sr
    except Exception as e:
        print(f"An error occurred during audio conversion: {e}")
        return None, None

def normalize_audio(y):
    """Normalize the audio signal to have consistent volume."""
    try:
        print("Normalizing audio...")
        y_norm = librosa.util.normalize(y)
        print("Audio normalization complete.")
        return y_norm
    except Exception as e:
        print(f"An error occurred during audio normalization: {e}")
        return y

def reduce_noise(y, sr):
    """Reduce noise in the audio signal using spectral gating."""
    try:
        print("Reducing noise using spectral gating...")
        # Estimate the noise sample. We'll take the first 0.5 seconds assuming it is noise.
        noise_duration = 0.5  # seconds
        noise_sample = y[:int(noise_duration * sr)]
        y_reduced_noise = nr.reduce_noise(y=y, sr=sr, y_noise=noise_sample, prop_decrease=1.0)
        print("Noise reduction complete.")
        return y_reduced_noise
    except Exception as e:
        print(f"An error occurred during noise reduction: {e}")
        return y

def audio_to_midi_with_basic_pitch(audio_file, output_dir, song_name):
    """Convert audio file to MIDI using Basic Pitch."""
    if not os.path.exists(audio_file):
        print(f"Error: Audio file {audio_file} not found.")
        return
    
    try:
        print(f"Converting audio to MIDI using Basic Pitch...")
        # Load audio
        y, sr = librosa.load(audio_file, sr=None, mono=True)
        
        # Normalize audio
        y = normalize_audio(y)
        
        # Reduce noise
        y = reduce_noise(y, sr)
        
        # Save preprocessed audio
        sf.write(audio_file, y, sr, subtype='PCM_16')
        print("Preprocessed audio saved.")
        
        # Set Basic Pitch parameters to improve accuracy
        onset_threshold = 0.7  # Increased to reduce false positives
        frame_threshold = 0.4
        minimum_note_length = 100  # Increased minimum note length in milliseconds
        
        # Run inference and save MIDI file
        predict_and_save(
            [audio_file],  # List of audio files
            output_directory=output_dir,  # Where to save outputs
            save_midi=True,  # Save the MIDI file
            sonify_midi=False,  # Disable sonification
            save_model_outputs=False,  # Skip saving model outputs
            save_notes=False,  # Skip saving detected notes
            model_or_model_path=ICASSP_2022_MODEL_PATH,
            onset_threshold=onset_threshold,
            frame_threshold=frame_threshold,
            minimum_note_length=minimum_note_length,
        )
        
        # The actual MIDI file created will have the format: original_file_name_basic_pitch.mid
        base_name = os.path.splitext(os.path.basename(audio_file))[0]  # Extract the base name of the audio file
        midi_file = os.path.join(output_dir, f"{base_name}_basic_pitch.mid")
        
        # Remove any file extension (e.g., .mp3) from the song name before renaming
        clean_song_name = os.path.splitext(song_name)[0]
        new_midi_file = os.path.join(output_dir, f"{clean_song_name}_piano.mid")
        os.rename(midi_file, new_midi_file)
        
        print(f"Conversion complete. MIDI file saved as {new_midi_file}.")
        
        # Optional: Post-process MIDI to enhance quality
        post_process_midi(new_midi_file)
        
        # Render MIDI to audio using a piano soundfont
        midi_to_audio(new_midi_file, output_dir, clean_song_name)
        
        # Delete temporary WAV file
        if os.path.exists(audio_file):
            os.remove(audio_file)
            print(f"Deleted temporary WAV file: {audio_file}")
        
    except Exception as e:
        print(f"An error occurred during audio to MIDI conversion: {e}")

def post_process_midi(midi_file):
    """Adjust MIDI velocities, quantize notes, and remove short notes using pretty_midi."""
    try:
        print("Post-processing MIDI file to enhance quality...")
        midi_data = pretty_midi.PrettyMIDI(midi_file)
        
        # Get tempo changes (if any)
        tempo_changes_times, tempo_changes = midi_data.get_tempo_changes()
        
        # For simplicity, we're going to assume the first tempo
        if len(tempo_changes) > 0:
            bpm = tempo_changes[0]
        else:
            bpm = 120  # Default tempo if no tempo changes are detected
        
        # Compute time per quarter note (seconds per quarter note)
        spq = 60 / bpm  # Seconds per quarter note
        
        # Compute time per 32nd note for quantization
        quantization_unit = spq / 8  # 32nd note duration in seconds
        
        # Set minimum note length to 0.1 seconds (100 milliseconds)
        minimum_note_duration = 0.1  # seconds
        
        for instrument in midi_data.instruments:
            # Optionally, set all instruments to a piano sound
            instrument.program = pretty_midi.instrument_name_to_program('Acoustic Grand Piano')
            instrument.is_drum = False
            # Remove pitch bends and control changes
            instrument.pitch_bends = []
            instrument.control_changes = []
            
            # Process notes
            new_notes = []
            for note in instrument.notes:
                # Quantize note start and end times
                note.start = round(note.start / quantization_unit) * quantization_unit
                note.end = round(note.end / quantization_unit) * quantization_unit
                # Ensure note.end is after note.start
                if note.end <= note.start:
                    note.end = note.start + quantization_unit  # Set to at least one quantization unit
                # Remove notes that are too short
                if (note.end - note.start) < minimum_note_duration:
                    continue  # Skip this note
                # Adjust velocities
                note.velocity = min(127, max(70, note.velocity))  # Set a reasonable velocity range
                new_notes.append(note)
            instrument.notes = new_notes  # Update notes for the instrument
        
        # Remove unnecessary control changes and pitch bends at the MIDI level
        midi_data.control_changes = []
        midi_data.program_changes = []
        
        # Save the modified MIDI file
        midi_data.write(midi_file)
        print("MIDI post-processing complete. Enhanced MIDI saved.")
        
    except Exception as e:
        print(f"An error occurred during MIDI post-processing: {e}")

def midi_to_audio(midi_file, output_dir, song_name):
    """Render the MIDI file to an audio file using FluidSynth and a piano soundfont."""
    try:
        print("Rendering MIDI to audio with a smooth piano sound...")
        # Specify the path to your soundfont file
        soundfont_path = 'SoundFonts/FluidR3_GM.sf2'  # Update this path to your soundfont file
        
        # Ensure the soundfont exists
        if not os.path.exists(soundfont_path):
            print(f"Error: SoundFont file '{soundfont_path}' not found.")
            print("Please download a piano SoundFont (e.g., 'FluidR3_GM.sf2') and place it in the 'SoundFonts' directory.")
            return
        
        # Initialize the FluidSynth synthesizer
        fs = fluidsynth.Synth()
        fs.start(driver="dsound")  # Use 'alsa' on Linux. For Windows or MacOS, you may need to change the driver.
        
        sfid = fs.sfload(soundfont_path)
        fs.program_select(0, sfid, 0, 0)
        
        # Load the MIDI file
        midi_data = pretty_midi.PrettyMIDI(midi_file)
        
        # Synthesize the MIDI data to audio samples
        audio_data = midi_data.fluidsynth(fs=fs)
        
        # Convert to 16-bit PCM audio
        audio_data = librosa.util.normalize(audio_data)  # Normalize audio data
        audio_data = audio_data * 32767  # Scale to int16 range
        audio_data = audio_data.astype('int16')
        
        # Save the audio file
        audio_file = os.path.join(output_dir, f"{song_name}_piano.wav")
        sf.write(audio_file, audio_data, 44100, 'PCM_16')
        print(f"Audio rendering complete. Audio file saved as {audio_file}.")
        
        fs.delete()  # Clean up the FluidSynth instance
        
    except Exception as e:
        print(f"An error occurred during MIDI to audio conversion: {e}")

def clean_up(temp_dirs):
    """Clean up temporary directories."""
    for tmpdirname in temp_dirs:
        if os.path.exists(tmpdirname):
            shutil.rmtree(tmpdirname)
            print(f"Deleted temporary directory: {tmpdirname}")

# ---- Main Program ----
if __name__ == "__main__":
    song_query = input("Enter the song title or Spotify link: ")
    
    temp_dirs = []  # Keep track of temporary directories to clean up
    
    # Step 1: Download the song
    downloaded_song, song_name, tmpdirname = download_song_from_spotify(song_query)
    temp_dirs.append(tmpdirname)
    
    if downloaded_song:
        # Step 2: Convert audio to WAV
        wav_file, sr = convert_audio_to_wav(downloaded_song)
        if wav_file:
            # Step 3: Convert audio to MIDI and render to audio
            current_dir = os.path.dirname(os.path.abspath(__file__))
            output_dir = os.path.join(current_dir, 'songs')
            
            if not os.path.exists(output_dir):
                os.makedirs(output_dir)  # Create the 'songs' directory if it doesn't exist
        
            audio_to_midi_with_basic_pitch(wav_file, output_dir, song_name)
        else:
            print("Failed to convert audio to WAV.")
        
        # Clean up temporary directories
        clean_up(temp_dirs)
    else:
        print("Failed to download the song.")