fix(voice): proper handling of waveforms
This commit is contained in:
parent
96d133dd2f
commit
4c717ffc63
4 changed files with 74 additions and 6 deletions
|
@ -16,6 +16,7 @@ import { extractFileName } from '../../utils/file-utils'
|
||||||
import { assertTypeIs } from '../../utils/type-assertion'
|
import { assertTypeIs } from '../../utils/type-assertion'
|
||||||
import bigInt from 'big-integer'
|
import bigInt from 'big-integer'
|
||||||
import { normalizeDate } from '../../utils/misc-utils'
|
import { normalizeDate } from '../../utils/misc-utils'
|
||||||
|
import { encodeWaveform } from '../../utils/voice-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize an {@link InputMediaLike} to `InputMedia`,
|
* Normalize an {@link InputMediaLike} to `InputMedia`,
|
||||||
|
@ -381,7 +382,10 @@ export async function _normalizeInputMedia(
|
||||||
duration: media.duration || 0,
|
duration: media.duration || 0,
|
||||||
title: media.type === 'audio' ? media.title : undefined,
|
title: media.type === 'audio' ? media.title : undefined,
|
||||||
performer: media.type === 'audio' ? media.performer : undefined,
|
performer: media.type === 'audio' ? media.performer : undefined,
|
||||||
waveform: media.type === 'voice' ? media.waveform : undefined,
|
waveform:
|
||||||
|
media.type === 'voice' && media.waveform
|
||||||
|
? encodeWaveform(media.waveform)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,11 +114,15 @@ export interface InputMediaVoice extends BaseInputMedia {
|
||||||
duration?: number
|
duration?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waveform of the voice message
|
* Waveform of the voice message.
|
||||||
|
*
|
||||||
|
* Represented with integers in range [0, 31],
|
||||||
|
* usually has length of 100.
|
||||||
|
* Generated by the server if omitted.
|
||||||
*
|
*
|
||||||
* Only applicable to newly uploaded files.
|
* Only applicable to newly uploaded files.
|
||||||
*/
|
*/
|
||||||
waveform?: Buffer
|
waveform?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { tl } from '@mtcute/tl'
|
||||||
import { TelegramClient } from '../../client'
|
import { TelegramClient } from '../../client'
|
||||||
import { makeInspectable } from '../utils'
|
import { makeInspectable } from '../utils'
|
||||||
import { tdFileId } from '@mtcute/file-id'
|
import { tdFileId } from '@mtcute/file-id'
|
||||||
|
import { decodeWaveform } from '../../utils/voice-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An voice note.
|
* An voice note.
|
||||||
|
@ -35,10 +36,13 @@ export class Voice extends RawDocument {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Voice note's waveform
|
* Voice note's waveform
|
||||||
|
*
|
||||||
|
* Represented with integers in range [0, 31],
|
||||||
|
* usually has length of 100
|
||||||
*/
|
*/
|
||||||
get waveform(): Buffer {
|
get waveform(): number[] {
|
||||||
return this.attr.waveform!
|
return decodeWaveform(this.attr.waveform!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeInspectable(Voice, ['fileSize', 'dcId'], ['inputMedia', 'inputDocument'])
|
makeInspectable(Voice, ['fileSize', 'dcId'], ['inputMedia', 'inputDocument', 'waveform'])
|
||||||
|
|
56
packages/client/src/utils/voice-utils.ts
Normal file
56
packages/client/src/utils/voice-utils.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
export function decodeWaveform(wf: Buffer): number[] {
|
||||||
|
const bitsCount = wf.length * 8
|
||||||
|
const valuesCount = ~~(bitsCount / 5)
|
||||||
|
|
||||||
|
if (!valuesCount) return []
|
||||||
|
|
||||||
|
const lastIdx = valuesCount - 1
|
||||||
|
|
||||||
|
// Read each 5 bit of encoded5bit as 0-31 unsigned char.
|
||||||
|
// We count the index of the byte in which the desired 5-bit sequence starts.
|
||||||
|
// And then we read a uint16 starting from that byte to guarantee to get all of those 5 bits.
|
||||||
|
//
|
||||||
|
// BUT! if it is the last byte we have, we're not allowed to read a uint16 starting with it.
|
||||||
|
// Because it will be an overflow (we'll access one byte after the available memory).
|
||||||
|
// We see, that only the last 5 bits could start in the last available byte and be problematic.
|
||||||
|
// So we read in a general way all the entries except the last one.
|
||||||
|
|
||||||
|
const result: number[] = []
|
||||||
|
for (let i = 0, j = 0; i < lastIdx; i++, j += 5) {
|
||||||
|
const byteIdx = ~~(j / 8)
|
||||||
|
const bitShift = j % 8
|
||||||
|
result[i] = (wf.readUInt16LE(byteIdx) >> bitShift) & 0b11111
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastByteIdx = ~~((lastIdx * 5) / 8)
|
||||||
|
const lastBitShift = (lastIdx * 5) % 8
|
||||||
|
const lastValue =
|
||||||
|
lastByteIdx === wf.length - 1
|
||||||
|
? wf[lastByteIdx]
|
||||||
|
: wf.readUInt16LE(lastByteIdx)
|
||||||
|
result[lastIdx] = (lastValue >> lastBitShift) & 0b11111
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function encodeWaveform(wf: number[]): Buffer {
|
||||||
|
const bitsCount = wf.length * 5
|
||||||
|
const bytesCount = ~~(bitsCount + 7) / 8
|
||||||
|
const result = Buffer.alloc(bytesCount + 1)
|
||||||
|
|
||||||
|
// Write each 0-31 unsigned char as 5 bit to result.
|
||||||
|
// We reserve one extra byte to be able to dereference any of required bytes
|
||||||
|
// as a uint16 without overflowing, even the byte with index "bytesCount - 1".
|
||||||
|
|
||||||
|
for (let i = 0, j = 0; i < wf.length; i++, j += 5) {
|
||||||
|
const byteIdx = ~~(j / 8)
|
||||||
|
const bitShift = j % 8
|
||||||
|
const value = (wf[i] & 0b11111) << bitShift
|
||||||
|
|
||||||
|
const old = result.readUInt16LE(byteIdx)
|
||||||
|
result.writeUInt16LE(old | value, byteIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.slice(0, bytesCount)
|
||||||
|
}
|
Loading…
Reference in a new issue