diff --git a/src/chat/functions/prepareAudioWaveform.ts b/src/chat/functions/prepareAudioWaveform.ts new file mode 100644 index 0000000000..1db8fd5520 --- /dev/null +++ b/src/chat/functions/prepareAudioWaveform.ts @@ -0,0 +1,80 @@ +/*! + * Copyright 2023 WPPConnect Team + * + * 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. + */ + +import Debug from 'debug'; + +import type { AudioMessageOptions } from './sendFileMessage'; + +const debug = Debug('WA-JS:chat:sendFileMessage'); + +/** + * Prepare waveform form message audio file + * + * @category Message + * @internal + */ +export async function prepareAudioWaveform( + options: AudioMessageOptions, + file: File +): Promise< + | undefined + | { + duration: number; + waveform: Uint8Array; + } +> { + if (!options.isPtt || !options.waveform) { + return; + } + + /** + * @see https://css-tricks.com/making-an-audio-waveform-visualizer-with-vanilla-javascript/ + */ + try { + const audioData = await file.arrayBuffer(); + const audioContext = new AudioContext(); + const audioBuffer = await audioContext.decodeAudioData(audioData); + + const rawData = audioBuffer.getChannelData(0); // We only need to work with one channel of data + const samples = 64; // Number of samples we want to have in our final data set + const blockSize = Math.floor(rawData.length / samples); // the number of samples in each subdivision + const filteredData = []; + for (let i = 0; i < samples; i++) { + const blockStart = blockSize * i; // the location of the first sample in the block + let sum = 0; + for (let j = 0; j < blockSize; j++) { + sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block + } + filteredData.push(sum / blockSize); // divide the sum by the block size to get the average + } + + // This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally. + const multiplier = Math.pow(Math.max(...filteredData), -1); + const normalizedData = filteredData.map((n) => n * multiplier); + + // Generate waveform like WhatsApp + const waveform = new Uint8Array( + normalizedData.map((n) => Math.floor(100 * n)) + ); + + return { + duration: Math.floor(audioBuffer.duration), + waveform, + }; + } catch (error) { + debug('Failed to generate waveform', error); + } +} diff --git a/src/chat/functions/sendFileMessage.ts b/src/chat/functions/sendFileMessage.ts index 511ed6ee22..7abee1ceee 100644 --- a/src/chat/functions/sendFileMessage.ts +++ b/src/chat/functions/sendFileMessage.ts @@ -39,6 +39,7 @@ import { prepareMessageButtons, prepareRawMessage, } from '.'; +import { prepareAudioWaveform } from './prepareAudioWaveform'; const debug = Debug('WA-JS:message'); @@ -54,9 +55,53 @@ export interface AutoDetectMessageOptions extends FileMessageOptions { type: 'auto-detect'; } +/** + * Send an audio message as a PTT, like a recorded message + * + * @example + * ```javascript + * // PTT audio + * WPP.chat.sendFileMessage( + * '[number]@c.us', + * 'data:audio/mp3;base64,', + * { + * type: 'audio', + * isPtt: true // false for common audio + * } + * ); + * ``` + */ export interface AudioMessageOptions extends FileMessageOptions { type: 'audio'; isPtt?: boolean; + /** + * Send an audio message as a PTT with waveform + * + * @example + * ```javascript + * // Enable waveform + * WPP.chat.sendFileMessage( + * '[number]@c.us', + * 'data:audio/mp3;base64,', + * { + * type: 'audio', + * isPtt: true, + * waveform: true // false to disable + * } + * ); + * // Disable waveform + * WPP.chat.sendFileMessage( + * '[number]@c.us', + * 'data:audio/mp3;base64,', + * { + * type: 'audio', + * isPtt: true, + * waveform: false + * } + * ); + * ``` + */ + waveform?: boolean; } export interface DocumentMessageOptions @@ -169,6 +214,7 @@ export async function sendFileMessage( ...defaultSendMessageOptions, ...{ type: 'auto-detect', + waveform: true, }, ...options, }; @@ -189,12 +235,20 @@ export async function sendFileMessage( asGif?: boolean; isAudio?: boolean; asSticker?: boolean; + precomputedFields?: { + duration: number; + waveform: Uint8Array; + }; } = {}; let isViewOnce: boolean | undefined; if (options.type === 'audio') { rawMediaOptions.isPtt = options.isPtt; + rawMediaOptions.precomputedFields = await prepareAudioWaveform( + options as any, + file + ); } else if (options.type === 'image') { isViewOnce = options.isViewOnce; } else if (options.type === 'video') { diff --git a/src/whatsapp/models/MsgModel.ts b/src/whatsapp/models/MsgModel.ts index df1d36aec4..8f1a14a7b5 100644 --- a/src/whatsapp/models/MsgModel.ts +++ b/src/whatsapp/models/MsgModel.ts @@ -74,7 +74,8 @@ interface Props { deprecatedMms3Url?: any; directPath?: any; mimetype?: any; - duration?: any; + waveform?: any; + duration?: number; filehash?: any; encFilehash?: any; size?: any;