181 lines
5 KiB
JavaScript
181 lines
5 KiB
JavaScript
"use strict";
|
|
|
|
const pull = require('../server/node_modules/pull-stream');
|
|
|
|
const NOTE_NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
|
const OCTAVES = [3, 4, 5];
|
|
|
|
const TYPE_TO_DEGREE = {
|
|
post: 0,
|
|
vote: 2,
|
|
about: 4,
|
|
contact: 5,
|
|
forum: 7,
|
|
tribe: 9,
|
|
transfer: 11,
|
|
shop: 1,
|
|
shopProduct: 1,
|
|
job: 3,
|
|
project: 6,
|
|
event: 8,
|
|
task: 10,
|
|
bookmark: 4,
|
|
feed: 5,
|
|
pad: 6,
|
|
chat: 7,
|
|
audio: 8,
|
|
video: 9,
|
|
image: 10,
|
|
document: 11,
|
|
torrent: 0,
|
|
map: 2,
|
|
pixelia: 3,
|
|
gameScore: 5,
|
|
votes: 7,
|
|
calendar: 8,
|
|
curriculum: 9,
|
|
report: 10,
|
|
parliament: 11,
|
|
courts: 0,
|
|
market: 1,
|
|
aiExchange: 6,
|
|
tombstone: 4
|
|
};
|
|
|
|
const NOTE_FREQS = (() => {
|
|
const a4 = 440;
|
|
const map = {};
|
|
for (const oct of [2, 3, 4, 5, 6]) {
|
|
for (let i = 0; i < NOTE_NAMES.length; i++) {
|
|
const name = NOTE_NAMES[i] + oct;
|
|
const semis = (oct - 4) * 12 + (i - 9);
|
|
map[name] = a4 * Math.pow(2, semis / 12);
|
|
}
|
|
}
|
|
return map;
|
|
})();
|
|
|
|
function blockToNote(msg) {
|
|
const c = msg && msg.value && msg.value.content;
|
|
if (!c || typeof c !== 'object') return null;
|
|
const type = String(c.type || '').trim() || 'unknown';
|
|
const degree = TYPE_TO_DEGREE[type] != null ? TYPE_TO_DEGREE[type] : (Math.abs(hashStr(type)) % 12);
|
|
const size = Buffer.byteLength(JSON.stringify(msg.value), 'utf8');
|
|
const octIdx = size < 256 ? 0 : (size < 1024 ? 1 : 2);
|
|
const octave = OCTAVES[octIdx];
|
|
const name = NOTE_NAMES[degree] + octave;
|
|
const freq = NOTE_FREQS[name] || 440;
|
|
const durMs = Math.min(600, 200 + Math.round(size / 64));
|
|
return {
|
|
id: msg.key,
|
|
ts: msg.value.timestamp,
|
|
type,
|
|
size,
|
|
name,
|
|
freq,
|
|
durMs
|
|
};
|
|
}
|
|
|
|
function hashStr(s) {
|
|
let h = 0;
|
|
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
return h;
|
|
}
|
|
|
|
module.exports = ({ cooler }) => {
|
|
let ssb = null;
|
|
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
|
|
|
async function getUserMelodyInternal(userId, limit = 200) {
|
|
const client = await openSsb();
|
|
const me = userId || client.id;
|
|
const msgs = await new Promise((resolve, reject) => {
|
|
pull(
|
|
client.createUserStream({ id: me, reverse: true, limit }),
|
|
pull.collect((err, rows) => err ? reject(err) : resolve(rows))
|
|
);
|
|
});
|
|
const seq = msgs
|
|
.filter(m => m && m.value && m.value.content && typeof m.value.content === 'object')
|
|
.map(blockToNote)
|
|
.filter(Boolean)
|
|
.reverse();
|
|
const stats = {};
|
|
for (const n of seq) {
|
|
stats[n.type] = (stats[n.type] || 0) + 1;
|
|
}
|
|
return { feedId: me, total: seq.length, sequence: seq, stats };
|
|
}
|
|
|
|
return {
|
|
NOTE_NAMES,
|
|
OCTAVES,
|
|
TYPE_TO_DEGREE,
|
|
getUserMelody: getUserMelodyInternal,
|
|
embedTextInWav,
|
|
extractTextFromWav
|
|
};
|
|
};
|
|
|
|
const STEG_MAGIC = Buffer.from([0xBC, 0x53]);
|
|
const STEG_HEADER_BITS = 32;
|
|
|
|
function embedTextInWav(wavBuffer, text) {
|
|
if (!Buffer.isBuffer(wavBuffer) || wavBuffer.length < 44) return wavBuffer;
|
|
const payload = Buffer.from(String(text || ''), 'utf8');
|
|
if (payload.length === 0) return wavBuffer;
|
|
if (payload.length > 4096) return wavBuffer;
|
|
const lenBuf = Buffer.alloc(2);
|
|
lenBuf.writeUInt16BE(payload.length, 0);
|
|
const fullData = Buffer.concat([STEG_MAGIC, lenBuf, payload]);
|
|
const totalBits = fullData.length * 8;
|
|
const dataLen = wavBuffer.readUInt32LE(40);
|
|
const numSamples = Math.floor(dataLen / 2);
|
|
if (totalBits > numSamples) return wavBuffer;
|
|
const out = Buffer.from(wavBuffer);
|
|
for (let i = 0; i < totalBits; i++) {
|
|
const byteIdx = i >> 3;
|
|
const bitIdx = i & 7;
|
|
const bit = (fullData[byteIdx] >> (7 - bitIdx)) & 1;
|
|
const sampleOffset = 44 + (i * 2);
|
|
let sample = out.readInt16LE(sampleOffset);
|
|
sample = (sample & ~1) | bit;
|
|
out.writeInt16LE(sample, sampleOffset);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function readBitsAsBuffer(wavBuffer, startBit, numBits) {
|
|
const bytes = Math.ceil(numBits / 8);
|
|
const out = Buffer.alloc(bytes, 0);
|
|
for (let i = 0; i < numBits; i++) {
|
|
const sampleOffset = 44 + ((startBit + i) * 2);
|
|
if (sampleOffset + 2 > wavBuffer.length) return null;
|
|
const sample = wavBuffer.readInt16LE(sampleOffset);
|
|
const bit = sample & 1;
|
|
const byteIdx = i >> 3;
|
|
const bitIdx = i & 7;
|
|
out[byteIdx] |= bit << (7 - bitIdx);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function extractTextFromWav(wavBuffer) {
|
|
if (!Buffer.isBuffer(wavBuffer) || wavBuffer.length < 44 + STEG_HEADER_BITS * 2) return null;
|
|
if (wavBuffer.slice(0, 4).toString('ascii') !== 'RIFF') return null;
|
|
if (wavBuffer.slice(8, 12).toString('ascii') !== 'WAVE') return null;
|
|
const magic = readBitsAsBuffer(wavBuffer, 0, 16);
|
|
if (!magic || magic[0] !== STEG_MAGIC[0] || magic[1] !== STEG_MAGIC[1]) return null;
|
|
const lenBuf = readBitsAsBuffer(wavBuffer, 16, 16);
|
|
if (!lenBuf) return null;
|
|
const len = lenBuf.readUInt16BE(0);
|
|
if (len === 0 || len > 4096) return null;
|
|
const payload = readBitsAsBuffer(wavBuffer, 32, len * 8);
|
|
if (!payload) return null;
|
|
try {
|
|
const text = payload.toString('utf8');
|
|
if (!text) return null;
|
|
return text;
|
|
} catch (_) { return null; }
|
|
}
|