UNPKG

pixeltone.js

Version:

An auditory creation tool that turns your images into sounds.

587 lines (523 loc) 18.1 kB
import { loadImage, createCanvas } from 'canvas'; import fs from 'fs'; import path from 'path'; import wavefile from 'wavefile'; const DEFAULT_RGB_FREQUENCY_RANGES = { r: { min: 30.0, max: 500.0, offset: 60.0 }, g: { min: 500.0, max: 2000.0, offset: 250.0 }, b: { min: 2000.0, max: 10000.0, offset: 1000.0 }, }; const DEFAULT_SAMPLE_RATE = 44100; const DEFAULT_DURATION = 0.01; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var eventemitter3 = {exports: {}}; var hasRequiredEventemitter3; function requireEventemitter3 () { if (hasRequiredEventemitter3) return eventemitter3.exports; hasRequiredEventemitter3 = 1; (function (module) { var has = Object.prototype.hasOwnProperty , prefix = '~'; /** * Constructor to create a storage for our `EE` objects. * An `Events` instance is a plain object whose properties are event names. * * @constructor * @private */ function Events() {} // // We try to not inherit from `Object.prototype`. In some engines creating an // instance in this way is faster than calling `Object.create(null)` directly. // If `Object.create(null)` is not supported we prefix the event names with a // character to make sure that the built-in object properties are not // overridden or used as an attack vector. // if (Object.create) { Events.prototype = Object.create(null); // // This hack is needed because the `__proto__` property is still inherited in // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. // if (!new Events().__proto__) prefix = false; } /** * Representation of a single event listener. * * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} [once=false] Specify if the listener is a one-time listener. * @constructor * @private */ function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } /** * Add a listener for a given event. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} once Specify if the listener is a one-time listener. * @returns {EventEmitter} * @private */ function addListener(emitter, event, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('The listener must be a function'); } var listener = new EE(fn, context || emitter, once) , evt = prefix ? prefix + event : event; if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); else emitter._events[evt] = [emitter._events[evt], listener]; return emitter; } /** * Clear event by name. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} evt The Event name. * @private */ function clearEvent(emitter, evt) { if (--emitter._eventsCount === 0) emitter._events = new Events(); else delete emitter._events[evt]; } /** * Minimal `EventEmitter` interface that is molded against the Node.js * `EventEmitter` interface. * * @constructor * @public */ function EventEmitter() { this._events = new Events(); this._eventsCount = 0; } /** * Return an array listing the events for which the emitter has registered * listeners. * * @returns {Array} * @public */ EventEmitter.prototype.eventNames = function eventNames() { var names = [] , events , name; if (this._eventsCount === 0) return names; for (name in (events = this._events)) { if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events)); } return names; }; /** * Return the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Array} The registered listeners. * @public */ EventEmitter.prototype.listeners = function listeners(event) { var evt = prefix ? prefix + event : event , handlers = this._events[evt]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { ee[i] = handlers[i].fn; } return ee; }; /** * Return the number of listeners listening to a given event. * * @param {(String|Symbol)} event The event name. * @returns {Number} The number of listeners. * @public */ EventEmitter.prototype.listenerCount = function listenerCount(event) { var evt = prefix ? prefix + event : event , listeners = this._events[evt]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; }; /** * Calls each of the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Boolean} `true` if the event had listeners, else `false`. * @public */ EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt] , len = arguments.length , args , i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; /** * Add a listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.on = function on(event, fn, context) { return addListener(this, event, fn, context, false); }; /** * Add a one-time listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.once = function once(event, fn, context) { return addListener(this, event, fn, context, true); }; /** * Remove the listeners of a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn Only remove the listeners that match this function. * @param {*} context Only remove the listeners that have this context. * @param {Boolean} once Only remove one-time listeners. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { clearEvent(this, evt); return this; } var listeners = this._events[evt]; if (listeners.fn) { if ( listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context) ) { clearEvent(this, evt); } } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) { if ( listeners[i].fn !== fn || (once && !listeners[i].once) || (context && listeners[i].context !== context) ) { events.push(listeners[i]); } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; else clearEvent(this, evt); } return this; }; /** * Remove all listeners, or those of the specified event. * * @param {(String|Symbol)} [event] The event name. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) clearEvent(this, evt); } else { this._events = new Events(); this._eventsCount = 0; } return this; }; // // Alias methods names because people roll like that. // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; // // Expose the prefix. // EventEmitter.prefixed = prefix; // // Allow `EventEmitter` to be imported as module namespace. // EventEmitter.EventEmitter = EventEmitter; // // Expose the module. // { module.exports = EventEmitter; } } (eventemitter3)); return eventemitter3.exports; } var eventemitter3Exports = requireEventemitter3(); var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports); class ImageLoader extends EventEmitter { constructor() { super(); } async load(inputImagePath) { return await loadImage(inputImagePath); } destroy() { super.removeAllListeners(); } } class ImageUtils { static getMetadata(inputImagePath) { const fileStat = fs.statSync(inputImagePath); const fileSize = fileStat.size; return { name: path.basename(inputImagePath), sizeKB: Math.round(fileSize / 1024), }; } static extractImageData(image) { if (!image) { return new Uint8ClampedArray(0); } const width = image.width; const height = image.height; const canvas = createCanvas(width, height); const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData(0, 0, width, height); return imageData.data; } } class MathUtils { static normalizeRGB(rgb) { const { r, g, b } = rgb; return { r: r / 255, g: g / 255, b: b / 255, }; } static mapFrequency(options) { const { value, frequencyRange } = options; const { offset, min, max } = frequencyRange; return offset + value * (max - min); } } class PixelToFrequencyMapper { constructor() { } serialRGBMapper(options) { const { data, width, height, rgbFrequencyRange } = options; const frequencies = []; const amplitudes = []; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; // RGBA format const rgb = MathUtils.normalizeRGB({ r: data[idx], g: data[idx + 1], b: data[idx + 2], }); this._mapFrequencies([{ value: rgb.r, frequencyRange: rgbFrequencyRange.r }, { value: rgb.g, frequencyRange: rgbFrequencyRange.g }, { value: rgb.b, frequencyRange: rgbFrequencyRange.b }, ]).forEach(value => { frequencies.push(value); }); amplitudes.push(rgb.r, rgb.g, rgb.b); } } return { frequencies, amplitudes }; } _mapFrequencies(options) { // Assuming we have 3 (RGB) as in order return options.map(option => { const { value, frequencyRange } = option; return MathUtils.mapFrequency({ value, frequencyRange, }); }); } destroy() { // TODO } } class RGBToAudioSampleGenerator { constructor() { // TODO } generateAsSinWave(options) { const { mappedFreqAmp, sampleRate, duration } = options; const { amplitudes, frequencies } = mappedFreqAmp; const totalSamples = Math.floor(frequencies.length / 3) * Math.floor(sampleRate * duration); const audioSamples = new Float32Array(totalSamples); let sampleIndex = 0; for (let i = 0; i < frequencies.length; i += 3) { const [freqR, freqG, freqB] = [frequencies[i], frequencies[i + 1], frequencies[i + 2]]; const [ampR, ampG, ampB] = [amplitudes[i], amplitudes[i + 1], amplitudes[i + 2]]; for (let t = 0; t < sampleRate * duration; t++) { const time = t / sampleRate; // TODO add this to MathUtil as createSinWave with options. const sample = (ampR * Math.sin(2 * Math.PI * freqR * time) + ampG * Math.sin(2 * Math.PI * freqG * time) + ampB * Math.sin(2 * Math.PI * freqB * time)) / 3; audioSamples[sampleIndex++] = sample; } } return audioSamples; } destroy() { // TODO } } const { WaveFile } = wavefile; class WaveFileFactory { _waveFile; constructor() { this._waveFile = new WaveFile(); } createBuffer(audioSamples, sampleRate) { this._waveFile.fromScratch(1, sampleRate, '32f', audioSamples); return Buffer.from(this._getBuffer()); } _getBuffer() { return this._waveFile.toBuffer(); } destroy() { } } class FileUtils { static createFile(outputAudioPath, data) { fs.writeFileSync(outputAudioPath, data); } } /** * Pixeltone class provides functionality to generate audio files from image data. */ class Pixeltone { _imageLoader; _pixelToFrequencyMapper; _rgbToAudioSampleGenerator; _wavFileFactory; constructor() { this._imageLoader = new ImageLoader(); this._pixelToFrequencyMapper = new PixelToFrequencyMapper(); this._rgbToAudioSampleGenerator = new RGBToAudioSampleGenerator(); this._wavFileFactory = new WaveFileFactory(); } /** * Converts an image into an audio file by mapping pixel data (RGB) to audio frequencies. * * @param {PixeltoneOptions} options - Configuration options for generating audio. * @param {string} options.inputImagePath - Path to the input image file. * @param {string} options.outputAudioPath - Path where the output audio file will be saved. * @param {number} [options.sampleRate=DEFAULT_SAMPLE_RATE] - The audio sample rate in Hz. * @param {number} [options.duration=DEFAULT_DURATION] - Duration of the each pixel sample in seconds. * @param {number[][]} [options.rgbFrequencyRange=DEFAULT_RGB_FREQUENCY_RANGES] - Frequency range for RGB channels. * * @returns {Promise<void>} Resolves when the audio file has been successfully created. * * @example * const pixeltone = new Pixeltone(); * await pixeltone.createAudioFromImage({ * inputImagePath: './input.png', * outputAudioPath: './output.wav', * sampleRate: 44100, * duration: 10, * rgbFrequencyRange: { * r: { min: 30, max: 500, offset: 60 }, * g: { min: 500, max: 2000, offset: 250 }, * b: { min: 2000, max: 10000, offset: 1000 }, * } * }); */ async createAudioFromImage(options) { const { inputImagePath, outputAudioPath, sampleRate = DEFAULT_SAMPLE_RATE, duration = DEFAULT_DURATION, rgbFrequencyRange = DEFAULT_RGB_FREQUENCY_RANGES, } = options; const image = await this._imageLoader.load(inputImagePath); ImageUtils.getMetadata(inputImagePath); const imageData = ImageUtils.extractImageData(image); const mappedFreqAmp = this._pixelToFrequencyMapper.serialRGBMapper({ data: imageData, width: image.width, height: image.height, rgbFrequencyRange, }); const audioSamples = this._rgbToAudioSampleGenerator.generateAsSinWave({ mappedFreqAmp, sampleRate, duration, }); const buffer = this._wavFileFactory.createBuffer(audioSamples, sampleRate); FileUtils.createFile(outputAudioPath, buffer); } destroy() { this._imageLoader.destroy(); this._pixelToFrequencyMapper.destroy(); this._rgbToAudioSampleGenerator.destroy(); this._wavFileFactory.destroy(); } } export { Pixeltone }; //# sourceMappingURL=index.esm.js.map