Python is my Love Language
A Valentine’s day gift for Laura
Hey,
This year for Valentine’s day instead of getting you some flowers that will die a few days later (I may have gotten you some at the last minute, but that is a problem for future Kyle) I have decided to express my love in the most natural way for me.
In a short-form technical document that merges Python coding and 3D printing, my other two loves.
Now, if you are reading this I hope you were able to access it via the NFC tag embedded in the Lovogram 3D print that I have hopefully given to you by now. Fingers crossed I followed through. As for this article, I wanted to chronicle my creation to show you it is indeed a labour of love. So without further ado, and in all likeliness you will skip over the most of it, here we go.
Step 1: Saying I Love You
To kick this off I had to get me and kids to record a sweet “I Love You” message and have it available online. You may listed to it here:
The next step was to take this recording and use python code to convert the .mp3 file to a spectrogram. A spectrogram is a 2 dimensional representation of a Fourier transform over time. You love Fourier transforms, I know, but settle down please. #LoveOnTheSpectrum
Part 2: Sound to Picture
What this means is that each column of data is a moment in time, and each row is a frequency. The darker the region (or higher the print) the more the frequency is binned into this section. You do that for each moment in time and get a 2d heatmap. Here is the actual spectrogram from our love message:
Woosh! That is super cool! Look at that! Love in visual form! Of course I know you want to see the code also!
import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np
import sys
import pandas as pd
def mp3_to_spectrogram(mp3_file, output_image='spectrogram.png', output_csv='spectrogram.csv'):
# Load the audio file
y, sr = librosa.load(mp3_file, sr=None)
# Compute the spectrogram
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=60, fmax=8000)
S_db = librosa.power_to_db(S, ref=np.max)
# Normalize amplitude values to range [0, 1]
S_db_norm = 5*(S_db - S_db.min()) / (S_db.max() - S_db.min())
# Resample time dimension to 60 steps
# S_db_resampled = librosa.util.fix_length(S_db_norm, size=60, axis=1)
# # Get time and frequency bins
# times = np.linspace(0, len(y) / sr, num=60) # 60 evenly spaced time steps
# freqs = librosa.mel_frequencies(n_mels=60, fmax=8000)
# Get time and frequency bins
times = librosa.times_like(S_db, sr=sr)
freqs = librosa.mel_frequencies(n_mels=60, fmax=8000)
# Save spectrogram data as CSV with integer time and frequency values
data = []
for i, t in enumerate(times):
for j, f in enumerate(freqs):
# Round time and frequency to the nearest integer
t_int = int(round(t)) # Round time to nearest integer
f_int = int(round(f)) # Round frequency to nearest integer
data.append([i, j, S_db_norm[j, i]])
df = pd.DataFrame(data, columns=['Time', 'Frequency', 'Amplitude'])
df.to_csv(output_csv, index=False)
print(f"Spectrogram data saved as {output_csv}")
# Plot the spectrogram
plt.figure(figsize=(10, 4))
librosa.display.specshow(S_db_norm, sr=sr, x_axis='time', y_axis='mel')
plt.colorbar(format='%+2.0f')
plt.title('We Love You Mommy')
plt.xlabel('Time')
plt.ylabel('Frequency')
plt.tight_layout()
# Save the spectrogram as an image
plt.savefig(output_image)
plt.close()
print(f"Spectrogram saved as {output_image}")
# Example call
mp3_to_spectrogram('C:\\Bitbucket\\Lovogram\\weloveumommy.mp3')
But Kyle! Where do you find the time!
With love Laura. I find the time with love.
Part 3: Picture to 3D Model
A picture is just data. In this case it has an x and y location of the pixels and the colour intensity is the z (height). All I did in the above was make a csv that could be read in. In Fusion 360 (the modelling software) you can use a “Add-In” and write python code. Here it is:
"""This file acts as the main module for this script."""
import adsk.core
import adsk.fusion
import csv
import os
import traceback
# Initialize the global variables for the Application and UserInterface objects.
app = adsk.core.Application.get()
ui = app.userInterface
design = app.activeProduct
rootComp = design.rootComponent
sketches = rootComp.sketches
design.designType = adsk.fusion.DesignTypes.DirectDesignType
def run(_context: str):
"""This function is called by Fusion when the script is run."""
try:
# Your code goes here.
xyPlane = rootComp.xYConstructionPlane
extrudes = rootComp.features.extrudeFeatures
# Open and read the CSV file
with open('C:\\Bitbucket\\Lovogram\\spectrogram.csv', mode='r', newline='') as file:
csv_reader = csv.reader(file)
data = [row for row in csv_reader] # Convert CSV to a list of rows
#10-25
#25-40
for i,row in enumerate(data):
if int(row[0])<100:
continue
if int(row[0])>150:
return
sketch = sketches.add(xyPlane)
sketch.name = "TheGrid" # Set the name of the sketch
lines = sketch.sketchCurves.sketchLines
recLines = lines.addTwoPointRectangle(adsk.core.Point3D.create(int(row[0]), int(row[1]), 0), adsk.core.Point3D.create(int(row[0])+1, int(row[1])+1, 0))
# Now get the laest profile
profile = sketch.profiles[len(sketch.profiles)-1]
# Extrude the profile
extrusion_height = float(row[2])+1
extrude = extrudes.addSimple(profile, adsk.core.ValueInput.createByString(str(abs(extrusion_height))), adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
# extrude.bodies[0].name = 'John'
# Get the whole set of bodies
sketch_name = "TheGrid" # Change to your sketch name
for sketch in rootComp.sketches:
if sketch.name == sketch_name:
sketch.deleteMe()
# ui.messageBox(f"{sketch_name} has been deleted.", "Success")
break
except: #pylint:disable=bare-except
# Write the error message to the TEXT COMMANDS window.
app.log(f'Failed:\n{traceback.format_exc()}')
To do this I actually had to split it into 3 sections because my computer is not good enough….
After the code finished it made something that looks like this!
Part 4: Making love come to life
Now I just printed it! And glued! And added an NFC to the back! and here we are.
Long story short… I love you :)
— — — —
Today is Valentines and you loved it. Here is a pic.