melody.test.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. const { eq, ok, notOk } = require('../../helpers/assert');
  2. const { makeNetwork, makePeer } = require('../../helpers/setup');
  3. function makeSilentWav(numSamples = 4096) {
  4. const sampleRate = 22050;
  5. const dataLen = numSamples * 2;
  6. const buf = Buffer.alloc(44 + dataLen);
  7. buf.write('RIFF', 0);
  8. buf.writeUInt32LE(36 + dataLen, 4);
  9. buf.write('WAVE', 8);
  10. buf.write('fmt ', 12);
  11. buf.writeUInt32LE(16, 16);
  12. buf.writeUInt16LE(1, 20);
  13. buf.writeUInt16LE(1, 22);
  14. buf.writeUInt32LE(sampleRate, 24);
  15. buf.writeUInt32LE(sampleRate * 2, 28);
  16. buf.writeUInt16LE(2, 32);
  17. buf.writeUInt16LE(16, 34);
  18. buf.write('data', 36);
  19. buf.writeUInt32LE(dataLen, 40);
  20. for (let i = 0; i < numSamples; i += 1) {
  21. buf.writeInt16LE(((Math.sin(i / 50) * 2000) | 0), 44 + i * 2);
  22. }
  23. return buf;
  24. }
  25. describe('melody: steganography embed/extract round-trip', (t) => {
  26. t('extracts the same ascii message that was embedded', async () => {
  27. const net = makeNetwork();
  28. const A = makePeer(net); A.setActor();
  29. const mm = A.use('melody');
  30. const wav = makeSilentWav();
  31. const message = 'hello blockchain sounds';
  32. const encoded = mm.embedTextInWav(wav, message);
  33. const decoded = mm.extractTextFromWav(encoded);
  34. eq(decoded, message);
  35. });
  36. t('extracts a multi-line UTF-8 message with non-ascii characters', async () => {
  37. const net = makeNetwork();
  38. const A = makePeer(net); A.setActor();
  39. const mm = A.use('melody');
  40. const wav = makeSilentWav(32768);
  41. const message = 'línea 1\nlínea 2 — ñ — 中文 🎵';
  42. const encoded = mm.embedTextInWav(wav, message);
  43. const decoded = mm.extractTextFromWav(encoded);
  44. eq(decoded, message);
  45. });
  46. t('extracts a JSON payload identical to what was embedded', async () => {
  47. const net = makeNetwork();
  48. const A = makePeer(net); A.setActor();
  49. const mm = A.use('melody');
  50. const wav = makeSilentWav(65536);
  51. const payload = { id: '@feedid.ed25519', ts: 1747512345678, msg: 'embedded text' };
  52. const encoded = mm.embedTextInWav(wav, JSON.stringify(payload));
  53. const decoded = mm.extractTextFromWav(encoded);
  54. ok(decoded);
  55. const parsed = JSON.parse(decoded);
  56. eq(parsed.id, payload.id);
  57. eq(parsed.ts, payload.ts);
  58. eq(parsed.msg, payload.msg);
  59. });
  60. t('preserves the WAV RIFF header bytes after embedding', async () => {
  61. const net = makeNetwork();
  62. const A = makePeer(net); A.setActor();
  63. const mm = A.use('melody');
  64. const wav = makeSilentWav();
  65. const encoded = mm.embedTextInWav(wav, 'data');
  66. eq(encoded.slice(0, 4).toString('ascii'), 'RIFF');
  67. eq(encoded.slice(8, 12).toString('ascii'), 'WAVE');
  68. eq(encoded.slice(12, 16).toString('ascii'), 'fmt ');
  69. eq(encoded.slice(36, 40).toString('ascii'), 'data');
  70. eq(encoded.length, wav.length);
  71. });
  72. t('only the LSB of samples is modified; >99% of bits unchanged', async () => {
  73. const net = makeNetwork();
  74. const A = makePeer(net); A.setActor();
  75. const mm = A.use('melody');
  76. const wav = makeSilentWav();
  77. const encoded = mm.embedTextInWav(wav, 'small');
  78. let diffBits = 0;
  79. let diffNonLsb = 0;
  80. for (let off = 44; off < wav.length; off += 2) {
  81. const a = wav.readInt16LE(off);
  82. const b = encoded.readInt16LE(off);
  83. if (a !== b) {
  84. diffBits += 1;
  85. if ((a & ~1) !== (b & ~1)) diffNonLsb += 1;
  86. }
  87. }
  88. eq(diffNonLsb, 0);
  89. ok(diffBits >= 1);
  90. });
  91. t('extractTextFromWav returns null when no payload was embedded', async () => {
  92. const net = makeNetwork();
  93. const A = makePeer(net); A.setActor();
  94. const mm = A.use('melody');
  95. const wav = makeSilentWav();
  96. const decoded = mm.extractTextFromWav(wav);
  97. eq(decoded, null);
  98. });
  99. t('extractTextFromWav returns null for a non-WAV buffer', async () => {
  100. const net = makeNetwork();
  101. const A = makePeer(net); A.setActor();
  102. const mm = A.use('melody');
  103. const bogus = Buffer.alloc(2048, 0xAA);
  104. const decoded = mm.extractTextFromWav(bogus);
  105. eq(decoded, null);
  106. });
  107. t('embedTextInWav returns input unchanged for empty message', async () => {
  108. const net = makeNetwork();
  109. const A = makePeer(net); A.setActor();
  110. const mm = A.use('melody');
  111. const wav = makeSilentWav();
  112. const encoded = mm.embedTextInWav(wav, '');
  113. eq(encoded.length, wav.length);
  114. eq(encoded.equals(wav), true);
  115. });
  116. t('embedTextInWav returns input unchanged when payload exceeds 4096 bytes', async () => {
  117. const net = makeNetwork();
  118. const A = makePeer(net); A.setActor();
  119. const mm = A.use('melody');
  120. const wav = makeSilentWav(200000);
  121. const big = 'x'.repeat(5000);
  122. const encoded = mm.embedTextInWav(wav, big);
  123. eq(encoded.equals(wav), true);
  124. });
  125. });
  126. describe('melody: blockchain-to-note mapping', (t) => {
  127. t('exports note + octave tables', async () => {
  128. const net = makeNetwork();
  129. const A = makePeer(net); A.setActor();
  130. const mm = A.use('melody');
  131. eq(mm.NOTE_NAMES.length, 12);
  132. eq(mm.OCTAVES.length, 3);
  133. ok(mm.TYPE_TO_DEGREE.post != null);
  134. });
  135. t('getUserMelody returns a sequence of notes for a user with published content', async () => {
  136. const net = makeNetwork();
  137. const A = makePeer(net); A.setActor();
  138. await A.use('audios').createAudio('[a](&blob0000000000000000000000000000000000000000000.sha256)', [], 'X', '', '');
  139. await A.use('audios').createAudio('[b](&blob0000000000000000000000000000000000000000000.sha256)', [], 'Y', '', '');
  140. const m = await A.use('melody').getUserMelody(A.keypair.id);
  141. eq(m.feedId, A.keypair.id);
  142. ok(m.total >= 2);
  143. ok(Array.isArray(m.sequence));
  144. for (const n of m.sequence) {
  145. ok(typeof n.name === 'string');
  146. ok(typeof n.type === 'string');
  147. ok(typeof n.freq === 'number');
  148. ok(typeof n.durMs === 'number');
  149. ok(typeof n.id === 'string');
  150. }
  151. });
  152. });