Skip to content

Product Biografie Giovanni

Giovanni D edited this page Jul 6, 2023 · 19 revisions

Product Biografie

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

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 4
19 Juni → 23 Juni
Standup & Checkup Reviews Reviews Iteratie 4
Week 5   
26 Juni → 30 Juni
EXPO voorbereiden Final prototype EXPO

Werkwijze

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.

Setup

  • Notion
    • Project Management
      • View Project Issues
    • Calendar
    • Task List
    • Resource Sharing
  • Figma
    • Design Docs
  • Github
    • Version Control
    • Deployment
    • issues

Code Style Guidelines

Prettier

{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none"
}

ESLint

/* 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"
  }
}

Week 1

26 Mei → 2 Juni 2023

Research

MindMap

mindmap

Hardcoded Sequencer

Planning

Voor volgende week:

  • Nieuwe rij kunnen toevoegen en verwijderen
  • ook de sample wijzigen
  • experimenteren met de layout/visuals
  • Met Robbert bespreken over VueJS

1e Iteratie

Week 2

June 5, 2023 → June 9, 2023

Data Cleaning

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:

  1. Input String
    "crash_1_0_IJ-pont_varen.wav"

  2. Check String Values:
    crash: Type
    1_0: Version
    IJ-pont_varen: Name
    IJ-pont_varen.wav: File Name

  3. 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"
}

State Management

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

Week 3

June 12, 2023 → June 16, 2023

Deze Week

  • 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

Logo Design

Logo

Code Splitting

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>

to HitloopSequencer V3:

<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>

Week 4

19 Juni → 23 Juni

This Week

  • Generated Logo Assets
  • Add Web App Manifest
  • Reverb on Global Sequencer
  • Clicking Play now resumes instead of start over.
  • Remove/Add A sequence

Add & Remove 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

Week 5

June 26, 2023 → June 30, 2023

This Week

  • 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

How I Fixed the Sample Loading.

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.

Reflectie