Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add audio playback demo to verify audio fingerprint code works as exp… #33

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions features/audio-playback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Audio sample: THROW THE SWITCH by Robert Meyers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for nitpicking but it takes a lot of time for the modified sample to start playing. I'm guessing that it'd be faster if we used a shorter input sample (current one is 6min)?

https://sampleswap.org/mp3/song.php?id=1315
179 changes: 179 additions & 0 deletions features/audio-playback/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Audio playback</title>
</head>
<body>
<p><a href="../index.html">[Home]</a></p>

<p>For testing fingerprinting, ensure that both sound the same and playing the copy several times also sounds the same but makes a different hash.</p>

<main>
<audio src="sample.mpga" id="sample" controls ></audio>
<br>
<button id="copy" disabled >Playback copy</button>
<dl id="stats">
</dl>
</main>

<style>
#canvas, #output {
width: 300px;
height: 300px;
border: 1px solid black;
}
#canvas {
border-color: red;
}
#stats {
white-space: pre;
}
textarea {
width: 600px;
height: 600px;
}
</style>
<script>
const AudioContext = window.AudioContext || window.webkitAudioContext;
const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;

async function sha256 (str) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder('utf-8').encode(str));
return Array.prototype.map.call(new Uint8Array(buf), x => (('00' + x.toString(16)).slice(-2))).join('');
}

class Playback {
constructor (copyPlaybackElement, sampleElement, statsElement) {
this.copyPlaybackElement = copyPlaybackElement;
this.sampleElement = sampleElement;
this.statsElement = statsElement;
this.copyPlaybackElement.addEventListener('click', this);
this.sampleElement.addEventListener('durationchange', this, false);

this.copyCount = 0;
this.sampleRate = 44100;
}

async init () {
if (this.audioData) {
return;
}
this.audioData = await this.getData();
this.outputHash("Original copy", this.audioData);
}

async outputHash (reason, data) {
const outputElement = document.createElement('div');
const dtElement = document.createElement('dt');
dtElement.textContent = reason;
outputElement.appendChild(dtElement);
const ddElement = document.createElement('dd');
ddElement.textContent = await sha256(data);
outputElement.appendChild(ddElement);
this.statsElement.appendChild(outputElement);
}

// Gets data from the sample element, fetching it and rendering it then passing it into an offline audioContext to get the channelData from it.
async getData () {
const audioCtx = new AudioContext();
const source = this.offlineAudioContext.createBufferSource();

const response = await fetch(this.sampleElement.src);
const buffer = await response.arrayBuffer();

return new Promise((resolve) => {
audioCtx.decodeAudioData(
buffer,
async (decodedBuffer) => {
source.buffer = decodedBuffer;
source.connect(this.offlineAudioContext.destination);
source.start();
// Push source into an offline context
// Get channel data
const renderedBuffer = await this.offlineAudioContext.startRendering();
resolve(renderedBuffer.getChannelData(0));
},
(e) => { console.log('Error with decoding audio data' + e.err); }
);
});
}

async handlePlayToggle (e) {
const element = e.target;

if (element.dataset.playing === 'false' || !element.dataset.playing) {
this.copyPlaybackStarted();

await this.init();

let data = this.audioData;
if (this.nextArray) {
data = this.nextArray;
}

// Reconstruct a source as stopping it prevents playing again
const audioCtx = new AudioContext();
this.outputSource = audioCtx.createBufferSource();
const buffer = audioCtx.createBuffer(1, data.length, audioCtx.sampleRate);
// Check for Safari
if (buffer.copyToChannel) {
buffer.copyToChannel(data, 0, 0);
} else {
let out = buffer.getChannelData(0);
for (let i in data) {
out[i] = data[i];
}
}
this.outputSource.buffer = buffer;
this.outputSource.connect(audioCtx.destination);
this.outputSource.start();
this.outputSource.addEventListener('ended', this, false);

// Copy over the existing data, in the fingerprint resistance case this should produce a different hash each time
this.copyCount += 1;
this.nextArray = buffer.getChannelData(0);
this.outputHash("Copy " + this.copyCount, this.nextArray);
} else {
this.outputSource.stop();
this.copyPlaybackFinished();
}
}

copyPlaybackStarted() {
this.copyPlaybackElement.textContent = 'Pause playback copy';
this.copyPlaybackElement.dataset.playing = 'true';
}

copyPlaybackFinished() {
this.copyPlaybackElement.textContent = 'Playback copy';
this.copyPlaybackElement.dataset.playing = 'false';
}

async handleEvent (e) {
switch (e.type) {
case 'durationchange':
this.offlineAudioContext = new OfflineAudioContext(1, this.sampleElement.duration * this.sampleRate, this.sampleRate);
this.copyPlaybackElement.removeAttribute("disabled");
break;
case 'click':
this.handlePlayToggle(e);
break;
case 'ended':
this.copyPlaybackFinished();
break;
}
}
}

// eslint-disable-next-line no-unused-vars
const instance = new Playback(
document.getElementById('copy'),
document.getElementById('sample'),
document.getElementById('stats')
);
</script>

</body>
</html>
Binary file added features/audio-playback/sample.mpga
Binary file not shown.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ <h2>Browser Features</h2>
<li><a href="./features/canvas-draw.html">Canvas draw</a></li>
<li><a href="./features/download.html">Downloads</a></li>
<li><a href="./features/fonts.html">Fonts</a></li>
<li><a href="./features/audio-playback/">Audio playback</a></li>
</ul>

<h2>Security</h2>
Expand Down
Loading