-
Notifications
You must be signed in to change notification settings - Fork 1
Design Rationale
./
├── Debriefing.md
├── LICENSE
├── README.md
├── docs // Build Folder
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── app.webmanifest
│ ├── apple-touch-icon.png
│ ├── assets
│ │ ├── index-235f95f1.js
│ │ └── index-25416e2f.css
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── favicon_package_v0.16-3.zip
│ ├── index.html
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── safari-pinned-tab.svg
│ └── site.webmanifest
├── index.html
├── jsconfig.json
├── package.json
├── public // Public folder wil be copied to root. ./
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── app.webmanifest
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── safari-pinned-tab.svg
│ └── site.webmanifest
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── Logo.svg
│ │ ├── base.css
│ │ ├── hva-logo.svg
│ │ ├── main.css
│ │ └── play-icon.svg
│ ├── components
│ │ ├── BaseButton.vue
│ │ ├── BaseIcon.vue
│ │ ├── Input.vue
│ │ ├── InputBpm.vue
│ │ ├── Modal.vue
│ │ ├── SampleSelect.vue
│ │ ├── SequenceItem.vue
│ │ ├── SequenceItemArc.vue
│ │ ├── SequenceItemControl.vue
│ │ ├── SequenceSteps.vue
│ │ └── Sequencer.vue
│ ├── composables
│ │ ├── getSampleData.js
│ │ ├── useMakeSamplers.js
│ │ └── useTone.js
│ ├── helpers
│ │ ├── dataClean.js
│ │ └── toneHelpers.js
│ ├── main.js
│ ├── router
│ │ └── index.js // all routing
│ ├── stores
│ │ └── sequence.js // State Management File
│ └── views
│ └── HomeView.vue
└── vite.config.jsMomenteel is er een API gehost op https://api-hitloop.responsible-it.nl/.
Op de API staan samples gehost en kunnen 1-bar MIDI sequences worden gemaakt.
Voor de samples kun je een lijst van de huidige gehoste files krijgen met
api-hitloop.responsible-it.nl/samples_test_list?sample_pack=b
{
"files": [
"crash_1_0_IJ-pont_varen.wav",
"crash_1_0_Tram_noodrem_Amsterdam.wav",
"crash_1_0_Tramhalte_Amsterdam.wav",
"crash_1_2_Tramhalte_Amsterdam.wav",
"crash_1_3_Tramhalte_Amsterdam.wav",
"crash_1_4_Tramhalte_Amsterdam.wav",
"crash_1_5_Tramhalte_Amsterdam.wav",
"kick_2_0_REPORTAGE OVER DE METRO.wav",
"kick_2_0_TRAM AMSTERDAM-ZANDVOORT.wav",
"kick_2_0_Tram_oudeelektrischetramstop.wav",
"kick_2_1_REPORTAGE OVER DE METRO.wav",
"sfx_2_0_TRAM AMSTERDAM-ZANDVOORT.wav",
"sfx_2_0_Tramhalte_Amsterdam.wav",
"sfx_2_1_Tramhalte_Amsterdam.wav",
"sfx_2_2_Tramhalte_Amsterdam.wav",
"sfx_2_3_Tramhalte_Amsterdam.wav",
"sfx_2_4_Tramhalte_Amsterdam.wav",
"snare_1_0_REPORTAGE OVER DE METRO.wav",
"snare_1_0_TRAM AMSTERDAM-ZANDVOORT.wav",
"snare_1_0_Tram_oudeelektrischetramstopt.wav",
"snare_1_0_Tramhalte_Amsterdam.wav",
"snare_1_1_REPORTAGE OVER DE METRO.wav",
"snare_2_0_REPORTAGE OVER DE METRO.wav",
"snare_2_0_TRAM AMSTERDAM-ZANDVOORT.wav",
"snare_2_1_REPORTAGE OVER DE METRO.wav"
]
}Voor de samples kun je een lijst van de huidige gehoste files krijgen met
api-hitloop.responsible-it.nl/samples_test_list?sample_pack=b
Dan kun je met api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=Filename. Een .wav file krijgen van de sample
De volgende functie hebben we geschreven om de data uit de api te halen en op te schonen.
getSampleData(url, ‘b’, fileName)
export const getSampleData = async (BASE_URL, samplePack, file) => {
let samplePackQuery = `?sample_pack=${samplePack}`
let samplePackQueryFile = `?sample_pack=${samplePack}&file=`
let URL = ref(null)
const sampleFileURL = url + sampleFilePath + samplePackQueryFile
// https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav
const result = ref(null)
if (url !== BASE_URL) {
console.log('url is not base url')
}
if (file === 'list') {
URL.value = BASE_URL + sampleListPath + samplePackQuery
} else {
URL.value = url + sampleFilePath + samplePackQuery + file
return URL.value
}
try {
const { data, isFetching, error } = await useFetch(URL, { refetch: true }).json()
if (data || !isFetching) {
result.value = await createSampleObjectList(data, sampleFileURL)
if (result.value !== null) {
return result
}
}
} catch (error) {
console.log(error)
}
}Door elke sample in een object te zetten met hierbij de informatie van de
De data die we uit de api kregen, was alleen een titel die er als volgt uit ziet:
"snare_2_1_REPORTAGE OVER DE METRO.wav"Door deze titel op de splitsen met regex. kunnen we op basis van de titel de benodigde informatie per sample in het object plaatsen. Hierdoor is het mogelijk deze informatie weer te geven aan de gebruiker. En ook gebruiken bij het managen van de state, bijvoorbeeld als er aan andere sample word gekozen. is de benodigde informatie beschikbaar.
export const createSampleObjectList = async (sampleData, url) => {
const data = sampleData.value
let id = 0
let currentNote = 21 // MIDI note value for A0 (lowest note)
const sampleObjectList = data.files
.map((str) => {
const regex = /^(hi-hat|.+)[-_\s](\d+)_(\d+)_(.+)\.wav$/
const matches = str.match(regex)
if (matches && matches.length === 5) {
const [, type, version1, version2, name] = matches
const version = `${version1}.${version2}`
const nameOnly = name.replace(/_/g, '-')
let sampleType = type
if (type === 'hi-hat') {
sampleType = 'Hi-Hat'
}
try {
const note = getValidMidiNoteFromId(currentNote) // Get a valid MIDI note value based on currentNote
if (note === null) {
// If there are no available MIDI notes, skip this sample
return null
}
const encodedStr = encodeURIComponent(str)
// const nameCaseChange = useChangeCase(nameOnly, 'capitalCase')
const sampleObject = {
id: id,
name: name.replace(/_/g, '-'),
type: sampleType.charAt(0).toUpperCase() + sampleType.slice(1),
version,
file: str,
url: url + encodedStr,
sampleId: note.midiNote,
note: note.normalNote
}
id++
currentNote = currentNote % 12 === 7 ? currentNote + 1 : currentNote + 2
if (currentNote > 127) {
// Reset currentNote if it exceeds the maximum MIDI note value
currentNote = 21
}
return sampleObject
} catch (error) {
console.error(error)
// Handle any specific error if needed
return null
}
} else {
// Handle invalid file name format if needed
return null
}
})
.filter((obj) => obj !== null)
return sampleObjectList
}sequence = new Tone.Sequence(tick, createSequenceArrayIndex(columns.value), '16n').start(0)const tick = (time, col) => {
Tone.Draw.schedule(() => {
if (isPlaying.value) {
setCurrentStepIndex(col)
}
}, time)
}Met deze functie kunnen gebruikers de sequencer starten en pauzeren.
const togglePlay = (e) => {
togglePlayPause()
if (isPlaying.value) {
Tone.getDestination()
Tone.Transport.start(Tone.now())
} else {
Tone.Transport.pause(Tone.now())
}
}const tick = (time, col) => {
Tone.Draw.schedule(() => {
if (isPlaying.value) {
setCurrentStepIndex(col)
}
}, time)
}Met deze code wordt er een nieuwe Tone.js sampler aangeroepen.
let samples = new Tone.Sampler({
urls: store.sampleObjectMidi,
onload: () => {
console.log('onload Mini players')
setSamplesLoaded(true)
}
}).toDestination()store.SampleObjectMidi is een object dat word gegenereerd zodat elke sample verbonden is aan een Midi nummer zodat de samples per Midi nummer aangeroepen kunnen worden.
const sampleObjectMidi = computed(() => {
let newObj = {}
if (sampleData.value) {
sampleData.value.forEach((obj) => {
if (obj && obj.url) {
newObj[obj.sampleMidi] = obj.url
}
})
}
return newObj
})De Tone.Sequence is een functie van ToneJS. die helpt met de timing van geluiden afspelen in een sequentie. De tick parameter is een callback functie die bij elke index van de createSequenceArrayIndex(columns.value) word aangeroepen. als er 16 elementen in de array zitten word deze functie dus ook 16 keer aangeroepen.
const tick = (time, col) => {
Tone.Draw.schedule(() => {
if (isPlaying.value) {
setCurrentStepIndex(col)
}
}, time)
}Maakt de bolletjes voor de sequencer
<button
v-for="(step, stepIndex) in item.steps"
class="step-item"
:key="stepIndex"
:class="{ active: item.steps[stepIndex], highlighted: stepIndex === currentStepIndex }"
@click="toggleStep(item, stepIndex)"
:name="stepIndex"
></button><div class="input-group">
<label for="reverb">Reverb:</label>
<input
id="reverb"
type="range"
min="0"
max="10"
@change="$emit('update:reverb', $event.target.value)"
:value="reverb"
step="0.5"
/>
</div>
<div class="input-group">
<label for="volume">Volume:</label>
<input
id="volume"
type="range"
min="-70"
max="0"
@input="$emit('update:volume', $event.target.value)"
:value="volume"
step="1"
/>
</div>For each new sequence an object is added to the sequenceData array.
let newSequenceData = {
id: sequenceID++,
sampleId: 31,
sampleDataId: 0,
sample: uniqueNote,
steps: createSequenceArraySteps(columns.value),
url: getSampleUrl(apiBaseURL, samplePack.value, sampleData.value[0].file),
color: thisColor,
volume: 0,
type: 'Crash',
blob: null,
note: 'G7',
sampleName: 'IJ pont varen',
reverb: 0
}
return sequenceData.value.splice(sequenceID, 0, newSequenceData)
}when a sample is changed by a user, a item in the sequenceData is updated with the new sample data.
const updateSequenceSample = async (sequenceDataId, sampleDataId) => {
try {
const setSequence = sequenceData.value[sequenceDataId]
const toSample = sampleData.value[sampleDataId]
if (!setSequence || toSample === undefined) return console.log('nodata')
return Object.assign(setSequence, {
sampleId: useToNumber(toSample.sampleId).value,
sampleDataId: useToNumber(sampleDataId).value,
type: toSample.type,
blob: toSample.blob,
url: toSample.url,
note: toSample.note,
sampleName: toSample.name
})
} catch (error) {
console.log(error)
}
}export const createSequenceArraySteps = (count) => {
return Array(count).fill(false)
}
// createSequenceArraySteps(4)
// [false,false,false,false]The Reverb decay value is a time value, so when its 0 ToneJS will give an error.
To fix this error the reverb wil only be connected to the sampler then the value is above 0.001, otherwise it will be disconnected.
watch(
() => props.reverb,
(newRev) => {
if (newRev > 0.001 && newRev !== 0 && newRev !== null) {
rev.set({ decay: newRev })
sampler.connect(rev)
} else {
sampler.disconnect(rev)
}
}
)