-
Notifications
You must be signed in to change notification settings - Fork 1
Product Biografie Giovanni
In de Product Biografie hou je per week bij wat je allemaal hebt gedaan. Je schrijft over het proces, de iteraties, de werkwijze en de planning. Ook schetsen, testen, voorbeeld code en inspiratie zijn deel van de Product Biografie. De Product Biografie is individueel, ook als je in een team werkt.
Planning 2023 | Maandag | Dinsdag | Woensdag | Donderdag | Vrijdag |
---|---|---|---|---|---|
Week 1 26 Mei → 2 Juni
|
Tweede pinksterdag | 9:30 Kickoff @CMD | Debriefing | Iteratie 1 | |
Week 2 5 Juni → 9 Juni
|
Standup & Checkup | Reviews | CSS Day | Iteratie 2 | |
Week 3 12 Juni → 16 Juni
|
Standup & Checkup | Reviews | Reviews | Iteratie 3 | |
Week 419 Juni → 23 Juni
|
Standup & Checkup | Reviews | Reviews | Iteratie 4 | |
Week 5 26 Juni → 30 Juni
|
EXPO voorbereiden | Final prototype | EXPO |
Om ervoor te zorgen dat het project zo gemakkelijk mogelijk verloopt, gebruiken we deze tools omdat ze specifiek zijn ontworpen om teamwork makkelijker te maken en de samenwerking te verbeteren.
-
Notion
- Project Management
- View Project Issues
- Calendar
- Task List
- Resource Sharing
- Project Management
-
Figma
- Design Docs
-
Github
- Version Control
- Deployment
- issues
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
"no-unused-vars": "off"
}
}
26 Mei → 2 Juni 2023
MindMap
Hardcoded Sequencer
Voor volgende week:
- Nieuwe rij kunnen toevoegen en verwijderen
- ook de sample wijzigen
- experimenteren met de layout/visuals
- Met Robbert bespreken over VueJS
June 5, 2023 → June 9, 2023
During the first meeting with the client we received the data we had to use.
Base URL: https://api-hitloop.responsible-it.nl/
Sample List URL: https://api-hitloop.responsible-it.nl/samples_test_list?sample_pack=b
Sample File URL: https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=fileName.wav
The Function that gets the sample data:
const sampleData = await getSampleData(apiBaseURL, samplePack.value)
The Responce getSampleData()
returns.
{
"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"
]
}
We didn't want to show the filename url in the client side when selecting a sample. So we decided to create an object for each item in the array. I Picked up this task, and did the following.
I added the createSampleObjectList()
function inside the getSampleData()
function.
The createSampleObjectList()
function creates new Object for each item in the files array.
How createSampleObjectList()
basicaly works is:
-
Input String
"crash_1_0_IJ-pont_varen.wav"
-
Check String Values:
crash
: Type
1_0
: Version
IJ-pont_varen
: Name
IJ-pont_varen.wav
: File Name -
Create Object:
{ "name": "IJ-pont-varen", "type": "Crash", "version": "1.0", "file": "crash_1_0_IJ-pont_varen.wav", "url": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav", }
getSampleData()
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}`)
}
URL.value = BASE_URL + sampleListPath + samplePackQuery
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')
console.log(error)
}
}
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
}
The Function createSampleObjectList()
was improved little by little, and this is the final versions response:
{
"id": 0,
"name": "IJ-pont-varen",
"type": "Crash",
"version": "1.0",
"file": "crash_1_0_IJ-pont_varen.wav",
"url": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav",
"sampleId": 31,
"note": "G7",
"blob": "blob:http://localhost:3000/f9fe04a6-087f-441d-bbfd-0ee0d3eff25c"
}
Because most logic was used in more than one component. and multiple components where dependent on the same state. we decided it was a good idea to implement state management.
In the vueJS docs pinia was recomended because of how it is integrated with the vuejs composition API.
The only thing i had to get used to is not directly modify a state in a component, but write a function in the pinia state that modifies this state.
To toggle a step in a sequence for example.
in VueJS without state management
June 12, 2023 → June 16, 2023
- experimenteren met de circles
- in elkaar, dunne lijn met rondje etc
- maak meerdere versies en kies de leukste uit voor volgende week.
- zelf sequence maken laten genereren eventueel ook bewerken.
- data opslaan met structuur die ook geladen kan worden.
- geluid visualisatie experimenteren.
- 1st Logo Design
- Code Splitting to Components
From HitloopSequencer V2:
<template>
<div id="sequencer">
<div v-for="(row, index) in sequenceData" class="row" :key="index">
<select v-model="row.url" :id="index">
<option v-for="sample in sampleList" :key="sample" :value="BaseURL + sample">
{{ sample }}
</option>
</select>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<line
v-for="(step, stepIndex) in row.steps"
:key="stepIndex"
:x1="100 + 80 * Math.cos((2 * Math.PI * stepIndex) / columns - Math.PI / 2)"
:y1="100 + 80 * Math.sin((2 * Math.PI * stepIndex) / columns - Math.PI / 2)"
:x2="100 + 90 * Math.cos((2 * Math.PI * stepIndex) / columns - Math.PI / 2)"
:y2="100 + 90 * Math.sin((2 * Math.PI * stepIndex) / columns - Math.PI / 2)"
:class="{ active: step, highlighted: stepIndex === highlighted }"
@click="toggleStep(row, stepIndex)"
stroke-width="50"
stroke="#8B8B8B"
/>
</svg>
</div>
</div>
<div>
<label for="bpm">BPM:</label>
<input id="bpm" type="number" min="20" max="300" v-model.number="bpm" />
</div>
<button @click="togglePlay" v-if="!isPlaying"><BaseIcon name="play_arrow" /></button>
<button @click="togglePlay" v-else><BaseIcon name="pause" /></button>
<button v-show="availableSamples > activeSamples" @click="addRow(sequenceData)">add row</button>
</template>
<template>
<div id="sequencer">
<TransitionGroup name="fade">
<div v-for="(row, index) in sequenceData" class="row" :key="index">
<SequenceItem>
<template v-slot:select>
<SampleSelect
v-model:url="row.url"
:selectedValue="row.url"
@update:="updateURL(row, $event)"
:item="row"
:sampleTypeList="sampleTypeList"
:sampleData="sampleData"
/>
</template>
<template v-slot:arc>
<SequenceItemArc
:columns="columns"
:row="row"
:highlighted="highlighted"
@toggle-step="toggleStep"
/>
</template>
</SequenceItem>
</div>
</TransitionGroup>
<button v-show="availableSamples > activeSamples" @click="addRow(sequenceData)">
<BaseIcon name="add" />
</button>
</div>
<div class="controlls">
<div>
<label for="bpm">BPM:</label>
<input id="bpm" type="number" min="20" max="300" v-model.number="bpm" />
</div>
<BaseButton v-if="!isPlaying" @click="togglePlay" icon="play_arrow" />
<BaseButton v-else @click="togglePlay" icon="pause" />
</div>
</template>
19 Juni → 23 Juni
- Generated Logo Assets
- Add Web App Manifest
- Reverb on Global Sequencer
- Clicking Play now resumes instead of start over.
- Remove/Add A sequence
the data used for the sequences is now a variable sequenceData
in the pinia State.
sequenceData
is an array of objects:
// sequenceData
[
{
"id": 0,
"sampleId": 31,
"sampleDataId": 0,
"sample": "A3",
"steps": [
false,
false,
false,
false
],
"url": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav",
"color": "red",
"volume": 0,
"type": "Crash",
"blob": "blob:http://localhost:3000/355d6da5-04c3-49c4-85b2-9157566eb309",
"note": "G7",
"sampleName": "IJ-pont-varen",
"reverb": 0
},
{
"id": 2,
"sampleId": 31,
"sampleDataId": 0,
"sample": "C3",
"steps": [
false,
false,
false,
false
],
"url": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav",
"color": "green",
"volume": 0,
"type": "Crash",
"blob": null,
"note": "G7",
"sampleName": "IJ pont varen",
"reverb": 0
}
]
the pinia state exports the functions to add and remove a sequence, these functions can now be user everywhere they are imported.
addSequence()
adds a sequence to the sequenceData
array with all the data it needs.
const addSequence = () => {
const uniqueNote = availableNotes.value[sequenceData.value.length % availableNotes.value.length]
if (sequenceData.value.length === availableNotes.value.length) {
// All available notes are used, disable the function or handle the case as needed
return
}
// if (!sequenceData.value) return
let currentMaxId = sequenceData.value.reduce((maxId, item) => {
return item.id > maxId ? item.id : maxId
}, 0)
const newId = currentMaxId + 1
let all = sequenceData.value.length
console.log(all)
let thisSample = availableNotes.value[all]
let thisColor = availableColors.value[all]
const i = Math.round(Math.random() * sequenceData.value.length)
// activeNotes.value.push(thisSample)
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)
}
removeSequence(id)
removes a sequence with the given id
from the sequenceData
array. This function is uses in every SequenceItem.vue
. This component gets the prop id
from the sequenceData
loop in Sequencer.vue
. This is why the correct item will be deleted from the list.
const removeSequence = (id, e) => {
const index = sequenceData.value.findIndex((item) => item.id === id)
if (index !== -1) {
sequenceData.value.splice(index, 1)
}
}
June 19, 2023 → June 23, 2023
June 26, 2023 → June 30, 2023
- Fixed samples loading.
- Move Sequencer & Samplers into components to have individual effects
- Reverb
- Volume
- Add Web App Manifest
- Put Effects input in the Modal @wongsrila created
- Randomize Sequence
The Tone.Sampler uses a objects that maps a Note to a URL.
At first I created a new SampleObject when the user selects another sample. I fixed the loading times by adding a Midi Note Value to each Sample that is fetched. And used this value to create a SampleObject with all the samples instead of mapping a note to a sample URL each time the user selects another sample. Instead of using the same note value for the 1st, 2nd, 3rd,....
Before this loading fix the note A3
was always connects to the value of the first row in a Sequence.
Like in this example:
{
"A3": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav",
"B3": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav",
"C3": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_0_IJ-pont_varen.wav"
}
By giving each URL their own Midi Number up front like so:
{
...
"41": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_2_Tramhalte_Amsterdam.wav",
"49": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_4_Tramhalte_Amsterdam.wav",
"50": "https://api-hitloop.responsible-it.nl/test_samples?sample_pack=b&file=crash_1_5_Tramhalte_Amsterdam.wav"
...
}
Before the fix, the first Sequence row always played the note A3
, even when a different Sample was selected.
Now when a different sample is selected by the user only the Midi Number that will be played is changed. Because of this method all the samples are already loaded resulting in much better performance.