Discuss Scratch

VedanshS933
Scratcher
1000+ posts

Minute Time in Sound Editor

This is not a duplicate as that is closed and not for being a duplicate or being rejected.

Hi!
I am proposing a suggestion so that we can see the minute time in the sound editor. The second time would on by default and this would be toggleable.

Scratch On!

Post rewritten on 9th of March 2025.

Last edited by VedanshS933 (March 9, 2025 13:32:10)

960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

(removed)

Last edited by 960times10_5is106044 (April 3, 2022 05:15:36)

VedanshS933
Scratcher
1000+ posts

Minute Time in Sound Editor

Bring Up My Post
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

That was existed in Scratch 2.0, but it's removed in Scratch 3.0.
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

Like this?
VedanshS933
Scratcher
1000+ posts

Minute Time in Sound Editor

960times10_5is106044 wrote:

Like this?

Yes
Euglena73
Scratcher
1000+ posts

Minute Time in Sound Editor

Not a bad idea, but from what I remember there was such a possibility in Scratch 2.0. Semi-support.
-gr
Scratcher
1000+ posts

Minute Time in Sound Editor

Bump
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

Look at this: https://github.com/LLK/scratch-gui/blob/ca71cbc/src/containers/sound-tab.jsx#L194 (it is the code to show the length in seconds, but I want them to show the sound length with the exact minute time)
I'm trying to make an add-on that shows the length in minutes and seconds but it's suddenly hard for me to do it. Can anyone make it?

Also,
@Euglena73 & @-gr
How do you get the ninja cat icons?

historical_supa
Scratcher
1000+ posts

Minute Time in Sound Editor

k0d3rrr
Scratcher
1000+ posts

Minute Time in Sound Editor

historical_supa wrote:

https://scratch.mit.edu/discuss/topic/574333/ this is dupe

VedanshS933 wrote:

NOTE : This is not a dupe. That topic was closed so I made a new topic. And it was not closed for being a dupe/rejected
Here's the link : https://scratch.mit.edu/discuss/topic/574333/
historical_supa
Scratcher
1000+ posts

Minute Time in Sound Editor

k0d3rrr wrote:

historical_supa wrote:

https://scratch.mit.edu/discuss/topic/574333/ this is dupe

VedanshS933 wrote:

NOTE : This is not a dupe. That topic was closed so I made a new topic. And it was not closed for being a dupe/rejected
Here's the link : https://scratch.mit.edu/discuss/topic/574333/
oops forgot
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

It looks like they were removed in Scratch 3.0. It shows just seconds. Can somebody make code that shows the exact minute time in the sound editor?
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

I support this
k0d3rrr
Scratcher
1000+ posts

Minute Time in Sound Editor

960times10_5is106044 wrote:

It looks like they were removed in Scratch 3.0. It shows just seconds. Can somebody make code that shows the exact minute time in the sound editor?
I think using maths, you can decode the amount of time a sound clip plays for (or listening to it on Windows Media Player to view the amount of time), but this would be a great idea!
I also think Scratch 2.0 was a good version. Sadly, I wasn't a registered user back then…
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

History:
In Scratch 1.4 and earlier: It shows hours, minutes, and seconds, but no decimal places.
In earlier versions of Scratch 2.0: It shows minutes, seconds, and 1 decimal place.
In latest versions of Scratch 2.0: It shows minutes, seconds, and 2 decimal places.
In Scratch 3.0: It shows just seconds and 2 decimal places. The minutes were ignored.

So, why does this change?
So if it is, let's support this to show the minutes.
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

I modified the code of https://sheeptester.github.io/javascripts/audio-editor/ and showed the minutes of sound length!
Code:
// reconstruction of https://github.com/LLK/scratch-gui/blob/develop/src/containers/sound-editor.jsx

import WavEncoder from './wav-encoder/index.js';
import lamejs from './lame.min.js';

import {computeChunkedRMS} from './audio/audio-util.js';
import AudioEffects from './audio/audio-effects.js';
import AudioBufferPlayer from './audio/audio-buffer-player.js';

import SharedAudioContext from './audio/shared-audio-context.js';

let reverbImpulseResponse, magicImpulseResponse, meowImpulseResponse;
fetch('./audio/york-minster-short.wav').then(r => r.arrayBuffer()).then(b => reverbImpulseResponse = b);
fetch('./audio/magic-spell.wav').then(r => r.arrayBuffer()).then(b => magicImpulseResponse = b);
fetch('./audio/scratch-meow.wav').then(r => r.arrayBuffer()).then(b => meowImpulseResponse = b);

const UNDO_STACK_SIZE = 99;

const HEIGHT = 100;
const DEFAULT_ZOOM = 200;
const MIN_ZOOM = 0.03125;
const MAX_ZOOM = 50;
const MAX_CANVAS_WIDTH = 5000;

const MAX_MP3_SAMPLES = 1152;

class SoundEditor {

constructor(props) {
[
'copy',
'copyCurrentBuffer',
'handleCopyToNew',
'handleStoppedPlaying',
'handlePlay',
'handleStopPlaying',
'handleUpdatePlayhead',
'handleDelete',
'handleUpdateTrim',
'handleEffect',
'handleUndo',
'handleRedo',
'submitNewSamples',
'handleCopy',
'handlePaste',
'paste',
'handleKeyPress',
'handleRemove',
'handleDownload',
'handleDownloadMp3',
'handleZoomIn',
'handleZoomOut',
'handleUpdateZoom'
].forEach(method => this[method] = this[method].bind(this));

this.props = props;

const zoom = this.props.sampleRate / DEFAULT_ZOOM;
const predictedWidth = Math.ceil(this.props.samples.length / zoom);
this.state = {
playhead: null, // null is not playing, [0 -> 1] is playing percent
trimStart: 0,
trimEnd: 0,
selectDirection: 'end',
zoom: predictedWidth > MAX_CANVAS_WIDTH
// Derivation:
// zoom = (sample rate / default zoom) * 2^n, where n is an integer
// canvas width = ceil(samples / zoom)
// = ceil(samples / ((sample rate / default zoom) * 2^n))
// = default zoom * samples / sample rate * 1 / 2^n
// 2^n = default zoom * samples / (sample rate * canvas width)
// n = log_2 (default zoom * samples / (sample rate * canvas width))
? zoom * Math.pow(2, Math.ceil(Math.log2((DEFAULT_ZOOM * this.props.samples.length) / (this.props.sampleRate * MAX_CANVAS_WIDTH))))
: zoom
};

this.redoStack = [];
this.undoStack = [];

this.audioContext = new SharedAudioContext();
const {effectTypes} = AudioEffects;
this.impulseResponses = {};
// HACK to allow reverb-effect to accept different sample rates
this.audioContext.decodeAudioData(reverbImpulseResponse.slice(0))
.then(buffer => this.resampleAudioBufferToRate(buffer, this.props.sampleRate))
.then(buffer => {
this.impulseResponses[effectTypes.REVERB] = buffer;
});
this.audioContext.decodeAudioData(magicImpulseResponse.slice(0))
.then(buffer => this.resampleAudioBufferToRate(buffer, this.props.sampleRate))
.then(buffer => {
this.impulseResponses[effectTypes.MAGIC] = buffer;
});
this.audioContext.decodeAudioData(meowImpulseResponse.slice(0))
.then(buffer => this.resampleAudioBufferToRate(buffer, this.props.sampleRate))
.then(buffer => {
this.impulseResponses[effectTypes.MEOW] = buffer;
});

this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
this.audioContext = new SharedAudioContext();

document.addEventListener('keydown', this.handleKeyPress);
}

unmount() {
this.audioBufferPlayer.stop();

document.removeEventListener('keydown', this.handleKeyPress);

if (this.elems.wrapper.parentNode) {
this.elems.wrapper.parentNode.removeChild(this.elems.wrapper);
}
}

handleKeyPress(event) {
if (event.target instanceof HTMLInputElement) {
// Ignore keyboard shortcuts if a text input field is focused
return;
}
if (!this.elems.wrapper.contains(event.target)) {
// Ignore keyboard shortcuts if a different editor is focused
return;
}
if (event.key === ' ') {
event.preventDefault();
if (this.state.playhead) {
this.handleStopPlaying();
} else {
this.handlePlay();
}
}
if (event.key === 'Delete' || event.key === 'Backspace') {
event.preventDefault();
if (event.shiftKey) {
this.handleDeleteInverse();
} else {
this.handleDelete();
}
}
if (event.key === 'Escape') {
event.preventDefault();
this.handleUpdateTrim(null, null);
}
const start = this.state.trimStart;
const end = this.state.trimEnd;
if (event.key === 'ArrowLeft') {
if (event.metaKey || event.ctrlKey) {
if (event.shiftKey) this.handleUpdateTrim(0, end);
else if (start === end) this.handleUpdateTrim(0, 0);
else this.handleUpdateTrim(start, start);
} else {
if (event.shiftKey) {
if (this.state.selectDirection === 'start') {
this.handleUpdateTrim(start - 0.01, end);
} else if (end - 0.01 < start) {
this.handleUpdateTrim(end - 0.01, start);
this.state.selectDirection = 'start';
} else {
this.handleUpdateTrim(start, end - 0.01);
}
} else {
this.handleUpdateTrim(start - 0.01, end - 0.01);
}
}
}
if (event.key === 'ArrowRight') {
if (event.metaKey || event.ctrlKey) {
if (event.shiftKey) this.handleUpdateTrim(start, 1);
else if (start === end) this.handleUpdateTrim(1, 1);
else this.handleUpdateTrim(end, end);
} else {
if (event.shiftKey) {
if (this.state.selectDirection === 'end') {
this.handleUpdateTrim(start, end + 0.01);
} else if (end < start + 0.01) {
this.handleUpdateTrim(end, start + 0.01);
this.state.selectDirection = 'end';
} else {
this.handleUpdateTrim(start + 0.01, end);
}
} else {
this.handleUpdateTrim(start + 0.01, end + 0.01);
}
}
}
if (event.metaKey || event.ctrlKey) {
if (event.shiftKey && event.key.toLowerCase() === 'z' || event.key === 'y') {
event.preventDefault();
if (this.redoStack.length > 0) {
this.handleRedo();
}
} else if (event.key === 'z') {
if (this.undoStack.length > 0) {
event.preventDefault();
this.handleUndo();
}
} else if (event.key === 'x') {
this.handleCopy();
this.handleDelete();
} else if (event.key === 'c') {
event.preventDefault();
this.handleCopy();
} else if (event.key === 'v') {
event.preventDefault();
this.handlePaste();
} else if (event.key === 'a') {
event.preventDefault();
this.handleUpdateTrim(0, 1);
} else if (event.key === 's') {
event.preventDefault();
if (event.altKey) this.handleDownloadMp3();
else this.handleDownload();
}
}
}

resetState(samples, sampleRate) {
this.audioBufferPlayer.stop();
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
this.state = {
playhead: null,
trimStart: this.state.trimStart,
trimEnd: this.state.trimEnd,
selectDirection: this.state.selectDirection,
zoom: this.state.zoom
};
this.renderWaveform(samples);
this.displayLength();
this.elems.redoBtn.disabled = !this.redoStack.length;
this.elems.undoBtn.disabled = !this.undoStack.length;
}

submitNewSamples(samples, sampleRate, skipUndo) {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.getUndoItem());
}
this.resetState(samples, sampleRate);
}

handlePlay() {
this.audioBufferPlayer.stop();
this.audioBufferPlayer.play(
this.state.trimStart || 0,
this.state.trimStart === this.state.trimEnd ? 1 : this.state.trimEnd || 1,
this.handleUpdatePlayhead,
this.handleStoppedPlaying
);

this.elems.playBtn.disabled = true;
this.elems.stopBtn.disabled = false;
this.elems.playhead.style.display = 'block';
}

handleStopPlaying() {
this.audioBufferPlayer.stop();
this.handleStoppedPlaying();
}

handleStoppedPlaying() {
this.elems.playBtn.disabled = false;
this.elems.stopBtn.disabled = true;
this.elems.playhead.style.display = null;
this.state.playhead = null;
}

handleUpdatePlayhead(playhead) {
this.elems.playhead.style.left = playhead * 100 + '%';
this.state.playhead = playhead;
}

handleDelete() {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
const firstPart = samples.slice(0, startIndex);
const secondPart = samples.slice(endIndex, sampleCount);
const newLength = firstPart.length + secondPart.length;
let newSamples;
if (newLength === 0) {
newSamples = new Float32Array(1);
} else {
newSamples = new Float32Array(newLength);
newSamples.set(firstPart, 0);
newSamples.set(secondPart, firstPart.length);
}
this.submitNewSamples(newSamples, sampleRate);
const trimPos = firstPart.length / newLength;
this.handleUpdateTrim(trimPos, trimPos);
}

handleDeleteInverse() {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
let clippedSamples = samples.slice(startIndex, endIndex);
if (clippedSamples.length === 0) {
clippedSamples = new Float32Array(1);
}
this.submitNewSamples(clippedSamples, sampleRate);
this.handleUpdateTrim(0, 1);
}

handleUpdateTrim(trimStart, trimEnd) {
if (trimStart < 0) trimStart = 0;
if (trimEnd < 0) trimEnd = 0;
if (trimStart > 1) trimStart = 1;
if (trimEnd > 1) trimEnd = 1;
else if (trimEnd < trimStart) throw new Error("smh that's not my job")
const buffer = this.copyCurrentBuffer();
this.elems.trim.style.left = trimStart * 100 + '%';
this.elems.trim.dataset.leftTime = (trimStart * buffer.samples.length / buffer.sampleRate).toFixed(2) + 's';
this.state.trimStart = trimStart;
this.elems.trim.style.right = (1 - trimEnd) * 100 + '%';
this.elems.trim.dataset.rightTime = (trimEnd * buffer.samples.length / buffer.sampleRate).toFixed(2) + 's';
this.state.trimEnd = trimEnd;
this.handleStopPlaying();
}

effectFactory(name) {
return () => this.handleEffect(name);
}

copyCurrentBuffer() {
// Cannot reliably use props.samples because it gets detached by Firefox
return {
samples: this.audioBufferPlayer.buffer.getChannelData(0),
sampleRate: this.audioBufferPlayer.buffer.sampleRate
};
}

handleEffect(name) {
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;

// Offline audio context needs at least 2 samples
if (this.audioBufferPlayer.buffer.length < 2) {
return;
}

const effects = new AudioEffects(this.audioBufferPlayer.buffer, name, trimStart, trimEnd, this.impulseResponses);
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
const samples = renderedBuffer.getChannelData(0);
const sampleRate = renderedBuffer.sampleRate;
this.submitNewSamples(samples, sampleRate);
this.handleUpdateTrim(adjustedTrimStart, adjustedTrimEnd);
this.handlePlay();
});
}

getUndoItem() {
return {
...this.copyCurrentBuffer(),
trimStart: this.state.trimStart,
trimEnd: this.state.trimEnd
};
}

handleUndo() {
this.redoStack.push(this.getUndoItem());
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
if (samples) {
this.submitNewSamples(samples, sampleRate, true);
this.handleUpdateTrim(trimStart, trimEnd);
this.handlePlay();
}
}

handleRedo() {
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
if (samples) {
this.undoStack.push(this.getUndoItem());
this.submitNewSamples(samples, sampleRate, true);
this.handleUpdateTrim(trimStart, trimEnd);
this.handlePlay();
}
}

handleCopy() {
this.copy();
}

copy(callback) {
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;

const newCopyBuffer = this.copyCurrentBuffer();
const trimStartSamples = trimStart * newCopyBuffer.samples.length;
const trimEndSamples = trimEnd * newCopyBuffer.samples.length;
newCopyBuffer.samples = newCopyBuffer.samples.slice(trimStartSamples, trimEndSamples);

this.props.clipboard.copyBuffer = newCopyBuffer;
if (callback) callback();
}

handleCopyToNew() {
this.copy(() => {
const {samples, sampleRate} = this.props.clipboard.copyBuffer;
this.props.addSound(
'Copy of ' + this.elems.name.value,
samples,
sampleRate
);
});
}

resampleBufferToRate(buffer, newRate) {
return new Promise(resolve => {
if (window.OfflineAudioContext) {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
const source = offlineContext.createBufferSource();
const audioBuffer = this.audioContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
}
});
}

resampleAudioBufferToRate(buffer, newRate) {
return new Promise(resolve => {
if (window.OfflineAudioContext) {
const sampleRateRatio = newRate / buffer.sampleRate;
const samples = buffer.getChannelData(0);
const newLength = sampleRateRatio * samples.length;
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
const source = offlineContext.createBufferSource();
const audioBuffer = this.audioContext.createBuffer(1, samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => resolve(renderedBuffer);
}
});
}

paste() {
const {samples} = this.copyCurrentBuffer();
const trimStartSamples = this.state.trimStart * samples.length;
const trimEndSamples = this.state.trimEnd * samples.length;
const firstPart = samples.slice(0, trimStartSamples);
const lastPart = samples.slice(trimEndSamples);
const newLength = firstPart.length + this.props.clipboard.copyBuffer.samples.length + lastPart.length;
const newSamples = new Float32Array(newLength);
newSamples.set(firstPart, 0);
newSamples.set(this.props.clipboard.copyBuffer.samples, firstPart.length);
newSamples.set(lastPart, firstPart.length + this.props.clipboard.copyBuffer.samples.length);

const trimStartSeconds = trimStartSamples / this.props.sampleRate;
const trimEndSeconds = trimStartSeconds +
(this.props.clipboard.copyBuffer.samples.length / this.props.clipboard.copyBuffer.sampleRate);
const newDurationSeconds = newSamples.length / this.props.clipboard.copyBuffer.sampleRate;
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.handleUpdateTrim(adjustedTrimStart, adjustedTrimEnd);
this.handlePlay();
}

handlePaste() {
if (!this.props.clipboard.copyBuffer) return;
if (this.props.clipboard.copyBuffer.sampleRate === this.props.sampleRate) {
this.paste();
} else {
this.resampleBufferToRate(this.props.clipboard.copyBuffer, this.props.sampleRate).then(buffer => {
this.props.clipboard.copyBuffer = buffer;
this.paste();
});
}
}

download(content, type) {
const saveLink = document.createElement('a');
document.body.appendChild(saveLink);

const filename = `${this.elems.name.value}.${type}`;

// Use special ms version if available to get it working on Edge.
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(content, filename);
return;
}

const url = window.URL.createObjectURL(content);
saveLink.href = url;
saveLink.download = filename;
saveLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(saveLink);
}

handleDownload() {
try {
const {samples, sampleRate} = this.copyCurrentBuffer();
const wavBuffer = WavEncoder.encode.sync({
sampleRate: sampleRate,
channelData: [samples]
});
const content = new Blob([wavBuffer], {type: 'audio/wav'});
this.download(content, 'wav');
} catch (e) {
console.log(e);
}
}

handleDownloadMp3() {
try {
const {samples: floatSamples, sampleRate} = this.copyCurrentBuffer();

// from https://github.com/zhuker/lamejs/issues/55#issuecomment-417944979
const samples = new Int16Array(floatSamples.length);
for (let i = 0; i < floatSamples.length; i++) {
samples[i] = (floatSamples[i] < 0 ? 0x8000 : 0x7fff) * floatSamples[i];
}

const encoder = new lamejs.Mp3Encoder(1, sampleRate, 128);
const result = [];
for (let i = 0; i < samples.length; i += MAX_MP3_SAMPLES) {
const buffer = encoder.encodeBuffer(samples.subarray(i, i + MAX_MP3_SAMPLES));
if (buffer.length > 0) {
result.push(new Int8Array(buffer));
}
}
const buffer = encoder.flush();
if (buffer.length > 0) {
result.push(new Int8Array(buffer));
}
const content = new Blob(result, {type: 'audio/mp3'});
this.download(content, 'mp3');
} catch (e) {
console.log(e);
}
}

handleRemove() {
if (confirm('Are you sure you want to remove this sound?')) {
this.unmount();
}
}

renderWaveform(samples) {
const chunkLevels = computeChunkedRMS(samples, this.state.zoom);
const canvas = this.elems.waveform;
const c = this.elems.waveformContext;
const levels = chunkLevels.length;
canvas.width = levels;
const waveHeight = HEIGHT / 3;
c.fillStyle = '#333';
chunkLevels.forEach((v, i) => {
c.fillRect(i, Math.floor(HEIGHT / 2 - v * waveHeight), 1, Math.ceil(v * waveHeight * 2));
});
}

displayLength() {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
if ((sampleCount / sampleRate % 60) >= 10) {
this.elems.length.textContent = Math.floor(sampleCount / sampleRate / 60) + ':' + (sampleCount / sampleRate % 60).toFixed(2);
} else {
this.elems.length.textContent = Math.floor(sampleCount / sampleRate / 60) + ':0' + (sampleCount / sampleRate % 60).toFixed(2);
}
}

handleZoomIn() {
this.state.zoom /= 2;
this.handleUpdateZoom();
}

handleZoomOut() {
this.state.zoom *= 2;
this.handleUpdateZoom();
}

handleUpdateZoom() {
const {samples, sampleRate} = this.copyCurrentBuffer();

const baseZoom = sampleRate / DEFAULT_ZOOM;
const zoomFactor = baseZoom / this.state.zoom;
if (zoomFactor < MIN_ZOOM) this.state.zoom = baseZoom / MIN_ZOOM;
if (zoomFactor > MAX_ZOOM) this.state.zoom = baseZoom / MAX_ZOOM;
if (this.state.zoom < 1) this.state.zoom = 1;

// not using zoomFactor because .zoom changes
this.elems.zoomDisplay.textContent = baseZoom / this.state.zoom * 100 + '%';
this.renderWaveform(samples);
}

render() {
const {effectTypes} = AudioEffects;
const elems = {};
this.elems = elems;
elems.waveform = Elem('canvas', {
className: 'waveform',
height: HEIGHT
});
elems.waveformContext = elems.waveform.getContext('2d');
let thatWasAFinger;
const trimHandles = e => {
const isTouch = e.type.includes('touch');
if (isTouch) thatWasAFinger = true;
else if (thatWasAFinger) {
thatWasAFinger = false;
return;
}
const rect = elems.waveform.getBoundingClientRect();
const pointerID = isTouch && e.changedTouches[0].identifier;
const pointer = isTouch ? e.touches[pointerID] : e;
const initialPosition = e.shiftKey
? (this.state.selectDirection === 'end'
? this.state.trimStart
: this.state.trimEnd)
: (pointer.clientX - rect.left) / rect.width;
const updatePosition = e => {
const rect = elems.waveform.getBoundingClientRect();
const pointer = isTouch ? e.touches[pointerID] : e;
const position = (pointer.clientX - rect.left) / rect.width;
if (position < initialPosition) {
this.state.selectDirection = 'start';
this.handleUpdateTrim(position, initialPosition);
} else {
this.state.selectDirection = 'end';
this.handleUpdateTrim(initialPosition, position);
}
}
if (e.shiftKey) {
updatePosition(e);
} else {
this.handleUpdateTrim(initialPosition, initialPosition);
}
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', updatePosition);
document.addEventListener(isTouch ? 'touchend' : 'mouseup', e => {
document.removeEventListener(isTouch ? 'touchmove' : 'mousemove', updatePosition);
}, {once: true});
};
return elems.wrapper = Elem('div', {
className: 'sound-editor',
tabindex: 0
}, [
elems.name = Elem('input', {
className: 'name-input',
type: 'text',
value: this.props.name
}),
elems.zoomOut = Elem('button', {
className: 'zoom-btn',
onclick: this.handleZoomOut
}, ['-']),
elems.zoomDisplay = Elem('span', {
className: 'zoom-display'
}),
elems.zoomIn = Elem('button', {
className: 'zoom-btn',
onclick: this.handleZoomIn
}, ['+']),
elems.downloadBtn = Elem('button', {
className: 'download-btn',
onclick: this.handleDownload
}, ['download as wav']),
elems.downloadBtn = Elem('button', {
className: 'download-btn',
onclick: this.handleDownloadMp3
}, ['download as mp3']),
elems.downloadBtn = Elem('button', {
className: 'delete-btn',
onclick: this.handleRemove
}, ['remove']),
elems.length = Elem('span', {className: 'sound-length'}),
elems.preview = Elem('div', {
className: 'preview',
onwheel: e => {
if (e.ctrlKey || e.metaKey) {
if (e.deltaY > 0) {
this.state.zoom *= e.deltaY / 500 + 1;
} else {
this.state.zoom /= -e.deltaY / 500 + 1;
}
this.handleUpdateZoom();
e.preventDefault();
}
}
}, [Elem('div', {
className: 'waveform-wrapper',
onmousedown: trimHandles,
ontouchstart: trimHandles
}, [
elems.waveform,
elems.playhead = Elem('div', {className: 'playhead'}),
elems.trim = Elem('div', {className: 'selection-range'})
])]),
elems.playBtn = Elem('button', {
className: 'play-btn',
onclick: this.handlePlay
}, ['play']),
elems.stopBtn = Elem('button', {
className: 'stop-btn',
disabled: true,
onclick: this.handleStopPlaying
}, ['stop']),
elems.edit = Select('Edit', [
['cut (ctrl + X)', () => {
this.handleCopy();
this.handleDelete();
}],
['copy (ctrl + C)', this.handleCopy],
['paste (ctrl + V)', this.handlePaste],
['---'],
['delete (backspace)', this.handleDelete],
['select all (ctrl + A)', () => {
this.handleUpdateTrim(0, 1);
}],
['trim (shift + backspace)', () => {
this.handleDeleteInverse();
}]
]),
elems.effects = Select('Effects', [
['reverse', this.effectFactory(effectTypes.REVERSE)],
['---'],
['slower', this.effectFactory(effectTypes.SLOWER)],
['faster', this.effectFactory(effectTypes.FASTER)],
['softer', this.effectFactory(effectTypes.SOFTER)],
['louder', this.effectFactory(effectTypes.LOUDER)],
['mute', this.effectFactory(effectTypes.MUTE)],
['fade in', this.effectFactory(effectTypes.FADEIN)],
['fade out', this.effectFactory(effectTypes.FADEOUT)],
['---'],
['robot', this.effectFactory(effectTypes.ROBOT)],
['echo', this.effectFactory(effectTypes.ECHO)],
['alien', this.effectFactory(effectTypes.ALIEN)],
['reverb', this.effectFactory(effectTypes.REVERB)],
['magic', this.effectFactory(effectTypes.MAGIC)],
['meow', this.effectFactory(effectTypes.MEOW)]
]),
elems.undoBtn = Elem('button', {
className: 'undo-btn',
disabled: true,
onclick: this.handleUndo
}, ['undo (ctrl + Z)']),
elems.redoBtn = Elem('button', {
className: 'redo-btn',
disabled: true,
onclick: this.handleRedo
}, ['redo (ctrl + Y)']),
Elem('button', {
className: 'copy-to-new-btn',
onclick: this.handleCopyToNew
}, ['copy to new'])
]);
}

}

export default SoundEditor;

Here's what it looked like (this is usually just in seconds):
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

But I can't modify this code because it's too laggy. Can someone help?
https://llk.github.io/scratch-gui/master/
VedanshS933
Scratcher
1000+ posts

Minute Time in Sound Editor

960times10_5is106044 wrote:

But I can't modify this code because it's too laggy. Can someone help?
https://llk.github.io/scratch-gui/master/

I am not an expert with code

Bump
960times10_5is106044
Scratcher
500+ posts

Minute Time in Sound Editor

Please, World_Languages, we already have add-ons for features that were removed in Scratch 3.0. Why don't we add the minute value for sounds tab?

Powered by DjangoBB