Skip to content

Instantly share code, notes, and snippets.

@wschutzer
Created October 29, 2024 23:23
Show Gist options
  • Save wschutzer/798fb8e7de1b60c34351be2123491df8 to your computer and use it in GitHub Desktop.
Save wschutzer/798fb8e7de1b60c34351be2123491df8 to your computer and use it in GitHub Desktop.
Moonlight Sonata Waterfall
#!python
# Spectrogram preprocessor
#
#
# Python code and Mathematics by Waldeck Schutzer (@infinitymathart)
# See the license information at the end of this file.
#
# This app uses the Fast Fourier Transform to separate the frequencies
# present in an audio file. It creates a binary file with each individual
# frequency, typically resampled to 60Hz to drive an animation (other
# sample rates are possible). The file format is easily inferred from
# the code.
#
import numpy as np
import argparse
import matplotlib.pyplot as plt
from scipy.signal import resample, get_window, detrend
from scipy.fft import rfft, rfftfreq
from scipy.io import wavfile
import struct
def old_compute_spectrogram(data, sample_rate, NFFT, noverlap, window_func, detrend_func):
hop_size = NFFT - noverlap
num_frames = (len(data) - NFFT) // hop_size + 1
window = get_window(window_func, NFFT)
spectrogram = np.zeros((NFFT // 2 + 1, num_frames), dtype=np.float32)
print("Audio data is",len(data),"long")
print("Have",num_frames,"to process")
for i in range(num_frames):
start_index = i * hop_size
end_index = start_index + NFFT
segment = data[start_index:end_index]
if detrend_func:
segment = detrend_func(segment)
segment = segment * window
fft_result = rfft(segment, n=NFFT)
power_density = np.abs(fft_result) ** 2
window_power = np.sum(window ** 2)
power_density /= (window_power * sample_rate)
spectrogram[:, i] = power_density
times = np.arange(num_frames) * hop_size / sample_rate
freqs = rfftfreq(NFFT, 1 / sample_rate)
print("Computed",spectrogram.shape[1],"frames")
return spectrogram, freqs, times
def compute_spectrogram(data, sample_rate, NFFT, target_fps, window_func, detrend_func):
# Calculate hop size based on the desired output frame rate
hop_size = int(sample_rate / target_fps)
noverlap = NFFT - hop_size
# Calculate the total number of frames based on hop size
num_frames = (len(data) - NFFT) // hop_size + 1
window = get_window(window_func, NFFT)
spectrogram = np.zeros((NFFT // 2 + 1, num_frames), dtype=np.float32)
print("Audio data length:", len(data))
print("Window size (NFFT):", NFFT)
print("Hop size:", hop_size)
print("Number of frames to process:", num_frames)
for i in range(num_frames):
start_index = i * hop_size
end_index = start_index + NFFT
segment = data[start_index:end_index]
if detrend_func:
segment = detrend_func(segment)
segment = segment * window
fft_result = rfft(segment)
power_density = np.abs(fft_result) ** 2
window_power = np.sum(window ** 2)
power_density /= (window_power * sample_rate)
spectrogram[:, i] = power_density
times = np.arange(num_frames) * hop_size / sample_rate
freqs = rfftfreq(NFFT, 1 / sample_rate)
print("Computed", spectrogram.shape[1], "frames")
return spectrogram, freqs, times
def resample_spectrogram(spectrogram, original_times, target_rate):
num_target_frames = int(original_times[-1] * target_rate)
resampled_spectrogram = np.zeros((spectrogram.shape[0], num_target_frames), dtype=np.float32)
# Resample each frequency bin independently
for i in range(spectrogram.shape[0]):
resampled_spectrogram[i] = resample(spectrogram[i], num_target_frames)
return resampled_spectrogram
def filter_frequencies(spectrogram, freqs, lower_freq, higher_freq):
# Find the indices of the frequencies within the specified range
freq_indices = np.where((freqs >= lower_freq) & (freqs <= higher_freq))[0]
# Filter the spectrogram and frequencies based on these indices
filtered_spectrogram = spectrogram[freq_indices, :]
filtered_freqs = freqs[freq_indices]
return filtered_spectrogram, filtered_freqs
def compute_min_max(spectrogram):
min_values = np.min(spectrogram, axis=1)
max_values = np.max(spectrogram, axis=1)
return min_values, max_values
def clean_values(arr):
# Replace any NaN, -Inf, or +Inf values with 0.0
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
return arr
def write_spectrogram_to_file(output_file, sample_rate, freqs, resampled_spectrogram):
with open(output_file, 'wb') as f:
# Prepare header information
num_frequencies = len(freqs)
total_packets = resampled_spectrogram.shape[1]
# Create the header packet
header = struct.pack('<i', sample_rate) # Sample Rate as 32-bit integer
header += struct.pack('<i', num_frequencies) # Number of frequencies as 32-bit integer
header += struct.pack('<i', total_packets) # Total packets as 32-bit integer
# Pad the header with zeros to reach 128 bytes
header = header.ljust(128, b'\x00')
f.write(header)
# Write the frequency values (first packet)
f.write(freqs.astype(np.float32).tobytes())
# Clean the spectrogram values before computing min and max
cleaned_spectrogram = clean_values(resampled_spectrogram)
# Compute min and max values for each frequency bin
min_values = np.min(cleaned_spectrogram, axis=1)
max_values = np.max(cleaned_spectrogram, axis=1)
# Write the min and max values
f.write(min_values.astype(np.float32).tobytes())
f.write(max_values.astype(np.float32).tobytes())
# Write the spectrum values (subsequent packets)
for frame in cleaned_spectrogram.T:
f.write(frame.astype(np.float32).tobytes())
print("\n\nHeader and", total_packets, "packets written.")
print("Sample rate:", sample_rate)
print("Number of frequencies:", num_frequencies)
def plot_spectrogram(spectrogram, freqs, times):
# Convert the power spectrum to dB scale
dB_spectrogram = 10 * np.log10(spectrogram + 1e-10)
# Replace any -inf or NaN values with -100 dB
dB_spectrogram = np.where(np.isfinite(dB_spectrogram), dB_spectrogram, -100)
plt.figure(figsize=(10, 6))
plt.imshow(dB_spectrogram, aspect='auto', origin='lower',
extent=[times.min(), times.max(), freqs.min(), freqs.max()])
plt.colorbar(label='Power Spectral Density (dB)')
plt.xlabel('Time (s)')
plt.ylabel('Frequency (Hz)')
plt.title('Spectrogram')
plt.show()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Compute and save a spectrogram resampled to 60Hz.")
parser.add_argument("input_file", type=str, help="Path to the input WAV file")
parser.add_argument("output_file", type=str, help="Path to the output binary file")
parser.add_argument("-n", "--nfft", type=int, default=1024, help="Number of data points per segment (default: 1024)")
parser.add_argument("-r", "--frame_rate", type=float, default=60.0, help="Target output frame rate in FPS (default: 60)")
parser.add_argument("-w", "--window", type=str, default='hann', help="Window function to use (default: 'hann')")
parser.add_argument("-d", "--detrend", type=str, choices=['none', 'mean', 'linear'], default='none', help="Detrending method (default: 'none')")
parser.add_argument("-lf", "--lower_frequency", type=float, default=0.0, help="Lower frequency limit for filtering (default: 0.0 Hz)")
parser.add_argument("-hf", "--higher_frequency", type=float, default=float('inf'), help="Higher frequency limit for filtering (default: no upper limit)")
args = parser.parse_args()
# Read the WAV file
sample_rate, audio_data = wavfile.read(args.input_file)
if len(audio_data.shape) == 2: # Stereo
audio_data = np.mean(audio_data, axis=1) # Convert to mono by averaging channels
if args.detrend == 'none':
detrend_func = None
elif args.detrend == 'mean':
detrend_func = lambda x: detrend(x, type='constant')
elif args.detrend == 'linear':
detrend_func = lambda x: detrend(x, type='linear')
spectrogram, freqs, times = compute_spectrogram(audio_data, sample_rate, args.nfft, args.frame_rate, args.window, detrend_func)
# Filter the spectrogram to keep only frequencies within the specified range
filtered_spectrogram, filtered_freqs = filter_frequencies(spectrogram, freqs, args.lower_frequency, args.higher_frequency)
# Write the resampled and filtered spectrogram to a file
write_spectrogram_to_file(args.output_file, sample_rate, filtered_freqs, filtered_spectrogram)
# Optional: plot the spectrogram
plot_spectrogram(filtered_spectrogram, filtered_freqs, np.linspace(0, times[-1], filtered_spectrogram.shape[1]))
# License:
#
# Copyright (c) 2024 Waldeck Schutzer
#
# All rights reserved.
#
# This code after the template and the related animations are the property of the
# copyright holder. Any reproduction, distribution, or use of this material,
# in whole or in part, without the express written permission of the copyright
# holder is strictly prohibited.
// Sound visualizer animation
//
// Processing code and Mathematics by Waldeck Schutzer (@infinitymathart)
// See the license information at the end of this file.
//
// The sound is first processed with a separate python app available here:
// which creates a binary spectrogram file with a sample rate of 60Hz with
// each individual frequency, thanks to the Fast Fourier Transform.
// This binary data is read into this app by the SpectrogramReader class.
//
import peasy.*;
PeasyCam cam;
boolean recording = false;
int frameWidth = recording ? 1080 : 600;
int frameHeight = recording ? 1920 : 1067;
SpectrogramReader reader;
int numFrames = 0; // Total number of frames in the spectrogram
int currentFrame = 0; // Current frame index for display
PImage spectrogramImage; // Image to display the spectrogram
boolean reading = true;
boolean stamping = true;
track[] tracks;
float fac = 1.0;
float track_dist = 20.0;
float hook_size = 0.1;
final int padding = 380;
int padding_count = 0;
float h_stretch = 1.4;
final int max_samples = int(h_stretch*600);
final float filterq = 0.7;
double h = 4*frameWidth; // height of waterfall
double d = h_stretch*frameWidth; // horizontal distance until water hits the ground
double d2 = d*d;
double alpha = Math.sqrt(2*h)/d;
double alpha2 = 2*h/d2;
float t = 0;
final float[] track_freqs = { // Notes of a Grand Piano
27.5000, 29.1352, 30.8677, // A0, A#0, B0
32.7032, 34.6478, 36.7081, 38.8909, 41.2034, 43.6535, 46.2493, 48.9994, 51.9131, // C1 to B1
55.0000, 58.2705, 61.7354, 65.4064, 69.2957, 73.4162, 77.7817, 82.4069, 87.3071, 92.4986, 97.9989, 103.826, // A2 to B2
110.000, 116.541, 123.471, 130.813, 138.591, 146.832, 155.563, 164.814, 174.614, 184.997, 195.998, 207.652, // A3 to B3
220.000, 233.082, 246.942, 261.626, 277.183, 293.665, 311.127, 329.628, 349.228, 369.994, 391.995, 415.305, // A4 to B4
440.000, 466.164, 493.883, 523.251, 554.365, 587.330, 622.254, 659.255, 698.456, 739.989, 783.991, 830.609, // A5 to B5
880.000, 932.328, 987.767, 1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, // A6 to B6
1760.00, 1864.66, 1975.53, 2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, // A7 to B7
3520.00, 3729.31, 3951.07, 4186.01
};
PFont courier;
class track
{
float freq; // Track frequency
int num_samples = 0;
int max_samples;
float[] samples = null;
int index = 0;
float sbuf = 0;
float ampf = 1.0;
float fq = filterq;
track(float _freq, int _max_samples)
{
freq = _freq;
max_samples = _max_samples;
samples = new float[max_samples];
}
void store_sample(float s)
{
if (num_samples < max_samples)
num_samples++;
sbuf = fq*sbuf + (1-fq)*s;
samples[index] = sbuf;
index = (index + 1) % max_samples;
}
int get_num_samples()
{
return num_samples;
}
float[] get_samples()
{
if (num_samples == 0)
return null;
float[] res = new float[num_samples];
if (num_samples < max_samples)
for(int i=0; i<num_samples; i++)
res[i] = constrain(ampf*samples[(index+i)%max_samples],-100,160);
else
for(int i=0; i<num_samples; i++)
res[i] = constrain(ampf*samples[(index+i)%max_samples],-100,160);
return res;
}
}
float ease(float p, float g) {
if (p < 0.5)
return 0.5 * pow(2*p, g);
else
return 1 - 0.5 * pow(2*(1 - p), g);
}
/**
* A function to emulate a "jet" colormap similar to Matplotlib's.
* It maps a normalized value (0 to 1) to a color ranging from blue to red,
* passing through cyan, green, and yellow.
*/
int jetColormap(float value) {
value = constrain(value, 0, 1);
float hue, saturation = 250, brightness = 255;
if (value < 0.25f) {
hue = map(value, 0.0f, 0.25f, 240, 180); // Blue to Cyan
} else if (value < 0.5f) {
hue = map(value, 0.25f, 0.5f, 180, 120); // Cyan to Green
} else if (value < 0.75f) {
hue = map(value, 0.5f, 0.75f, 120, 60); // Green to Yellow
} else {
hue = map(value, 0.75f, 1.0f, 60, 0); // Yellow to Red
}
colorMode(HSB, 360, 255, 255); // Set HSB mode with hue range [0, 360]
int col = color(hue, saturation, brightness);
colorMode(RGB, 255); // Reset color mode to default RGB for other parts of the program
return col;
}
int jetColormap_old(float value) {
float r, g, b;
if (value < 0.25f) {
r = 0;
g = 4 * value;
b = 1;
} else if (value < 0.5f) {
r = 0;
g = 1;
b = 1 - 4 * (value - 0.25f);
} else if (value < 0.75f) {
r = 4 * (value - 0.5f);
g = 1;
b = 0;
} else {
r = 1;
g = 1 - 4 * (value - 0.75f);
b = 0;
}
return color(r * 255, g * 255, b * 255);
}
public static double asinh(double a) {
final double sign;
// check the sign bit of the raw representation to handle -0
if (Double.doubleToRawLongBits(a) < 0) {
a = Math.abs(a);
sign = -1.0d;
} else {
sign = 1.0d;
}
return sign * Math.log(Math.sqrt(a * a + 1.0d) + a);
}
// Function s(t) as defined
double s(double t) {
return (t*Math.sqrt(1+alpha2*t*t) + asinh(alpha*t)/alpha)/2;
}
// Derivative of s(t) with respect to t: s'(t)
double ds(double t) {
return Math.sqrt(1+alpha2*t*t);
}
// Function to approximate gamma(s) by numerically solving for t such that s(t) ≈ input sValue
PVector gamma(float t)
{
return new PVector(t, (float)(h*(1-t*t/d2)));
}
PVector gamma_tangent(double t)
{
return new PVector(1, -(float)(alpha2*t));
}
PVector gamma_normal(double t)
{
return new PVector((float)(alpha2*t), 1);
}
// Numerical method to solve for t given sValue, using Newton's method
double solve_for_t(double sValue) {
if (Math.abs(sValue)<1e-5)
return 0;
final double eps = 1e-5; // Tolerance level for precision
double t = sValue/ds(0);
// Newton's method loop
for (int i = 0; i < 10; i++) { // Limit iterations to prevent infinite loops
double t1 = t - (s(t)-sValue)/ds(t);
if (Math.abs(t1-t) <= eps*Math.abs(t1))
return t1;
t = t1;
}
throw new ArithmeticException("Newton's method did not converge for "+sValue);
}
float constrain_freq(float f)
{
if (Float.isNaN(f) || f == Float.NEGATIVE_INFINITY)
return 0.0f;
else
return f;
}
void settings()
{
size(frameWidth, frameHeight, P3D);
smooth(8);
}
void setup() {
fac = width/600.0;
frameRate(1000);
//cam = new PeasyCam(this, 0*fac, 0*fac, 0*fac, 550*fac);
// cam = new PeasyCam(this, 69.31465 *fac, 16.644705 *fac, 69.999 *fac, 411.6035061837172 *fac);
// cam.setRotations( 0.8818339 , -0.69729364 , 0.7555049 );
//cam = new PeasyCam(this, 69.31465 *fac, 16.644705 *fac, 69.999 *fac, 411.603515625 *fac);
//cam.setRotations( 0.8818339 , -0.69729364 , 0.7555049 );
//cam = new PeasyCam(this, 69.31465 *fac, 16.644705 *fac, 69.999 *fac, 411.603515625 *fac);
//cam.setRotations( -0.0 , 0.0 , -0.0 );
// cam = new PeasyCam(this, 0.0 *fac, 0.0 *fac, 0.0 *fac, 650.0 *fac);
// cam.setRotations( 1.5072619 , -0.6465731 , 1.4747462 );
//cam = new PeasyCam(this, 222.64006 *fac, 415.07318 *fac, 428.07187 *fac, 1273.063676001737 *fac);
//cam.setRotations( 0.48491418 , -0.37045673 , 0.2441992 );
cam = new PeasyCam(this, 244.18518 *fac, 601.8838 *fac, -264.27887 *fac, 1660.7410047886365 *fac);
cam.setRotations( 1.0536625 , 0.96110266 , -1.0081584 );
courier = createFont("Courier New",24*fac,true);
// Initialize the reader with the path to your binary file
reader = new SpectrogramReader(dataPath("beethoven.bin"),false);
// Initialize the tracks
tracks = new track[track_freqs.length];
for(int i=0; i<track_freqs.length; i++)
{
tracks[i] = new track(track_freqs[i], max_samples);
for(int j=0; j<tracks[i].max_samples; j++)
tracks[i].store_sample(-100.0);
tracks[i].ampf = 1.2;
tracks[i].fq = pow(filterq,1/fac);
}
tracks[0].ampf=2.1;
}
void draw()
{
t = 1.0*(frameCount-1)/(reader.totalPackets+padding);
background(0);
pushMatrix();
rotateY(HALF_PI);
//translate(0, height/2);
final float rho = 7;
//rotateY( constrain(map(1.0*frameCount/reader.totalPackets, 0, 0.1, 0, PI/3),0,PI/3) );
//rotateY(-PI/3*sin((frameCount-1)*rho));
//rotateX(-PI/12*sin((frameCount-1)*rho));
// translate(0, 0, -track_dist*fac*tracks.length/4*sin((frameCount-1)*rho));
float t1 = ease(constrain(rho*t,0,1), 2.2);
float t2 = ease(constrain(rho*t-1, 0, 1), 2.2);
float t3 = ease(constrain(rho*t-2, 0, 1), 2.2);
float t4 = ease(constrain(rho*t-3, 0, 1), 2.2);
float t5 = ease(constrain(rho*t-4, 0, 1), 2.2);
float t6 = ease(constrain(rho*t-5, 0, 1), 2.2);
float t7 = ease(constrain(rho*t-6, 0, 1), 2.2);
rotateY( map(t1, 0, 1, 0, 1.05*PI) );
rotateX( map(t1, 0, 1, 0, PI/6) );
rotateZ( map(t1, 0, 1, 0, PI/6) );
translate(0,-width*t2,-width*t2);
rotateY( map(t3, 0, 1, 0, -PI) );
rotateX( map(t3, 0, 1, 0, -PI/6) );
translate(0, width*t4, 0);
rotate(PI/4*t4);
rotateX(PI/3*t5);
translate(0,-width*t5,0);
translate(-width*t6,width*t6,0);
translate(4*width*t7,0,-4*0.6*width*t7);
if (reading && reader.readNextPacket())
{
for(track t: tracks)
{
float sample = constrain_freq(reader.get(t.freq));
float dBValue = 10 * (float) Math.log10(sample + 1e-10);
if (Float.isNaN(dBValue) || dBValue == Float.NEGATIVE_INFINITY) {
dBValue = -100.0f;
}
dBValue = constrain(dBValue, -100.0f, 160.0f);
t.store_sample(dBValue);
}
}
else
reading = false;
if (!reading)
{
padding_count++;
for(track t: tracks)
{
t.store_sample(-100.0f);
}
}
{
float y = 0;
float x = -width/2;
float xw = width/2; // waterfall x
float yw = (float)h;
float z = -track_dist*fac*tracks.length/2;
for(track t: tracks)
{
float sample = constrain_freq(reader.get(t.freq));
float dBValue = 10 * (float) Math.log10(sample + 1e-10);
if (Float.isNaN(dBValue) || dBValue == Float.NEGATIVE_INFINITY) {
dBValue = -100.0f;
}
dBValue = constrain(dBValue, -100.0f, 160.0f);
t.store_sample(dBValue);
float[] vals = t.get_samples();
int n = t.get_num_samples();
// Draw the waterfall
stroke(255);
fill(0);
strokeWeight(2*fac);
beginShape();
// vertex(x, y+100*fac, z);
float last_val = -100.0;
for(float i=0; i<(float)d; i++)
{
int k = int(map(i, 0, (float)d-1, min(n, max_samples)-1, 0));
last_val = vals[k];
stroke(jetColormap(map(last_val,-24,60,0,1)));
PVector pg = gamma(i);
PVector png = gamma_normal(i).normalize().mult(last_val*fac);
pg = PVector.add(pg, png);
vertex(xw-pg.x, yw-pg.y, z);
}
stroke(0);
vertex(xw, yw, z);
endShape();
// Draw the pegs
stroke(jetColormap(map(vals[n-1],-24,60,0,1)));
noFill();
beginShape();
vertex(x+width,y+100*fac, z);
vertex(x+width+hook_size*fac, y+100*fac, z);
vertex(x+width+hook_size*fac, y-75*fac, z);
endShape();
// Draw the thin lines
strokeWeight(0.5);
stroke(80);
noFill();
line(xw, y+100*fac, z, xw+4*width, y+100*fac, z); // thin lines
line(xw, yw, z, xw-4*width, yw, z);
// Draw the peg's texts
pushMatrix();
translate(width/2,y,z);
textSize(14*fac);
rotateY(-HALF_PI);
rotateZ(-HALF_PI);
noStroke();
fill(255);
String ffreq = String.format("%.1f Hz", t.freq);
text(ffreq,80*fac,5*fac,-hook_size*fac);
popMatrix();
z += track_dist*fac;
}
}
popMatrix();
// texts
/* Need to compute the position of a frame sitting in the field of view
pushMatrix(); pushStyle();
float[] rot = cam.getRotations();
float[] pos = cam.getPosition();
//rotateX(rot[0]);
//rotateY(-rot[1]);
//rotateZ(rot[2]);
translate(pos[0],pos[1],pos[2]-260*fac);
fill(255);stroke(255);
textAlign(CENTER, CENTER);
textFont(courier);
textSize(7*fac);
text("@infinitymathart • 2024",0.0*width,0.0*height);
popStyle(); popMatrix(); */
if (recording)
{
saveFrame("/tmp/f/frame_#####.png");
if (stamping)
println(frameCount,"/",reader.totalPackets);
if (padding_count >= padding)
{
reader.close();
exit();
}
}
}
void mousePressed()
{
float[] rotations = cam.getRotations();
float[] look = cam.getLookAt();
double dist = cam.getDistance();
stamping = false;
println("..:");
println(" cam = new PeasyCam(this, ", look[0]/fac, "*fac, ", look[1]/fac, "*fac, ", look[2]/fac, "*fac, ", dist/fac,"*fac);" );
println(" cam.setRotations(", rotations[0], ", ", rotations[1], ", ", rotations[2],");\n\n");
}
/* License:
*
* Copyright (c) 2024 Waldeck Schutzer
*
* All rights reserved.
*
* This code after the template and the related animations are the property of the
* copyright holder. Any reproduction, distribution, or use of this material,
* in whole or in part, without the express written permission of the copyright
* holder is strictly prohibited.
*/
import java.io.*;
import java.nio.*;
class SpectrogramReader {
float[] frequencies; // Array of frequencies in Hz
float[] minValues; // Min values for each frequency bin
float[] maxValues; // Max values for each frequency bin
int numFrequencies; // Number of frequency bins
int sampleRate;
int totalPackets;
DataInputStream dataInputStream;
boolean entireFileLoaded = false;
float[][] allPackets; // Optional: store all packets if loading the entire file
float[] currentPacket; // Current packet of frequency data
// Constructor to load the binary file and initialize the frequency array
SpectrogramReader(String filePath, boolean loadEntireFile) {
try {
// Open the binary file
dataInputStream = new DataInputStream(new FileInputStream(filePath));
// Read the header (128 bytes)
sampleRate = Integer.reverseBytes(dataInputStream.readInt());
numFrequencies = Integer.reverseBytes(dataInputStream.readInt());
totalPackets = Integer.reverseBytes(dataInputStream.readInt());
// Skip the padding bytes to complete the 128-byte header
dataInputStream.skipBytes(128 - 12);
// Read the frequencies array as little-endian floats
frequencies = new float[numFrequencies];
for (int i = 0; i < numFrequencies; i++) {
frequencies[i] = readLittleEndianFloat(dataInputStream);
}
// Read the min and max values for each frequency bin
minValues = new float[numFrequencies];
maxValues = new float[numFrequencies];
for (int i = 0; i < numFrequencies; i++)
minValues[i] = readLittleEndianFloat(dataInputStream);
for (int i = 0; i < numFrequencies; i++)
maxValues[i] = readLittleEndianFloat(dataInputStream);
// If requested, load the entire file into memory
if (loadEntireFile) {
//loadAllPackets();
entireFileLoaded = true;
}
// Initialize the current packet array
currentPacket = new float[numFrequencies];
} catch (IOException e) {
System.out.println("Error loading file: " + e.getMessage());
}
}
// Method to read the next packet of spectrum data
boolean readNextPacket() {
if (entireFileLoaded) {
return false; // If the entire file is loaded, use the stored data instead
}
try {
// Read the spectrum values as little-endian floats
for (int i = 0; i < numFrequencies; i++) {
currentPacket[i] = readLittleEndianFloat(dataInputStream);
}
return true; // Packet read successfully
} catch (EOFException e) {
System.out.println("End of file reached.");
return false; // Properly handle the end of file
} catch (IOException e) {
System.out.println("Error reading packet: " + e.getMessage());
return false;
}
}
// Method to get the value of the i-th frequency bin
float get(int i) {
if (i >= 0 && i < numFrequencies) {
return currentPacket[i];
} else {
System.out.println("Index out of bounds: " + i);
return 0.0f;
}
}
// Method to get the minimum value of the i-th frequency bin
float getMin(int i) {
if (i >= 0 && i < numFrequencies) {
return minValues[i];
} else {
System.out.println("Index out of bounds: " + i);
return 0.0f;
}
}
// Method to get the maximum value of the i-th frequency bin
float getMax(int i) {
if (i >= 0 && i < numFrequencies) {
return maxValues[i];
} else {
System.out.println("Index out of bounds: " + i);
return 0.0f;
}
}
// Method to get the value for a specific frequency with interpolation
float get(float freq) {
int lowerIndex = binarySearch(frequencies, freq);
if (lowerIndex == -1 || lowerIndex == numFrequencies - 1) {
System.out.println("Frequency out of range: " + freq);
return 0.0f;
}
int upperIndex = lowerIndex + 1;
float lowerFreq = frequencies[lowerIndex];
float upperFreq = frequencies[upperIndex];
float lowerValue = currentPacket[lowerIndex];
float upperValue = currentPacket[upperIndex];
// Linear interpolation
float proportion = (freq - lowerFreq) / (upperFreq - lowerFreq);
return lowerValue + proportion * (upperValue - lowerValue);
}
// Method to get the minimum value for a specific frequency with interpolation
float getMin(float freq) {
int lowerIndex = binarySearch(minValues, freq);
if (lowerIndex == -1 || lowerIndex == numFrequencies - 1) {
System.out.println("Frequency out of range: " + freq);
return 0.0f;
}
int upperIndex = lowerIndex + 1;
float lowerFreq = frequencies[lowerIndex];
float upperFreq = frequencies[upperIndex];
float lowerValue = minValues[lowerIndex];
float upperValue = minValues[upperIndex];
float proportion = (freq - lowerFreq) / (upperFreq - lowerFreq);
return lowerValue + proportion * (upperValue - lowerValue);
}
// Method to get the maximum value for a specific frequency with interpolation
float getMax(float freq) {
int lowerIndex = binarySearch(maxValues, freq);
if (lowerIndex == -1 || lowerIndex == numFrequencies - 1) {
System.out.println("Frequency out of range: " + freq);
return 0.0f;
}
int upperIndex = lowerIndex + 1;
float lowerFreq = frequencies[lowerIndex];
float upperFreq = frequencies[upperIndex];
float lowerValue = maxValues[lowerIndex];
float upperValue = maxValues[upperIndex];
float proportion = (freq - lowerFreq) / (upperFreq - lowerFreq);
return lowerValue + proportion * (upperValue - lowerValue);
}
// Method to average the minimum values within a frequency range
float getMin(float startFreq, float endFreq) {
return getRangeValue(startFreq, endFreq, minValues);
}
// Method to average the maximum values within a frequency range
float getMax(float startFreq, float endFreq) {
return getRangeValue(startFreq, endFreq, maxValues);
}
// Method to average the values within a frequency range
float get(float startFreq, float endFreq) {
return getRangeValue(startFreq, endFreq, currentPacket);
}
// Helper method to average values within a range
private float getRangeValue(float startFreq, float endFreq, float[] values) {
if (startFreq > endFreq) {
float temp = startFreq;
startFreq = endFreq;
endFreq = temp;
}
int startIndex = binarySearch(frequencies, startFreq);
int endIndex = binarySearch(frequencies, endFreq);
if (startIndex == -1 || endIndex == -1) {
System.out.println("Frequency range out of bounds: " + startFreq + " - " + endFreq);
return 0.0f;
}
if (startIndex > endIndex) {
int temp = startIndex;
startIndex = endIndex;
endIndex = temp;
}
float sum = 0.0f;
int count = 0;
for (int i = startIndex; i <= endIndex; i++) {
sum += values[i];
count++;
}
return count == 0 ? 0.0f : sum / count;
}
// Helper method to perform binary search to find the closest frequency bin
private int binarySearch(float[] array, float target) {
int left = 0;
int right = array.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (array[mid] == target) {
return mid; // Exact match
} else if (array[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
if (right < 0 || left >= array.length) {
return -1;
}
return right;
}
// Method to read a little-endian float from the DataInputStream using ByteBuffer
private float readLittleEndianFloat(DataInputStream dis) throws IOException {
byte[] bytes = new byte[4];
dis.readFully(bytes);
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
}
// Close the file stream when done
void close() {
try {
dataInputStream.close();
} catch (IOException e) {
System.out.println("Error closing file: " + e.getMessage());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment