Skip to content

Commit

Permalink
[v1.1.7] adds spectro() method; image() method now works with PNGs in…
Browse files Browse the repository at this point in the history
…staed of JPGs
  • Loading branch information
nnirror committed Dec 3, 2023
1 parent d21f3cc commit bb437b8
Show file tree
Hide file tree
Showing 4 changed files with 935 additions and 539 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,13 @@ When a generator takes a FacetPattern or an array as an argument, it uses that p
- the lowest pixels in the image correspond to the lowest frequencies in the output, and the highest pixels in the image correspond to the highest frequencies in the output.
- the default `columnsPerSecond` value of 512 means that each second of audio will contain 512 columns of pixels. This value can be larger or smaller, but keep in mind that as this value decreases, the file will take more time to generate. This method can be CPU intensive and works best with smaller image files or larger `columnsPerSecond values`.
- since pixel brightness corresponds with loudness, images with dark backgrounds and high contrast will produce clearer tones.
- This method currently only works with JPEG files, and sometimes even certain JPEG files won't work. (I have submitted a GitHub issue: https://github.com/revisitors/readimage/issues/4) Re-saving the JPEG files in GIMP seems to create files that the middleware this method uses can parse correctly.
- This method currently only works with PNG files.
- the `minimumFrequency` and `maximumFrequency` values control the range of frequencies that the pixels will map onto.
- the `frequencyPattern` argument allows you to remap the rows of pixels with a FacetPattern. It should be scaled between 0 and 1. It will automatically be resized so its data length matches the height of the image in pixels. Lower values in `frequencyPattern` will map onto lower frequencies inside the range of `minimumFrequency` and `maximumFrequency`. Higher values in `frequencyPattern` will map onto higher frequencies inside the range of `minimumFrequency` and `maximumFrequency`.
- output range is from -1 - 1.
- __Note__: this example uses MacOS / Linux file paths with forward slashes (e.g. `my/path/here`). For Windows, you will need to use back slashes (e.g `my\path\here`)
- example:
- `$('example').image('/path/to/file/goes/here.jpg',1024).play(); // each column lasts 1024 samples`
- `$('example').image('/path/to/file/goes/here.png',1024).play(); // each column lasts 1024 samples`
---
- **noise** ( _length_ )
- generates a random series of values between -1 and 1 for `length`.
Expand Down Expand Up @@ -1145,16 +1145,25 @@ For more examples, refer to the `examples/this.md` file.
- the `width` and `height` arguments are optional. They default to the square root of the FacetPattern's length. Other values will rotate the data in a different way, around a different center point.
- example:
- `$('example').sine(1).size(10000).scale(0,1).layer2d(_.noise(10000), _.ramp(0,100,128), _.ramp(0,100,128)).saveimg('example').once(); // layers a ramp from 0,0 to 100,100 over a sine wave background`
---
- **mutechunks2d** ( _num_chunks_, _probabilty_ )
- slices the input FacetPattern into `chunks` chunks in 2D space and mutes `prob` percent of them.
- `num_chunks` must have an integer square root, e.g. 9, 16, 25, 36.
- example:
`$('example').sine(0.3,1000).scale(0,1).mutechunks2d(36,0.5).saveimg('example').once();`
---
- **rechunk2d** ( _num_chunks_ )
- slices the input FacetPattern into `chunks` chunks in 2D space and shuffles the chunks around.
- `num_chunks` must have an integer square root, e.g. 9, 16, 25, 36.
- example:
`$('example').sine(0.3,1000).scale(0,1).rechunk2d(36).saveimg('example').once();`
---
- **rotate** ( _angle_, _width_, _height_ )
- rotates the FacetPattern `angle` degrees around a center point, as if it were suspended in 2D space.
- the `width` and `height` arguments are optional. They default to the square root of the FacetPattern's length. Other values will rotate the data in a different way, around a different center point.
- example:
- `$('example').sine(1).scale(0,1).size(512*512).rotate(35).saveimg('example').once(); // rotates a sine wave background 35 degrees`
---
- **saveimg** ( _filepath_, _rgbData_, _width_, _height_ )
- saves the FacetPattern data as a PNG file in the `img/` directory or a sub-directory. If a sub-directory is specified in the `filepath` argument and it doesn't exist, it will be created.
- the `width` and `height` arguments are optional. They default to the square root of the FacetPattern's length. They control the width and height of the PNG image file, in pixels. If the FacetPattern has more data `d` than there are total pixels `p` in the image, the data will be truncated after `p`.
Expand Down Expand Up @@ -1185,18 +1194,20 @@ For more examples, refer to the `examples/this.md` file.
)
.once();
```
- **rechunk2d** ( _num_chunks_ )
- slices the input FacetPattern into `chunks` chunks in 2D space and shuffles the chunks around.
- `num_chunks` must have an integer square root, e.g. 9, 16, 25, 36.
- example:
`$('example').sine(0.3,1000).scale(0,1).rechunk2d(36).saveimg('example').once();`
---
- **shift2d** ( _xAmt_, _yAmt_, _width_ )
- shifts the FacetPattern in 2D space, by `xAmt` pixels to the left/right, and by `yAmt` pixels up/down.
- the `width` argument is optional. It defaults to the square root of the FacetPattern's length. Other values will shift the data in a different way.
- example:
- `$('example').noise(100*100).prob(0.001).iter(4,()=>{this.mix(0.5,()=>{this.shift2d(0,1)})}).saveimg('example').once(); // slides all the pixels up 4`
---
- **size2d** ( _size_ )
- creates a smaller image of the FacetPattern in 2D Space, according to the relative amount `size`.
- `size` must be between 0 and 1. The new pattern will be a smaller 2D image of the input, surrounded by padding of black pixels (0s).
- example:
- `$('example').noise(10000).size2d(0.5).saveimg('example'); // 100 x 100 image with a square of noise in the center`
- `$('example').noise(10000).size2d(0.5).saveimg('example'); // 100 x 100 image with a square of noise in the center`
---
- **spectro** ( _filePath_, _windowSize_ = 2048 )
- saves a PNG file in the `img/` directory named `fileName.png`, with the FacetPattern's spectrogram.
- example:
- `$('example').noise(n1).ffilter(_.ramp(0,NYQUIST/2),_.ramp(NYQUIST,NYQUIST/2)).spectro('mytri'+Date.now()).once();`
108 changes: 84 additions & 24 deletions js/FacetPattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const vm = require('vm');
const { exec } = require('child_process');
const path = require('path');
const wav = require('node-wav');
const fft = require('fft-js').fft;
const fftUtil = require('fft-js').util;
const WaveFile = require('wavefile').WaveFile;
const FacetConfig = require('./config.js');
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
Expand All @@ -13,7 +15,6 @@ const KarplusStrongString = require('./lib/KarplusStrongString.js').KarplusStron
const Complex = require('./lib/Complex.js');
const { Scale } = require('tonal');
const PNG = require('pngjs').PNG;
const readimage = require('readimage');
let cross_platform_slash = process.platform == 'win32' ? '\\' : '/';

class FacetPattern {
Expand Down Expand Up @@ -229,39 +230,30 @@ class FacetPattern {

image (imagePath, columnsPerSecond = 512, minimumFrequency = 20, maximumFrequency = SAMPLE_RATE / 2, frequencyPattern = false) {
const fileData = fs.readFileSync(imagePath);
let imageData;
const png = PNG.sync.read(fileData);
let samplesPerColumn = Math.round(SAMPLE_RATE / columnsPerSecond);
readimage(fileData, function (err, image) {
if (err) {
throw `image file: ${imagePath} could not be read correctly. Make sure the file is a JPEG, and try re-exporting it with GIMP or another image editor.`;
}
imageData = image;
});
this.silence(samplesPerColumn*imageData.width);
if ( frequencyPattern !== false ) {
// custom frequencyPattern
if ( !this.isFacetPattern(frequencyPattern) ) {
this.silence(samplesPerColumn * png.width);
if (frequencyPattern !== false) {
if (!this.isFacetPattern(frequencyPattern)) {
throw `frequencyPattern for image() must be a FacetPattern object if included as an argument; type found: ${typeof frequencyPattern}`;
}
frequencyPattern.size(imageData.height);
}
else {
// linear frequencyPattern based on ramp from minimumFrequency to maximumFrequency
frequencyPattern = new FacetPattern().ramp(0,1,imageData.height);
frequencyPattern.size(png.height);
} else {
frequencyPattern = new FacetPattern().ramp(0, 1, png.height);
}
frequencyPattern.reverse();
for (let y = 0; y < imageData.height; y++) {
for (let y = 0; y < png.height; y++) {
let brightness_data = [];
let frequency = (frequencyPattern.data[y] * (maximumFrequency - minimumFrequency)) + minimumFrequency;
for (let x = 0; x < imageData.width; x++) {
let pixelIndex = (y * imageData.width + x) * 4;
let r = imageData.frames[0].data[pixelIndex];
let g = imageData.frames[0].data[pixelIndex + 1];
let b = imageData.frames[0].data[pixelIndex + 2];
for (let x = 0; x < png.width; x++) {
let idx = (png.width * y + x) << 2;
let r = png.data[idx];
let g = png.data[idx + 1];
let b = png.data[idx + 2];
let brightness = (r + g + b) / (255 * 3);
brightness_data.push(brightness);
}
this.sup(new FacetPattern().sine(frequency,samplesPerColumn*imageData.width).times(new FacetPattern().from(brightness_data).curve()),0);
this.sup(new FacetPattern().sine(frequency, samplesPerColumn * png.width).times(new FacetPattern().from(brightness_data).curve()), 0);
}
this.full();
return this;
Expand Down Expand Up @@ -4016,6 +4008,74 @@ ffilter (minFreqs, maxFreqs, invertMode = false) {
return this;
}

spectro (filename, windowSize = 2048 ) {
// define the overlap between windows
let overlap = 0.5; // 25% overlap

// calculate the number of windows in the data
let stepSize = Math.round(windowSize * (1 - overlap));
let numWindows = Math.ceil((this.data.length - windowSize) / stepSize) + 1;

// create a new PNG image
let png = new PNG({
width: numWindows,
height: windowSize / 2,
filterType: -1
});

for (let i = 0; i < numWindows; i++) {
// get the data for this window
let start = i * stepSize;
let windowData = this.data.slice(start, start + windowSize);
while (windowData.length < windowSize) {
windowData.push(0);
}

// convert the window data to complex numbers
let complexWindowData = windowData.map(value => [value, 0]);

// apply the Fourier transform to the window data
let phasors = fft(complexWindowData);

// convert the phasors to decibels
let magnitudes = fftUtil.fftMag(phasors);

// apply a logarithmic scale to the magnitudes
let logMagnitudes = magnitudes.map(x => Math.log10(x + 1e-9));

// write the magnitudes to the PNG data
for (let j = 0; j < windowSize / 2; j++) {
let idx = (png.width * (png.height - j - 1) + i) << 2; // flip the spectrogram
let value = logMagnitudes[j];
// scale it from [0, 1] to [0, 255]
value = Math.round(value * 255);

// map the value to a color
let color = [];

if (value < 128) {
// interpolate between blue and green
color[0] = 0;
color[1] = Math.round(value * 2);
color[2] = 96 - color[1];
} else {
// interpolate between green and red
color[0] = Math.round((value - 128) * 2);
color[1] = 96 - color[0];
color[2] = 0;
}

// write the pixel data
png.data[idx] = color[0];
png.data[idx + 1] = color[1];
png.data[idx + 2] = color[2];
png.data[idx + 3] = 255;
}
}
png.pack().pipe(fs.createWriteStream(`img/${filename}.png`));
return this;
}

saveimg (filename, rgbData, width = Math.round(Math.sqrt(this.data.length)), height = Math.round(Math.sqrt(this.data.length))) {
width = Math.round(width);
height = Math.round(height);
Expand Down
Loading

0 comments on commit bb437b8

Please sign in to comment.