diff --git a/CardListing/ab800371-b455-4a23-85f4-42fd0b786abb.json b/CardListing/ab800371-b455-4a23-85f4-42fd0b786abb.json new file mode 100644 index 0000000..45c3b30 --- /dev/null +++ b/CardListing/ab800371-b455-4a23-85f4-42fd0b786abb.json @@ -0,0 +1,96 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CardListing", + "module": "../catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Beat Maker Studio", + "images": [ + "https://boxel-images.boxel.ai/app-assets/catalog/beat-maker-listing/screenshot_01.png" + ], + "summary": "Professional beat maker and rhythm sequencer for creating music beats, patterns, and drum sequences with customizable instruments and sound kits.", + "cardInfo": { + "notes": "Includes multiple instrument kits, pattern sequencing, and real-time beat creation capabilities", + "name": "Beat Maker Studio", + "summary": "A comprehensive beat making application that allows users to create custom drum patterns, sequence beats, and mix audio with various instrument kits. Features include BPM control, swing settings, pattern management, and multiple sound libraries for professional music production.", + "cardThumbnailURL": "https://boxel-images.boxel.ai/app-assets/catalog/beat-maker-listing/thumbnail.png" + } + }, + "relationships": { + "tags": { + "links": { + "self": null + } + }, + "skills": { + "links": { + "self": null + } + }, + "license": { + "links": { + "self": null + } + }, + "specs.0": { + "links": { + "self": "../Spec/5ae71ae1-9cab-4003-b1b4-550a2305f442" + } + }, + "specs.1": { + "links": { + "self": "../Spec/1ae19cab-8003-41b4-950a-2305f442fd0b" + } + }, + "specs.2": { + "links": { + "self": "../Spec/e71ae19c-ab80-4371-b455-0a2305f442fd" + } + }, + "specs.3": { + "links": { + "self": "../Spec/e19cab80-0371-4455-8a23-05f442fd0b78" + } + }, + "specs.4": { + "links": { + "self": "../Spec/9cab8003-71b4-450a-a305-f442fd0b786a" + } + }, + "publisher": { + "links": { + "self": null + } + }, + "examples.0": { + "links": { + "self": "../beat-maker/BeatMakerCard/rhythm-studio" + } + }, + "categories.0": { + "links": { + "self": "../Category/entertainment-media" + } + }, + "categories.1": { + "links": { + "self": "../Category/creative-projects" + } + }, + "categories.2": { + "links": { + "self": "../Category/content-creation" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/Spec/1ae19cab-8003-41b4-950a-2305f442fd0b.json b/Spec/1ae19cab-8003-41b4-950a-2305f442fd0b.json new file mode 100644 index 0000000..811af2f --- /dev/null +++ b/Spec/1ae19cab-8003-41b4-950a-2305f442fd0b.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../beat-maker/beat-maker", + "name": "BeatPatternField" + }, + "specType": "field", + "containedExamples": [], + "cardTitle": "BeatPatternField", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} diff --git a/Spec/5ae71ae1-9cab-4003-b1b4-550a2305f442.json b/Spec/5ae71ae1-9cab-4003-b1b4-550a2305f442.json new file mode 100644 index 0000000..05806ab --- /dev/null +++ b/Spec/5ae71ae1-9cab-4003-b1b4-550a2305f442.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../beat-maker/beat-maker", + "name": "DrumKitField" + }, + "specType": "field", + "containedExamples": [], + "cardTitle": "DrumKitField", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} diff --git a/Spec/9cab8003-71b4-450a-a305-f442fd0b786a.json b/Spec/9cab8003-71b4-450a-a305-f442fd0b786a.json new file mode 100644 index 0000000..40f74cb --- /dev/null +++ b/Spec/9cab8003-71b4-450a-a305-f442fd0b786a.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../beat-maker/beat-maker", + "name": "BeatMakerCard" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "BeatMakerCard", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} diff --git a/Spec/e19cab80-0371-4455-8a23-05f442fd0b78.json b/Spec/e19cab80-0371-4455-8a23-05f442fd0b78.json new file mode 100644 index 0000000..ac0239a --- /dev/null +++ b/Spec/e19cab80-0371-4455-8a23-05f442fd0b78.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../beat-maker/beat-maker", + "name": "BeatPatternCard" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "BeatPatternCard", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} diff --git a/Spec/e71ae19c-ab80-4371-b455-0a2305f442fd.json b/Spec/e71ae19c-ab80-4371-b455-0a2305f442fd.json new file mode 100644 index 0000000..7dcc81e --- /dev/null +++ b/Spec/e71ae19c-ab80-4371-b455-0a2305f442fd.json @@ -0,0 +1,35 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../beat-maker/beat-maker", + "name": "DrumKitCard" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "DrumKitCard", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} diff --git a/beat-maker/BeatMakerCard/rhythm-studio.json b/beat-maker/BeatMakerCard/rhythm-studio.json new file mode 100644 index 0000000..8a073a9 --- /dev/null +++ b/beat-maker/BeatMakerCard/rhythm-studio.json @@ -0,0 +1,101 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "BeatMakerCard", + "module": "../beat-maker" + } + }, + "type": "card", + "attributes": { + "bpm": 110, + "swing": 0, + "pattern": "House-Standard", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "masterVolume": 75, + "instrumentKit": "Rock Kit" + }, + "relationships": { + "currentKit": { + "links": { + "self": "../DrumKitCard/rock-kit" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + }, + "currentPattern": { + "links": { + "self": "../BeatPatternCard/jazz-swing" + } + }, + "availableKits.0": { + "links": { + "self": "../DrumKitCard/808-analog-kit" + } + }, + "availableKits.1": { + "links": { + "self": "../DrumKitCard/trap-kit" + } + }, + "availableKits.2": { + "links": { + "self": "../DrumKitCard/house-kit" + } + }, + "availableKits.3": { + "links": { + "self": "../DrumKitCard/techno-kit" + } + }, + "availableKits.4": { + "links": { + "self": "../DrumKitCard/jazz-kit" + } + }, + "availableKits.5": { + "links": { + "self": "../DrumKitCard/rock-kit" + } + }, + "availablePatterns.0": { + "links": { + "self": "../BeatPatternCard/basic-4-4-beat" + } + }, + "availablePatterns.1": { + "links": { + "self": "../BeatPatternCard/trap-pattern" + } + }, + "availablePatterns.2": { + "links": { + "self": "../BeatPatternCard/house-groove" + } + }, + "availablePatterns.3": { + "links": { + "self": "../BeatPatternCard/breakbeat" + } + }, + "availablePatterns.4": { + "links": { + "self": "../BeatPatternCard/jazz-swing" + } + }, + "availablePatterns.5": { + "links": { + "self": "../BeatPatternCard/rock-beat" + } + } + } + } +} diff --git a/beat-maker/BeatPatternCard/basic-4-4-beat.json b/beat-maker/BeatPatternCard/basic-4-4-beat.json new file mode 100644 index 0000000..b87339c --- /dev/null +++ b/beat-maker/BeatPatternCard/basic-4-4-beat.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "Basic 4/4 Beat", + "cardDescription": "Classic four-on-the-floor pattern with kick on every beat and snare on 2 and 4", + "bpm": 120, + "genre": "Basic", + "creator": "Beat Maker Studio", + "pattern": { + "name": "Basic 4/4 Beat", + "kick": "[true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false]", + "snare": "[false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false]", + "hihat": "[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]", + "openhat": "[false,false,false,true,false,false,false,true,false,false,false,true,false,false,false,true]", + "clap": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]" + }, + "cardTitle": "Basic 4/4 Beat" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/BeatPatternCard/breakbeat.json b/beat-maker/BeatPatternCard/breakbeat.json new file mode 100644 index 0000000..1bc8762 --- /dev/null +++ b/beat-maker/BeatPatternCard/breakbeat.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "Breakbeat", + "cardDescription": "Classic breakbeat pattern with syncopated kick and snare placement", + "bpm": 130, + "genre": "Breakbeat", + "creator": "Beat Maker Studio", + "pattern": { + "name": "Breakbeat", + "kick": "[true,false,false,false,false,false,true,false,false,true,false,false,false,false,false,false]", + "snare": "[false,false,false,false,true,false,false,false,false,false,true,false,false,false,false,false]", + "hihat": "[false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true]", + "openhat": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "clap": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]" + }, + "cardTitle": "Breakbeat" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/BeatPatternCard/house-groove.json b/beat-maker/BeatPatternCard/house-groove.json new file mode 100644 index 0000000..f03b18a --- /dev/null +++ b/beat-maker/BeatPatternCard/house-groove.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "House Groove", + "cardDescription": "Steady four-on-the-floor house pattern with off-beat hi-hats", + "bpm": 125, + "genre": "House", + "creator": "Beat Maker Studio", + "pattern": { + "name": "House Groove", + "kick": "[true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false]", + "snare": "[false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false]", + "hihat": "[false,false,true,false,false,false,true,false,false,false,true,false,false,false,true,false]", + "openhat": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true]", + "clap": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]" + }, + "cardTitle": "House Groove" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/BeatPatternCard/jazz-swing.json b/beat-maker/BeatPatternCard/jazz-swing.json new file mode 100644 index 0000000..6294273 --- /dev/null +++ b/beat-maker/BeatPatternCard/jazz-swing.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "Jazz Swing", + "cardDescription": "Laid-back jazz pattern with subtle kick and brushed snare feel", + "bpm": 110, + "genre": "Jazz", + "creator": "Beat Maker Studio", + "pattern": { + "name": "Jazz Swing", + "kick": "[true,false,false,false,false,false,false,false,true,false,false,false,false,false,false,false]", + "snare": "[false,false,false,false,false,false,true,false,false,false,false,false,false,false,true,false]", + "hihat": "[true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false]", + "openhat": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true]", + "clap": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]" + }, + "cardTitle": "Jazz Swing" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/BeatPatternCard/rock-beat.json b/beat-maker/BeatPatternCard/rock-beat.json new file mode 100644 index 0000000..413cc0f --- /dev/null +++ b/beat-maker/BeatPatternCard/rock-beat.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "Rock Beat", + "cardDescription": "Driving rock pattern with steady kick and punchy snare backbeat", + "bpm": 115, + "genre": "Rock", + "creator": "Beat Maker Studio", + "pattern": { + "name": "Rock Beat", + "kick": "[true,false,false,false,true,false,true,false,true,false,false,false,true,false,true,false]", + "snare": "[false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false]", + "hihat": "[true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false]", + "openhat": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "clap": "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,true,false,false,false,false,false,false,false]" + }, + "cardTitle": "Rock Beat" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/BeatPatternCard/trap-pattern.json b/beat-maker/BeatPatternCard/trap-pattern.json new file mode 100644 index 0000000..3c6d843 --- /dev/null +++ b/beat-maker/BeatPatternCard/trap-pattern.json @@ -0,0 +1,29 @@ +{ + "data": { + "type": "card", + "attributes": { + "patternName": "Trap Pattern", + "cardDescription": "Modern trap beat with syncopated kicks and rapid hi-hat rolls", + "bpm": 140, + "genre": "Trap", + "creator": "Beat Maker Studio", + "pattern": { + "name": "Trap Pattern", + "kick": "[true,false,false,true,false,false,true,false,false,false,true,false,false,true,false,false]", + "snare": "[false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false]", + "hihat": "[true,false,true,true,false,true,true,false,true,false,true,true,false,true,true,false]", + "openhat": "[false,false,false,false,false,false,false,true,false,false,false,false,false,false,false,true]", + "clap": "[false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false]", + "crash": "[true,false,false,false,false,false,false,false,true,false,false,false,false,false,false,false]" + }, + "cardTitle": "Trap Pattern" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "BeatPatternCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/808-analog-kit.json b/beat-maker/DrumKitCard/808-analog-kit.json new file mode 100644 index 0000000..9826649 --- /dev/null +++ b/beat-maker/DrumKitCard/808-analog-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "808 Analog", + "cardDescription": "Classic analog drum machine sounds with deep kicks and crisp hi-hats", + "category": "Analog", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "808 Analog", + "kickParams": "{\"type\": \"808\", \"frequency\": 60, \"decay\": 0.3, \"amplitude\": 1.0, \"shape\": 2}", + "snareParams": "{\"type\": \"808\", \"frequency\": 200, \"decay\": 0.1, \"amplitude\": 0.8, \"shape\": 1.5, \"resonance\": 5}", + "hihatParams": "{\"type\": \"808\", \"frequency\": 8000, \"decay\": 0.05, \"amplitude\": 0.6, \"shape\": 4, \"resonance\": 2}", + "openhatParams": "{\"type\": \"808\", \"frequency\": 6000, \"decay\": 0.3, \"amplitude\": 0.8, \"shape\": 1.5, \"resonance\": 2}", + "clapParams": "{\"type\": \"808\", \"frequency\": 2000, \"decay\": 0.1, \"amplitude\": 0.6, \"shape\": 2, \"resonance\": 5}", + "crashParams": "{\"type\": \"808\", \"frequency\": 3000, \"decay\": 1.0, \"amplitude\": 0.5, \"shape\": 0.5, \"resonance\": 1}" + }, + "cardTitle": "808 Analog" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/house-kit.json b/beat-maker/DrumKitCard/house-kit.json new file mode 100644 index 0000000..42cc95f --- /dev/null +++ b/beat-maker/DrumKitCard/house-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "House Kit", + "cardDescription": "Punchy four-on-the-floor kicks with clean snares and smooth hi-hats", + "category": "House", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "House Kit", + "kickParams": "{\"type\": \"house\", \"frequency\": 65, \"decay\": 0.25, \"amplitude\": 1.0, \"shape\": 1.5, \"resonance\": 1}", + "snareParams": "{\"type\": \"house\", \"frequency\": 180, \"decay\": 0.2, \"amplitude\": 0.8, \"shape\": 2, \"resonance\": 0.5}", + "hihatParams": "{\"type\": \"house\", \"frequency\": 7000, \"decay\": 0.08, \"amplitude\": 0.5, \"shape\": 2, \"resonance\": 1}", + "openhatParams": "{\"type\": \"house\", \"frequency\": 6500, \"decay\": 0.25, \"amplitude\": 0.6, \"shape\": 1.5, \"resonance\": 1}", + "clapParams": "{\"type\": \"house\", \"frequency\": 1800, \"decay\": 0.15, \"amplitude\": 0.7, \"shape\": 2, \"resonance\": 3}", + "crashParams": "{\"type\": \"house\", \"frequency\": 3500, \"decay\": 0.8, \"amplitude\": 0.4, \"shape\": 1, \"resonance\": 1}" + }, + "cardTitle": "House Kit" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/jazz-kit.json b/beat-maker/DrumKitCard/jazz-kit.json new file mode 100644 index 0000000..726c306 --- /dev/null +++ b/beat-maker/DrumKitCard/jazz-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "Jazz Kit", + "cardDescription": "Warm acoustic simulation with natural textures and gentle filtering", + "category": "Jazz", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "Jazz Kit", + "kickParams": "{\"type\": \"jazz\", \"frequency\": 70, \"decay\": 0.2, \"amplitude\": 0.8, \"shape\": 1.5, \"resonance\": 1}", + "snareParams": "{\"type\": \"jazz\", \"frequency\": 150, \"decay\": 0.25, \"amplitude\": 0.6, \"shape\": 1.5, \"resonance\": 0.5}", + "hihatParams": "{\"type\": \"jazz\", \"frequency\": 5000, \"decay\": 0.12, \"amplitude\": 0.4, \"shape\": 1.5, \"resonance\": 0.3}", + "openhatParams": "{\"type\": \"jazz\", \"frequency\": 4500, \"decay\": 0.35, \"amplitude\": 0.5, \"shape\": 1.2, \"resonance\": 0.5}", + "clapParams": "{\"type\": \"jazz\", \"frequency\": 1600, \"decay\": 0.18, \"amplitude\": 0.5, \"shape\": 1.8, \"resonance\": 2}", + "crashParams": "{\"type\": \"jazz\", \"frequency\": 3200, \"decay\": 1.2, \"amplitude\": 0.3, \"shape\": 0.8, \"resonance\": 0.5}" + }, + "cardTitle": "Jazz Kit" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/rock-kit.json b/beat-maker/DrumKitCard/rock-kit.json new file mode 100644 index 0000000..37fb2a7 --- /dev/null +++ b/beat-maker/DrumKitCard/rock-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "Rock Kit", + "cardDescription": "Aggressive punchy drums with heavy compression and beater clicks", + "category": "Rock", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "Rock Kit", + "kickParams": "{\"type\": \"rock\", \"frequency\": 58, \"decay\": 0.25, \"amplitude\": 1.1, \"shape\": 0.7, \"resonance\": 1}", + "snareParams": "{\"type\": \"rock\", \"frequency\": 220, \"decay\": 0.18, \"amplitude\": 1.0, \"shape\": 2, \"resonance\": 3}", + "hihatParams": "{\"type\": \"rock\", \"frequency\": 7500, \"decay\": 0.07, \"amplitude\": 0.8, \"shape\": 3, \"resonance\": 3}", + "openhatParams": "{\"type\": \"rock\", \"frequency\": 7000, \"decay\": 0.3, \"amplitude\": 0.7, \"shape\": 2, \"resonance\": 2}", + "clapParams": "{\"type\": \"rock\", \"frequency\": 2100, \"decay\": 0.12, \"amplitude\": 0.9, \"shape\": 2.5, \"resonance\": 4}", + "crashParams": "{\"type\": \"rock\", \"frequency\": 3800, \"decay\": 0.9, \"amplitude\": 0.6, \"shape\": 1, \"resonance\": 2}" + }, + "cardTitle": "Rock Kit" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/techno-kit.json b/beat-maker/DrumKitCard/techno-kit.json new file mode 100644 index 0000000..1d6d06e --- /dev/null +++ b/beat-maker/DrumKitCard/techno-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "Techno Kit", + "cardDescription": "Industrial driving sounds with hard distortion and aggressive filtering", + "category": "Techno", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "Techno Kit", + "kickParams": "{\"type\": \"techno\", \"frequency\": 55, \"decay\": 0.3, \"amplitude\": 1.3, \"shape\": 0.7, \"resonance\": 1}", + "snareParams": "{\"type\": \"techno\", \"frequency\": 1500, \"decay\": 0.15, \"amplitude\": 1.1, \"shape\": 1.5, \"resonance\": 8}", + "hihatParams": "{\"type\": \"techno\", \"frequency\": 9000, \"decay\": 0.06, \"amplitude\": 0.7, \"shape\": 4, \"resonance\": 5}", + "openhatParams": "{\"type\": \"techno\", \"frequency\": 8500, \"decay\": 0.2, \"amplitude\": 0.6, \"shape\": 2, \"resonance\": 4}", + "clapParams": "{\"type\": \"techno\", \"frequency\": 2200, \"decay\": 0.1, \"amplitude\": 0.8, \"shape\": 3, \"resonance\": 6}", + "crashParams": "{\"type\": \"techno\", \"frequency\": 4500, \"decay\": 0.7, \"amplitude\": 0.5, \"shape\": 1, \"resonance\": 3}" + }, + "cardTitle": "Techno Kit" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/DrumKitCard/trap-kit.json b/beat-maker/DrumKitCard/trap-kit.json new file mode 100644 index 0000000..4034109 --- /dev/null +++ b/beat-maker/DrumKitCard/trap-kit.json @@ -0,0 +1,28 @@ +{ + "data": { + "type": "card", + "attributes": { + "kitName": "Trap Kit", + "cardDescription": "Sub-heavy kicks with snappy snares and metallic hi-hats for modern trap production", + "category": "Trap", + "creator": "Beat Maker Studio", + "kit": { + "kitName": "Trap Kit", + "kickParams": "{\"type\": \"trap\", \"frequency\": 50, \"decay\": 0.8, \"amplitude\": 1.5, \"shape\": 1, \"resonance\": 1}", + "snareParams": "{\"type\": \"trap\", \"frequency\": 2000, \"decay\": 0.08, \"amplitude\": 1.2, \"shape\": 3, \"resonance\": 10}", + "hihatParams": "{\"type\": \"trap\", \"frequency\": 10000, \"decay\": 0.03, \"amplitude\": 0.8, \"shape\": 5, \"resonance\": 2}", + "openhatParams": "{\"type\": \"trap\", \"frequency\": 9000, \"decay\": 0.2, \"amplitude\": 0.7, \"shape\": 2, \"resonance\": 3}", + "clapParams": "{\"type\": \"trap\", \"frequency\": 2500, \"decay\": 0.12, \"amplitude\": 0.8, \"shape\": 2.5, \"resonance\": 8}", + "crashParams": "{\"type\": \"trap\", \"frequency\": 4000, \"decay\": 0.6, \"amplitude\": 0.4, \"shape\": 1, \"resonance\": 2}" + }, + "cardTitle": "Trap Kit" + }, + "relationships": {}, + "meta": { + "adoptsFrom": { + "module": "../beat-maker", + "name": "DrumKitCard" + } + } + } +} diff --git a/beat-maker/beat-maker.gts b/beat-maker/beat-maker.gts new file mode 100644 index 0000000..944bf63 --- /dev/null +++ b/beat-maker/beat-maker.gts @@ -0,0 +1,3115 @@ +import { + CardDef, + FieldDef, + field, + contains, + linksTo, + linksToMany, + Component, +} from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import MusicIcon from '@cardstack/boxel-icons/music'; +import { Button } from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import { fn, get, array, concat } from '@ember/helper'; +import { eq, gt, or } from '@cardstack/boxel-ui/helpers'; + +// ⁸⁹ Drum Kit Definition - stores sound parameters for each kit +export class DrumKitField extends FieldDef { + static displayName = 'Drum Kit'; + static icon = MusicIcon; + + @field kitName = contains(StringField); + @field kickParams = contains(StringField); // JSON string of kick sound parameters + @field snareParams = contains(StringField); + @field hihatParams = contains(StringField); + @field openhatParams = contains(StringField); + @field clapParams = contains(StringField); + @field crashParams = contains(StringField); + + // ⁹⁰ Parse sound parameters from JSON strings + get soundParams() { + try { + return { + kick: JSON.parse( + this.kickParams || '{"type": "808", "frequency": 60, "decay": 0.3}', + ), + snare: JSON.parse( + this.snareParams || '{"type": "808", "frequency": 200, "decay": 0.1}', + ), + hihat: JSON.parse( + this.hihatParams || + '{"type": "808", "frequency": 8000, "decay": 0.05}', + ), + openhat: JSON.parse( + this.openhatParams || + '{"type": "808", "frequency": 6000, "decay": 0.3}', + ), + clap: JSON.parse( + this.clapParams || '{"type": "808", "frequency": 2000, "decay": 0.1}', + ), + crash: JSON.parse( + this.crashParams || + '{"type": "808", "frequency": 3000, "decay": 1.0}', + ), + }; + } catch (e) { + console.error('Error parsing sound parameters:', e); + return { + kick: { type: '808', frequency: 60, decay: 0.3 }, + snare: { type: '808', frequency: 200, decay: 0.1 }, + hihat: { type: '808', frequency: 8000, decay: 0.05 }, + openhat: { type: '808', frequency: 6000, decay: 0.3 }, + clap: { type: '808', frequency: 2000, decay: 0.1 }, + crash: { type: '808', frequency: 3000, decay: 1.0 }, + }; + } + } + + static embedded = class Embedded extends Component { + + }; +} + +// ⁹¹ Drum Kit Card Definition - stores complete drum kits as cards +export class DrumKitCard extends CardDef { + static displayName = 'Drum Kit'; + static icon = MusicIcon; + + @field kitName = contains(StringField); + @field cardDescription = contains(StringField); + @field category = contains(StringField); // e.g., "Analog", "Trap", "House" + @field creator = contains(StringField); + @field kit = contains(DrumKitField); // The actual sound parameters + + @field cardTitle = contains(StringField, { + computeVia: function (this: DrumKitCard) { + try { + return this.kitName ?? 'Untitled Kit'; + } catch (e) { + console.error('DrumKitCard: Error computing title', e); + return 'Untitled Kit'; + } + }, + }); + + static embedded = class Embedded extends Component { + + }; + + static isolated = class Isolated extends Component { + + }; +} + +export class BeatPatternField extends FieldDef { + static displayName = 'Beat Pattern'; + static icon = MusicIcon; + + @field name = contains(StringField); + @field kick = contains(StringField); // JSON string of boolean array + @field snare = contains(StringField); + @field hihat = contains(StringField); + @field openhat = contains(StringField); + @field clap = contains(StringField); + @field crash = contains(StringField); + + get patternData() { + try { + return { + kick: JSON.parse( + this.kick || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + snare: JSON.parse( + this.snare || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + hihat: JSON.parse( + this.hihat || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + openhat: JSON.parse( + this.openhat || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + clap: JSON.parse( + this.clap || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + crash: JSON.parse( + this.crash || + '[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]', + ), + }; + } catch (e) { + console.error('Error parsing pattern data:', e); + return { + kick: new Array(16).fill(false), + snare: new Array(16).fill(false), + hihat: new Array(16).fill(false), + openhat: new Array(16).fill(false), + clap: new Array(16).fill(false), + crash: new Array(16).fill(false), + }; + } + } + + static embedded = class Embedded extends Component { + + }; +} + +export class BeatPatternCard extends CardDef { + static displayName = 'Beat Pattern'; + static icon = MusicIcon; + + @field patternName = contains(StringField); + @field cardDescription = contains(StringField); + @field bpm = contains(NumberField); + @field genre = contains(StringField); + @field creator = contains(StringField); + @field pattern = contains(BeatPatternField); // The actual pattern data + + @field cardTitle = contains(StringField, { + computeVia: function (this: BeatPatternCard) { + try { + return this.patternName ?? 'Untitled Beat'; + } catch (e) { + console.error('BeatPatternCard: Error computing title', e); + return 'Untitled Beat'; + } + }, + }); + + static embedded = class Embedded extends Component { + + }; +} + +class BeatMakerIsolated extends Component { + // ²¹ Beat maker embedded format - ⁷⁴ Now uses model values directly + @tracked isPlaying = false; + @tracked currentStep = 0; + @tracked volumes = { + kick: 85, + snare: 75, + hihat: 60, + openhat: 50, + clap: 70, + crash: 40, + }; + + // ⁷⁵ Use model values directly instead of separate tracked properties + get bpm() { + return this.args.model?.bpm || 120; + } + + get swing() { + return this.args.model?.swing || 0; + } + + get masterVolume() { + return this.args.model?.masterVolume || 75; + } + + // ⁹⁵ Get current kit name from loaded kit card or fallback to string + get selectedKit() { + return ( + this.args.model?.currentKit?.kitName || + this.args.model?.instrumentKit || + '808 Analog' + ); + } + + getInstrumentVolume = (instrument: string): number => { + return (this.volumes as any)[instrument] || 0; + }; + + // ¹⁰⁶ Get current kit sound parameters from loaded kit card - properly parse JSON strings + get currentKitParams() { + try { + const kit = this.args.model?.currentKit?.kit; + if (kit) { + return { + kick: JSON.parse( + kit.kickParams || '{"type": "808", "frequency": 60, "decay": 0.3}', + ), + snare: JSON.parse( + kit.snareParams || + '{"type": "808", "frequency": 200, "decay": 0.1}', + ), + hihat: JSON.parse( + kit.hihatParams || + '{"type": "808", "frequency": 8000, "decay": 0.05}', + ), + openhat: JSON.parse( + kit.openhatParams || + '{"type": "808", "frequency": 6000, "decay": 0.3}', + ), + clap: JSON.parse( + kit.clapParams || + '{"type": "808", "frequency": 2000, "decay": 0.1}', + ), + crash: JSON.parse( + kit.crashParams || + '{"type": "808", "frequency": 3000, "decay": 1.0}', + ), + }; + } + // Fallback to default 808 parameters + return { + kick: { type: '808', frequency: 60, decay: 0.3 }, + snare: { type: '808', frequency: 200, decay: 0.1 }, + hihat: { type: '808', frequency: 8000, decay: 0.05 }, + openhat: { type: '808', frequency: 6000, decay: 0.3 }, + clap: { type: '808', frequency: 2000, decay: 0.1 }, + crash: { type: '808', frequency: 3000, decay: 1.0 }, + }; + } catch (e) { + console.error('Error accessing kit parameters:', e); + return { + kick: { type: '808', frequency: 60, decay: 0.3 }, + snare: { type: '808', frequency: 200, decay: 0.1 }, + hihat: { type: '808', frequency: 8000, decay: 0.05 }, + openhat: { type: '808', frequency: 6000, decay: 0.3 }, + clap: { type: '808', frequency: 2000, decay: 0.1 }, + crash: { type: '808', frequency: 3000, decay: 1.0 }, + }; + } + } + + // ⁹⁷ Get available drum kits from the model + get availableKits() { + try { + if ( + this.args.model?.availableKits && + this.args.model.availableKits.length > 0 + ) { + return this.args.model.availableKits; + } + // Return empty array if no kits available + return []; + } catch (e) { + console.error('Error accessing available kits:', e); + return []; + } + } + + // ⁹⁸ Get available patterns from the model + get availablePatterns() { + try { + if ( + this.args.model?.availablePatterns && + this.args.model.availablePatterns.length > 0 + ) { + return this.args.model.availablePatterns; + } + return []; + } catch (e) { + console.error('Error accessing available patterns:', e); + return []; + } + } + // ⁸⁵ Get patterns from current pattern card or use default + get patterns() { + try { + if (this.args.model?.currentPattern?.pattern?.patternData) { + return this.args.model.currentPattern.pattern.patternData; + } + + // Default patterns if no pattern card is loaded + return { + kick: [ + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + ], + snare: [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + ], + hihat: [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + openhat: [ + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + ], + clap: [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + ], + crash: [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + ], + }; + } catch (e) { + console.error('Error getting patterns:', e); + return { + kick: new Array(16).fill(false), + snare: new Array(16).fill(false), + hihat: new Array(16).fill(false), + openhat: new Array(16).fill(false), + clap: new Array(16).fill(false), + crash: new Array(16).fill(false), + }; + } + } + + // Visual current step that's one behind the internal current step for proper highlighting + get visualCurrentStep() { + return this.currentStep === 0 ? 15 : this.currentStep - 1; + } + + // Computed getter that creates a flattened step-state structure for easier template access + get stepStates() { + try { + const patterns = this.patterns; + const states: { [key: string]: boolean } = {}; + + Object.keys(patterns).forEach((instrument) => { + const instrumentPattern = patterns[instrument as keyof typeof patterns]; + if (instrumentPattern) { + for (let step = 0; step < 16; step++) { + states[`${instrument}-${step}`] = instrumentPattern[step] || false; + } + } + }); + + return states; + } catch (e) { + console.error('Error creating step states:', e); + return {}; + } + } + + // ⁸⁶ Update patterns in the current pattern card + updatePatterns(newPatterns: any) { + try { + if (this.args.model?.currentPattern?.pattern) { + // Update the pattern field data + this.args.model.currentPattern.pattern.kick = JSON.stringify( + newPatterns.kick, + ); + this.args.model.currentPattern.pattern.snare = JSON.stringify( + newPatterns.snare, + ); + this.args.model.currentPattern.pattern.hihat = JSON.stringify( + newPatterns.hihat, + ); + this.args.model.currentPattern.pattern.openhat = JSON.stringify( + newPatterns.openhat, + ); + this.args.model.currentPattern.pattern.clap = JSON.stringify( + newPatterns.clap, + ); + this.args.model.currentPattern.pattern.crash = JSON.stringify( + newPatterns.crash, + ); + } + } catch (e) { + console.error('Error updating patterns:', e); + } + } + + // ⁴⁸ Audio context and sound generation + audioContext: AudioContext | null = null; + sequenceTimer: number | null = null; + nextStepTime = 0; + lookahead = 25.0; // How frequently to call scheduling function (in milliseconds) + scheduleAheadTime = 0.1; // How far ahead to schedule audio (sec) + + constructor(owner: any, args: any) { + super(owner, args); + // Initialize audio context on first user interaction + this.initializeAudio(); + } + + initializeAudio() { + try { + this.audioContext = new (window.AudioContext || + (window as any).webkitAudioContext)(); + } catch (e) { + console.warn('Web Audio API not supported'); + } + } + + // Ensure audio stops when this instance is torn down (e.g., switching cards) + willDestroy(): void { + this.stop(); + if (this.audioContext) { + // Close without awaiting to avoid blocking teardown + this.audioContext.close().catch(() => undefined); + } + this.audioContext = null; + } + + // ⁴⁹ Dynamic sound synthesis using kit parameters + playKick(time: number, volume: number) { + if (!this.audioContext) return; + + const kickParams = this.currentKitParams.kick; + this.playDynamicKick(time, volume, kickParams); + } + + // ⁹⁹ Universal kick synthesis using dynamic parameters + playDynamicKick(time: number, volume: number, params: any) { + const osc = this.audioContext!.createOscillator(); + const gain = this.audioContext!.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(params.frequency || 60, time); + osc.frequency.exponentialRampToValueAtTime( + (params.frequency || 60) * 0.5, + time + 0.05, + ); + osc.frequency.exponentialRampToValueAtTime( + 0.01, + time + (params.decay || 0.3), + ); + + gain.gain.setValueAtTime(0, time); + gain.gain.linearRampToValueAtTime( + volume * (params.amplitude || 1.0), + time + 0.01, + ); + gain.gain.exponentialRampToValueAtTime(0.01, time + (params.decay || 0.3)); + + osc.connect(gain); + gain.connect(this.audioContext!.destination); + + osc.start(time); + osc.stop(time + (params.decay || 0.3)); + } + + playSnare(time: number, volume: number) { + if (!this.audioContext) return; + + const snareParams = this.currentKitParams.snare; + this.playDynamicSnare(time, volume, snareParams); + } + + playHihat(time: number, volume: number) { + if (!this.audioContext) return; + + const hihatParams = this.currentKitParams.hihat; + this.playDynamicHihat(time, volume, hihatParams); + } + + playOpenhat(time: number, volume: number) { + if (!this.audioContext) return; + + const openhatParams = this.currentKitParams.openhat; + this.playDynamicHihat(time, volume, openhatParams); // ¹⁰⁷ Use dynamic parameters for open hat too + } + + playClap(time: number, volume: number) { + if (!this.audioContext) return; + + const clapParams = this.currentKitParams.clap; + this.playDynamicSnare(time, volume, clapParams); // ¹⁰⁸ Use dynamic parameters for clap + } + + playCrash(time: number, volume: number) { + if (!this.audioContext) return; + + const crashParams = this.currentKitParams.crash; + this.playDynamicHihat(time, volume, crashParams); // ¹⁰⁹ Use dynamic parameters for crash + } + + // ¹⁰⁰ Universal snare synthesis using dynamic parameters + playDynamicSnare(time: number, volume: number, params: any) { + const noise = this.audioContext!.createBufferSource(); + const gain = this.audioContext!.createGain(); + const filter = this.audioContext!.createBiquadFilter(); + + // Create noise buffer + const bufferSize = this.audioContext!.sampleRate * (params.decay || 0.1); + const buffer = this.audioContext!.createBuffer( + 1, + bufferSize, + this.audioContext!.sampleRate, + ); + const output = buffer.getChannelData(0); + + for (let i = 0; i < bufferSize; i++) { + output[i] = + (Math.random() * 2 - 1) * + Math.pow(1 - i / bufferSize, params.shape || 2); + } + noise.buffer = buffer; + + filter.type = 'highpass'; + filter.frequency.value = params.frequency || 200; + filter.Q.value = params.resonance || 5; + + gain.gain.setValueAtTime(volume * (params.amplitude || 0.8), time); + gain.gain.exponentialRampToValueAtTime(0.01, time + (params.decay || 0.1)); + + noise.connect(filter); + filter.connect(gain); + gain.connect(this.audioContext!.destination); + + noise.start(time); + noise.stop(time + (params.decay || 0.1)); + } + + // ¹⁰¹ Universal hihat synthesis using dynamic parameters + playDynamicHihat(time: number, volume: number, params: any) { + const noise = this.audioContext!.createBufferSource(); + const gain = this.audioContext!.createGain(); + const filter = this.audioContext!.createBiquadFilter(); + + const bufferSize = this.audioContext!.sampleRate * (params.decay || 0.05); + const buffer = this.audioContext!.createBuffer( + 1, + bufferSize, + this.audioContext!.sampleRate, + ); + const output = buffer.getChannelData(0); + + for (let i = 0; i < bufferSize; i++) { + output[i] = + (Math.random() * 2 - 1) * + Math.pow(1 - i / bufferSize, params.shape || 4); + } + noise.buffer = buffer; + + filter.type = 'highpass'; + filter.frequency.value = params.frequency || 8000; + filter.Q.value = params.resonance || 2; + + gain.gain.setValueAtTime(volume * (params.amplitude || 0.6), time); + gain.gain.exponentialRampToValueAtTime(0.01, time + (params.decay || 0.05)); + + noise.connect(filter); + filter.connect(gain); + gain.connect(this.audioContext!.destination); + + noise.start(time); + noise.stop(time + (params.decay || 0.05)); + } + + // ¹¹⁰ All instruments now use universal dynamic sound generation + + // ⁵⁰ Sequencer timing and playback + scheduler() { + while ( + this.nextStepTime < + this.audioContext!.currentTime + this.scheduleAheadTime + ) { + this.scheduleStep(this.currentStep, this.nextStepTime); + this.nextStep(); + } + } + + scheduleStep(stepNumber: number, time: number) { + const masterVol = (this.masterVolume / 100) * 0.3; // Scale down for comfortable listening + + Object.keys(this.patterns).forEach((instrument) => { + if (this.patterns[instrument as keyof typeof this.patterns][stepNumber]) { + const volume = + (this.volumes[instrument as keyof typeof this.volumes] / 100) * + masterVol; + + switch (instrument) { + case 'kick': + this.playKick(time, volume); + break; + case 'snare': + this.playSnare(time, volume); + break; + case 'hihat': + this.playHihat(time, volume); + break; + case 'openhat': + this.playOpenhat(time, volume); + break; + case 'clap': + this.playClap(time, volume); + break; + case 'crash': + this.playCrash(time, volume); + break; + } + } + }); + } + + nextStep() { + const baseStepLength = 60.0 / this.bpm / 4; // 16th note length ⁸⁰ Uses model bpm value + + // Apply swing timing - affects every other beat (8th note pairs) + const swingAmount = this.swing / 100; // Convert to 0-1 range + let stepLength = baseStepLength; + + // Apply swing to off-beats (steps 1, 3, 5, 7, 9, 11, 13, 15) + if (this.currentStep % 2 === 1) { + // Delay off-beats based on swing amount (max 67% swing = triplet feel) + stepLength = baseStepLength * (1 + swingAmount * 0.67); + } else if (this.currentStep % 2 === 0 && this.currentStep > 0) { + // Compensate on-beats to maintain overall timing + stepLength = baseStepLength * (1 - swingAmount * 0.33); + } + + this.nextStepTime += stepLength; + this.currentStep = (this.currentStep + 1) % 16; + } + + start() { + if (!this.audioContext) { + this.initializeAudio(); + } + + if (this.audioContext?.state === 'suspended') { + this.audioContext.resume(); + } + + this.isPlaying = true; + this.currentStep = 0; + this.nextStepTime = this.audioContext!.currentTime; + this.sequenceTimer = window.setInterval( + () => this.scheduler(), + this.lookahead, + ); + } + + stop() { + this.isPlaying = false; + this.currentStep = 0; + if (this.sequenceTimer) { + clearInterval(this.sequenceTimer); + this.sequenceTimer = null; + } + } + + // ¹⁰² No more hardcoded arrays - all dynamic from card data + + @action + togglePlay() { + if (this.isPlaying) { + this.stop(); + } else { + this.start(); + } + } + + @action + toggleStep(instrument: string, step: number) { + const currentPatterns = this.patterns; + const newPatterns = { ...currentPatterns }; + newPatterns[instrument as keyof typeof newPatterns] = [ + ...newPatterns[instrument as keyof typeof newPatterns], + ]; + newPatterns[instrument as keyof typeof newPatterns][step] = + !newPatterns[instrument as keyof typeof newPatterns][step]; + this.updatePatterns(newPatterns); + } + + @action + clearPattern(instrument: string) { + const currentPatterns = this.patterns; + const newPatterns = { ...currentPatterns }; + newPatterns[instrument as keyof typeof newPatterns] = new Array(16).fill( + false, + ); + this.updatePatterns(newPatterns); + } + + @action + fillPattern(instrument: string) { + const currentPatterns = this.patterns; + const newPatterns = { ...currentPatterns }; + newPatterns[instrument as keyof typeof newPatterns] = new Array(16).fill( + true, + ); + this.updatePatterns(newPatterns); + } + + @action + loadPreset(patternCard: any) { + // ¹⁰³ Load pattern from a pattern card instead of hardcoded presets + if (this.args.model && patternCard) { + this.args.model.currentPattern = patternCard; + // Also sync BPM if the pattern has one + if (patternCard.bpm && this.args.model.bpm !== patternCard.bpm) { + this.args.model.bpm = patternCard.bpm; + } + } + } + + @action + randomizePattern(instrument: string) { + const currentPatterns = this.patterns; + const newPatterns = { ...currentPatterns }; + newPatterns[instrument as keyof typeof newPatterns] = new Array(16) + .fill(false) + .map(() => Math.random() > 0.7); + this.updatePatterns(newPatterns); + } + + // ⁸⁷ Action to save current patterns as a new pattern card + @action + saveCurrentPattern() { + // This would create a new BeatPatternCard instance with current patterns + // Implementation would depend on card creation workflow + console.log('Save current pattern functionality would be implemented here'); + } + + // ⁸⁸ Action to load a different pattern card + @action + loadPatternCard(patternCard: any) { + if (this.args.model) { + this.args.model.currentPattern = patternCard; + // Also sync BPM if the pattern has one + if (patternCard.bpm && this.args.model.bpm !== patternCard.bpm) { + this.args.model.bpm = patternCard.bpm; + } + } + } + + @action + updateBpm(event: Event) { + const target = event.target as HTMLInputElement; + const value = parseInt(target.value); + // ⁷⁶ Update the model's BPM value + if (this.args.model) { + this.args.model.bpm = value; + } + } + + @action + updateSwing(event: Event) { + const target = event.target as HTMLInputElement; + const value = parseInt(target.value); + // ⁷⁷ Update the model's swing value + if (this.args.model) { + this.args.model.swing = value; + } + } + + @action + updateMasterVolume(event: Event) { + const target = event.target as HTMLInputElement; + const value = parseInt(target.value); + // ⁷⁸ Update the model's master volume value + if (this.args.model) { + this.args.model.masterVolume = value; + } + } + + @action + updateVolume(instrument: string, event: Event) { + const target = event.target as HTMLInputElement; + this.volumes = { ...this.volumes, [instrument]: parseInt(target.value) }; + } + + @action + handleKitSelection(event: Event) { + // ¹⁰⁵ Handle kit selection from dropdown + const target = event.target as HTMLSelectElement; + const selectedKitId = target.value; + + // Find the kit card by ID + const selectedKit = this.availableKits.find( + (kit) => kit.id === selectedKitId, + ); + + if (this.args.model && selectedKit) { + this.args.model.currentKit = selectedKit; + this.args.model.instrumentKit = selectedKit.kitName; // Keep for backward compatibility + console.log('Kit switched to:', selectedKit.kitName); + } + } + + @action + selectKit(kitCard: any) { + // ¹⁰⁴ Load kit from a kit card instead of hardcoded string - kept for pattern loading + if (this.args.model && kitCard) { + this.args.model.currentKit = kitCard; + this.args.model.instrumentKit = kitCard.kitName; // Keep for backward compatibility + } + } + + +} + +export class BeatMakerCard extends CardDef { + static displayName = 'Beat Maker'; + static icon = MusicIcon; + + @field bpm = contains(NumberField); + @field pattern = contains(StringField); // Keep for backward compatibility + @field instrumentKit = contains(StringField); + @field swing = contains(NumberField); + @field masterVolume = contains(NumberField); + @field currentPattern = linksTo(() => BeatPatternCard); // ⁸⁴ Current loaded pattern + @field currentKit = linksTo(() => DrumKitCard); // ⁹² Current loaded drum kit + @field availableKits = linksToMany(() => DrumKitCard); // ⁹³ Available drum kits library + @field availablePatterns = linksToMany(() => BeatPatternCard); // ⁹⁴ Available patterns library + + @field cardTitle = contains(StringField, { + computeVia: function (this: BeatMakerCard) { + return 'Beat Maker'; + }, + }); + + static isolated = BeatMakerIsolated; + + static fitted = class Fitted extends Component { + + }; +}